1
0
Files
DiAL/tests/server/checker/runner/tcp/execute.test.ts
lanyuanxiaoyao 7a635a0a9f refactor: 统一 expect 断言体系,引入共享 ValueMatcher/ContentRules/KeyValueExpect 模型
- 引入共享 ValueMatcher(equals/contains/regex/exists/empty/gt/gte/lt/lte)
- 引入共享 ContentRules 数组(direct/json/css/xpath 提取器)
- 引入共享 KeyValueExpect(动态键值断言,字面量等价 equals)
- maxDurationMs → durationMs: ValueMatcher(所有 checker)
- match → regex(固定无 flags)
- Ping max* → packetLossPercent/avgLatencyMs/maxLatencyMs(ValueMatcher)
- LLM finishReason/rawFinishReason → ValueMatcher
- DB 新增 result: ContentRules
- TCP banner → ContentRules 数组
- 删除旧模块:operator.ts、validate-operator.ts、duration.ts、body.ts、text.ts、output.ts
- 更新全部 checker schema/validate/expect/execute
- 更新 probe-config.schema.json、probes.example.yaml
- 更新 README.md、DEVELOPMENT.md(含 expect 字段选择规范)
- 同步 10 个 delta specs 到主 specs,归档 change
2026-05-19 14:24:27 +08:00

