refactor: 将 checker normalize 职责下沉到各 runner 目录
- 新增 CheckerDefinition.normalize 必需方法,typecheck 兜底遗漏实现 - 新增 expect/normalize.ts 共享 helper(compactExpect、normalizeValue、 normalizeContent、normalizeKeyed) - 为 HTTP、Cmd、DB、TCP、UDP、ICMP、LLM、WS、DNS 各新增独立 normalize.ts - 简化 normalizer.ts:删除所有 checker type switch,改为 registry 委托 - 修复 DNS authoring 简写 bug:durationMs、valueCount、result 等字段 现可通过完整加载链路 - 新增 DNS 回归测试和 registry 级合同测试 - 更新 docs/development/checker.md:补充 normalize 规范、文件结构、 测试要求和 checklist
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import Ajv from "ajv";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { normalizeAuthoringConfig } from "../../../../src/server/checker/normalizer";
|
||||
import { createDefaultCheckerRegistry } from "../../../../src/server/checker/runner";
|
||||
import { createProbeConfigJsonSchema } from "../../../../src/server/checker/schema/export";
|
||||
import { formatConfigIssues, issue } from "../../../../src/server/checker/schema/issues";
|
||||
@@ -274,4 +275,115 @@ describe("config contract", () => {
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("所有 checker 的 authoring ValueMatcher 简写经 normalize 后通过 normalized contract 校验", () => {
|
||||
const authoringShorthandExamples: Record<string, object> = {
|
||||
cmd: {
|
||||
targets: [
|
||||
{
|
||||
cmd: { exec: "echo hello" },
|
||||
expect: { durationMs: 1000 },
|
||||
id: "cmd-test",
|
||||
type: "cmd",
|
||||
},
|
||||
],
|
||||
},
|
||||
db: {
|
||||
targets: [
|
||||
{
|
||||
db: { url: "sqlite://:memory:" },
|
||||
expect: { durationMs: 2000 },
|
||||
id: "db-test",
|
||||
type: "db",
|
||||
},
|
||||
],
|
||||
},
|
||||
dns: {
|
||||
targets: [
|
||||
{
|
||||
dns: { name: "example.com", resolver: "system" },
|
||||
expect: { durationMs: 500 },
|
||||
id: "dns-test",
|
||||
type: "dns",
|
||||
},
|
||||
],
|
||||
},
|
||||
http: {
|
||||
targets: [
|
||||
{
|
||||
expect: { durationMs: 5000 },
|
||||
http: { url: "https://example.com" },
|
||||
id: "http-test",
|
||||
type: "http",
|
||||
},
|
||||
],
|
||||
},
|
||||
icmp: {
|
||||
targets: [
|
||||
{
|
||||
expect: { packetLossPercent: 0 },
|
||||
icmp: { host: "example.com" },
|
||||
id: "icmp-test",
|
||||
type: "icmp",
|
||||
},
|
||||
],
|
||||
},
|
||||
llm: {
|
||||
targets: [
|
||||
{
|
||||
expect: { durationMs: 10000 },
|
||||
id: "llm-test",
|
||||
llm: {
|
||||
model: "gpt-4o-mini",
|
||||
prompt: "ping",
|
||||
provider: "openai",
|
||||
url: "https://example.com/v1/chat/completions",
|
||||
},
|
||||
type: "llm",
|
||||
},
|
||||
],
|
||||
},
|
||||
tcp: {
|
||||
targets: [
|
||||
{
|
||||
expect: { durationMs: 3000 },
|
||||
id: "tcp-test",
|
||||
tcp: { host: "example.com", port: 80 },
|
||||
type: "tcp",
|
||||
},
|
||||
],
|
||||
},
|
||||
udp: {
|
||||
targets: [
|
||||
{
|
||||
expect: { durationMs: 1000 },
|
||||
id: "udp-test",
|
||||
type: "udp",
|
||||
udp: { host: "example.com", port: 53 },
|
||||
},
|
||||
],
|
||||
},
|
||||
ws: {
|
||||
targets: [
|
||||
{
|
||||
expect: { durationMs: 5000 },
|
||||
id: "ws-test",
|
||||
type: "ws",
|
||||
ws: { url: "wss://example.com/ws" },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
for (const [type, config] of Object.entries(authoringShorthandExamples)) {
|
||||
const normalizeResult = normalizeAuthoringConfig(config, createDefaultCheckerRegistry());
|
||||
expect(normalizeResult.issues).toHaveLength(0);
|
||||
const contract = validateProbeConfigContract(normalizeResult.config, createDefaultCheckerRegistry());
|
||||
expect(contract.config).not.toBeNull();
|
||||
expect(
|
||||
contract.issues,
|
||||
`Checker "${type}" authoring shorthand should pass normalized contract, got issues: ${JSON.stringify(contract.issues.map((i) => `${i.path}: ${i.message}`))}`,
|
||||
).toHaveLength(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
87
tests/server/checker/runner/dns/normalize.test.ts
Normal file
87
tests/server/checker/runner/dns/normalize.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { normalizeAuthoringConfig } from "../../../../../src/server/checker/normalizer";
|
||||
import { checkerRegistry } from "../../../../../src/server/checker/runner";
|
||||
import { validateProbeConfigContract } from "../../../../../src/server/checker/schema/validate";
|
||||
|
||||
describe("DNS normalize", () => {
|
||||
test("ValueMatcher 简写被展开", () => {
|
||||
const result = normalizeAuthoringConfig(
|
||||
{
|
||||
targets: [
|
||||
{
|
||||
dns: { name: "example.com", resolver: "server", server: "8.8.8.8" },
|
||||
expect: { durationMs: 1000, valueCount: { gte: 1 } },
|
||||
id: "dns-test",
|
||||
type: "dns",
|
||||
},
|
||||
],
|
||||
},
|
||||
checkerRegistry,
|
||||
);
|
||||
expect(result.issues).toHaveLength(0);
|
||||
const target = (result.config as { targets: Array<{ expect: Record<string, unknown> }> }).targets[0]!;
|
||||
expect(target.expect["durationMs"]).toEqual({ equals: 1000 });
|
||||
expect(target.expect["valueCount"]).toEqual({ gte: 1 });
|
||||
});
|
||||
|
||||
test("ContentExpectations 简写被展开", () => {
|
||||
const result = normalizeAuthoringConfig(
|
||||
{
|
||||
targets: [
|
||||
{
|
||||
dns: { name: "example.com", resolver: "server", server: "8.8.8.8" },
|
||||
expect: { result: [{ contains: "NOERROR" }] },
|
||||
id: "dns-test",
|
||||
type: "dns",
|
||||
},
|
||||
],
|
||||
},
|
||||
checkerRegistry,
|
||||
);
|
||||
expect(result.issues).toHaveLength(0);
|
||||
const target = (result.config as { targets: Array<{ expect: Record<string, unknown> }> }).targets[0]!;
|
||||
const resultExpect = target.expect["result"] as Array<Record<string, unknown>>;
|
||||
expect(resultExpect[0]!["kind"]).toBe("value");
|
||||
expect((resultExpect[0]!["matcher"] as Record<string, unknown>)["contains"]).toBe("NOERROR");
|
||||
});
|
||||
|
||||
test("DNS authoring 简写经 normalize 后通过 normalized contract 校验", () => {
|
||||
const result = normalizeAuthoringConfig(
|
||||
{
|
||||
targets: [
|
||||
{
|
||||
dns: { name: "example.com", resolver: "server", server: "8.8.8.8" },
|
||||
expect: { durationMs: 1000, result: [{ contains: "NOERROR" }], valueCount: { gte: 1 } },
|
||||
id: "dns-test",
|
||||
type: "dns",
|
||||
},
|
||||
],
|
||||
},
|
||||
checkerRegistry,
|
||||
);
|
||||
expect(result.issues).toHaveLength(0);
|
||||
const contract = validateProbeConfigContract(result.config, checkerRegistry);
|
||||
expect(contract.config).not.toBeNull();
|
||||
expect(contract.issues).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("DNS system 模式 ValueMatcher 简写被展开", () => {
|
||||
const result = normalizeAuthoringConfig(
|
||||
{
|
||||
targets: [
|
||||
{
|
||||
dns: { name: "example.com", resolver: "system" },
|
||||
expect: { durationMs: 500, valueCount: { gte: 1 } },
|
||||
id: "dns-system-test",
|
||||
type: "dns",
|
||||
},
|
||||
],
|
||||
},
|
||||
checkerRegistry,
|
||||
);
|
||||
expect(result.issues).toHaveLength(0);
|
||||
const target = (result.config as { targets: Array<{ expect: Record<string, unknown> }> }).targets[0]!;
|
||||
expect(target.expect["durationMs"]).toEqual({ equals: 500 });
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { Type } from "@sinclair/typebox";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import type { Checker } from "../../../../src/server/checker/runner/types";
|
||||
import type { CheckResult, ResolvedTargetBase } from "../../../../src/server/checker/types";
|
||||
import type { CheckResult, RawTargetConfig, ResolvedTargetBase } from "../../../../src/server/checker/types";
|
||||
|
||||
import { createDefaultCheckerRegistry } from "../../../../src/server/checker/runner";
|
||||
import { CheckerRegistry } from "../../../../src/server/checker/runner/registry";
|
||||
@@ -12,6 +12,7 @@ function createChecker(type: string): Checker {
|
||||
buildDetail: () => null,
|
||||
configKey: type,
|
||||
execute: () => Promise.resolve<CheckResult>({} as unknown as CheckResult),
|
||||
normalize: (t: RawTargetConfig) => t,
|
||||
resolve: () => ({}) as unknown as ResolvedTargetBase,
|
||||
schemas: {
|
||||
authoring: {
|
||||
|
||||
Reference in New Issue
Block a user