1
0
Files
DiAL/tests/server/checker/runner/udp/execute.test.ts
lanyuanxiaoyao 375dd3492b feat: 结构化 observation 替代 statusDetail,API 层动态构造 detail
- CheckResult: statusDetail -> observation (持久化) + detail (API 动态派生)
- 存储: status_detail 列 -> observation TEXT (JSON)
- CheckerDefinition: 新增 buildDetail(observation) 方法
- 各 checker 返回结构化 observation,API 层通过 registry 调用 buildDetail
- HTTP: bodyPreview 在 status/header 失败时也提前采集
- UDP: observation 包含 durationMs,未响应归为 error failure
- CMD: 超时/输出超限时保留已收集 observation
- TCP: connectTimeMs 仅含连接建立耗时,不含 banner 等待
- 新增 buildDetail 单测和 mapCheckResult 覆盖测试
- 同步 openspec 主规范,归档 checker-observation 变更
2026-05-19 22:49:00 +08:00

369 lines
12 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 { 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.observation).toMatchObject({ responded: true });
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).toMatchObject({ kind: "error", phase: "response" });
expect(result.observation?.["durationMs"]).toBeGreaterThanOrEqual(0);
expect(result.observation?.["error"]).toBeTruthy();
expect(result.observation).toMatchObject({ responded: false });
} 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.observation?.["durationMs"]).toBeGreaterThanOrEqual(0);
expect(result.observation).toMatchObject({ error: null, responded: false });
} 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.observation).toMatchObject({ responded: true, responseSize: 5 });
} 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);
});
});