- 引入共享 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
365 lines
12 KiB
TypeScript
365 lines
12 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
||
|
||
import type { ResolvedUdpTarget, UdpExpectConfig } from "../../../../../src/server/checker/runner/udp/types";
|
||
|
||
import { UdpChecker } from "../../../../../src/server/checker/runner/udp/execute";
|
||
|
||
async function createEchoServer(): Promise<{ close: () => void; port: number }> {
|
||
const socket = await Bun.udpSocket({
|
||
socket: {
|
||
data(sock, buf, port, addr) {
|
||
sock.send(buf, port, addr);
|
||
},
|
||
drain() {
|
||
// Bun UDP socket handler 必填项
|
||
},
|
||
error() {
|
||
// Bun UDP socket handler 必填项
|
||
},
|
||
},
|
||
});
|
||
return { close: () => socket.close(), port: socket.port };
|
||
}
|
||
|
||
function makeSignal(timeoutMs: number): { cleanup: () => void; signal: AbortSignal } {
|
||
const controller = new AbortController();
|
||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||
return { cleanup: () => clearTimeout(timer), signal: controller.signal };
|
||
}
|
||
|
||
function makeTarget(overrides: Partial<ResolvedUdpTarget["udp"]> = {}, expect?: UdpExpectConfig): ResolvedUdpTarget {
|
||
return {
|
||
description: null,
|
||
expect,
|
||
group: "default",
|
||
id: "test-udp",
|
||
intervalMs: 30000,
|
||
name: null,
|
||
timeoutMs: 10000,
|
||
type: "udp",
|
||
udp: {
|
||
encoding: "text",
|
||
host: "127.0.0.1",
|
||
maxResponseBytes: 4096,
|
||
payload: "PING",
|
||
port: 0,
|
||
responseEncoding: "text",
|
||
...overrides,
|
||
},
|
||
};
|
||
}
|
||
|
||
describe("UdpChecker execute", () => {
|
||
it("should resolve and respond successfully", async () => {
|
||
const server = await createEchoServer();
|
||
try {
|
||
const checker = new UdpChecker();
|
||
const target = makeTarget({ port: server.port });
|
||
const { cleanup, signal } = makeSignal(5000);
|
||
const result = await checker.execute(target, { signal });
|
||
cleanup();
|
||
expect(result.matched).toBe(true);
|
||
expect(result.failure).toBeNull();
|
||
expect(result.statusDetail).toContain("responded");
|
||
expect(result.durationMs).toBeGreaterThanOrEqual(0);
|
||
} finally {
|
||
server.close();
|
||
}
|
||
});
|
||
|
||
it("should work with localhost hostname", async () => {
|
||
const server = await createEchoServer();
|
||
try {
|
||
const checker = new UdpChecker();
|
||
const target = makeTarget({ host: "localhost", port: server.port });
|
||
const { cleanup, signal } = makeSignal(5000);
|
||
const result = await checker.execute(target, { signal });
|
||
cleanup();
|
||
expect(result.matched).toBe(true);
|
||
} finally {
|
||
server.close();
|
||
}
|
||
});
|
||
|
||
it("should fail when no response and default expect.responded=true", async () => {
|
||
const server = await Bun.udpSocket({
|
||
socket: {
|
||
data() {
|
||
// sink - 不回包以模拟无响应
|
||
},
|
||
drain() {
|
||
// Bun UDP socket handler 必填项
|
||
},
|
||
error() {
|
||
// Bun UDP socket handler 必填项
|
||
},
|
||
},
|
||
});
|
||
try {
|
||
const checker = new UdpChecker();
|
||
const target = makeTarget({ port: server.port });
|
||
const controller = new AbortController();
|
||
const timer = setTimeout(() => controller.abort(), 200);
|
||
const result = await checker.execute(target, { signal: controller.signal });
|
||
clearTimeout(timer);
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure).not.toBeNull();
|
||
} finally {
|
||
server.close();
|
||
}
|
||
});
|
||
|
||
it("should match when expect.responded=false and no response", async () => {
|
||
const server = await Bun.udpSocket({
|
||
socket: {
|
||
data() {
|
||
// sink - 不回包以模拟无响应
|
||
},
|
||
drain() {
|
||
// Bun UDP socket handler 必填项
|
||
},
|
||
error() {
|
||
// Bun UDP socket handler 必填项
|
||
},
|
||
},
|
||
});
|
||
try {
|
||
const checker = new UdpChecker();
|
||
const target = makeTarget({ port: server.port }, { responded: false });
|
||
const controller = new AbortController();
|
||
const timer = setTimeout(() => controller.abort(), 200);
|
||
const result = await checker.execute(target, { signal: controller.signal });
|
||
clearTimeout(timer);
|
||
expect(result.matched).toBe(true);
|
||
expect(result.statusDetail).toContain("no response");
|
||
} finally {
|
||
server.close();
|
||
}
|
||
});
|
||
|
||
it("should fail when expect.responded=false but response received", async () => {
|
||
const server = await createEchoServer();
|
||
try {
|
||
const checker = new UdpChecker();
|
||
const target = makeTarget({ port: server.port }, { responded: false });
|
||
const { cleanup, signal } = makeSignal(5000);
|
||
const result = await checker.execute(target, { signal });
|
||
cleanup();
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure?.phase).toBe("responded");
|
||
} finally {
|
||
server.close();
|
||
}
|
||
});
|
||
|
||
it("should validate response content with expect.response", async () => {
|
||
const server = await createEchoServer();
|
||
try {
|
||
const checker = new UdpChecker();
|
||
const target = makeTarget({ port: server.port }, { response: [{ contains: "PING" }] });
|
||
const { cleanup, signal } = makeSignal(5000);
|
||
const result = await checker.execute(target, { signal });
|
||
cleanup();
|
||
expect(result.matched).toBe(true);
|
||
} finally {
|
||
server.close();
|
||
}
|
||
});
|
||
|
||
it("should fail response content mismatch", async () => {
|
||
const server = await createEchoServer();
|
||
try {
|
||
const checker = new UdpChecker();
|
||
const target = makeTarget({ port: server.port }, { response: [{ contains: "PONG" }] });
|
||
const { cleanup, signal } = makeSignal(5000);
|
||
const result = await checker.execute(target, { signal });
|
||
cleanup();
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure?.phase).toBe("response");
|
||
} finally {
|
||
server.close();
|
||
}
|
||
});
|
||
|
||
it("should validate responseSize", async () => {
|
||
const server = await createEchoServer();
|
||
try {
|
||
const checker = new UdpChecker();
|
||
const target = makeTarget({ port: server.port }, { responseSize: { gte: 1 } });
|
||
const { cleanup, signal } = makeSignal(5000);
|
||
const result = await checker.execute(target, { signal });
|
||
cleanup();
|
||
expect(result.matched).toBe(true);
|
||
} finally {
|
||
server.close();
|
||
}
|
||
});
|
||
|
||
it("should only use the first datagram when server sends multiple", async () => {
|
||
const server = await Bun.udpSocket({
|
||
socket: {
|
||
data(sock, buf, port, addr) {
|
||
sock.send(Buffer.from("FIRST"), port, addr);
|
||
setTimeout(() => {
|
||
try {
|
||
sock.send(Buffer.from("SECOND"), port, addr);
|
||
} catch {
|
||
// checker 已关闭 connected socket,第二次发送可能失败
|
||
}
|
||
}, 30);
|
||
},
|
||
drain() {
|
||
// Bun UDP socket handler 必填项
|
||
},
|
||
error() {
|
||
// Bun UDP socket handler 必填项
|
||
},
|
||
},
|
||
});
|
||
try {
|
||
const checker = new UdpChecker();
|
||
const target = makeTarget({ payload: "PING", port: server.port }, { response: [{ contains: "FIRST" }] });
|
||
const { cleanup, signal } = makeSignal(5000);
|
||
const result = await checker.execute(target, { signal });
|
||
cleanup();
|
||
expect(result.matched).toBe(true);
|
||
expect(result.statusDetail).toContain("5 bytes");
|
||
} finally {
|
||
server.close();
|
||
}
|
||
});
|
||
|
||
it("should fail when response exceeds maxResponseBytes", async () => {
|
||
const server = await createEchoServer();
|
||
try {
|
||
const checker = new UdpChecker();
|
||
const longPayload = "X".repeat(100);
|
||
const target = makeTarget({
|
||
maxResponseBytes: 10,
|
||
payload: longPayload,
|
||
port: server.port,
|
||
});
|
||
const { cleanup, signal } = makeSignal(5000);
|
||
const result = await checker.execute(target, { signal });
|
||
cleanup();
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure?.phase).toBe("response");
|
||
expect(result.failure?.kind).toBe("error");
|
||
} finally {
|
||
server.close();
|
||
}
|
||
});
|
||
|
||
it("should fail when duration exceeds durationMs", async () => {
|
||
const server = await Bun.udpSocket({
|
||
socket: {
|
||
data() {
|
||
// sink - 不回包,通过 abort 触发 no-response 路径
|
||
},
|
||
drain() {
|
||
// Bun UDP socket handler 必填项
|
||
},
|
||
error() {
|
||
// Bun UDP socket handler 必填项
|
||
},
|
||
},
|
||
});
|
||
try {
|
||
const checker = new UdpChecker();
|
||
const target = makeTarget({ port: server.port }, { durationMs: { lte: 1 }, responded: false });
|
||
const controller = new AbortController();
|
||
const timer = setTimeout(() => controller.abort(), 200);
|
||
const result = await checker.execute(target, { signal: controller.signal });
|
||
clearTimeout(timer);
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure?.phase).toBe("duration");
|
||
} finally {
|
||
server.close();
|
||
}
|
||
});
|
||
|
||
it("should match sourceHost assertion", async () => {
|
||
const server = await createEchoServer();
|
||
try {
|
||
const checker = new UdpChecker();
|
||
const target = makeTarget({ port: server.port }, { sourceHost: { equals: "127.0.0.1" } });
|
||
const { cleanup, signal } = makeSignal(5000);
|
||
const result = await checker.execute(target, { signal });
|
||
cleanup();
|
||
expect(result.matched).toBe(true);
|
||
} finally {
|
||
server.close();
|
||
}
|
||
});
|
||
|
||
it("should match sourcePort assertion", async () => {
|
||
const server = await createEchoServer();
|
||
try {
|
||
const checker = new UdpChecker();
|
||
const target = makeTarget({ port: server.port }, { sourcePort: { equals: server.port } });
|
||
const { cleanup, signal } = makeSignal(5000);
|
||
const result = await checker.execute(target, { signal });
|
||
cleanup();
|
||
expect(result.matched).toBe(true);
|
||
} finally {
|
||
server.close();
|
||
}
|
||
});
|
||
});
|
||
|
||
describe("UdpChecker resolve", () => {
|
||
it("should fill defaults for minimal config", () => {
|
||
const checker = new UdpChecker();
|
||
const target = checker.resolve(
|
||
{ id: "test", type: "udp", udp: { host: "127.0.0.1", port: 9000 } },
|
||
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
|
||
);
|
||
expect(target.udp.payload).toBe("");
|
||
expect(target.udp.encoding).toBe("text");
|
||
expect(target.udp.responseEncoding).toBe("text");
|
||
expect(target.udp.maxResponseBytes).toBe(4096);
|
||
});
|
||
|
||
it("should use defaults.udp for missing fields", () => {
|
||
const checker = new UdpChecker();
|
||
const target = checker.resolve(
|
||
{ id: "test", type: "udp", udp: { host: "127.0.0.1", port: 9000 } },
|
||
{
|
||
configDir: "/tmp",
|
||
defaultIntervalMs: 30000,
|
||
defaults: { udp: { encoding: "hex", maxResponseBytes: "8KB", responseEncoding: "hex" } },
|
||
defaultTimeoutMs: 10000,
|
||
},
|
||
);
|
||
expect(target.udp.encoding).toBe("hex");
|
||
expect(target.udp.responseEncoding).toBe("hex");
|
||
expect(target.udp.maxResponseBytes).toBe(8192);
|
||
});
|
||
|
||
it("should override defaults with target-level config", () => {
|
||
const checker = new UdpChecker();
|
||
const target = checker.resolve(
|
||
{ id: "test", type: "udp", udp: { encoding: "base64", host: "127.0.0.1", port: 9000 } },
|
||
{
|
||
configDir: "/tmp",
|
||
defaultIntervalMs: 30000,
|
||
defaults: { udp: { encoding: "hex" } },
|
||
defaultTimeoutMs: 10000,
|
||
},
|
||
);
|
||
expect(target.udp.encoding).toBe("base64");
|
||
});
|
||
});
|
||
|
||
describe("UdpChecker serialize", () => {
|
||
it("should produce udp host:port target summary", () => {
|
||
const checker = new UdpChecker();
|
||
const target = makeTarget({ host: "10.0.0.1", port: 9000 });
|
||
const { config, target: display } = checker.serialize(target);
|
||
expect(display).toBe("udp 10.0.0.1:9000");
|
||
const parsed = JSON.parse(config) as Record<string, unknown>;
|
||
expect(parsed["host"]).toBe("10.0.0.1");
|
||
expect(parsed["port"]).toBe(9000);
|
||
});
|
||
});
|