1
0
Files
DiAL/tests/server/checker/runner/udp/execute.test.ts
lanyuanxiaoyao cf847ccd7a feat: 重构配置生命周期为 Authoring/Normalized/Resolved 三层
将变量替换和 expect 简写展开统一放入 Normalized 阶段,
运行时 AJV 使用 Normalized schema,导出 schema 面向 Authoring Config。

主要变更:
- 新增 normalizer.ts 实现 normalizeAuthoringConfig()
- 拆分 Authoring/Normalized 双 schema,checker 接口支持 authoring/normalized 片段
- config-loader 流程:normalize → Normalized AJV → semantic → resolve
- validator 兼容层自动分派 raw/normalized expect 形态
- 删除 rawExpect,store.expect 列写入 null
- Authoring schema 对 integer/boolean/enum 字段接受变量引用
- 修复 DB/HTTP validate 入口守卫和 LLM options integer 变量引用
- 优化 compact() 避免 undefined 覆盖隐患
- 移除 content.ts 恒为 true 的前置条件
- 同步 5 个主规范并归档 change
2026-05-22 14:00:47 +08:00

368 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 {
RawUdpExpectConfig,
ResolvedUdpExpectConfig,
ResolvedUdpTarget,
} from "../../../../../src/server/checker/runner/udp/types";
import { resolveContentExpectations } from "../../../../../src/server/checker/expect/content";
import { resolveValueExpectation } from "../../../../../src/server/checker/expect/value";
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"]> = {}, raw?: RawUdpExpectConfig): ResolvedUdpTarget {
const resolvedExpect: ResolvedUdpExpectConfig | undefined = raw
? {
durationMs: resolveValueExpectation(raw.durationMs),
responded: raw.responded ?? true,
response: resolveContentExpectations(raw.response),
responseSize: resolveValueExpectation(raw.responseSize),
sourceHost: resolveValueExpectation(raw.sourceHost),
sourcePort: resolveValueExpectation(raw.sourcePort),
}
: undefined;
return {
description: null,
expect: resolvedExpect,
group: "default",
id: "test-udp",
intervalMs: 30000,
name: null,
rawExpect: raw,
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, 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);
expect("rawExpect" in target).toBe(false);
expect(target.expect).toEqual({ responded: true });
});
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, 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);
});
});