1
0

refactor: 移除顶层 defaults 配置段,简化为 target 显式字段 > 代码内置默认值

- 移除 DefaultsConfig 类型、ProbeConfig.defaults 字段
- 移除 CheckerSchemas.defaults、ResolveContext.defaults、CheckerValidationInput.defaults
- 更新所有 checker schema/resolve/validate 删除 defaults 合并逻辑
- 更新 config-loader 不再读取传递 defaults
- 更新测试、README、DEVELOPMENT、probes.example.yaml
- 重新生成 probe-config.schema.json(不含 defaults)
- 同步 delta specs 到主规范
- 归档 openspec change
This commit is contained in:
2026-05-21 16:53:12 +08:00
parent e448cb4654
commit 79358ba50d
52 changed files with 196 additions and 940 deletions

View File

@@ -190,16 +190,6 @@ describe("loadConfig", () => {
probes:
execution:
maxConcurrentChecks: 5
defaults:
interval: "15s"
timeout: "5s"
http:
headers:
Authorization: "Bearer token"
maxBodyBytes: "50MB"
cmd:
cwd: "/tmp"
maxOutputBytes: "10MB"
targets:
- name: "http-target"
id: "http-target"
@@ -236,19 +226,18 @@ targets:
expect(http.type).toBe("http");
expect(http.http.url).toBe("http://example.com");
expect(http.http.method).toBe("POST");
expect(http.http.headers).toEqual({ Authorization: "Bearer token" });
expect(http.http.ignoreSSL).toBe(true);
expect(http.http.maxBodyBytes).toBe(52428800);
expect(http.http.maxBodyBytes).toBe(104857600);
expect(http.http.maxRedirects).toBe(5);
expect(http.expect?.status).toEqual(["2xx", 301]);
expect(http.intervalMs).toBe(60000);
expect(http.timeoutMs).toBe(5000);
expect(http.timeoutMs).toBe(10000);
const cmd = config.targets[1]! as ResolvedCommandTarget;
expect(cmd.type).toBe("cmd");
expect(cmd.cmd.exec).toBe("ls");
expect(cmd.cmd.args).toEqual(["/tmp"]);
expect(cmd.cmd.maxOutputBytes).toBe(10485760);
expect(cmd.cmd.maxOutputBytes).toBe(104857600);
});
test("name 缺省时保留为 null", async () => {
@@ -526,16 +515,11 @@ targets:
expect(config.dataDir).toBe(dataDir);
});
test("per-target 覆盖 defaults", async () => {
test("per-target interval 和 timeout 覆盖全局默认", async () => {
const configPath = join(tempDir, "override.yaml");
await writeFile(
configPath,
`defaults:
interval: "30s"
timeout: "10s"
http:
maxBodyBytes: "10MB"
targets:
`targets:
- name: "override-all"
id: "override-all"
type: http
@@ -799,15 +783,13 @@ targets:
const configPath = join(tempDir, "bad-size.yaml");
await writeFile(
configPath,
`defaults:
http:
maxBodyBytes: "100TB"
targets:
`targets:
- name: "t"
id: "t"
type: http
http:
url: "http://a.com"
maxBodyBytes: "100TB"
`,
);
await expectConfigLoadError(configPath, "无效的 size 格式");
@@ -1388,9 +1370,9 @@ targets:
);
});
test("defaults.http.method 触发未知字段错误", async () => {
test("defaults 顶层字段触发未知字段错误", async () => {
await expectConfigError(
"unknown-default-method.yaml",
"unknown-defaults.yaml",
`defaults:
http:
method: POST
@@ -1401,7 +1383,7 @@ targets:
http:
url: "http://example.com"
`,
"defaults.http.method 是未知字段",
"defaults 是未知字段",
);
});
@@ -1428,11 +1410,7 @@ targets:
const configPath = join(tempDir, "dynamic-maps.yaml");
await writeFile(
configPath,
`defaults:
http:
headers:
X-Default-Header: "default"
targets:
`targets:
- name: "http-test"
id: "http-test"
type: http
@@ -1458,7 +1436,6 @@ targets:
const cmdTarget = config.targets[1] as ResolvedCommandTarget;
expect(http.type).toBe("http");
expect(cmdTarget.type).toBe("cmd");
expect(http.http.headers["X-Default-Header"]).toBe("default");
expect(http.http.headers["X-Custom-Header"]).toBe("custom");
expect(cmdTarget.cmd.env["CUSTOM_ENV_NAME"]).toBe("custom");
});
@@ -1924,15 +1901,11 @@ targets:
);
});
test("tcp defaults 覆盖 banner 参数", async () => {
test("tcp per-target banner 参数覆盖", async () => {
const configPath = join(tempDir, "tcp-defaults.yaml");
await writeFile(
configPath,
`defaults:
tcp:
bannerReadTimeout: 1000
maxBannerBytes: "8KB"
targets:
`targets:
- id: "t1"
type: tcp
tcp:
@@ -1949,12 +1922,12 @@ targets:
const config = await loadConfig(configPath);
const t1 = config.targets[0]! as ResolvedTcpTarget;
expect(t1.tcp.bannerReadTimeout).toBe(1000);
expect(t1.tcp.maxBannerBytes).toBe(8192);
expect(t1.tcp.bannerReadTimeout).toBe(2000);
expect(t1.tcp.maxBannerBytes).toBe(4096);
const t2 = config.targets[1]! as ResolvedTcpTarget;
expect(t2.tcp.bannerReadTimeout).toBe(3000);
expect(t2.tcp.maxBannerBytes).toBe(8192);
expect(t2.tcp.maxBannerBytes).toBe(4096);
});
test("tcp expect 未知字段抛出错误", async () => {

View File

@@ -18,7 +18,7 @@ function makeCtx(timeoutMs = 5000): CheckerContext {
}
function makeResolveContext(): ResolveContext {
return { configDir: process.cwd(), defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 };
return { configDir: process.cwd(), defaultIntervalMs: 30000, defaultTimeoutMs: 10000 };
}
function makeTarget(

View File

@@ -4,13 +4,12 @@ import { validateDbConfig } from "../../../../../src/server/checker/runner/db/va
describe("validateDbConfig", () => {
test("空配置无问题", () => {
const result = validateDbConfig({ defaults: {}, targets: [] });
const result = validateDbConfig({ targets: [] });
expect(result).toHaveLength(0);
});
test("缺少 db.url 返回错误", () => {
const result = validateDbConfig({
defaults: {},
targets: [{ id: "test", name: "test", type: "db" }],
});
expect(result.length).toBeGreaterThan(0);
@@ -21,7 +20,6 @@ describe("validateDbConfig", () => {
test("db.url 为空字符串返回错误", () => {
const result = validateDbConfig({
defaults: {},
targets: [{ db: { url: "" }, id: "test", name: "test", type: "db" }],
});
const urlError = result.find((e) => e.path.includes("db.url"));
@@ -31,7 +29,6 @@ describe("validateDbConfig", () => {
test("db.query 为空字符串返回错误", () => {
const result = validateDbConfig({
defaults: {},
targets: [{ db: { query: "", url: "sqlite://:memory:" }, id: "test", name: "test", type: "db" }],
});
const queryError = result.find((e) => e.path.includes("db.query"));
@@ -41,7 +38,6 @@ describe("validateDbConfig", () => {
test("db 分组未知字段返回错误", () => {
const result = validateDbConfig({
defaults: {},
targets: [{ db: { timeout: 5, url: "sqlite://:memory:" }, id: "test", name: "test", type: "db" }],
});
const unknownError = result.find((e) => e.path.includes("db.timeout"));
@@ -51,7 +47,6 @@ describe("validateDbConfig", () => {
test("expect.durationMs 数组简写返回错误", () => {
const result = validateDbConfig({
defaults: {},
targets: [
{
db: { url: "sqlite://:memory:" },
@@ -69,7 +64,6 @@ describe("validateDbConfig", () => {
test("expect.rowCount 非法 operator 返回错误", () => {
const result = validateDbConfig({
defaults: {},
targets: [
{ db: { url: "sqlite://:memory:" }, expect: { rowCount: { foo: 1 } }, id: "test", name: "test", type: "db" },
],
@@ -81,7 +75,6 @@ describe("validateDbConfig", () => {
test("expect.rows 不是数组返回错误", () => {
const result = validateDbConfig({
defaults: {},
targets: [
{ db: { url: "sqlite://:memory:" }, expect: { rows: "not-array" }, id: "test", name: "test", type: "db" },
],
@@ -93,7 +86,6 @@ describe("validateDbConfig", () => {
test("expect.rows 元素不是对象返回错误", () => {
const result = validateDbConfig({
defaults: {},
targets: [
{ db: { url: "sqlite://:memory:" }, expect: { rows: ["not-object"] }, id: "test", name: "test", type: "db" },
],
@@ -105,7 +97,6 @@ describe("validateDbConfig", () => {
test("expect.rows 中 regex 正则非法返回错误", () => {
const result = validateDbConfig({
defaults: {},
targets: [
{
db: { url: "sqlite://:memory:" },
@@ -123,7 +114,6 @@ describe("validateDbConfig", () => {
test("expect 未知字段返回错误", () => {
const result = validateDbConfig({
defaults: {},
targets: [{ db: { url: "sqlite://:memory:" }, expect: { status: [200] }, id: "test", name: "test", type: "db" }],
});
const unknownError = result.find((e) => e.path.includes("expect.status"));
@@ -133,7 +123,6 @@ describe("validateDbConfig", () => {
test("有效配置无错误", () => {
const result = validateDbConfig({
defaults: {},
targets: [
{
db: { query: "SELECT 1", url: "sqlite://:memory:" },
@@ -149,7 +138,6 @@ describe("validateDbConfig", () => {
test("忽略非 db 类型 target", () => {
const result = validateDbConfig({
defaults: {},
targets: [{ id: "test", name: "test", type: "http" }],
});
expect(result).toHaveLength(0);
@@ -157,7 +145,6 @@ describe("validateDbConfig", () => {
test("多个 db target 分别校验", () => {
const result = validateDbConfig({
defaults: {},
targets: [
{ db: { url: "sqlite://:memory:" }, id: "db1", name: "db1", type: "db" },
{ db: { url: "" }, id: "db2", name: "db2", type: "db" },

View File

@@ -19,7 +19,7 @@ const SLOW_BODY_DELAY_MS = 1000;
const FAST_RESPONSE_LIMIT_MS = 500;
function validateHttpTarget(target: unknown): string {
return formatConfigIssues(checker.validate({ defaults: {}, targets: [target as never] }));
return formatConfigIssues(checker.validate({ targets: [target as never] }));
}
const SELF_SIGNED_CERT = `-----BEGIN CERTIFICATE-----
@@ -946,7 +946,6 @@ describe("HttpChecker.resolve", () => {
return {
configDir: ".",
defaultIntervalMs: 30000,
defaults: {},
defaultTimeoutMs: 10000,
};
}

View File

@@ -124,7 +124,7 @@ describe("IcmpChecker resolve", () => {
test("解析默认值", () => {
const target = checker.resolve(
{ icmp: { host: "10.0.0.1" }, id: "ping", type: "icmp" },
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
{ configDir: "/tmp", defaultIntervalMs: 30000, defaultTimeoutMs: 10000 },
);
expect(target.icmp).toEqual({ count: 3, host: "10.0.0.1", packetSize: 56 });
expect(target.group).toBe("default");

View File

@@ -5,7 +5,7 @@ import type { RawTargetConfig } from "../../../../../src/server/checker/types";
import { validatePingConfig } from "../../../../../src/server/checker/runner/icmp/validate";
function validate(target: RawTargetConfig) {
return validatePingConfig({ defaults: {}, targets: [target] });
return validatePingConfig({ targets: [target] });
}
describe("validatePingConfig", () => {

View File

@@ -17,14 +17,12 @@ describe("LLM registry integration", () => {
test("llm checker schemas 有效", () => {
const checker = checkerRegistry.get("llm");
expect(checker.schemas.config).toBeDefined();
expect(checker.schemas.defaults).toBeDefined();
expect(checker.schemas.expect).toBeDefined();
});
test("llm checker validate 方法可用", () => {
const checker = checkerRegistry.get("llm");
const issues = checker.validate({
defaults: {},
targets: [],
});
expect(issues).toHaveLength(0);

View File

@@ -42,7 +42,6 @@ function makeResolveContext(overrides?: Partial<ResolveContext>): ResolveContext
return {
configDir: "/tmp",
defaultIntervalMs: 30000,
defaults: {},
defaultTimeoutMs: 10000,
...overrides,
};
@@ -61,16 +60,15 @@ describe("LlmChecker schema", () => {
expect(checker?.configKey).toBe("llm");
});
test("schemas 包含 config、defaults、expect", () => {
test("schemas 包含 config、expect", () => {
expect(checker).toBeDefined();
expect(Object.keys(checker!.schemas).sort()).toEqual(["config", "defaults", "expect"].sort());
expect(Object.keys(checker!.schemas).sort()).toEqual(["config", "expect"].sort());
});
});
describe("LlmChecker validate", () => {
test("合法 LLM target 无校验问题", () => {
const issues = validateLlmConfig({
defaults: {},
targets: [makeRawTarget()],
});
expect(issues).toHaveLength(0);
@@ -78,7 +76,6 @@ describe("LlmChecker validate", () => {
test("provider 非法报错", () => {
const issues = validateLlmConfig({
defaults: {},
targets: [
makeRawTarget({
llm: { model: "m", prompt: "p", provider: "gemini", url: "https://x" },
@@ -91,7 +88,6 @@ describe("LlmChecker validate", () => {
test("url 非法报错", () => {
const issues = validateLlmConfig({
defaults: {},
targets: [
makeRawTarget({
llm: { model: "m", prompt: "p", provider: "openai", url: "ftp://bad" },
@@ -104,7 +100,6 @@ describe("LlmChecker validate", () => {
test("model 为空报错", () => {
const issues = validateLlmConfig({
defaults: {},
targets: [
makeRawTarget({
llm: { model: "", prompt: "p", provider: "openai", url: "https://x" },
@@ -117,7 +112,6 @@ describe("LlmChecker validate", () => {
test("prompt 为空报错", () => {
const issues = validateLlmConfig({
defaults: {},
targets: [
makeRawTarget({
llm: { model: "m", prompt: "", provider: "openai", url: "https://x" },
@@ -130,7 +124,6 @@ describe("LlmChecker validate", () => {
test("mode 非法报错", () => {
const issues = validateLlmConfig({
defaults: {},
targets: [
makeRawTarget({
llm: { mode: "batch", model: "m", prompt: "p", provider: "openai", url: "https://x" },
@@ -143,7 +136,6 @@ describe("LlmChecker validate", () => {
test("openai provider 不允许 authToken", () => {
const issues = validateLlmConfig({
defaults: {},
targets: [
makeRawTarget({
llm: { authToken: "tok", model: "m", prompt: "p", provider: "openai", url: "https://x" },
@@ -155,7 +147,6 @@ describe("LlmChecker validate", () => {
test("anthropic 同时配置 key 和 authToken 报错", () => {
const issues = validateLlmConfig({
defaults: {},
targets: [
makeRawTarget({
llm: { authToken: "tok", key: "k", model: "m", prompt: "p", provider: "anthropic", url: "https://x" },
@@ -167,7 +158,6 @@ describe("LlmChecker validate", () => {
test("ignoreSSL 非布尔值报错", () => {
const issues = validateLlmConfig({
defaults: {},
targets: [
makeRawTarget({
llm: { ignoreSSL: "yes", model: "m", prompt: "p", provider: "openai", url: "https://x" },
@@ -179,7 +169,6 @@ describe("LlmChecker validate", () => {
test("options.maxOutputTokens 非正整数报错", () => {
const issues = validateLlmConfig({
defaults: {},
targets: [
makeRawTarget({
llm: { model: "m", options: { maxOutputTokens: -1 }, prompt: "p", provider: "openai", url: "https://x" },
@@ -191,7 +180,6 @@ describe("LlmChecker validate", () => {
test("options.stopSequences 非字符串数组报错", () => {
const issues = validateLlmConfig({
defaults: {},
targets: [
makeRawTarget({
llm: { model: "m", options: { stopSequences: [123] }, prompt: "p", provider: "openai", url: "https://x" },
@@ -203,7 +191,6 @@ describe("LlmChecker validate", () => {
test("expect.output 缺少规则类型报错", () => {
const issues = validateLlmConfig({
defaults: {},
targets: [makeRawTarget({ expect: { output: [{}] } })],
});
expect(issues.some((i) => i.code === "empty-matcher")).toBe(true);
@@ -211,7 +198,6 @@ describe("LlmChecker validate", () => {
test("expect.output 直接 matcher 混入 extractor 报错", () => {
const issues = validateLlmConfig({
defaults: {},
targets: [makeRawTarget({ expect: { output: [{ contains: "y", json: { path: "$.status" } }] } })],
});
expect(issues.some((i) => i.code === "invalid-content-expectation")).toBe(true);
@@ -219,7 +205,6 @@ describe("LlmChecker validate", () => {
test("expect.output regex ReDoS 报错", () => {
const issues = validateLlmConfig({
defaults: {},
targets: [makeRawTarget({ expect: { output: [{ regex: "(a+)+" }] } })],
});
expect(issues.some((i) => i.code === "unsafe-regex")).toBe(true);
@@ -227,7 +212,6 @@ describe("LlmChecker validate", () => {
test("expect.stream 在 mode:http 下报错", () => {
const issues = validateLlmConfig({
defaults: {},
targets: [
makeRawTarget({
expect: { stream: { completed: true } },
@@ -240,7 +224,6 @@ describe("LlmChecker validate", () => {
test("expect.stream 在 mode:stream 下合法", () => {
const issues = validateLlmConfig({
defaults: {},
targets: [
makeRawTarget({
expect: { stream: { completed: true } },
@@ -250,24 +233,6 @@ describe("LlmChecker validate", () => {
});
expect(issues).toHaveLength(0);
});
test("defaults.llm 合法配置", () => {
const issues = validateLlmConfig({
defaults: {
llm: { headers: { "X-Custom": "val" }, ignoreSSL: false, mode: "http", options: { maxOutputTokens: 32 } },
},
targets: [makeRawTarget()],
});
expect(issues).toHaveLength(0);
});
test("defaults.llm mode 非法报错", () => {
const issues = validateLlmConfig({
defaults: { llm: { mode: "batch" } },
targets: [makeRawTarget()],
});
expect(issues.some((i) => i.path.includes("defaults.llm.mode"))).toBe(true);
});
});
describe("LlmChecker resolve", () => {
@@ -324,61 +289,6 @@ describe("LlmChecker resolve", () => {
expect(resolved.expect?.stream).toEqual({ completed: true, firstTokenMs: { equals: 100 } });
});
test("defaults.llm 与 target.llm 浅合并", () => {
const raw = makeRawTarget({
llm: {
headers: { Authorization: "Bearer test" },
model: "gpt-4o-mini",
prompt: "Say OK",
provider: "openai",
url: "https://api.openai.com/v1",
},
});
const ctx = makeResolveContext({
defaults: {
llm: {
headers: { "X-Custom": "default" },
ignoreSSL: true,
mode: "stream",
options: { maxOutputTokens: 64, temperature: 0.5 },
},
},
});
const resolved = asLlm(checker.resolve(raw, ctx));
expect(resolved.llm.mode).toBe("stream");
expect(resolved.llm.ignoreSSL).toBe(true);
expect(resolved.llm.headers).toEqual({ Authorization: "Bearer test", "X-Custom": "default" });
expect(resolved.llm.options.maxOutputTokens).toBe(64);
expect(resolved.llm.options.temperature).toBe(0.5);
});
test("target 字段覆盖 defaults", () => {
const raw = makeRawTarget({
llm: {
ignoreSSL: false,
mode: "http",
model: "gpt-4o-mini",
options: { maxOutputTokens: 8 },
prompt: "Say OK",
provider: "openai",
url: "https://api.openai.com/v1",
},
});
const ctx = makeResolveContext({
defaults: {
llm: {
ignoreSSL: true,
mode: "stream",
options: { maxOutputTokens: 64 },
},
},
});
const resolved = asLlm(checker.resolve(raw, ctx));
expect(resolved.llm.mode).toBe("http");
expect(resolved.llm.ignoreSSL).toBe(false);
expect(resolved.llm.options.maxOutputTokens).toBe(8);
});
test("serialize 返回正确格式", () => {
const resolved = asLlm(checker.resolve(makeRawTarget(), makeResolveContext()));
const serialized = checker.serialize(resolved);
@@ -398,25 +308,4 @@ describe("LlmChecker resolve", () => {
const config = parseSerializedConfig(serialized.config);
expect(config.key).toBe("***");
});
test("providerOptions 浅合并", () => {
const raw = makeRawTarget({
llm: {
model: "m",
prompt: "p",
provider: "openai",
providerOptions: { openai: { store: true } },
url: "https://x",
},
});
const ctx = makeResolveContext({
defaults: {
llm: {
providerOptions: { openai: { user: "default-user" } },
},
},
});
const resolved = asLlm(checker.resolve(raw, ctx));
expect(resolved.llm.providerOptions).toEqual({ openai: { store: true } });
});
});

View File

@@ -15,7 +15,6 @@ function createChecker(type: string): Checker {
resolve: () => ({}) as unknown as ResolvedTargetBase,
schemas: {
config: Type.Object({}, { additionalProperties: false }),
defaults: Type.Object({}, { additionalProperties: false }),
expect: Type.Object({}, { additionalProperties: false }),
},
serialize: () => ({ config: "", target: "" }),
@@ -69,11 +68,7 @@ describe("CheckerRegistry", () => {
expect(first.supportedTypes).toEqual(["http", "cmd", "db", "tcp", "icmp", "udp", "llm", "custom"]);
expect(second.supportedTypes).toEqual(["http", "cmd", "db", "tcp", "icmp", "udp", "llm"]);
expect(
first.definitions.every(
(checker) => checker.schemas.config && checker.schemas.defaults && checker.schemas.expect,
),
).toBe(true);
expect(first.definitions.every((checker) => checker.schemas.config && checker.schemas.expect)).toBe(true);
});
test("默认 registry 注册 icmp type", () => {

View File

@@ -7,7 +7,7 @@ import { validateHttpConfig } from "../../../../../src/server/checker/runner/htt
import { validateLlmConfig } from "../../../../../src/server/checker/runner/llm/validate";
function input(target: Record<string, unknown>): CheckerValidationInput {
return { defaults: {}, targets: [target as CheckerValidationInput["targets"][number]] };
return { targets: [target as CheckerValidationInput["targets"][number]] };
}
describe("HTTP/LLM headers reject case-insensitive duplicate keys", () => {

View File

@@ -11,7 +11,7 @@ import { validateTcpConfig } from "../../../../../src/server/checker/runner/tcp/
import { validateUdpConfig } from "../../../../../src/server/checker/runner/udp/validate";
function input(target: Record<string, unknown>): CheckerValidationInput {
return { defaults: {}, targets: [target as CheckerValidationInput["targets"][number]] };
return { targets: [target as CheckerValidationInput["targets"][number]] };
}
describe("ValueMatcher primitive shorthand in checker validators", () => {

View File

@@ -303,7 +303,7 @@ describe("TcpChecker resolve", () => {
test("最简 tcp 配置解析默认值", () => {
const target = checker.resolve(
{ id: "t", tcp: { host: "127.0.0.1", port: 6379 }, type: "tcp" },
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
{ configDir: "/tmp", defaultIntervalMs: 30000, defaultTimeoutMs: 10000 },
);
expect(target.tcp.host).toBe("127.0.0.1");
expect(target.tcp.port).toBe(6379);
@@ -325,44 +325,17 @@ describe("TcpChecker resolve", () => {
tcp: { bannerReadTimeout: 5000, host: "127.0.0.1", maxBannerBytes: "1KB", port: 80, readBanner: true },
type: "tcp",
},
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
{ configDir: "/tmp", defaultIntervalMs: 30000, defaultTimeoutMs: 10000 },
);
expect(target.tcp.bannerReadTimeout).toBe(5000);
expect(target.tcp.maxBannerBytes).toBe(1024);
expect(target.tcp.readBanner).toBe(true);
});
test("defaults.tcp 合并到 target", () => {
const target = checker.resolve(
{ id: "t", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" },
{
configDir: "/tmp",
defaultIntervalMs: 30000,
defaults: { tcp: { bannerReadTimeout: 1000, maxBannerBytes: "8KB" } },
defaultTimeoutMs: 10000,
},
);
expect(target.tcp.bannerReadTimeout).toBe(1000);
expect(target.tcp.maxBannerBytes).toBe(8192);
});
test("per-target 覆盖 defaults.tcp", () => {
const target = checker.resolve(
{ id: "t", tcp: { bannerReadTimeout: 5000, host: "127.0.0.1", port: 80 }, type: "tcp" },
{
configDir: "/tmp",
defaultIntervalMs: 30000,
defaults: { tcp: { bannerReadTimeout: 1000 } },
defaultTimeoutMs: 10000,
},
);
expect(target.tcp.bannerReadTimeout).toBe(5000);
});
test("maxBannerBytes 整数默认值解析", () => {
const target = checker.resolve(
{ id: "t", tcp: { host: "127.0.0.1", maxBannerBytes: 2048, port: 80 }, type: "tcp" },
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
{ configDir: "/tmp", defaultIntervalMs: 30000, defaultTimeoutMs: 10000 },
);
expect(target.tcp.maxBannerBytes).toBe(2048);
});
@@ -375,7 +348,7 @@ describe("TcpChecker resolve", () => {
tcp: { host: "127.0.0.1", port: 80, readBanner: true },
type: "tcp",
},
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
{ configDir: "/tmp", defaultIntervalMs: 30000, defaultTimeoutMs: 10000 },
);
expect(target.expect).toEqual({
banner: [{ kind: "value", matcher: { contains: "ESMTP" } }],
@@ -388,7 +361,7 @@ describe("TcpChecker resolve", () => {
test("name 和 group 解析", () => {
const target = checker.resolve(
{ group: "infra", id: "t", name: "redis", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" },
{ configDir: "/tmp", defaultIntervalMs: 30000, defaults: {}, defaultTimeoutMs: 10000 },
{ configDir: "/tmp", defaultIntervalMs: 30000, defaultTimeoutMs: 10000 },
);
expect(target.name).toBe("redis");
expect(target.group).toBe("infra");

View File

@@ -4,9 +4,8 @@ import type { CheckerValidationInput } from "../../../../../src/server/checker/r
import { validateTcpConfig } from "../../../../../src/server/checker/runner/tcp/validate";
function makeInput(targets: unknown[], defaults?: Record<string, unknown>): CheckerValidationInput {
function makeInput(targets: unknown[]): CheckerValidationInput {
return {
defaults: defaults ?? {},
targets: targets as CheckerValidationInput["targets"],
};
}
@@ -152,40 +151,4 @@ describe("validateTcpConfig", () => {
const issues = validateTcpConfig(makeInput([{ http: { url: "http://example.com" }, id: "t1", type: "http" }]));
expect(issues).toHaveLength(0);
});
test("defaults.tcp 合法字段无错误", () => {
const issues = validateTcpConfig(
makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" }], {
tcp: { bannerReadTimeout: 1000, maxBannerBytes: "8KB" },
}),
);
expect(issues).toHaveLength(0);
});
test("defaults.tcp 未知字段", () => {
const issues = validateTcpConfig(
makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" }], {
tcp: { bannerReadTimeout: 1000, host: "127.0.0.1" },
}),
);
expect(issues.some((i) => i.message.includes("未知字段"))).toBe(true);
});
test("defaults.tcp bannerReadTimeout 非法", () => {
const issues = validateTcpConfig(
makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" }], {
tcp: { bannerReadTimeout: "slow" },
}),
);
expect(issues.some((i) => i.path.includes("bannerReadTimeout"))).toBe(true);
});
test("defaults.tcp maxBannerBytes 非法", () => {
const issues = validateTcpConfig(
makeInput([{ id: "t1", tcp: { host: "127.0.0.1", port: 80 }, type: "tcp" }], {
tcp: { maxBannerBytes: true },
}),
);
expect(issues.some((i) => i.path.includes("maxBannerBytes"))).toBe(true);
});
});

View File

@@ -334,7 +334,7 @@ describe("UdpChecker resolve", () => {
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 },
{ configDir: "/tmp", defaultIntervalMs: 30000, defaultTimeoutMs: 10000 },
);
expect(target.udp.payload).toBe("");
expect(target.udp.encoding).toBe("text");
@@ -344,32 +344,11 @@ describe("UdpChecker resolve", () => {
expect(target.expect).toEqual({ responded: true });
});
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,
},
{ configDir: "/tmp", defaultIntervalMs: 30000, defaultTimeoutMs: 10000 },
);
expect(target.udp.encoding).toBe("base64");
});

View File

@@ -5,11 +5,7 @@ import type { CheckerValidationInput } from "../../../../../src/server/checker/r
import { validateUdpConfig } from "../../../../../src/server/checker/runner/udp/validate";
describe("validateUdpConfig", () => {
const makeInput = (overrides: {
defaults?: Record<string, unknown>;
targets?: Array<Record<string, unknown>>;
}): CheckerValidationInput => ({
defaults: overrides.defaults ?? {},
const makeInput = (overrides: { targets?: Array<Record<string, unknown>> }): CheckerValidationInput => ({
targets: (overrides.targets ?? []) as CheckerValidationInput["targets"],
});
@@ -67,29 +63,6 @@ describe("validateUdpConfig", () => {
expect(issues[0]!.path).toContain("udp");
});
it("accepts valid defaults.udp with encoding, responseEncoding, maxResponseBytes", () => {
const issues = validateUdpConfig(
makeInput({
defaults: {
udp: { encoding: "hex", maxResponseBytes: 1024, responseEncoding: "text" },
},
targets: [{ id: "test", type: "udp", udp: { host: "127.0.0.1", port: 53 } }],
}),
);
expect(issues).toHaveLength(0);
});
it("reports unknown-field in defaults.udp", () => {
const issues = validateUdpConfig(
makeInput({
defaults: { udp: { unknownField: true } },
targets: [{ id: "test", type: "udp", udp: { host: "127.0.0.1", port: 53 } }],
}),
);
expect(issues).toHaveLength(1);
expect(issues[0]!.code).toBe("unknown-field");
});
it("reports invalid-value for udp.encoding with bad value", () => {
const issues = validateUdpConfig(
makeInput({