368 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { ResolvedTcpTarget } from "../../../../../src/server/checker/runner/tcp/types";
import type { CheckerContext } from "../../../../../src/server/checker/runner/types";
import { TcpChecker } from "../../../../../src/server/checker/runner/tcp/execute";
const checker = new TcpChecker();
let server: Bun.TCPSocketListener;
let serverPort: number;
let bannerServer: Bun.TCPSocketListener;
let bannerServerPort: number;
let largeBannerServer: Bun.TCPSocketListener;
let largeBannerServerPort: number;
function makeCtx(timeoutMs = 5000): CheckerContext {
const controller = new AbortController();
setTimeout(() => controller.abort(), timeoutMs);
return { signal: controller.signal };
}
function makeTarget(tcp: Partial<ResolvedTcpTarget["tcp"]>, overrides?: Partial<ResolvedTcpTarget>): ResolvedTcpTarget {
return {
description: null,
group: "default",
id: "test-tcp",
intervalMs: 60000,
name: "test-tcp",
tcp: {
bannerReadTimeout: 2000,
host: "127.0.0.1",
maxBannerBytes: 4096,
port: serverPort,
readBanner: false,
...tcp,
},
timeoutMs: 5000,
type: "tcp",
...overrides,
};
}
beforeAll(() => {
server = Bun.listen({
hostname: "127.0.0.1",
port: 0,
socket: {
data() {
// Bun.listen 必填 handlerecho server 不处理数据
},
end(socket) {
try {
socket.close();
} catch {
// best-effort 关闭
}
},
error() {
// Bun.listen 必填 handler测试 server 忽略错误
},
open() {
// Bun.listen 必填 handleropen 时无需操作
},
},
});
serverPort = server.port;
bannerServer = Bun.listen({
hostname: "127.0.0.1",
port: 0,
socket: {
data() {
// Bun.listen 必填 handler
},
end(socket) {
try {
socket.close();
} catch {
// best-effort 关闭
}
},
error() {
// Bun.listen 必填 handler
},
open(socket) {
socket.write("220 smtp.example.com ESMTP\r\n");
},
},
});
bannerServerPort = bannerServer.port;
largeBannerServer = Bun.listen({
hostname: "127.0.0.1",
port: 0,
socket: {
data() {
// Bun.listen 必填 handler
},
end(socket) {
try {
socket.close();
} catch {
// best-effort 关闭
}
},
error() {
// Bun.listen 必填 handler
},
open(socket) {
socket.write("X".repeat(8192));
},
},
});
largeBannerServerPort = largeBannerServer.port;
});
afterAll(() => {
server.stop();
bannerServer.stop();
largeBannerServer.stop();
});
describe("TcpChecker execute", () => {
test("TCP 连接成功", async () => {
const result = await checker.execute(makeTarget({}), makeCtx());
expect(result.matched).toBe(true);
expect(result.failure).toBeNull();
expect(result.durationMs).toBeGreaterThanOrEqual(0);
expect(result.statusDetail).toMatch(/^connected in \d+ms$/);
});
test("TCP 连接失败", async () => {
const result = await checker.execute(makeTarget({ host: "127.0.0.1", port: 1 }), makeCtx());
expect(result.matched).toBe(false);
expect(result.failure!.kind).toBe("error");
expect(result.failure!.phase).toBe("connect");
expect(result.failure!.message).toBeTruthy();
});
test("期望端口不可达且连接失败", async () => {
const result = await checker.execute(
makeTarget({ host: "127.0.0.1", port: 1 }, { expect: { connected: false } }),
makeCtx(),
);
expect(result.matched).toBe(true);
expect(result.failure).toBeNull();
expect(result.statusDetail).toBeTruthy();
});
test("期望端口不可达但连接成功", async () => {
const result = await checker.execute(makeTarget({}, { expect: { connected: false } }), makeCtx());
expect(result.matched).toBe(false);
expect(result.failure!.kind).toBe("mismatch");
expect(result.failure!.phase).toBe("connected");
});
test("durationMs 超时返回失败", async () => {
const result = await checker.execute(makeTarget({}, { expect: { durationMs: { lt: 0 } } }), makeCtx());
expect(result.matched).toBe(false);
expect(result.failure!.phase).toBe("duration");
});
test("读取服务端 banner 成功", async () => {
const result = await checker.execute(
makeTarget({
host: "127.0.0.1",
port: bannerServerPort,
readBanner: true,
}),
makeCtx(),
);
expect(result.matched).toBe(true);
expect(result.statusDetail).toContain("banner:");
expect(result.statusDetail).toContain("220 smtp.example.com ESMTP");
});
test("banner operator 校验通过", async () => {
const result = await checker.execute(
makeTarget(
{ host: "127.0.0.1", port: bannerServerPort, readBanner: true },
{ expect: { banner: [{ contains: "ESMTP" }] } },
),
makeCtx(),
);
expect(result.matched).toBe(true);
});
test("banner operator 校验失败", async () => {
const result = await checker.execute(
makeTarget(
{ host: "127.0.0.1", port: bannerServerPort, readBanner: true },
{ expect: { banner: [{ contains: "POSTFIX" }] } },
),
makeCtx(),
);
expect(result.matched).toBe(false);
expect(result.failure!.kind).toBe("mismatch");
expect(result.failure!.phase).toBe("banner");
expect(result.failure!.path).toBe("banner[0]");
});
test("默认不读取 banner", async () => {
const result = await checker.execute(
makeTarget({ host: "127.0.0.1", port: bannerServerPort, readBanner: false }),
makeCtx(),
);
expect(result.matched).toBe(true);
expect(result.statusDetail).not.toContain("banner:");
});
test("banner 超时空字符串继续执行", async () => {
const result = await checker.execute(
makeTarget({
bannerReadTimeout: 100,
host: "127.0.0.1",
port: serverPort,
readBanner: true,
}),
makeCtx(),
);
expect(result.matched).toBe(true);
});
test("banner 读取超过最大字节数", async () => {
const result = await checker.execute(
makeTarget({
bannerReadTimeout: 2000,
host: "127.0.0.1",
maxBannerBytes: 1024,
port: largeBannerServerPort,
readBanner: true,
}),
makeCtx(),
);
expect(result.matched).toBe(false);
expect(result.failure!.kind).toBe("error");
expect(result.failure!.phase).toBe("banner");
expect(result.failure!.message).toContain("字节限制");
});
test("TCP 执行超时(预 abort", async () => {
const controller = new AbortController();
controller.abort();
const result = await checker.execute(makeTarget({ host: "127.0.0.1", port: serverPort }), {
signal: controller.signal,
});
expect(result.matched).toBe(false);
expect(result.failure!.phase).toBe("connect");
});
test("banner 读取过程中 abort", async () => {
const controller = new AbortController();
setTimeout(() => controller.abort(), 10);
const result = await checker.execute(
makeTarget({
bannerReadTimeout: 5000,
host: "127.0.0.1",
port: serverPort,
readBanner: true,
}),
{ signal: controller.signal },
);
expect(result.matched).toBe(false);
expect(result.failure!.kind).toBe("error");
expect(["connect", "banner"]).toContain(result.failure!.phase);
});
test("serialize 返回 host:port 和 config JSON", () => {
const target = makeTarget({ host: "10.0.0.1", port: 8080 });
const s = checker.serialize(target);
expect(s.target).toBe("10.0.0.1:8080");
const config = JSON.parse(s.config) as Record<string, unknown>;
expect(config["host"]).toBe("10.0.0.1");
expect(config["port"]).toBe(8080);
expect(config["readBanner"]).toBe(false);
});
});
describe("TcpChecker resolve", () => {
test("最简 tcp 配置解析默认值", () => {
const target = checker.resolve(
{ id: "t", tcp: { host: "127.0.0.1", port: 6379 }, type: "tcp" },
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
);
expect(target.tcp.host).toBe("127.0.0.1");
expect(target.tcp.port).toBe(6379);
expect(target.tcp.readBanner).toBe(false);
expect(target.tcp.bannerReadTimeout).toBe(2000);
expect(target.tcp.maxBannerBytes).toBe(4096);
expect(target.group).toBe("default");
expect(target.name).toBeNull();
expect(target.intervalMs).toBe(30000);
expect(target.timeoutMs).toBe(10000);
});
test("bannerReadTimeout 和 maxBannerBytes 支持 per-target 覆盖", () => {
const target = checker.resolve(
{
id: "t",
tcp: { bannerReadTimeout: 5000, host: "127.0.0.1", maxBannerBytes: "1KB", port: 80, readBanner: true },
type: "tcp",
},
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
);
expect(target.tcp.bannerReadTimeout).toBe(5000);
expect(target.tcp.maxBannerBytes).toBe(1024);
expect(target.tcp.readBanner).toBe(true);
});
test("defaults.tcp 合并到 target", () => {
const target = checker.resolve(
{ id: "t", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" },
{
configDir: "/tmp",
defaultIntervalMs: 30000,
defaults: { tcp: { bannerReadTimeout: 1000, maxBannerBytes: "8KB" } },
defaultTimeoutMs: 10000,
},
);
expect(target.tcp.bannerReadTimeout).toBe(1000);
expect(target.tcp.maxBannerBytes).toBe(8192);
});
test("per-target 覆盖 defaults.tcp", () => {
const target = checker.resolve(
{ id: "t", tcp: { bannerReadTimeout: 5000, host: "127.0.0.1", port: 80 }, type: "tcp" },
{
configDir: "/tmp",
defaultIntervalMs: 30000,
defaults: { tcp: { bannerReadTimeout: 1000 } },
defaultTimeoutMs: 10000,
},
);
expect(target.tcp.bannerReadTimeout).toBe(5000);
});
test("maxBannerBytes 整数默认值解析", () => {
const target = checker.resolve(
{ id: "t", tcp: { host: "127.0.0.1", maxBannerBytes: 2048, port: 80 }, type: "tcp" },
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
);
expect(target.tcp.maxBannerBytes).toBe(2048);
});
test("expect 配置解析", () => {
const target = checker.resolve(
{
expect: { banner: [{ contains: "ESMTP" }], connected: false, durationMs: { lte: 5000 } },
id: "t",
tcp: { host: "127.0.0.1", port: 80, readBanner: true },
type: "tcp",
},
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
);
expect(target.expect).toEqual({ banner: [{ contains: "ESMTP" }], connected: false, durationMs: { lte: 5000 } });
});
test("name 和 group 解析", () => {
const target = checker.resolve(
{ group: "infra", id: "t", name: "redis", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" },
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
);
expect(target.name).toBe("redis");
expect(target.group).toBe("infra");
});
});