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

@@ -5,6 +5,7 @@ import { join } from "node:path";
import type { ResolvedCommandTarget } from "../../../src/server/checker/runner/cmd/types";
import type { ResolvedHttpTarget } from "../../../src/server/checker/runner/http/types";
import type { ResolvedTcpTarget } from "../../../src/server/checker/runner/tcp/types";
import { loadConfig, parseDuration } from "../../../src/server/checker/config-loader";
import { checkerRegistry } from "../../../src/server/checker/runner";
@@ -1778,4 +1779,199 @@ targets:
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("name");
});
test("解析最简 tcp 配置", async () => {
const configPath = join(tempDir, "minimal-tcp.yaml");
await writeFile(
configPath,
`targets:
- id: "redis-port"
type: tcp
tcp:
host: "127.0.0.1"
port: 6379
`,
);
const config = await loadConfig(configPath);
expect(config.targets).toHaveLength(1);
const t = config.targets[0]! as ResolvedTcpTarget;
expect(t.type).toBe("tcp");
expect(t.id).toBe("redis-port");
expect(t.name).toBeNull();
expect(t.tcp.host).toBe("127.0.0.1");
expect(t.tcp.port).toBe(6379);
expect(t.tcp.readBanner).toBe(false);
expect(t.tcp.bannerReadTimeout).toBe(2000);
expect(t.tcp.maxBannerBytes).toBe(4096);
expect(t.group).toBe("default");
expect(t.intervalMs).toBe(30000);
expect(t.timeoutMs).toBe(10000);
});
test("tcp 缺少 host 抛出错误", async () => {
await expectConfigError(
"tcp-no-host.yaml",
`targets:
- id: "t"
type: tcp
tcp:
port: 80
`,
"tcp.host",
);
});
test("tcp 缺少 port 抛出错误", async () => {
await expectConfigError(
"tcp-no-port.yaml",
`targets:
- id: "t"
type: tcp
tcp:
host: "127.0.0.1"
`,
"tcp.port",
);
});
test("tcp 非法端口范围抛出错误", async () => {
await expectConfigError(
"tcp-bad-port.yaml",
`targets:
- id: "t"
type: tcp
tcp:
host: "127.0.0.1"
port: 99999
`,
"tcp.port",
);
});
test("tcp 未知分组字段抛出错误", async () => {
await expectConfigError(
"tcp-unknown-field.yaml",
`targets:
- id: "t"
type: tcp
tcp:
host: "127.0.0.1"
port: 80
tls: true
`,
"是未知字段",
);
});
test("tcp readBanner 开启并配置 expect.banner", async () => {
const configPath = join(tempDir, "tcp-banner.yaml");
await writeFile(
configPath,
`targets:
- id: "smtp-check"
type: tcp
tcp:
host: "127.0.0.1"
port: 25
readBanner: true
expect:
banner:
contains: "ESMTP"
`,
);
const config = await loadConfig(configPath);
const t = config.targets[0]! as ResolvedTcpTarget;
expect(t.tcp.readBanner).toBe(true);
expect(t.expect?.banner).toEqual({ contains: "ESMTP" });
});
test("tcp expect.banner 未开启 readBanner 抛出错误", async () => {
await expectConfigError(
"tcp-banner-no-read.yaml",
`targets:
- id: "t"
type: tcp
tcp:
host: "127.0.0.1"
port: 25
expect:
banner:
contains: "ESMTP"
`,
"banner 断言需要启用 tcp.readBanner",
);
});
test("tcp defaults 覆盖 banner 参数", async () => {
const configPath = join(tempDir, "tcp-defaults.yaml");
await writeFile(
configPath,
`defaults:
tcp:
bannerReadTimeout: 1000
maxBannerBytes: "8KB"
targets:
- id: "t1"
type: tcp
tcp:
host: "127.0.0.1"
port: 80
- id: "t2"
type: tcp
tcp:
host: "127.0.0.1"
port: 81
bannerReadTimeout: 3000
`,
);
const config = await loadConfig(configPath);
const t1 = config.targets[0]! as ResolvedTcpTarget;
expect(t1.tcp.bannerReadTimeout).toBe(1000);
expect(t1.tcp.maxBannerBytes).toBe(8192);
const t2 = config.targets[1]! as ResolvedTcpTarget;
expect(t2.tcp.bannerReadTimeout).toBe(3000);
expect(t2.tcp.maxBannerBytes).toBe(8192);
});
test("tcp expect 未知字段抛出错误", async () => {
await expectConfigError(
"tcp-unknown-expect.yaml",
`targets:
- id: "t"
type: tcp
tcp:
host: "127.0.0.1"
port: 80
expect:
status: [200]
`,
"是未知字段",
);
});
test("tcp expect connected 和 maxDurationMs", async () => {
const configPath = join(tempDir, "tcp-expect-connected.yaml");
await writeFile(
configPath,
`targets:
- id: "t"
type: tcp
tcp:
host: "127.0.0.1"
port: 80
expect:
connected: false
maxDurationMs: 5000
`,
);
const config = await loadConfig(configPath);
const t = config.targets[0]! as ResolvedTcpTarget;
expect(t.expect?.connected).toBe(false);
expect(t.expect?.maxDurationMs).toBe(5000);
});
});