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:
@@ -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 },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user