import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { mkdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { ResolvedCommandTarget } from "../../../src/server/checker/runner/cmd/types"; import type { ResolvedHttpTarget } from "../../../src/server/checker/runner/http/types"; import type { ResolvedPingTarget } from "../../../src/server/checker/runner/icmp/types"; import type { ResolvedTcpTarget } from "../../../src/server/checker/runner/tcp/types"; import { loadConfig, parseDuration } from "../../../src/server/checker/config-loader"; import { checkValueExpectation } from "../../../src/server/checker/expect/value"; import { checkerRegistry } from "../../../src/server/checker/runner"; import { CommandChecker } from "../../../src/server/checker/runner/cmd/execute"; import { HttpChecker } from "../../../src/server/checker/runner/http/execute"; import { readRuntimeConfig } from "../../../src/server/config"; function ensureRegistered() { if (!checkerRegistry.supportedTypes.includes("http")) { checkerRegistry.register(new HttpChecker()); checkerRegistry.register(new CommandChecker()); } } beforeAll(() => { ensureRegistered(); }); describe("parseDuration", () => { test("解析秒", () => { expect(parseDuration("30s")).toBe(30000); expect(parseDuration("1s")).toBe(1000); }); test("解析分钟", () => { expect(parseDuration("5m")).toBe(300000); expect(parseDuration("1m")).toBe(60000); }); test("解析毫秒", () => { expect(parseDuration("500ms")).toBe(500); expect(parseDuration("100ms")).toBe(100); }); test("解析小数", () => { expect(parseDuration("1.5s")).toBe(1500); }); test("解析小时", () => { expect(parseDuration("2h")).toBe(7200000); expect(parseDuration("1h")).toBe(3600000); }); test("解析天", () => { expect(parseDuration("7d")).toBe(604800000); expect(parseDuration("1d")).toBe(86400000); }); test("拒绝非正整数毫秒结果", () => { expect(() => parseDuration("0ms")).toThrow("正整数毫秒"); expect(() => parseDuration("1.5ms")).toThrow("正整数毫秒"); }); test("无效格式抛出错误", () => { expect(() => parseDuration("30")).toThrow("无效的时长格式"); expect(() => parseDuration("abc")).toThrow("无效的时长格式"); expect(() => parseDuration("30x")).toThrow("无效的时长格式"); expect(() => parseDuration("")).toThrow("无效的时长格式"); }); }); describe("readRuntimeConfig", () => { test("返回配置文件路径", () => { expect(readRuntimeConfig(["./probes.yaml"])).toEqual({ configPath: "./probes.yaml" }); }); test("未提供参数抛出错误", () => { expect(() => readRuntimeConfig([])).toThrow("需要指定 YAML 配置文件路径"); }); }); describe("loadConfig", () => { let tempDir: string; beforeAll(async () => { tempDir = join(tmpdir(), `gc-test-${Date.now()}`); await mkdir(tempDir, { recursive: true }); }); afterAll(async () => { await rm(tempDir, { force: true, recursive: true }); }); async function expectConfigError(fileName: string, content: string, message: string): Promise { const configPath = join(tempDir, fileName); await writeFile(configPath, content); let error: unknown; try { await loadConfig(configPath); } catch (caught) { error = caught; } expect(error).toBeInstanceOf(Error); expect((error as Error).message).toContain(message); } async function expectConfigLoadError(configPath: string, message: string): Promise { let error: unknown; try { await loadConfig(configPath); } catch (caught) { error = caught; } expect(error).toBeInstanceOf(Error); expect((error as Error).message).toContain(message); } test("解析最简 HTTP 配置", async () => { const configPath = join(tempDir, "minimal-http.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.host).toBe("127.0.0.1"); expect(config.port).toBe(3000); expect(config.dataDir).toBe(join(tempDir, "data")); expect(config.maxConcurrentChecks).toBe(20); expect(config.targets).toHaveLength(1); const t = config.targets[0]! as ResolvedHttpTarget; expect(t.type).toBe("http"); expect(t.id).toBe("test"); expect(t.name).toBe("test"); expect(t.http.url).toBe("http://example.com"); expect(t.http.method).toBe("GET"); expect(t.http.headers).toEqual({}); expect(t.http.ignoreSSL).toBe(false); expect(t.http.maxBodyBytes).toBe(104857600); expect(t.http.maxRedirects).toBe(0); expect(t.intervalMs).toBe(30000); expect(t.timeoutMs).toBe(10000); }); test("解析最简 cmd 配置", async () => { const subdir = join(tempDir, "subdir"); await mkdir(subdir, { recursive: true }); const configPath = join(subdir, "cmd.yaml"); await writeFile( configPath, `targets: - name: "check-nginx" id: "check-nginx" type: cmd cmd: exec: "pgrep" args: ["nginx"] `, ); const config = await loadConfig(configPath); expect(config.targets).toHaveLength(1); const t = config.targets[0]! as ResolvedCommandTarget; expect(t.type).toBe("cmd"); expect(t.id).toBe("check-nginx"); expect(t.name).toBe("check-nginx"); expect(t.cmd.exec).toBe("pgrep"); expect(t.cmd.args).toEqual(["nginx"]); expect(t.cmd.cwd).toBe(subdir); expect(t.cmd.maxOutputBytes).toBe(104857600); expect(Object.keys(t.cmd.env).some((key) => key.toUpperCase() === "PATH")).toBe(true); }); test("解析完整配置", async () => { const configPath = join(tempDir, "full.yaml"); await writeFile( configPath, `server: listen: host: "0.0.0.0" port: 8080 storage: dataDir: "./my-data" 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" type: http interval: "1m" http: url: "http://example.com" method: "POST" ignoreSSL: true maxRedirects: 5 expect: status: ["2xx", 301] body: - contains: "ok" - name: "cmd-target" id: "cmd-target" type: cmd cmd: exec: "ls" args: ["/tmp"] expect: exitCode: [0] `, ); const config = await loadConfig(configPath); expect(config.host).toBe("0.0.0.0"); expect(config.port).toBe(8080); expect(config.dataDir).toBe(join(tempDir, "my-data")); expect(config.maxConcurrentChecks).toBe(5); expect(config.targets).toHaveLength(2); const http = config.targets[0]! as ResolvedHttpTarget; 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.maxRedirects).toBe(5); expect(http.expect?.status).toEqual(["2xx", 301]); expect(http.intervalMs).toBe(60000); expect(http.timeoutMs).toBe(5000); 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); }); test("name 缺省时保留为 null", async () => { const configPath = join(tempDir, "name-fallback.yaml"); await writeFile( configPath, `targets: - id: "api-health" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); const target = config.targets[0]!; expect(target.id).toBe("api-health"); expect(target.name).toBeNull(); }); test("name 显式 null 保留为 null", async () => { const configPath = join(tempDir, "name-explicit-null.yaml"); await writeFile( configPath, `targets: - id: "api-health" name: null type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.targets[0]!.name).toBeNull(); }); test("name YAML 空值保留为 null", async () => { const configPath = join(tempDir, "name-yaml-null.yaml"); await writeFile( configPath, `targets: - id: "api-health" name: type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.targets[0]!.name).toBeNull(); }); test("ValueMatcher primitive 简写在 resolve 后可运行期匹配", async () => { const configPath = join(tempDir, "matcher-shorthand.yaml"); await writeFile( configPath, `targets: - id: "api-health" type: http http: url: "http://example.com" expect: durationMs: 123 `, ); const config = await loadConfig(configPath); const target = config.targets[0]! as ResolvedHttpTarget; expect(target.expect?.durationMs).toEqual({ equals: 123 }); expect( checkValueExpectation(123, target.expect?.durationMs, { path: "durationMs", phase: "duration", }).matched, ).toBe(true); }); test("name 为空字符串抛出错误", async () => { const configPath = join(tempDir, "empty-name.yaml"); await writeFile( configPath, `targets: - id: "api-health" name: "" type: http http: url: "http://example.com" `, ); await expectConfigLoadError(configPath, "name 不能为空白"); }); test("name 仅包含空白字符抛出错误", async () => { const configPath = join(tempDir, "whitespace-name.yaml"); await writeFile( configPath, `targets: - id: "api-health" name: " " type: http http: url: "http://example.com" `, ); await expectConfigLoadError(configPath, "name 不能为空白"); }); test("description 显式 null 保留为 null", async () => { const configPath = join(tempDir, "description-null.yaml"); await writeFile( configPath, `targets: - id: "api-health" description: null type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.targets[0]!.description).toBeNull(); }); test("description YAML 空值保留为 null", async () => { const configPath = join(tempDir, "description-yaml-null.yaml"); await writeFile( configPath, `targets: - id: "api-health" description: type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.targets[0]!.description).toBeNull(); }); test("name 支持变量替换且不要求唯一", async () => { const configPath = join(tempDir, "name-variable.yaml"); await writeFile( configPath, `variables: env: "生产" targets: - id: "api-a" name: "\${env} API" type: http http: url: "http://a.example.com" - id: "api-b" name: "\${env} API" type: http http: url: "http://b.example.com" `, ); const config = await loadConfig(configPath); expect(config.targets.map((target) => [target.id, target.name])).toEqual([ ["api-a", "生产 API"], ["api-b", "生产 API"], ]); }); test("包含 variables 的完整配置在 schema 校验前完成替换", async () => { const configPath = join(tempDir, "variables-full.yaml"); await writeFile( configPath, `variables: env: "生产" base_url: "https://example.com" ignore_ssl: true max_redirects: 5 targets: - id: "api-health" name: "\${env} API 健康检查" type: http http: url: "\${base_url}/health" ignoreSSL: "\${ignore_ssl}" maxRedirects: "\${max_redirects}" `, ); const config = await loadConfig(configPath); const target = config.targets[0] as ResolvedHttpTarget; expect(target.id).toBe("api-health"); expect(target.name).toBe("生产 API 健康检查"); expect(target.http.url).toBe("https://example.com/health"); expect(target.http.ignoreSSL).toBe(true); expect(target.http.maxRedirects).toBe(5); }); test("变量替换后类型不匹配导致 schema 校验失败", async () => { const configPath = join(tempDir, "bad-var-type.yaml"); await writeFile( configPath, `variables: max_redirects: "not-a-number" targets: - id: "bad-var" type: http http: url: "http://example.com" maxRedirects: "\${max_redirects}" `, ); await expectConfigLoadError(configPath, "maxRedirects"); }); test("变量替换后通过 schema 校验", async () => { const configPath = join(tempDir, "good-var-type.yaml"); const origEnv = process.env["DIAL_VAR_MAX_REDIRECTS"]; process.env["DIAL_VAR_MAX_REDIRECTS"] = "3"; try { await writeFile( configPath, `targets: - id: "good-var" type: http http: url: "http://example.com" maxRedirects: "\${DIAL_VAR_MAX_REDIRECTS}" `, ); const config = await loadConfig(configPath); const target = config.targets[0] as ResolvedHttpTarget; expect(target.http.maxRedirects).toBe(3); } finally { if (origEnv === undefined) { delete process.env["DIAL_VAR_MAX_REDIRECTS"]; } else { process.env["DIAL_VAR_MAX_REDIRECTS"] = origEnv; } } }); test("未定义变量且无默认值阻止启动", async () => { const configPath = join(tempDir, "unresolved-var.yaml"); await writeFile( configPath, `targets: - id: "unresolved" type: http http: url: "\${MISSING_BASE_URL}/health" `, ); await expectConfigLoadError(configPath, "未定义的变量"); }); test("绝对 dataDir 保持不变", async () => { const dataDir = join(tempDir, "absolute-data"); const configPath = join(tempDir, "absolute-data-dir.yaml"); await writeFile( configPath, `server: storage: dataDir: ${JSON.stringify(dataDir)} targets: - name: "test" id: "test" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.dataDir).toBe(dataDir); }); test("per-target 覆盖 defaults", async () => { const configPath = join(tempDir, "override.yaml"); await writeFile( configPath, `defaults: interval: "30s" timeout: "10s" http: maxBodyBytes: "10MB" targets: - name: "override-all" id: "override-all" type: http interval: "5m" timeout: "30s" http: url: "http://example.com" method: "POST" maxBodyBytes: "1MB" `, ); const config = await loadConfig(configPath); const t = config.targets[0]! as ResolvedHttpTarget; expect(t.http.method).toBe("POST"); expect(t.intervalMs).toBe(300000); expect(t.timeoutMs).toBe(30000); expect(t.http.maxBodyBytes).toBe(1048576); }); test("配置文件不存在抛出错误", async () => { await expectConfigLoadError("/nonexistent/file.yaml", "配置文件不存在"); }); test("target 缺少 id 抛出错误", async () => { const configPath = join(tempDir, "no-id.yaml"); await writeFile( configPath, `targets: - type: http http: url: "http://example.com" `, ); await expectConfigLoadError(configPath, "缺少 id 字段"); }); test("target 缺少 type 抛出错误", async () => { const configPath = join(tempDir, "no-type.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" http: url: "http://example.com" `, ); await expectConfigLoadError(configPath, "缺少 type 字段"); }); test("HTTP target 缺少 url 抛出错误", async () => { const configPath = join(tempDir, "no-url.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: {} `, ); await expectConfigLoadError(configPath, "缺少 http.url 字段"); }); test("HTTP target 缺少 http 分组抛出清晰错误", async () => { const configPath = join(tempDir, "no-http-group.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http `, ); await expectConfigLoadError(configPath, "缺少 http.url 字段"); }); test("HTTP target ignoreSSL 非布尔值抛出错误", async () => { const configPath = join(tempDir, "bad-ignore-ssl.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" ignoreSSL: "true" `, ); await expectConfigLoadError(configPath, "http.ignoreSSL 类型不合法"); }); test("HTTP target maxRedirects 非负整数校验", async () => { const configPath = join(tempDir, "bad-max-redirects.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" maxRedirects: 1.5 `, ); await expectConfigLoadError(configPath, "http.maxRedirects 类型不合法"); }); test("HTTP target status 模式非法抛出错误", async () => { const configPath = join(tempDir, "bad-status-pattern.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" expect: status: ["abc"] `, ); await expectConfigLoadError(configPath, "status 模式"); }); test("cmd target 缺少 exec 抛出错误", async () => { const configPath = join(tempDir, "no-exec.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: cmd cmd: {} `, ); await expectConfigLoadError(configPath, "缺少 cmd.exec 字段"); }); test("非法 target type 抛出错误", async () => { const configPath = join(tempDir, "bad-type.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: dns `, ); await expectConfigLoadError(configPath, "不支持的 type"); }); test("target id 重复抛出错误", async () => { const configPath = join(tempDir, "dup-id.yaml"); await writeFile( configPath, `targets: - name: "dup" id: "dup" type: http http: url: "http://a.com" - name: "dup" id: "dup" type: http http: url: "http://b.com" `, ); await expectConfigLoadError(configPath, "target id 重复"); }); test("target id 为空字符串抛出错误", async () => { const configPath = join(tempDir, "empty-id.yaml"); await writeFile( configPath, `targets: - id: "" type: http http: url: "http://example.com" `, ); await expectConfigLoadError(configPath, "缺少 id 字段"); }); test("target id 命名不合法抛出错误", async () => { const configPath = join(tempDir, "bad-id.yaml"); await writeFile( configPath, `targets: - id: "_invalid" type: http http: url: "http://example.com" `, ); await expectConfigLoadError(configPath, "id 不符合命名规则"); }); test("target id 包含下划线和连字符通过", async () => { const configPath = join(tempDir, "id-underscore-dash.yaml"); await writeFile( configPath, `targets: - id: "db_check-01" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.targets[0]!.id).toBe("db_check-01"); }); test("targets 为空数组抛出错误", async () => { const configPath = join(tempDir, "empty-targets.yaml"); await writeFile(configPath, `targets: []`); await expectConfigLoadError(configPath, "至少一个 target"); }); test("无效端口号抛出错误", async () => { const configPath = join(tempDir, "bad-port.yaml"); await writeFile( configPath, `server: listen: port: 99999 targets: - name: "t" id: "t" type: http http: url: "http://a.com" `, ); await expectConfigLoadError(configPath, "server.listen.port 数值范围不合法"); }); test("非法 maxConcurrentChecks 抛出错误", async () => { const configPath = join(tempDir, "bad-concurrency.yaml"); await writeFile( configPath, `probes: execution: maxConcurrentChecks: -1 targets: - name: "t" id: "t" type: http http: url: "http://a.com" `, ); await expectConfigLoadError(configPath, "probes.execution.maxConcurrentChecks 数值范围不合法"); }); test("非法 size 格式抛出错误", async () => { const configPath = join(tempDir, "bad-size.yaml"); await writeFile( configPath, `defaults: http: maxBodyBytes: "100TB" targets: - name: "t" id: "t" type: http http: url: "http://a.com" `, ); await expectConfigLoadError(configPath, "无效的 size 格式"); }); test("非法 interval 格式抛出错误", async () => { const configPath = join(tempDir, "bad-interval.yaml"); await writeFile( configPath, `targets: - name: "t" id: "t" type: http interval: "30x" http: url: "http://a.com" `, ); await expectConfigLoadError(configPath, "无效的时长格式"); }); test("解析 expect 配置", async () => { const configPath = join(tempDir, "expect.yaml"); await writeFile( configPath, `targets: - name: "with-expect" id: "with-expect" type: http http: url: "http://example.com" expect: status: [200, 201] body: - contains: "ok" - json: path: "$.status" equals: "ok" durationMs: lte: 3000 `, ); const config = await loadConfig(configPath); const t = config.targets[0]!; if (t.type === "http") { expect(t.rawExpect).toEqual({ body: [{ contains: "ok" }, { json: { equals: "ok", path: "$.status" } }], durationMs: { lte: 3000 }, status: [200, 201], }); expect(t.expect).toEqual({ body: [ { kind: "value", matcher: { contains: "ok" } }, { kind: "json", matcher: { equals: "ok" }, path: "$.status" }, ], durationMs: { lte: 3000 }, status: [200, 201], }); } }); test("解析 cmd expect 配置", async () => { const configPath = join(tempDir, "cmd-expect.yaml"); await writeFile( configPath, `targets: - name: "cmd-with-expect" id: "cmd-with-expect" type: cmd cmd: exec: "mycheck" expect: exitCode: [0, 2] stdout: - contains: "ok" - regex: "done" stderr: - empty: true durationMs: lte: 5000 `, ); const config = await loadConfig(configPath); const t = config.targets[0]!; if (t.type === "cmd") { expect(t.rawExpect).toEqual({ durationMs: { lte: 5000 }, exitCode: [0, 2], stderr: [{ empty: true }], stdout: [{ contains: "ok" }, { regex: "done" }], }); expect(t.expect).toEqual({ durationMs: { lte: 5000 }, exitCode: [0, 2], stderr: [{ kind: "value", matcher: { empty: true } }], stdout: [ { kind: "value", matcher: { contains: "ok" } }, { kind: "value", matcher: { regex: "done" } }, ], }); } }); test("cmd cwd 相对配置文件目录", async () => { const subdir = join(tempDir, "cwd-test"); await mkdir(subdir, { recursive: true }); const configPath = join(subdir, "cwd.yaml"); await writeFile( configPath, `targets: - name: "cwd-test" id: "cwd-test" type: cmd cmd: exec: "ls" cwd: "scripts" `, ); const config = await loadConfig(configPath); const t = config.targets[0] as ResolvedCommandTarget; expect(t.cmd.cwd).toBe(join(subdir, "scripts")); }); test("cmd env 覆盖", async () => { const configPath = join(tempDir, "env.yaml"); await writeFile( configPath, `targets: - name: "env-test" id: "env-test" type: cmd cmd: exec: "echo" env: LANG: "C" CUSTOM_VAR: "test" `, ); const config = await loadConfig(configPath); const t = config.targets[0] as ResolvedCommandTarget; expect(t.cmd.env["LANG"]).toBe("C"); expect(t.cmd.env["CUSTOM_VAR"]).toBe("test"); expect(Object.keys(t.cmd.env).some((key) => key.toUpperCase() === "PATH")).toBe(true); }); test("解析 group 字段", async () => { const configPath = join(tempDir, "group.yaml"); await writeFile( configPath, `targets: - name: "grouped" id: "grouped" type: http group: "搜索引擎" http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.targets[0]!.group).toBe("搜索引擎"); }); test("group 字段默认为 default", async () => { const configPath = join(tempDir, "no-group.yaml"); await writeFile( configPath, `targets: - name: "no-group" id: "no-group" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.targets[0]!.group).toBe("default"); }); test("非法 group 类型抛出错误", async () => { const configPath = join(tempDir, "bad-group.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http group: 123 http: url: "http://example.com" `, ); await expectConfigLoadError(configPath, "group 必须为字符串"); }); test("HTTP headers 非字符串值抛出错误", async () => { const configPath = join(tempDir, "bad-headers-val.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" headers: X-Custom: 123 `, ); await expectConfigLoadError(configPath, "http.headers"); }); test("HTTP body 非字符串抛出错误", async () => { const configPath = join(tempDir, "bad-body.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" body: 123 `, ); await expectConfigLoadError(configPath, "http.body 类型不合法"); }); test("maxBodyBytes 负数抛出错误", async () => { const configPath = join(tempDir, "neg-bodybytes.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" maxBodyBytes: -1 `, ); await expectConfigLoadError(configPath, "非负安全整数"); }); test("maxBodyBytes 非整数抛出错误", async () => { const configPath = join(tempDir, "float-bodybytes.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" maxBodyBytes: 1.5 `, ); await expectConfigLoadError(configPath, "非负安全整数"); }); test("expect.status 数字不在 100-599 范围抛出错误", async () => { const configPath = join(tempDir, "bad-status-num.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" expect: status: [999] `, ); await expectConfigLoadError(configPath, "100-599"); }); test("expect.status 范围 6xx 抛出错误", async () => { const configPath = join(tempDir, "bad-status-6xx.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" expect: status: ["6xx"] `, ); await expectConfigLoadError(configPath, "5xx"); }); test("expect.durationMs 对象简写抛出错误", async () => { const configPath = join(tempDir, "bad-duration-object.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" expect: durationMs: foo: "bar" `, ); await expectConfigLoadError(configPath, "expect.durationMs.foo 是未知 matcher"); }); test("expect.body 非数组抛出错误", async () => { const configPath = join(tempDir, "bad-expect-body.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" expect: body: "not-array" `, ); await expectConfigLoadError(configPath, "expect.body 必须为数组"); }); test("body rule 缺少支持字段抛出错误", async () => { const configPath = join(tempDir, "bad-body-rule-nofield.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" expect: body: - foo: "bar" `, ); await expectConfigLoadError(configPath, "foo 是未知字段"); }); test("body rule 使用 match 字段(非支持)抛出错误", async () => { const configPath = join(tempDir, "bad-body-rule-match.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" expect: body: - match: "ok" `, ); await expectConfigLoadError(configPath, "match 是未知字段"); }); test("body rule 直接 matcher 混入 extractor 抛出错误", async () => { const configPath = join(tempDir, "bad-body-rule-multi.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" expect: body: - contains: "ok" json: path: "$.status" `, ); await expectConfigLoadError(configPath, "直接 matcher 不能与 extractor 混用"); }); test("body regex 非法正则抛出错误", async () => { const configPath = join(tempDir, "bad-body-regex.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" expect: body: - regex: "[invalid" `, ); await expectConfigLoadError(configPath, "regex 正则不合法"); }); test("body json path 不以 $. 开头抛出错误", async () => { const configPath = join(tempDir, "bad-json-path.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" expect: body: - json: path: "status" equals: "ok" `, ); await expectConfigLoadError(configPath, "json.path"); }); test("body css selector 为空抛出错误", async () => { const configPath = join(tempDir, "bad-css-sel.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" expect: body: - css: selector: "" `, ); await expectConfigLoadError(configPath, "css.selector 必须为非空字符串"); }); test("旧 match matcher 抛出错误", async () => { const configPath = join(tempDir, "bad-op-match.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" expect: headers: X-Test: match: "[invalid" `, ); await expectConfigLoadError(configPath, "match 是未知 matcher"); }); test("operator gte 非数字抛出错误", async () => { const configPath = join(tempDir, "bad-op-gte.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" expect: body: - json: path: "$.count" gte: "abc" `, ); await expectConfigLoadError(configPath, "gte 必须为有限数字"); }); test("operator exists 非布尔值抛出错误", async () => { const configPath = join(tempDir, "bad-op-exists.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" expect: body: - json: path: "$.status" exists: "yes" `, ); await expectConfigLoadError(configPath, "exists 必须为布尔值"); }); test("未知字段导致启动失败", async () => { const configPath = join(tempDir, "unknown-fields.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" unknownHttpField: "value" expect: status: [200] unknownExpectField: "value" body: - contains: "ok" note: "ignored" `, ); await expectConfigLoadError(configPath, "unknownHttpField 是未知字段"); }); test("xpath path 非空字符串校验", async () => { const configPath = join(tempDir, "bad-xpath-path.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" expect: body: - xpath: path: "" `, ); await expectConfigLoadError(configPath, "xpath.path 必须为非空字符串"); }); test("expect headers 非对象抛出错误", async () => { const configPath = join(tempDir, "bad-expect-headers.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" expect: headers: "invalid" `, ); await expectConfigLoadError(configPath, "expect.headers 类型不合法"); }); test("HTTP method 小写输入失败", async () => { await expectConfigError( "lowercase-method.yaml", `targets: - name: "test" id: "test" type: http http: url: "http://example.com" method: get `, "http.method 不在允许范围内", ); }); test("defaults.http.method 触发未知字段错误", async () => { await expectConfigError( "unknown-default-method.yaml", `defaults: http: method: POST targets: - name: "test" id: "test" type: http http: url: "http://example.com" `, "defaults.http.method 是未知字段", ); }); test("HTTP method 大写输入通过", async () => { const configPath = join(tempDir, "uppercase-method.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" method: POST `, ); const config = await loadConfig(configPath); const target = config.targets[0] as ResolvedHttpTarget; expect(target.type).toBe("http"); expect(target.http.method).toBe("POST"); }); test("动态 headers 和 env 允许任意键名", async () => { const configPath = join(tempDir, "dynamic-maps.yaml"); await writeFile( configPath, `defaults: http: headers: X-Default-Header: "default" targets: - name: "http-test" id: "http-test" type: http http: url: "http://example.com" headers: X-Custom-Header: "custom" expect: headers: X-Response-Header: contains: "ok" - name: "cmd-test" id: "cmd-test" type: cmd cmd: exec: "true" env: CUSTOM_ENV_NAME: "custom" `, ); const config = await loadConfig(configPath); const http = config.targets[0] as ResolvedHttpTarget; 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"); }); test("cmd args 类型非法", async () => { await expectConfigError( "bad-cmd-args.yaml", `targets: - name: "cmd" id: "cmd" type: cmd cmd: exec: "echo" args: "hello" `, "cmd.args 类型不合法", ); }); test("cmd cwd 类型非法", async () => { await expectConfigError( "bad-cmd-cwd.yaml", `targets: - name: "cmd" id: "cmd" type: cmd cmd: exec: "echo" cwd: 123 `, "cmd.cwd 类型不合法", ); }); test("cmd env 值类型非法", async () => { await expectConfigError( "bad-cmd-env.yaml", `targets: - name: "cmd" id: "cmd" type: cmd cmd: exec: "echo" env: COUNT: 123 `, "cmd.env.COUNT 类型不合法", ); }); test("cmd maxOutputBytes 非法", async () => { await expectConfigError( "bad-cmd-max-output.yaml", `targets: - name: "cmd" id: "cmd" type: cmd cmd: exec: "echo" maxOutputBytes: "1TB" `, "maxOutputBytes 无效的 size 格式", ); }); test("cmd expect exitCode 类型非法", async () => { await expectConfigError( "bad-cmd-exit-code.yaml", `targets: - name: "cmd" id: "cmd" type: cmd cmd: exec: "echo" expect: exitCode: [1.5] `, "expect.exitCode[0] 类型不合法", ); }); test("cmd stdout 空 text rule 非法", async () => { await expectConfigError( "bad-cmd-stdout-empty.yaml", `targets: - name: "cmd" id: "cmd" type: cmd cmd: exec: "echo" expect: stdout: - {} `, "stdout[0] 必须包含至少一个合法 matcher", ); }); test("cmd stderr 未知 operator 非法", async () => { await expectConfigError( "bad-cmd-stderr-operator.yaml", `targets: - name: "cmd" id: "cmd" type: cmd cmd: exec: "echo" expect: stderr: - foo: "bar" `, "expect.stderr[0].foo 是未知字段", ); }); test("cmd stdout 旧 match 字段非法", async () => { await expectConfigError( "bad-cmd-stdout-regex.yaml", `targets: - name: "cmd" id: "cmd" type: cmd cmd: exec: "echo" expect: stdout: - match: "[invalid" `, "stdout[0].match 是未知字段", ); }); test("cmd expect 未知字段失败", async () => { await expectConfigError( "bad-cmd-expect-unknown.yaml", `targets: - name: "cmd" id: "cmd" type: cmd cmd: exec: "echo" expect: status: [200] `, "expect.status 是未知字段", ); }); test("retention 默认值为 7d", async () => { const configPath = join(tempDir, "retention-default.yaml"); await writeFile( configPath, `targets: - name: "test" id: "test" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.retentionMs).toBe(604800000); }); test("retention 自定义值", async () => { const configPath = join(tempDir, "retention-custom.yaml"); await writeFile( configPath, `server: storage: retention: "24h" targets: - name: "test" id: "test" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.retentionMs).toBe(86400000); }); test("retention 非法格式抛出错误", async () => { await expectConfigError( "bad-retention.yaml", `server: storage: retention: "7x" targets: - name: "test" id: "test" type: http http: url: "http://example.com" `, "无效的时长格式", ); }); test("解析 description 字段", async () => { const configPath = join(tempDir, "description.yaml"); await writeFile( configPath, `targets: - id: "api-health" description: "检查生产 API 健康状态" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.targets[0]!.description).toBe("检查生产 API 健康状态"); }); test("description 使用变量替换", async () => { const configPath = join(tempDir, "description-var.yaml"); await writeFile( configPath, `variables: env: "生产" targets: - id: "api-health" description: "\${env} 环境健康检查" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.targets[0]!.description).toBe("生产 环境健康检查"); }); test("description 缺省为 null", async () => { const configPath = join(tempDir, "no-description.yaml"); await writeFile( configPath, `targets: - id: "api-health" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.targets[0]!.description).toBeNull(); }); test("description 为空字符串通过", async () => { const configPath = join(tempDir, "empty-description.yaml"); await writeFile( configPath, `targets: - id: "api-health" description: "" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.targets[0]!.description).toBe(""); }); test("description 非字符串抛出错误", async () => { const configPath = join(tempDir, "bad-description-type.yaml"); await writeFile( configPath, `targets: - id: "api-health" description: 123 type: http http: url: "http://example.com" `, ); await expectConfigLoadError(configPath, "description"); }); test("description 超过 500 字符抛出错误", async () => { const configPath = join(tempDir, "long-description.yaml"); await writeFile( configPath, `targets: - id: "api-health" description: "${"a".repeat(501)}" type: http http: url: "http://example.com" `, ); await expectConfigLoadError(configPath, "description"); }); test("变量替换后 description 超长抛出错误", async () => { const configPath = join(tempDir, "var-long-description.yaml"); await writeFile( configPath, `variables: prefix: "${"x".repeat(490)}" targets: - id: "api-health" description: "\${prefix}${"a".repeat(15)}" type: http http: url: "http://example.com" `, ); await expectConfigLoadError(configPath, "description"); }); test("id 超过 30 字符抛出错误", async () => { const configPath = join(tempDir, "long-id.yaml"); await writeFile( configPath, `targets: - id: "${"a".repeat(31)}" type: http http: url: "http://example.com" `, ); await expectConfigLoadError(configPath, "id"); }); test("name 超过 30 字符抛出错误", async () => { const configPath = join(tempDir, "long-name.yaml"); await writeFile( configPath, `targets: - id: "test" name: "${"a".repeat(31)}" type: http http: url: "http://example.com" `, ); await expectConfigLoadError(configPath, "name"); }); test("解析最简 tcp 配置", async () => { const configPath = join(tempDir, "minimal-tcp.yaml"); await writeFile( configPath, `targets: - id: "redis-port" type: tcp tcp: host: "127.0.0.1" port: 6379 `, ); const config = await loadConfig(configPath); expect(config.targets).toHaveLength(1); const t = config.targets[0]! as ResolvedTcpTarget; expect(t.type).toBe("tcp"); expect(t.id).toBe("redis-port"); expect(t.name).toBeNull(); expect(t.tcp.host).toBe("127.0.0.1"); expect(t.tcp.port).toBe(6379); expect(t.tcp.readBanner).toBe(false); expect(t.tcp.bannerReadTimeout).toBe(2000); expect(t.tcp.maxBannerBytes).toBe(4096); expect(t.group).toBe("default"); expect(t.intervalMs).toBe(30000); expect(t.timeoutMs).toBe(10000); }); test("tcp 缺少 host 抛出错误", async () => { await expectConfigError( "tcp-no-host.yaml", `targets: - id: "t" type: tcp tcp: port: 80 `, "tcp.host", ); }); test("tcp 缺少 port 抛出错误", async () => { await expectConfigError( "tcp-no-port.yaml", `targets: - id: "t" type: tcp tcp: host: "127.0.0.1" `, "tcp.port", ); }); test("tcp 非法端口范围抛出错误", async () => { await expectConfigError( "tcp-bad-port.yaml", `targets: - id: "t" type: tcp tcp: host: "127.0.0.1" port: 99999 `, "tcp.port", ); }); test("tcp 未知分组字段抛出错误", async () => { await expectConfigError( "tcp-unknown-field.yaml", `targets: - id: "t" type: tcp tcp: host: "127.0.0.1" port: 80 tls: true `, "是未知字段", ); }); test("tcp readBanner 开启并配置 expect.banner", async () => { const configPath = join(tempDir, "tcp-banner.yaml"); await writeFile( configPath, `targets: - id: "smtp-check" type: tcp tcp: host: "127.0.0.1" port: 25 readBanner: true expect: banner: - 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([{ kind: "value", matcher: { contains: "ESMTP" } }]); }); test("tcp expect.banner 未开启 readBanner 抛出错误", async () => { await expectConfigError( "tcp-banner-no-read.yaml", `targets: - id: "t" type: tcp tcp: host: "127.0.0.1" port: 25 expect: banner: contains: "ESMTP" `, "banner 断言需要启用 tcp.readBanner", ); }); test("tcp defaults 覆盖 banner 参数", async () => { const configPath = join(tempDir, "tcp-defaults.yaml"); await writeFile( configPath, `defaults: tcp: bannerReadTimeout: 1000 maxBannerBytes: "8KB" targets: - id: "t1" type: tcp tcp: host: "127.0.0.1" port: 80 - id: "t2" type: tcp tcp: host: "127.0.0.1" port: 81 bannerReadTimeout: 3000 `, ); const config = await loadConfig(configPath); const t1 = config.targets[0]! as ResolvedTcpTarget; expect(t1.tcp.bannerReadTimeout).toBe(1000); expect(t1.tcp.maxBannerBytes).toBe(8192); const t2 = config.targets[1]! as ResolvedTcpTarget; expect(t2.tcp.bannerReadTimeout).toBe(3000); expect(t2.tcp.maxBannerBytes).toBe(8192); }); test("tcp expect 未知字段抛出错误", async () => { await expectConfigError( "tcp-unknown-expect.yaml", `targets: - id: "t" type: tcp tcp: host: "127.0.0.1" port: 80 expect: status: [200] `, "是未知字段", ); }); test("tcp expect connected 和 durationMs", async () => { const configPath = join(tempDir, "tcp-expect-connected.yaml"); await writeFile( configPath, `targets: - id: "t" type: tcp tcp: host: "127.0.0.1" port: 80 expect: connected: false durationMs: lte: 5000 `, ); const config = await loadConfig(configPath); const t = config.targets[0]! as ResolvedTcpTarget; expect(t.expect?.connected).toBe(false); expect(t.expect?.durationMs).toEqual({ lte: 5000 }); }); test("解析最简 icmp 配置", async () => { const configPath = join(tempDir, "minimal-icmp.yaml"); await writeFile( configPath, `targets: - id: "gateway" type: icmp icmp: host: "10.0.0.1" `, ); const config = await loadConfig(configPath); expect(config.targets).toHaveLength(1); const t = config.targets[0]! as ResolvedPingTarget; expect(t.type).toBe("icmp"); expect(t.icmp).toEqual({ count: 3, host: "10.0.0.1", packetSize: 56 }); expect(t.group).toBe("default"); expect(t.intervalMs).toBe(30000); expect(t.timeoutMs).toBe(10000); }); test("解析 icmp expect 配置", async () => { const configPath = join(tempDir, "icmp-expect.yaml"); await writeFile( configPath, `targets: - id: "gateway" type: icmp icmp: host: "10.0.0.1" count: 5 packetSize: 1472 expect: alive: true packetLossPercent: lte: 10 avgLatencyMs: lte: 200 maxLatencyMs: lte: 500 durationMs: lte: 5000 `, ); const config = await loadConfig(configPath); const t = config.targets[0]! as ResolvedPingTarget; expect(t.icmp).toEqual({ count: 5, host: "10.0.0.1", packetSize: 1472 }); expect(t.expect).toEqual({ alive: true, avgLatencyMs: { lte: 200 }, durationMs: { lte: 5000 }, maxLatencyMs: { lte: 500 }, packetLossPercent: { lte: 10 }, }); }); test("icmp 缺少 host 抛出错误", async () => { await expectConfigError( "icmp-no-host.yaml", `targets: - id: "gateway" type: icmp icmp: {} `, "icmp.host", ); }); test("icmp count 非法抛出错误", async () => { await expectConfigError( "icmp-bad-count.yaml", `targets: - id: "gateway" type: icmp icmp: host: "10.0.0.1" count: 0 `, "icmp.count", ); }); test("icmp expect 未知字段抛出错误", async () => { await expectConfigError( "icmp-unknown-expect.yaml", `targets: - id: "gateway" type: icmp icmp: host: "10.0.0.1" expect: status: [200] `, "expect.status 是未知字段", ); }); describe("logging 配置", () => { test("logging 全部缺省时使用默认值", async () => { const configPath = join(tempDir, "logging-default.yaml"); await writeFile( configPath, `targets: - id: "t" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.logging.consoleLevel).toBe("info"); expect(config.logging.fileLevel).toBe("info"); expect(config.logging.filePath).toBe(join(config.dataDir, "logs/dial.log")); expect(config.logging.rotationFrequency).toBe("daily"); expect(config.logging.rotationMaxFiles).toBe(14); expect(config.logging.rotationSizeRaw).toBe("50MB"); expect(config.logging.rotationSizeBytes).toBe(52428800); }); test("logging.level 设置全局等级继承到 console 和 file", async () => { const configPath = join(tempDir, "logging-global-level.yaml"); await writeFile( configPath, `server: logging: level: "debug" targets: - id: "t" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.logging.consoleLevel).toBe("debug"); expect(config.logging.fileLevel).toBe("debug"); }); test("logging.console.level 覆盖全局等级", async () => { const configPath = join(tempDir, "logging-console-level.yaml"); await writeFile( configPath, `server: logging: level: "warn" console: level: "trace" targets: - id: "t" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.logging.consoleLevel).toBe("trace"); expect(config.logging.fileLevel).toBe("warn"); }); test("logging.file.level 独立覆盖", async () => { const configPath = join(tempDir, "logging-file-level.yaml"); await writeFile( configPath, `server: logging: level: "info" file: level: "error" targets: - id: "t" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.logging.consoleLevel).toBe("info"); expect(config.logging.fileLevel).toBe("error"); }); test("logging.file.path 绝对路径保持不变", async () => { const configPath = join(tempDir, "logging-abs-path.yaml"); await writeFile( configPath, `server: logging: file: path: "/var/log/dial/app.log" targets: - id: "t" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.logging.filePath).toBe("/var/log/dial/app.log"); }); test("logging.file.path 相对路径基于配置文件目录解析", async () => { const configPath = join(tempDir, "logging-rel-path.yaml"); await writeFile( configPath, `server: logging: file: path: "custom-logs/app.log" targets: - id: "t" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.logging.filePath).toBe(join(tempDir, "custom-logs/app.log")); }); test("logging.file.rotation 自定义参数", async () => { const configPath = join(tempDir, "logging-rotation.yaml"); await writeFile( configPath, `server: logging: file: rotation: size: "100MB" frequency: "hourly" maxFiles: 30 targets: - id: "t" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.logging.rotationSizeRaw).toBe("100MB"); expect(config.logging.rotationSizeBytes).toBe(104857600); expect(config.logging.rotationFrequency).toBe("hourly"); expect(config.logging.rotationMaxFiles).toBe(30); }); test("logging.level 非法等级抛出错误", async () => { await expectConfigError( "logging-bad-level.yaml", `server: logging: level: "verbose" targets: - id: "t" type: http http: url: "http://example.com" `, "server.logging.level", ); }); test("logging.console.level 非法等级抛出错误", async () => { await expectConfigError( "logging-bad-console-level.yaml", `server: logging: console: level: "nope" targets: - id: "t" type: http http: url: "http://example.com" `, "server.logging.console.level", ); }); test("logging.file.level 非法等级抛出错误", async () => { await expectConfigError( "logging-bad-file-level.yaml", `server: logging: file: level: 123 targets: - id: "t" type: http http: url: "http://example.com" `, "server.logging.file.level", ); }); test("logging.file.rotation.size 非法格式抛出错误", async () => { await expectConfigError( "logging-bad-rotation-size.yaml", `server: logging: file: rotation: size: "100TB" targets: - id: "t" type: http http: url: "http://example.com" `, "无效的 size 格式", ); }); test("logging.file.rotation.frequency 非法值抛出错误", async () => { await expectConfigError( "logging-bad-frequency.yaml", `server: logging: file: rotation: frequency: "monthly" targets: - id: "t" type: http http: url: "http://example.com" `, "server.logging.file.rotation.frequency", ); }); test("logging.file.rotation.maxFiles 非整数抛出错误", async () => { await expectConfigError( "logging-bad-maxfiles.yaml", `server: logging: file: rotation: maxFiles: 3.5 targets: - id: "t" type: http http: url: "http://example.com" `, "server.logging.file.rotation.maxFiles", ); }); test("logging.file.path 空字符串抛出错误", async () => { await expectConfigError( "logging-empty-path.yaml", `server: logging: file: path: "" targets: - id: "t" type: http http: url: "http://example.com" `, "server.logging.file.path", ); }); }); describe("旧路径拒绝", () => { test("顶层 runtime 段应被拒绝为未知字段", async () => { await expectConfigError( "legacy-runtime.yaml", `runtime: maxConcurrentChecks: 10 targets: - id: "t" type: http http: url: "http://example.com" `, "runtime 是未知字段", ); }); test("顶层 logging 段应被拒绝为未知字段", async () => { await expectConfigError( "legacy-logging.yaml", `logging: level: "info" targets: - id: "t" type: http http: url: "http://example.com" `, "logging 是未知字段", ); }); test("server.host 应被拒绝为未知字段", async () => { await expectConfigError( "legacy-server-host.yaml", `server: host: "0.0.0.0" targets: - id: "t" type: http http: url: "http://example.com" `, "host 是未知字段", ); }); test("server.port 应被拒绝为未知字段", async () => { await expectConfigError( "legacy-server-port.yaml", `server: port: 8080 targets: - id: "t" type: http http: url: "http://example.com" `, "port 是未知字段", ); }); test("server.dataDir 应被拒绝为未知字段", async () => { await expectConfigError( "legacy-server-datadir.yaml", `server: dataDir: "/tmp/data" targets: - id: "t" type: http http: url: "http://example.com" `, "dataDir 是未知字段", ); }); }); });