1
0

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
This commit is contained in:
2026-05-19 14:24:27 +08:00
parent 349896bd02
commit 7a635a0a9f
85 changed files with 4290 additions and 2028 deletions

View File

@@ -824,7 +824,8 @@ targets:
- json:
path: "$.status"
equals: "ok"
maxDurationMs: 3000
durationMs:
lte: 3000
`,
);
@@ -833,7 +834,7 @@ targets:
if (t.type === "http") {
expect(t.expect).toEqual({
body: [{ contains: "ok" }, { json: { equals: "ok", path: "$.status" } }],
maxDurationMs: 3000,
durationMs: { lte: 3000 },
status: [200, 201],
});
}
@@ -853,10 +854,11 @@ targets:
exitCode: [0, 2]
stdout:
- contains: "ok"
- match: "done"
- regex: "done"
stderr:
- empty: true
maxDurationMs: 5000
durationMs:
lte: 5000
`,
);
@@ -864,10 +866,10 @@ targets:
const t = config.targets[0]!;
if (t.type === "cmd") {
expect(t.expect).toEqual({
durationMs: { lte: 5000 },
exitCode: [0, 2],
maxDurationMs: 5000,
stderr: [{ empty: true }],
stdout: [{ contains: "ok" }, { match: "done" }],
stdout: [{ contains: "ok" }, { regex: "done" }],
});
}
});
@@ -1074,7 +1076,7 @@ targets:
await expect(loadConfig(configPath)).rejects.toThrow("5xx");
});
test("expect.maxDurationMs 负数抛出错误", async () => {
test("expect.durationMs 非 matcher 抛出错误", async () => {
const configPath = join(tempDir, "neg-duration.yaml");
await writeFile(
configPath,
@@ -1085,11 +1087,11 @@ targets:
http:
url: "http://example.com"
expect:
maxDurationMs: -100
durationMs: -100
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("maxDurationMs 必须为非负有限数字");
await expect(loadConfig(configPath)).rejects.toThrow("expect.durationMs 必须为 matcher 对象");
});
test("expect.body 非数组抛出错误", async () => {
@@ -1126,7 +1128,7 @@ targets:
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("缺少支持的规则类型");
await expect(loadConfig(configPath)).rejects.toThrow("foo 是未知字段");
});
test("body rule 使用 match 字段(非支持)抛出错误", async () => {
@@ -1145,10 +1147,10 @@ targets:
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("缺少支持的规则类型");
await expect(loadConfig(configPath)).rejects.toThrow("match 是未知字段");
});
test("body rule 多个支持字段抛出错误", async () => {
test("body rule 直接 matcher 混入 extractor 抛出错误", async () => {
const configPath = join(tempDir, "bad-body-rule-multi.yaml");
await writeFile(
configPath,
@@ -1161,11 +1163,12 @@ targets:
expect:
body:
- contains: "ok"
regex: "ok"
json:
path: "$.status"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("只能配置一种规则类型");
await expect(loadConfig(configPath)).rejects.toThrow("直接 matcher 不能与 extractor 混用");
});
test("body regex 非法正则抛出错误", async () => {
@@ -1228,7 +1231,7 @@ targets:
await expect(loadConfig(configPath)).rejects.toThrow("css.selector 必须为非空字符串");
});
test("operator match 非法正则抛出错误", async () => {
test("旧 match matcher 抛出错误", async () => {
const configPath = join(tempDir, "bad-op-match.yaml");
await writeFile(
configPath,
@@ -1245,7 +1248,7 @@ targets:
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("match 正则不合法");
await expect(loadConfig(configPath)).rejects.toThrow("match 是未知 matcher");
});
test("operator gte 非数字抛出错误", async () => {
@@ -1531,7 +1534,7 @@ targets:
stdout:
- {}
`,
"stdout[0] 必须包含至少一个合法 operator",
"stdout[0] 必须包含至少一个合法 matcher",
);
});
@@ -1552,7 +1555,7 @@ targets:
);
});
test("cmd stdout match 正则非法", async () => {
test("cmd stdout match 字段非法", async () => {
await expectConfigError(
"bad-cmd-stdout-regex.yaml",
`targets:
@@ -1565,7 +1568,7 @@ targets:
stdout:
- match: "[invalid"
`,
"stdout[0].match 正则不合法",
"stdout[0].match 是未知字段",
);
});
@@ -1878,14 +1881,14 @@ targets:
readBanner: true
expect:
banner:
contains: "ESMTP"
- 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" });
expect(t.expect?.banner).toEqual([{ contains: "ESMTP" }]);
});
test("tcp expect.banner 未开启 readBanner 抛出错误", async () => {
@@ -1954,7 +1957,7 @@ targets:
);
});
test("tcp expect connected 和 maxDurationMs", async () => {
test("tcp expect connected 和 durationMs", async () => {
const configPath = join(tempDir, "tcp-expect-connected.yaml");
await writeFile(
configPath,
@@ -1966,14 +1969,15 @@ targets:
port: 80
expect:
connected: false
maxDurationMs: 5000
durationMs:
lte: 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);
expect(t.expect?.durationMs).toEqual({ lte: 5000 });
});
test("解析最简 ping 配置", async () => {
@@ -2011,10 +2015,14 @@ targets:
packetSize: 1472
expect:
alive: true
maxPacketLoss: 10
maxAvgLatencyMs: 200
maxMaxLatencyMs: 500
maxDurationMs: 5000
packetLossPercent:
lte: 10
avgLatencyMs:
lte: 200
maxLatencyMs:
lte: 500
durationMs:
lte: 5000
`,
);
@@ -2023,10 +2031,10 @@ targets:
expect(t.ping).toEqual({ count: 5, host: "10.0.0.1", packetSize: 1472 });
expect(t.expect).toEqual({
alive: true,
maxAvgLatencyMs: 200,
maxDurationMs: 5000,
maxMaxLatencyMs: 500,
maxPacketLoss: 10,
avgLatencyMs: { lte: 200 },
durationMs: { lte: 5000 },
maxLatencyMs: { lte: 500 },
packetLossPercent: { lte: 10 },
});
});