1
0

feat: 新增 TCP checker,支持端口可达性探测与 banner 读取

- 新增 src/server/checker/runner/tcp/ 自包含目录(types/schema/validate/execute/expect)
- 注册 TcpChecker 到 checkerRegistry,schema/engine/store/config-loader 自动委托
- 支持 expect.connected 正反向语义(默认期待可达,可配置期待不可达)
- 支持 readBanner opt-in banner 读取,受 bannerReadTimeout + maxBannerBytes 双重限制
- 复用电有 expect/operator/duration/failure 基础设施
- 新增 3 个测试文件 51 条用例(execute/validate/expect),全量 634 测试通过
- 更新 README/DEVELOPMENT/probes.example.yaml,新增 tcp-checker capability spec
This commit is contained in:
2026-05-17 23:53:37 +08:00
parent 31fd3a2a43
commit 0a9a9016be
18 changed files with 1841 additions and 8 deletions

View File

@@ -0,0 +1,191 @@
import { describe, expect, test } from "bun:test";
import type { CheckerValidationInput } from "../../../../../src/server/checker/runner/types";
import { validateTcpConfig } from "../../../../../src/server/checker/runner/tcp/validate";
function makeInput(targets: unknown[], defaults?: Record<string, unknown>): CheckerValidationInput {
return {
defaults: defaults ?? {},
targets: targets as CheckerValidationInput["targets"],
};
}
describe("validateTcpConfig", () => {
test("合法 tcp target 无错误", () => {
const issues = validateTcpConfig(makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" }]));
expect(issues).toHaveLength(0);
});
test("缺少 tcp 分组", () => {
const issues = validateTcpConfig(makeInput([{ id: "t1", type: "tcp" }]));
expect(issues.length).toBeGreaterThan(0);
expect(issues.some((i) => i.message.includes("tcp"))).toBe(true);
});
test("缺少 host", () => {
const issues = validateTcpConfig(makeInput([{ id: "t1", tcp: { port: 80 }, type: "tcp" }]));
expect(issues.some((i) => i.path.includes("host"))).toBe(true);
});
test("缺少 port", () => {
const issues = validateTcpConfig(makeInput([{ id: "t1", tcp: { host: "127.0.0.1" }, type: "tcp" }]));
expect(issues.some((i) => i.path.includes("port"))).toBe(true);
});
test("端口超范围", () => {
const issues = validateTcpConfig(makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 99999 }, type: "tcp" }]));
expect(issues.some((i) => i.path.includes("port"))).toBe(true);
});
test("端口为 0", () => {
const issues = validateTcpConfig(makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 0 }, type: "tcp" }]));
expect(issues.some((i) => i.path.includes("port"))).toBe(true);
});
test("readBanner 非布尔值", () => {
const issues = validateTcpConfig(
makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 80, readBanner: "yes" }, type: "tcp" }]),
);
expect(issues.some((i) => i.path.includes("readBanner"))).toBe(true);
});
test("bannerReadTimeout 非数字", () => {
const issues = validateTcpConfig(
makeInput([{ id: "t1", tcp: { bannerReadTimeout: "slow", host: "127.0.0.1", port: 80 }, type: "tcp" }]),
);
expect(issues.some((i) => i.path.includes("bannerReadTimeout"))).toBe(true);
});
test("tcp 分组未知字段", () => {
const issues = validateTcpConfig(
makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 80, tls: true }, type: "tcp" }]),
);
expect(issues.some((i) => i.message.includes("未知字段"))).toBe(true);
});
test("expect.banner 未开启 readBanner", () => {
const issues = validateTcpConfig(
makeInput([
{
expect: { banner: { contains: "ESMTP" } },
id: "t1",
tcp: { host: "127.0.0.1", port: 25 },
type: "tcp",
},
]),
);
expect(issues.some((i) => i.message.includes("readBanner"))).toBe(true);
});
test("expect.banner 开启 readBanner 无错误", () => {
const issues = validateTcpConfig(
makeInput([
{
expect: { banner: { contains: "ESMTP" } },
id: "t1",
tcp: { host: "127.0.0.1", port: 25, readBanner: true },
type: "tcp",
},
]),
);
expect(issues).toHaveLength(0);
});
test("expect connected 非布尔值", () => {
const issues = validateTcpConfig(
makeInput([
{
expect: { connected: "yes" },
id: "t1",
tcp: { host: "127.0.0.1", port: 80 },
type: "tcp",
},
]),
);
expect(issues.some((i) => i.path.includes("connected"))).toBe(true);
});
test("expect maxDurationMs 非数字", () => {
const issues = validateTcpConfig(
makeInput([
{
expect: { maxDurationMs: "slow" },
id: "t1",
tcp: { host: "127.0.0.1", port: 80 },
type: "tcp",
},
]),
);
expect(issues.some((i) => i.path.includes("maxDurationMs"))).toBe(true);
});
test("expect 未知字段", () => {
const issues = validateTcpConfig(
makeInput([
{
expect: { status: [200] },
id: "t1",
tcp: { host: "127.0.0.1", port: 80 },
type: "tcp",
},
]),
);
expect(issues.some((i) => i.message.includes("未知字段"))).toBe(true);
});
test("expect.banner match 正则非法", () => {
const issues = validateTcpConfig(
makeInput([
{
expect: { banner: { match: "[invalid" } },
id: "t1",
tcp: { host: "127.0.0.1", port: 25, readBanner: true },
type: "tcp",
},
]),
);
expect(issues.some((i) => i.message.includes("正则"))).toBe(true);
});
test("非 tcp 类型 target 跳过", () => {
const issues = validateTcpConfig(makeInput([{ http: { url: "http://example.com" }, id: "t1", type: "http" }]));
expect(issues).toHaveLength(0);
});
test("defaults.tcp 合法字段无错误", () => {
const issues = validateTcpConfig(
makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" }], {
tcp: { bannerReadTimeout: 1000, maxBannerBytes: "8KB" },
}),
);
expect(issues).toHaveLength(0);
});
test("defaults.tcp 未知字段", () => {
const issues = validateTcpConfig(
makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" }], {
tcp: { bannerReadTimeout: 1000, host: "127.0.0.1" },
}),
);
expect(issues.some((i) => i.message.includes("未知字段"))).toBe(true);
});
test("defaults.tcp bannerReadTimeout 非法", () => {
const issues = validateTcpConfig(
makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" }], {
tcp: { bannerReadTimeout: "slow" },
}),
);
expect(issues.some((i) => i.path.includes("bannerReadTimeout"))).toBe(true);
});
test("defaults.tcp maxBannerBytes 非法", () => {
const issues = validateTcpConfig(
makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" }], {
tcp: { maxBannerBytes: true },
}),
);
expect(issues.some((i) => i.path.includes("maxBannerBytes"))).toBe(true);
});
});