From 79358ba50df8175c005e88c7745d5610602c5119 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Thu, 21 May 2026 16:53:12 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E9=A1=B6?= =?UTF-8?q?=E5=B1=82=20defaults=20=E9=85=8D=E7=BD=AE=E6=AE=B5=EF=BC=8C?= =?UTF-8?q?=E7=AE=80=E5=8C=96=E4=B8=BA=20target=20=E6=98=BE=E5=BC=8F?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=20>=20=E4=BB=A3=E7=A0=81=E5=86=85=E7=BD=AE?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 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 --- DEVELOPMENT.md | 32 ++- README.md | 102 +++------ opencode.json | 11 + .../specs/checker-cohesion-structure/spec.md | 2 +- .../specs/checker-runner-abstraction/spec.md | 6 +- openspec/specs/config-variables/spec.md | 10 +- openspec/specs/http-checker/spec.md | 32 +++ openspec/specs/probe-config/spec.md | 78 +++---- probe-config.schema.json | 206 ------------------ probes.example.yaml | 8 - src/server/checker/config-loader.ts | 19 +- src/server/checker/runner/cmd/execute.ts | 5 +- src/server/checker/runner/cmd/schema.ts | 7 - src/server/checker/runner/cmd/types.ts | 5 - src/server/checker/runner/cmd/validate.ts | 6 - src/server/checker/runner/db/schema.ts | 1 - src/server/checker/runner/http/execute.ts | 7 +- src/server/checker/runner/http/schema.ts | 7 - src/server/checker/runner/http/types.ts | 6 - src/server/checker/runner/http/validate.ts | 6 - src/server/checker/runner/icmp/schema.ts | 1 - src/server/checker/runner/icmp/validate.ts | 13 -- src/server/checker/runner/llm/execute.ts | 42 +--- src/server/checker/runner/llm/schema.ts | 10 - src/server/checker/runner/llm/types.ts | 8 - src/server/checker/runner/llm/validate.ts | 28 --- src/server/checker/runner/tcp/execute.ts | 7 +- src/server/checker/runner/tcp/schema.ts | 7 - src/server/checker/runner/tcp/types.ts | 5 - src/server/checker/runner/tcp/validate.ts | 38 ---- src/server/checker/runner/types.ts | 5 +- src/server/checker/runner/udp/execute.ts | 17 +- src/server/checker/runner/udp/schema.ts | 8 - src/server/checker/runner/udp/types.ts | 6 - src/server/checker/runner/udp/validate.ts | 25 --- src/server/checker/schema/builder.ts | 12 - src/server/checker/types.ts | 7 - tests/server/checker/config-loader.test.ts | 59 ++--- .../server/checker/runner/cmd/runner.test.ts | 2 +- .../server/checker/runner/db/validate.test.ts | 15 +- .../server/checker/runner/http/runner.test.ts | 3 +- .../checker/runner/icmp/execute.test.ts | 2 +- .../checker/runner/icmp/validate.test.ts | 2 +- .../checker/runner/llm/registry.test.ts | 2 - .../llm/schema-validate-resolve.test.ts | 115 +--------- tests/server/checker/runner/registry.test.ts | 7 +- .../shared/duplicate-header-key.test.ts | 2 +- .../shared/value-matcher-shorthand.test.ts | 2 +- .../server/checker/runner/tcp/execute.test.ts | 37 +--- .../checker/runner/tcp/validate.test.ts | 39 +--- .../server/checker/runner/udp/execute.test.ts | 25 +-- .../checker/runner/udp/validate.test.ts | 29 +-- 52 files changed, 196 insertions(+), 940 deletions(-) create mode 100644 openspec/specs/http-checker/spec.md diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 6283400..17a8a21 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -64,7 +64,7 @@ src/ metrics.ts GET /api/targets/:id/metrics history.ts GET /api/targets/:id/history checker/ - types.ts 基础类型定义(ResolvedTargetBase、RawTargetConfig、DefaultsConfig、CheckResult 等基础 interface) + types.ts 基础类型定义(ResolvedTargetBase、RawTargetConfig、CheckResult 等基础 interface) config-loader.ts YAML 配置解析、契约校验、语义校验与运行期解析(输出 ResolvedConfig) variables.ts 配置 variables 提取、target 字符串变量替换和 unresolved-variable issue 生成 schema/ TypeBox + Ajv 配置契约、schema fragments、issue 渲染和 schema 导出入口 @@ -237,7 +237,7 @@ export function handleMetrics(idStr: string, url: URL, store: ProbeStore, mode: - **后端内部扩展**:`checker/types.ts` 中 `CheckResult` 通过 `extends` 共享版本的 `ApiCheckResult` 增加 `targetId` 等内部字段 - 存储层类型(`StoredTarget`、`StoredCheckResult`)独立定义,与 API 类型分离 - **Checker 类型分层**: - - `checker/types.ts` 定义 base interface(`ResolvedTargetBase`、`RawTargetConfig`、`DefaultsConfig`),使用 index signature 支持扩展 + - `checker/types.ts` 定义 base interface(`ResolvedTargetBase`、`RawTargetConfig`),使用 index signature 支持扩展 - 各 checker 在自己的 `types.ts` 中定义具体类型(如 `ResolvedHttpTarget`、`ResolvedCommandTarget`),满足 base interface 约束 - 中间层(engine、store、config-loader)只依赖 base interface,不感知具体 checker 类型 - `CheckerDefinition` 使用泛型约束 `resolve` 返回值以及 `execute`、`serialize` 的 target 参数 @@ -267,9 +267,9 @@ export function handleMetrics(idStr: string, url: URL, store: ProbeStore, mode: 契约层使用 `src/server/checker/schema/` 中的 TypeBox fragments 生成 JSON Schema,并用 Ajv 执行启动期校验。Ajv 必须保持严格拒绝模式:`allErrors: true`、不启用类型强制转换、不注入默认值、不自动删除未知字段。 -默认对象策略是 `additionalProperties: false`。只有明确声明的动态键值表可以开放任意键名,例如 `variables`、`http.headers`、`defaults.http.headers`、`expect.headers`、`cmd.env`。 +默认对象策略是 `additionalProperties: false`。只有明确声明的动态键值表可以开放任意键名,例如 `variables`、`http.headers`、`expect.headers`、`cmd.env`。 -契约校验和语义 validator 都必须返回 `ConfigValidationIssue[]`,不要在 validator 内直接拼接最终用户错误字符串。最终错误由 `formatConfigIssues()` 统一渲染,错误路径需要尽量包含 `targetName` 或 `defaults`/root 路径。 +契约校验和语义 validator 都必须返回 `ConfigValidationIssue[]`,不要在 validator 内直接拼接最终用户错误字符串。最终错误由 `formatConfigIssues()` 统一渲染,错误路径需要尽量包含 `targetName` 或 root 路径。 新增或修改配置字段时必须同步更新:TypeBox schema fragments、`probe-config.schema.json` 导出、对应语义 validator、单元测试和 README/DEVELOPMENT 用户文档。提交前运行 `bun run schema:check` 确认导出 schema 与 fragments 一致。 @@ -303,7 +303,7 @@ checkerRegistry(单例) | ------------- | ------------------------------------------------------------------------------------------ | | `index.ts` | 模块入口,re-export Checker 类 | | `types.ts` | Checker 专属类型(RawXxxTargetConfig、Raw/Resolved XxxExpectConfig、ResolvedXxxTarget 等) | -| `schema.ts` | TypeBox 契约 schema(config / defaults / expect 三部分) | +| `schema.ts` | TypeBox 契约 schema(config / expect 两部分) | | `validate.ts` | 启动期语义校验(JSON Schema 无法表达的规则) | | `execute.ts` | Checker 类:resolve(默认值合并 + 解析)、execute(执行检查)、serialize(DB 持久化) | | `expect.ts` | Checker 专用断言函数 | @@ -315,14 +315,13 @@ checkerRegistry(单例) - `RawXxxTargetConfig` — YAML 原始配置类型 - `RawXxxExpectConfig` / `ResolvedXxxExpectConfig` — Raw expect 字段类型与运行期 Resolved expect 执行计划类型 -- `XxxDefaultsConfig` — defaults 专属字段类型 - `ResolvedXxxTarget extends ResolvedTargetBase` — resolve 后的完整类型,含 `type: "xxx"` 字面量 **注意**:不需要修改顶层 `checker/types.ts`。base interface 使用 index signature(`[key: string]: unknown`),checker 专属类型通过 `extends ResolvedTargetBase` 自动兼容。 #### 1.7.3 步骤二:创建 TypeBox 契约 Schema -在 `src/server/checker/runner/tcp/schema.ts` 中定义 `CheckerSchemas`(config / defaults / expect 三部分)。参考 `http/schema.ts`、`cmd/schema.ts`,使用 `schema/fragments.ts` 中的共享片段。 +在 `src/server/checker/runner/tcp/schema.ts` 中定义 `CheckerSchemas`(config / expect 两部分)。参考 `http/schema.ts`、`cmd/schema.ts`,使用 `schema/fragments.ts` 中的共享片段。 **可复用的共享 fragments**(来自 `schema/fragments.ts`): @@ -368,17 +367,16 @@ TcpChecker implements Checker readonly schemas ← tcpCheckerSchemas validate(input) ← 调用 validateTcpConfig(input) - resolve(target, ctx)← 默认值合并 + 解析,返回 satisfies ResolvedTcpTarget + resolve(target, ctx)← 内置默认值填充 + 解析,返回 satisfies ResolvedTcpTarget execute(target, ctx)← 执行检查,返回 CheckResult serialize(target) ← 返回 { config, target } 用于 DB 持久化 ``` **`resolve()` 规范**: -- 只做默认值合并、路径解析、单位转换,**不执行校验** +- 只做内置默认值填充、路径解析、单位转换,**不执行校验** - 若 checker 支持 expect,必须保留 `rawExpect` 为变量替换后的 Raw 配置快照,并生成只供 `execute()` 使用的 Resolved `expect` - 返回 `satisfies ResolvedXxxTarget` 确保类型正确 -- 通过 `context.defaults[this.configKey]` 访问 checker 专属默认值(需 `as` 断言为具体类型) **expect 五层管线**:每种断言模型从定义到执行经过五个阶段,每层使用对应的共享函数: @@ -457,13 +455,13 @@ if (r.body) { 注册后,以下管线会通过 registry 自动委托,**无需新增 type 分支**: -| 模块 | 自动行为 | -| -------------------- | ------------------------------------------------------------------------ | -| `schema/builder.ts` | 遍历 registry 生成全量 JSON Schema(defaults.tcp + target.tcp + expect) | -| `schema/validate.ts` | 按注册 checker 构建 Ajv 校验,自动识别 `type: tcp` | -| `config-loader.ts` | 遍历 registry 调用每个 checker 的 `validate()` + `resolve()` | -| `engine.ts` | 按 `target.type` 从 registry 取对应 checker 执行 `execute()` | -| `store.ts` | 按 `target.type` 从 registry 取对应 checker 执行 `serialize()` | +| 模块 | 自动行为 | +| -------------------- | -------------------------------------------------------------- | +| `schema/builder.ts` | 遍历 registry 生成全量 JSON Schema(target.tcp + expect) | +| `schema/validate.ts` | 按注册 checker 构建 Ajv 校验,自动识别 `type: tcp` | +| `config-loader.ts` | 遍历 registry 调用每个 checker 的 `validate()` + `resolve()` | +| `engine.ts` | 按 `target.type` 从 registry 取对应 checker 执行 `execute()` | +| `store.ts` | 按 `target.type` 从 registry 取对应 checker 执行 `serialize()` | 注意:自动适配指上述中间层不需要新增 `switch/case` 或类型分支;开发者仍需按后续步骤更新配置示例、文档和测试。 diff --git a/README.md b/README.md index 5e22dfb..985c363 100644 --- a/README.md +++ b/README.md @@ -135,13 +135,8 @@ probes: # 拨测运行时配置(可省略) variables: # 配置变量(可省略) env_name: "生产" base_url: "https://api.example.com" - -defaults: # 全局默认值(均可省略) - interval: "30s" - timeout: "10s" - # http: ... - # cmd: ... - # llm: ... + default_interval: "30s" # 通过变量在多个 target 间共享常用值 + default_timeout: "10s" targets: # 拨测目标列表(必填) - id: "baidu-home" @@ -197,14 +192,15 @@ targets: # 拨测目标列表(必填) 控制台始终输出(pretty 格式),文件始终输出 JSONL 格式并支持滚动。`rotation.size` 和 `rotation.frequency` 任一条件触发即滚动。 -### defaults — 全局默认值 +### 内置默认值 -| 字段 | 说明 | 必填 | 默认值 | -| ---------- | -------- | ---- | ------ | -| `interval` | 拨测间隔 | 否 | `30s` | -| `timeout` | 超时时间 | 否 | `10s` | +未显式配置时,系统使用以下内置默认值: -各 checker 专属的默认配置见对应章节。 +- `interval`:`30s`(拨测间隔) +- `timeout`:`10s`(超时时间) +- 各 checker 专属默认值见对应章节 + +如需在多个 target 间共享相同的配置值,可使用 `variables` 定义变量,然后在 target 中通过 `${var}` 引用。例如在 `variables` 中定义 `default_interval: "30s"`,在多个 target 的 `interval` 字段写 `${default_interval}`。 ### variables — 配置变量 @@ -227,30 +223,23 @@ targets: # 拨测目标列表(必填) | `description` | 目标描述,最长 500 字符,支持变量替换,可省略或显式 null,允许空字符串 | 否 | | | `type` | 目标类型:`http`、`cmd`、`db`、`tcp`、`udp`、`icmp`、`llm` | 是 | | | `group` | 分组名称 | 否 | `default` | -| `interval` | 覆盖全局拨测间隔 | 否 | | -| `timeout` | 覆盖全局超时时间 | 否 | | +| `interval` | 拨测间隔,未配置时使用内置默认值 `30s` | 否 | `30s` | +| `timeout` | 超时时间,未配置时使用内置默认值 `10s` | 否 | `10s` | --- ### HTTP Checker(`type: http`) -**全局默认值(`defaults.http`)** - -| 字段 | 说明 | 必填 | 默认值 | -| -------------- | -------------------------------------------------- | ---- | ------- | -| `maxBodyBytes` | 响应体最大字节数 | 否 | `100MB` | -| `headers` | 默认请求头(target 中的 headers 会合并覆盖同名头) | 否 | | - **配置项** -| 字段 | 说明 | 必填 | 默认值 | -| ------------------- | --------------------------------------- | ---- | ------- | -| `http.url` | 目标 URL | 是 | | -| `http.method` | HTTP 方法 | 否 | `GET` | -| `http.headers` | 请求头(与 defaults.http.headers 合并) | 否 | | -| `http.body` | 请求体 | 否 | | -| `http.ignoreSSL` | 忽略 HTTPS 证书校验 | 否 | `false` | -| `http.maxRedirects` | 最大重定向跟随次数 | 否 | `0` | +| 字段 | 说明 | 必填 | 默认值 | +| ------------------- | ------------------- | ---- | ------- | +| `http.url` | 目标 URL | 是 | | +| `http.method` | HTTP 方法 | 否 | `GET` | +| `http.headers` | 请求头 | 否 | | +| `http.body` | 请求体 | 否 | | +| `http.ignoreSSL` | 忽略 HTTPS 证书校验 | 否 | `false` | +| `http.maxRedirects` | 最大重定向跟随次数 | 否 | `0` | **expect 校验项** @@ -288,13 +277,6 @@ targets: # 拨测目标列表(必填) ### Cmd Checker(`type: cmd`) -**全局默认值(`defaults.cmd`)** - -| 字段 | 说明 | 必填 | 默认值 | -| ---------------- | -------------------------------------- | ---- | ------- | -| `maxOutputBytes` | 输出最大字节数 | 否 | `100MB` | -| `cwd` | 默认工作目录(相对于配置文件所在目录) | 否 | `.` | - **配置项** | 字段 | 说明 | 必填 | 默认值 | @@ -405,14 +387,6 @@ targets: # 拨测目标列表(必填) ### UDP Checker(`type: udp`) -**全局默认值(`defaults.udp`)** - -| 字段 | 说明 | 必填 | 默认值 | -| ------------------ | ---------------------------------------- | ---- | ------ | -| `encoding` | payload 编码 | 否 | `text` | -| `responseEncoding` | 响应解码 | 否 | `text` | -| `maxResponseBytes` | 响应最大字节数,支持 `KB`/`MB`/`GB` 单位 | 否 | `4KB` | - **配置项** | 字段 | 说明 | 必填 | 默认值 | @@ -503,33 +477,21 @@ ICMP checker 通过系统 `ping` 命令执行 ICMP 探测,支持 Linux、macOS ### LLM Checker(`type: llm`) -**全局默认值(`defaults.llm`)** - -| 字段 | 说明 | 必填 | 默认值 | -| ----------------- | ------------------- | ---- | ------- | -| `mode` | 调用模式 | 否 | | -| `headers` | 默认请求头 | 否 | | -| `ignoreSSL` | 忽略 HTTPS 证书校验 | 否 | `false` | -| `options` | 生成选项 | 否 | | -| `providerOptions` | Provider 专属选项 | 否 | | - -不支持 `provider`、`url`、`model`、`key`、`authToken`、`prompt`。 - **配置项** -| 字段 | 说明 | 必填 | 默认值 | -| --------------------- | ----------------------------------------------------------- | ---- | ------- | -| `llm.provider` | 模型提供方:`openai`、`openai-responses`、`anthropic` | 是 | | -| `llm.url` | API base URL | 是 | | -| `llm.model` | 模型名称 | 是 | | -| `llm.prompt` | 单轮 prompt | 是 | | -| `llm.mode` | 调用模式:`http`(非流式)或 `stream`(流式) | 否 | `http` | -| `llm.key` | API key,支持 `${VAR}` 变量替换 | 否 | `""` | -| `llm.authToken` | Bearer token(仅 `anthropic` provider,与 `key` 互斥) | 否 | | -| `llm.headers` | 附加请求头(与 `defaults.llm.headers` 合并) | 否 | | -| `llm.ignoreSSL` | 忽略 HTTPS 证书校验 | 否 | `false` | -| `llm.options` | 生成选项(与 `defaults.llm.options` 合并) | 否 | | -| `llm.providerOptions` | Provider 专属选项(与 `defaults.llm.providerOptions` 合并) | 否 | | +| 字段 | 说明 | 必填 | 默认值 | +| --------------------- | ------------------------------------------------------ | ---- | ------- | +| `llm.provider` | 模型提供方:`openai`、`openai-responses`、`anthropic` | 是 | | +| `llm.url` | API base URL | 是 | | +| `llm.model` | 模型名称 | 是 | | +| `llm.prompt` | 单轮 prompt | 是 | | +| `llm.mode` | 调用模式:`http`(非流式)或 `stream`(流式) | 否 | `http` | +| `llm.key` | API key,支持 `${VAR}` 变量替换 | 否 | `""` | +| `llm.authToken` | Bearer token(仅 `anthropic` provider,与 `key` 互斥) | 否 | | +| `llm.headers` | 附加请求头 | 否 | | +| `llm.ignoreSSL` | 忽略 HTTPS 证书校验 | 否 | `false` | +| `llm.options` | 生成选项 | 否 | | +| `llm.providerOptions` | Provider 专属选项 | 否 | | `llm.options` 支持 `maxOutputTokens`(默认 `16`)、`temperature`(默认 `0`)、`topP`、`topK`、`presencePenalty`、`frequencyPenalty`、`stopSequences`、`seed`。 diff --git a/opencode.json b/opencode.json index fc32d47..6c305c9 100644 --- a/opencode.json +++ b/opencode.json @@ -6,5 +6,16 @@ "type": "local", "command": ["bunx", "tdesign-mcp-server@latest"] } + }, + "permission": { + "bash": { + "npm *": "deny", + "npx *": "deny", + "pnpm *": "deny", + "pnpx *": "deny" + }, + "external_directory": { + "/tmp/**": "allow" + } } } diff --git a/openspec/specs/checker-cohesion-structure/spec.md b/openspec/specs/checker-cohesion-structure/spec.md index fbfa406..38966d2 100644 --- a/openspec/specs/checker-cohesion-structure/spec.md +++ b/openspec/specs/checker-cohesion-structure/spec.md @@ -31,7 +31,7 @@ - **THEN** 这些类型 SHALL 全部定义在该 checker 目录的 `types.ts` 中,不在顶层 `types.ts` 中 #### Scenario: schema.ts 包含 TypeBox schema 定义 -- **WHEN** 开发者需要该 checker 的 config/defaults/expect schema +- **WHEN** 开发者需要该 checker 的 config 或 expect schema - **THEN** 这些 schema SHALL 定义在该 checker 目录的 `schema.ts` 中 #### Scenario: execute.ts 包含 Checker 类实现 diff --git a/openspec/specs/checker-runner-abstraction/spec.md b/openspec/specs/checker-runner-abstraction/spec.md index 16050e0..0b47a54 100644 --- a/openspec/specs/checker-runner-abstraction/spec.md +++ b/openspec/specs/checker-runner-abstraction/spec.md @@ -5,15 +5,15 @@ ## Requirements ### Requirement: Checker 配置契约片段 -系统 SHALL 支持 checker 提供自身 TypeBox 配置契约片段,用于描述该 checker 的 defaults 分组、target 领域分组和 expect 分组。公共配置加载模块 SHALL 通过 registry 获取已注册 checker 的契约片段,并组合为启动期 Ajv 契约校验流程和外部 `probe-config.schema.json` 导出流程。 +系统 SHALL 支持 checker 提供自身 TypeBox 配置契约片段,用于描述该 checker 的 target 领域分组和 expect 分组。公共配置加载模块 SHALL 通过 registry 获取已注册 checker 的契约片段,并组合为启动期 Ajv 契约校验流程和外部 `probe-config.schema.json` 导出流程。 #### Scenario: HTTP checker 提供契约片段 - **WHEN** HTTP checker 被注册 -- **THEN** registry SHALL 能提供 HTTP defaults、HTTP target 和 HTTP expect 的 TypeBox 契约片段 +- **THEN** registry SHALL 能提供 HTTP target 和 HTTP expect 的 TypeBox 契约片段 #### Scenario: Cmd checker 提供契约片段 - **WHEN** Cmd checker 被注册 -- **THEN** registry SHALL 能提供 Cmd defaults、Cmd target 和 Cmd expect 的 TypeBox 契约片段 +- **THEN** registry SHALL 能提供 Cmd target 和 Cmd expect 的 TypeBox 契约片段 #### Scenario: 新 checker 只维护自身契约 - **WHEN** 开发者新增一个 checker 类型 diff --git a/openspec/specs/config-variables/spec.md b/openspec/specs/config-variables/spec.md index 211fca9..322a25a 100644 --- a/openspec/specs/config-variables/spec.md +++ b/openspec/specs/config-variables/spec.md @@ -121,7 +121,7 @@ targets 中的字符串值 SHALL 支持 `${key}` 语法引用变量。系统 SHA - **THEN** 系统 SHALL 将该字段替换为 string 类型的 "localhost" ### Requirement: 替换范围限制 -变量替换 SHALL 仅作用于 targets 段。`id` 和 `type` 字段 MUST NOT 参与变量替换。`server`、`probes`、`defaults` 段 MUST NOT 参与变量替换。系统 SHALL 递归遍历 target 对象树中所有字符串值进行替换(包括嵌套对象和数组元素中的字符串)。 +变量替换 SHALL 仅作用于 targets 段。`id` 和 `type` 字段 MUST NOT 参与变量替换。`server`、`probes` 段 MUST NOT 参与变量替换。系统 SHALL 递归遍历 target 对象树中所有字符串值进行替换(包括嵌套对象和数组元素中的字符串)。顶层 `defaults` 不再是合法配置段,因此不属于变量替换范围。 #### Scenario: target 嵌套对象中的变量替换 - **WHEN** target 配置 `http.headers.Authorization: "${token}"` 且 variables 中定义 `token: "Bearer abc"` @@ -139,10 +139,6 @@ targets 中的字符串值 SHALL 支持 `${key}` 语法引用变量。系统 SHA - **WHEN** target 配置 `type: "${checker_type}"` 且 variables 中定义 `checker_type: "http"` - **THEN** 系统 SHALL 保持 type 字段值为字面量 `"${checker_type}"`,不进行替换 -#### Scenario: defaults 段不替换 -- **WHEN** defaults 配置 `interval: "${default_interval}"` 且 variables 中定义 `default_interval: "30s"` -- **THEN** 系统 SHALL 保持 defaults.interval 为字面量 `"${default_interval}"`,不进行替换 - #### Scenario: server 段不替换 - **WHEN** server 配置 `listen.host: "${server_host}"` 且 variables 中定义 `server_host: "0.0.0.0"` - **THEN** 系统 SHALL 保持 server.listen.host 为字面量 `"${server_host}"`,不进行替换 @@ -151,6 +147,10 @@ targets 中的字符串值 SHALL 支持 `${key}` 语法引用变量。系统 SHA - **WHEN** probes 配置 `execution.maxConcurrentChecks: "${max_checks}"` 且 variables 中定义 `max_checks: 5` - **THEN** 系统 SHALL 保持 probes.execution.maxConcurrentChecks 为字面量 `"${max_checks}"`,不进行替换 +#### Scenario: defaults 段被拒绝 +- **WHEN** 配置文件声明顶层 `defaults` +- **THEN** 系统 SHALL 在契约校验阶段拒绝该未知字段,而不是尝试变量替换 + ### Requirement: 变量替换错误报告 变量替换阶段的错误 SHALL 作为 `ConfigValidationIssue` 输出,code 为 `unresolved-variable`。错误信息 SHALL 包含 target 索引、target id、字段路径和变量名。 diff --git a/openspec/specs/http-checker/spec.md b/openspec/specs/http-checker/spec.md new file mode 100644 index 0000000..65d6f7c --- /dev/null +++ b/openspec/specs/http-checker/spec.md @@ -0,0 +1,32 @@ +## Purpose + +定义 HTTP 类型拨测目标:通过 `type: http` 配置 HTTP/HTTPS 探测,支持请求方法、headers、body、SSL 校验、重定向和响应体大小限制,捕获 HTTP 状态码、响应头、响应体和执行耗时,按 expect 规则校验并生成 matched 判定。 + +## Requirements + +### Requirement: HTTP target 配置 +系统 SHALL 支持 `type: http` 的 target 配置,通过 `http.url` 描述目标 HTTP 地址,并通过可选字段控制请求方法、请求体、headers、SSL 校验、重定向和响应体大小限制。 + +#### Scenario: 解析最简 HTTP target +- **WHEN** YAML 中 target 配置 `type: http` 和 `http.url: "https://example.com"` +- **THEN** 系统 SHALL 将其解析为 HTTP checker,并填充 `method=GET`、`maxBodyBytes=100MB`、`ignoreSSL=false`、`maxRedirects=0`、headers、body、interval、timeout、group 和 expect 配置 + +#### Scenario: HTTP target 缺少 url +- **WHEN** YAML 中 target 配置 `type: http` 但缺少 `http.url` +- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 http.url 字段 + +#### Scenario: HTTP target 覆盖请求方法 +- **WHEN** YAML 中某个 HTTP target 显式配置 `http.method: POST` +- **THEN** 该 target SHALL 使用自身 method 字段的值,而不是内置默认值 GET + +#### Scenario: HTTP target 覆盖响应体大小限制 +- **WHEN** YAML 中某个 HTTP target 显式配置 `http.maxBodyBytes` +- **THEN** 该 target SHALL 使用自身 maxBodyBytes 字段的值,而不是内置默认值 100MB + +#### Scenario: HTTP target 覆盖 headers +- **WHEN** YAML 中某个 HTTP target 显式配置 `http.headers` +- **THEN** 该 target SHALL 使用自身 headers 字段的值,不再与规则文件级 defaults 合并 + +#### Scenario: HTTP 序列化展示摘要 +- **WHEN** 系统同步 HTTP target 到 targets 表 +- **THEN** `target` 展示摘要 SHALL 为 HTTP URL,`config` JSON SHALL 包含 resolved 后的 url、method、headers、body、ignoreSSL、maxBodyBytes 和 maxRedirects diff --git a/openspec/specs/probe-config/spec.md b/openspec/specs/probe-config/spec.md index 8de9a41..29dfd77 100644 --- a/openspec/specs/probe-config/spec.md +++ b/openspec/specs/probe-config/spec.md @@ -24,25 +24,27 @@ - **THEN** 系统 SHALL 以配置错误退出并提示 `server` 中存在未知字段 `dataDir` ### Requirement: YAML 配置文件格式 -系统 SHALL 支持通过 YAML 配置文件定义全部运行参数,包括 server 配置、probes 执行配置、可选的 variables 段、checker 默认值和 typed target 列表(含可选 group 字段)。server 配置 SHALL 将 HTTP 监听参数放在 `server.listen` 分组,将本地数据目录和历史数据保留时长放在 `server.storage` 分组,将运行时日志配置放在 `server.logging` 分组。拨测全局执行策略 SHALL 放在 `probes.execution` 分组。target MUST 使用 `id` 字段作为唯一标识符,MUST 使用 `type` 字段声明 checker 类型,SHALL 支持可选的 `name` 字段作为展示名称元信息,SHALL 支持可选的 `description` 字段作为目标说明。`name` 和 `description` 均 SHALL 允许省略或显式配置为 `null`;省略或显式 null 时解析结果 SHALL 保留为 null。HTTP 领域字段 MUST 放在 `http` 分组,cmd 领域字段 MUST 放在 `cmd` 分组,db 领域字段 MUST 放在 `db` 分组,tcp 领域字段 MUST 放在 `tcp` 分组,icmp 领域字段 MUST 放在 `icmp` 分组,udp 领域字段 MUST 放在 `udp` 分组,LLM 领域字段 MUST 放在 `llm` 分组。HTTP target 的 `http` 分组 SHALL 支持可选的 `ignoreSSL`(布尔值)和 `maxRedirects`(非负整数)字段。Db target 的 `db` 分组 SHALL 支持 `url`(必填)和 `query`(可选)字段。Tcp target 的 `tcp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`readBanner`(可选)、`bannerReadTimeout`(可选)和 `maxBannerBytes`(可选)字段。Icmp target 的 `icmp` 分组 SHALL 支持 `host`(必填)、`count`(可选,默认 3)和 `packetSize`(可选,默认 56)字段。Udp target 的 `udp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`payload`(可选,默认空字符串)、`encoding`(可选,默认 `text`)、`responseEncoding`(可选,默认 `text`)和 `maxResponseBytes`(可选,默认 4096)字段。LLM target 的 `llm` 分组 SHALL 支持 `provider`(必填)、`url`(必填)、`model`(必填)、`prompt`(必填)、`mode`(可选,默认 `http`)、`key`(可选,默认空字符串)、`authToken`(可选)、`headers`(可选)、`ignoreSSL`(可选,默认 `false`)、`options`(可选)和 `providerOptions`(可选)字段。 - -`defaults.http` 分组 SHALL 仅支持 `headers`(可选)和 `maxBodyBytes`(可选)字段。`defaults.http` 分组 MUST NOT 支持 `method` 字段。`defaults.tcp` 分组 SHALL 仅支持 `bannerReadTimeout`(可选)和 `maxBannerBytes`(可选)字段。`defaults.icmp` 分组 SHALL 仅支持空对象。`defaults.udp` 分组 SHALL 仅支持 `encoding`(可选)、`responseEncoding`(可选)和 `maxResponseBytes`(可选)字段。`defaults.llm` 分组 SHALL 仅支持 `mode`(可选)、`headers`(可选)、`ignoreSSL`(可选)、`options`(可选)和 `providerOptions`(可选)字段。 +系统 SHALL 支持通过 YAML 配置文件定义全部运行参数,包括 server 配置、probes 执行配置、可选的 variables 段和 typed target 列表(含可选 group 字段)。server 配置 SHALL 将 HTTP 监听参数放在 `server.listen` 分组,将本地数据目录和历史数据保留时长放在 `server.storage` 分组,将运行时日志配置放在 `server.logging` 分组。拨测全局执行策略 SHALL 放在 `probes.execution` 分组。target MUST 使用 `id` 字段作为唯一标识符,MUST 使用 `type` 字段声明 checker 类型,SHALL 支持可选的 `name` 字段作为展示名称元信息,SHALL 支持可选的 `description` 字段作为目标说明。`name` 和 `description` 均 SHALL 允许省略或显式配置为 `null`;省略或显式 null 时解析结果 SHALL 保留为 null。HTTP 领域字段 MUST 放在 `http` 分组,cmd 领域字段 MUST 放在 `cmd` 分组,db 领域字段 MUST 放在 `db` 分组,tcp 领域字段 MUST 放在 `tcp` 分组,icmp 领域字段 MUST 放在 `icmp` 分组,udp 领域字段 MUST 放在 `udp` 分组,LLM 领域字段 MUST 放在 `llm` 分组。HTTP target 的 `http` 分组 SHALL 支持可选的 `ignoreSSL`(布尔值)和 `maxRedirects`(非负整数)字段。Db target 的 `db` 分组 SHALL 支持 `url`(必填)和 `query`(可选)字段。Tcp target 的 `tcp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`readBanner`(可选)、`bannerReadTimeout`(可选)和 `maxBannerBytes`(可选)字段。Icmp target 的 `icmp` 分组 SHALL 支持 `host`(必填)、`count`(可选,默认 3)和 `packetSize`(可选,默认 56)字段。Udp target 的 `udp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`payload`(可选,默认空字符串)、`encoding`(可选,默认 `text`)、`responseEncoding`(可选,默认 `text`)和 `maxResponseBytes`(可选,默认 4096)字段。LLM target 的 `llm` 分组 SHALL 支持 `provider`(必填)、`url`(必填)、`model`(必填)、`prompt`(必填)、`mode`(可选,默认 `http`)、`key`(可选,默认空字符串)、`authToken`(可选)、`headers`(可选)、`ignoreSSL`(可选,默认 `false`)、`options`(可选)和 `providerOptions`(可选)字段。顶层 `defaults` 不再是合法配置段。 #### Scenario: 完整配置文件解析 -- **WHEN** 系统启动并读取包含 server.listen、server.storage、server.logging、probes.execution、variables、defaults、targets(含 id、group 字段)的 YAML 配置文件 +- **WHEN** 系统启动并读取包含 server.listen、server.storage、server.logging、probes.execution、variables 和 targets(含 id、group 字段)的 YAML 配置文件 - **THEN** 系统 SHALL 正确解析所有字段并用于初始化服务、调度引擎和对应 checker runner +#### Scenario: defaults 配置段被拒绝 +- **WHEN** 配置文件声明顶层 `defaults` +- **THEN** 系统 SHALL 以配置错误退出并提示存在未知字段 `defaults` + #### Scenario: 最简 HTTP 配置文件解析 -- **WHEN** 系统读取只包含一个 `type: http` target(含 `id` 和 `http.url`)的 YAML 配置文件(省略 server、probes、variables、defaults 和 expect) -- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(host=127.0.0.1, port=3000, dataDir=./data, interval=30s, timeout=10s, probes.execution.maxConcurrentChecks=20, server.storage.retention=7d, http.method=GET, http.maxBodyBytes=100MB, http.ignoreSSL=false, http.maxRedirects=0, group="default"),并保留 name=null、description=null +- **WHEN** 系统读取只包含一个 `type: http` target(含 `id` 和 `http.url`)的 YAML 配置文件(省略 server、probes、variables 和 expect) +- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(host=127.0.0.1, port=3000, dataDir=./data, interval=30s, timeout=10s, probes.execution.maxConcurrentChecks=20, server.storage.retention=7d, http.method=GET, http.maxBodyBytes=100MB, http.ignoreSSL=false, http.maxRedirects=0, group=default),并保留 name=null、description=null #### Scenario: 最简 cmd 配置文件解析 - **WHEN** 系统读取只包含一个 `type: cmd` target(含 `id` 和 `cmd.exec`)的 YAML 配置文件 - **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, cmd.cwd 为配置文件所在目录, cmd.maxOutputBytes=100MB),并保留 name=null、description=null -#### Scenario: per-target 配置覆盖全局默认值 -- **WHEN** 某个 target 指定 interval、timeout 或对应领域分组中的默认字段 -- **THEN** 该 target SHALL 使用其自身的值,不受 defaults 中对应字段影响 +#### Scenario: per-target 配置覆盖内置默认值 +- **WHEN** 某个 target 指定 interval、timeout 或对应领域分组中的可选字段 +- **THEN** 该 target SHALL 使用其自身的值,不受对应内置默认值影响 #### Scenario: HTTP target 配置 ignoreSSL - **WHEN** YAML 配置中 HTTP target 设置 `http.ignoreSSL: true` @@ -58,43 +60,27 @@ #### Scenario: 最简 tcp 配置文件解析 - **WHEN** 系统读取只包含一个 `type: tcp` target(含 `id`、`tcp.host` 和 `tcp.port`)的 YAML 配置文件 -- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group="default", tcp.readBanner=false, tcp.bannerReadTimeout=2000, tcp.maxBannerBytes=4096),并保留 name=null、description=null - -#### Scenario: defaults.tcp 配置 banner 默认值 -- **WHEN** YAML 配置中 defaults.tcp 设置 `bannerReadTimeout` 和 `maxBannerBytes` -- **THEN** 未显式覆盖对应字段的 tcp target SHALL 使用 defaults.tcp 中的值 - -#### Scenario: defaults.http.method 触发校验错误 -- **WHEN** 配置文件中出现 `defaults.http.method` 字段 -- **THEN** 系统 SHALL 以配置错误退出,提示 defaults.http 中存在未知字段 method +- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group=default, tcp.readBanner=false, tcp.bannerReadTimeout=2000, tcp.maxBannerBytes=4096),并保留 name=null、description=null #### Scenario: per-target http.method 仍然有效 - **WHEN** HTTP target 配置 `http.method: POST` - **THEN** 系统 SHALL 使用 POST 作为该 target 的请求方法 #### Scenario: 未配置 http.method 使用内置默认值 -- **WHEN** HTTP target 未配置 `http.method` 且 defaults.http 中无 method 字段 +- **WHEN** HTTP target 未配置 `http.method` - **THEN** 系统 SHALL 使用内置默认值 GET 作为该 target 的请求方法 #### Scenario: 最简 icmp 配置文件解析 - **WHEN** 系统读取只包含一个 `type: icmp` target(含 `id` 和 `icmp.host`)的 YAML 配置文件 -- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group="default", icmp.count=3, icmp.packetSize=56),并保留 name=null、description=null +- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group=default, icmp.count=3, icmp.packetSize=56),并保留 name=null、description=null #### Scenario: 最简 udp 配置文件解析 - **WHEN** 系统读取只包含一个 `type: udp` target(含 `id`、`udp.host` 和 `udp.port`)的 YAML 配置文件 -- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group="default", udp.payload="", udp.encoding="text", udp.responseEncoding="text", udp.maxResponseBytes=4096),并保留 name=null、description=null - -#### Scenario: defaults.udp 配置默认值 -- **WHEN** YAML 配置中 defaults.udp 设置 `encoding`、`responseEncoding` 和 `maxResponseBytes` -- **THEN** 未显式覆盖对应字段的 udp target SHALL 使用 defaults.udp 中的值 +- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group=default, udp.payload=空字符串, udp.encoding=text, udp.responseEncoding=text, udp.maxResponseBytes=4096),并保留 name=null、description=null #### Scenario: 最简 llm 配置文件解析 - **WHEN** 系统读取只包含一个 `type: llm` target(含 `id`、`llm.provider`、`llm.url`、`llm.model` 和 `llm.prompt`)的 YAML 配置文件 -- **THEN** 系统 SHALL 使用内置默认值填充未指定字段(interval=30s, timeout=10s, group="default", llm.mode="http", llm.key="", llm.ignoreSSL=false, llm.options.maxOutputTokens=16, llm.options.temperature=0),并保留 name=null、description=null - -#### Scenario: defaults.llm 配置默认值 -- **WHEN** YAML 配置中 defaults.llm 设置 `mode`、`headers`、`ignoreSSL`、`options` 或 `providerOptions` -- **THEN** 未显式覆盖对应字段的 llm target SHALL 使用 defaults.llm 中的值 +- **THEN** 系统 SHALL 使用内置默认值填充未指定字段(interval=30s, timeout=10s, group=default, llm.mode=http, llm.key=空字符串, llm.ignoreSSL=false, llm.options.maxOutputTokens=16, llm.options.temperature=0),并保留 name=null、description=null ### Requirement: CLI 参数 系统 SHALL 通过单一命令行参数接受 YAML 配置文件路径。 @@ -126,6 +112,10 @@ - **WHEN** YAML 中某个 target 缺少 id 或 type 字段 - **THEN** 系统 SHALL 以错误退出,提示哪个 target 缺少哪个字段 +#### Scenario: 顶层 defaults 字段非法 +- **WHEN** YAML 配置文件声明顶层 `defaults` +- **THEN** 系统 SHALL 以配置错误退出,提示 `defaults` 是未知字段 + #### Scenario: HTTP target 缺少 url - **WHEN** YAML 中某个 target 配置 `type: http` 但缺少 `http.url` - **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 http.url 字段 @@ -203,7 +193,7 @@ - **THEN** 系统 SHALL 以错误退出,提示该 target 的 http.body 必须为字符串 #### Scenario: maxBodyBytes 数字非法 -- **WHEN** YAML 中某个 HTTP target 的 `http.maxBodyBytes` 或 defaults.http.maxBodyBytes 是负数、非整数或非安全整数 +- **WHEN** YAML 中某个 HTTP target 的 `http.maxBodyBytes` 是负数、非整数或非安全整数 - **THEN** 系统 SHALL 以错误退出,提示 maxBodyBytes 必须为非负安全整数字节数或合法 size 字符串 #### Scenario: status 模式非法 @@ -291,7 +281,7 @@ - **THEN** 系统 SHALL 以错误退出,提示未知字段所在路径 #### Scenario: 动态 headers 字段允许 -- **WHEN** YAML 中 `http.headers`、`defaults.http.headers` 或 `expect.headers` 包含任意 header 名称,且对应值符合契约 +- **WHEN** YAML 中 `http.headers` 或 `expect.headers` 包含任意 header 名称,且对应值符合契约 - **THEN** 系统 SHALL 接受这些动态 header 名称 #### Scenario: 动态 env 字段允许 @@ -312,7 +302,7 @@ #### Scenario: 导出配置 JSON Schema - **WHEN** 仓库生成或检查配置契约 -- **THEN** 根目录 SHALL 存在 draft-07 `probe-config.schema.json`,且其内容 SHALL 与当前公共 fragments 和已注册 checker fragments 组装出的完整 schema 一致(包含 variables 段和 target 的 id/name 字段)。所有 `RawValueExpectation` 字段的 schema SHALL 声明为 `anyOf: [primitiveValue, matcherObject]` 联合类型,`RawKeyedExpectations` 的 dynamic value schema SHALL 复用 `RawValueExpectation` +- **THEN** 根目录 SHALL 存在 draft-07 `probe-config.schema.json`,且其内容 SHALL 与当前公共 fragments 和已注册 checker fragments 组装出的完整 schema 一致(包含 variables 段和 target 的 id/name 字段,且不包含顶层 defaults)。所有 `RawValueExpectation` 字段的 schema SHALL 声明为 `anyOf: [primitiveValue, matcherObject]` 联合类型,`RawKeyedExpectations` 的 dynamic value schema SHALL 复用 `RawValueExpectation` #### Scenario: JSON Schema RawValueExpectation 接受原始值 - **WHEN** 使用 JSON Schema 校验配置文件中 RawValueExpectation 字段值为数字 `5000` @@ -431,7 +421,7 @@ - **THEN** 系统 SHALL 接受该配置并在运行期使用完整执行耗时进行 matcher 校验 #### Scenario: 动态 headers 字段允许 -- **WHEN** YAML 中 `http.headers`、`defaults.http.headers`、`llm.headers`、`defaults.llm.headers` 或 `expect.headers` 包含任意 header 名称,且对应值符合契约 +- **WHEN** YAML 中 `http.headers`、`llm.headers` 或 `expect.headers` 包含任意 header 名称,且对应值符合契约 - **THEN** 系统 SHALL 接受这些动态 header 名称 #### Scenario: ContentExpectations 字段必须为数组 @@ -546,11 +536,11 @@ - **THEN** 系统 SHALL 以配置错误退出,提示 tcp.readBanner 必须为布尔值 #### Scenario: tcp bannerReadTimeout 非法 -- **WHEN** YAML 中 tcp target 或 defaults.tcp 的 `bannerReadTimeout` 不是非负有限数字 +- **WHEN** YAML 中 tcp target 的 `bannerReadTimeout` 不是非负有限数字 - **THEN** 系统 SHALL 以配置错误退出,提示 bannerReadTimeout 格式错误 #### Scenario: tcp maxBannerBytes 非法 -- **WHEN** YAML 中 tcp target 或 defaults.tcp 的 `maxBannerBytes` 不是合法 size 值 +- **WHEN** YAML 中 tcp target 的 `maxBannerBytes` 不是合法 size 值 - **THEN** 系统 SHALL 以配置错误退出,提示 maxBannerBytes 格式错误 #### Scenario: tcp expect connected 类型非法 @@ -569,12 +559,8 @@ - **WHEN** YAML 中 tcp target 的 `tcp` 分组包含 `tls: true` 等未知字段 - **THEN** 系统 SHALL 以配置错误退出,提示 tcp 分组包含未知字段 -#### Scenario: defaults.tcp 未知字段失败 -- **WHEN** YAML 中 defaults.tcp 包含 `host` 或其他非默认字段 -- **THEN** 系统 SHALL 以配置错误退出,提示 defaults.tcp 包含未知字段 - ### Requirement: LLM 配置校验 -系统 SHALL 在启动期对 llm checker 的配置契约和语义执行严格校验。LLM target 的 `llm` 分组 SHALL 只允许 `provider`、`url`、`model`、`prompt`、`mode`、`key`、`authToken`、`headers`、`ignoreSSL`、`options` 和 `providerOptions` 字段。`defaults.llm` 分组 SHALL 只允许 `mode`、`headers`、`ignoreSSL`、`options` 和 `providerOptions` 字段。LLM expect SHALL 只允许 `status`、`headers`、`output`、`finishReason`、`rawFinishReason`、`usage`、`stream` 和 `durationMs` 字段。`expect.output` MUST 为 `RawContentExpectations` 数组。`expect.finishReason`、`expect.rawFinishReason`、`expect.usage.*`、`expect.stream.firstTokenMs` 和 `expect.durationMs` SHALL 使用 `RawValueExpectation`。未知字段、非法 provider、非法 URL、非法 mode、非法认证组合、非法 options、非法 output expectation 和 `mode: http` 下配置 `expect.stream` MUST 导致启动期配置错误。语义校验 MUST NOT 修改 Raw llm expect 输入。 +系统 SHALL 在启动期对 llm checker 的配置契约和语义执行严格校验。LLM target 的 `llm` 分组 SHALL 只允许 `provider`、`url`、`model`、`prompt`、`mode`、`key`、`authToken`、`headers`、`ignoreSSL`、`options` 和 `providerOptions` 字段。LLM expect SHALL 只允许 `status`、`headers`、`output`、`finishReason`、`rawFinishReason`、`usage`、`stream` 和 `durationMs` 字段。`expect.output` MUST 为 `RawContentExpectations` 数组。`expect.finishReason`、`expect.rawFinishReason`、`expect.usage.*`、`expect.stream.firstTokenMs` 和 `expect.durationMs` SHALL 使用 `RawValueExpectation`。未知字段、非法 provider、非法 URL、非法 mode、非法认证组合、非法 options、非法 output expectation 和 `mode: http` 下配置 `expect.stream` MUST 导致启动期配置错误。语义校验 MUST NOT 修改 Raw llm expect 输入。 #### Scenario: llm provider 非法 - **WHEN** YAML 中 llm target 的 `llm.provider` 不是 `openai`、`openai-responses` 或 `anthropic` @@ -593,15 +579,15 @@ - **THEN** 系统 SHALL 以配置错误退出,提示 llm.prompt 必须为非空字符串 #### Scenario: llm mode 非法 -- **WHEN** YAML 中 llm target 或 defaults.llm 的 `mode` 不是 `http` 或 `stream` +- **WHEN** YAML 中 llm target 的 `mode` 不是 `http` 或 `stream` - **THEN** 系统 SHALL 以配置错误退出,提示 llm.mode 不合法 #### Scenario: llm headers 类型非法 -- **WHEN** YAML 中 llm target 或 defaults.llm 的 `headers` 不是对象,或任一 header 值不是字符串 +- **WHEN** YAML 中 llm target 的 `headers` 不是对象,或任一 header 值不是字符串 - **THEN** 系统 SHALL 以配置错误退出,提示 llm.headers 格式错误 #### Scenario: llm ignoreSSL 类型非法 -- **WHEN** YAML 中 llm target 或 defaults.llm 的 `ignoreSSL` 不是布尔值 +- **WHEN** YAML 中 llm target 的 `ignoreSSL` 不是布尔值 - **THEN** 系统 SHALL 以配置错误退出,提示 llm.ignoreSSL 必须为布尔值 #### Scenario: llm authToken provider 非法 @@ -613,11 +599,11 @@ - **THEN** 系统 SHALL 以配置错误退出,提示 key 与 authToken 不能同时配置 #### Scenario: llm options 非法 -- **WHEN** YAML 中 llm target 或 defaults.llm 的 `options.maxOutputTokens` 不是正整数,`options.temperature`/`topP`/`topK`/`presencePenalty`/`frequencyPenalty`/`seed` 类型不合法,或 `options.stopSequences` 不是字符串数组 +- **WHEN** YAML 中 llm target 的 `options.maxOutputTokens` 不是正整数,`options.temperature`/`topP`/`topK`/`presencePenalty`/`frequencyPenalty`/`seed` 类型不合法,或 `options.stopSequences` 不是字符串数组 - **THEN** 系统 SHALL 以配置错误退出,提示 llm.options 格式错误 #### Scenario: llm providerOptions 非法 -- **WHEN** YAML 中 llm target 或 defaults.llm 的 `providerOptions` 不是 JSON object +- **WHEN** YAML 中 llm target 的 `providerOptions` 不是 JSON object - **THEN** 系统 SHALL 以配置错误退出,提示 llm.providerOptions 格式错误 #### Scenario: llm 禁止字段失败 diff --git a/probe-config.schema.json b/probe-config.schema.json index cb2cd9e..4fae3fd 100644 --- a/probe-config.schema.json +++ b/probe-config.schema.json @@ -5,212 +5,6 @@ "targets" ], "properties": { - "defaults": { - "additionalProperties": false, - "type": "object", - "properties": { - "interval": { - "type": "string" - }, - "timeout": { - "type": "string" - }, - "http": { - "additionalProperties": false, - "type": "object", - "properties": { - "headers": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - "maxBodyBytes": { - "anyOf": [ - { - "type": "string" - }, - { - "minimum": 0, - "type": "integer" - } - ] - } - } - }, - "cmd": { - "additionalProperties": false, - "type": "object", - "properties": { - "cwd": { - "type": "string" - }, - "maxOutputBytes": { - "anyOf": [ - { - "type": "string" - }, - { - "minimum": 0, - "type": "integer" - } - ] - } - } - }, - "db": { - "additionalProperties": false, - "type": "object", - "properties": {} - }, - "tcp": { - "additionalProperties": false, - "type": "object", - "properties": { - "bannerReadTimeout": { - "minimum": 0, - "type": "number" - }, - "maxBannerBytes": { - "anyOf": [ - { - "type": "string" - }, - { - "minimum": 0, - "type": "integer" - } - ] - } - } - }, - "icmp": { - "additionalProperties": false, - "type": "object", - "properties": {} - }, - "udp": { - "additionalProperties": false, - "type": "object", - "properties": { - "encoding": { - "anyOf": [ - { - "const": "text", - "type": "string" - }, - { - "const": "hex", - "type": "string" - }, - { - "const": "base64", - "type": "string" - } - ] - }, - "maxResponseBytes": { - "anyOf": [ - { - "type": "string" - }, - { - "minimum": 0, - "type": "integer" - } - ] - }, - "responseEncoding": { - "anyOf": [ - { - "const": "text", - "type": "string" - }, - { - "const": "hex", - "type": "string" - }, - { - "const": "base64", - "type": "string" - } - ] - } - } - }, - "llm": { - "additionalProperties": false, - "type": "object", - "properties": { - "headers": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - "ignoreSSL": { - "type": "boolean" - }, - "mode": { - "anyOf": [ - { - "const": "http", - "type": "string" - }, - { - "const": "stream", - "type": "string" - } - ] - }, - "options": { - "additionalProperties": false, - "type": "object", - "properties": { - "frequencyPenalty": { - "type": "number" - }, - "maxOutputTokens": { - "minimum": 1, - "type": "integer" - }, - "presencePenalty": { - "type": "number" - }, - "seed": { - "type": "number" - }, - "stopSequences": { - "type": "array", - "items": { - "type": "string" - } - }, - "temperature": { - "type": "number" - }, - "topK": { - "type": "number" - }, - "topP": { - "type": "number" - } - } - }, - "providerOptions": { - "type": "object", - "patternProperties": { - "^(.*)$": { - "additionalProperties": true, - "type": "object", - "properties": {} - } - } - } - } - } - } - }, "probes": { "additionalProperties": false, "type": "object", diff --git a/probes.example.yaml b/probes.example.yaml index 2b568f9..c17f4c4 100644 --- a/probes.example.yaml +++ b/probes.example.yaml @@ -22,14 +22,6 @@ probes: execution: maxConcurrentChecks: 20 -defaults: - interval: "30s" - timeout: "10s" - http: - maxBodyBytes: "10MB" - cmd: - maxOutputBytes: "1MB" - variables: env_name: "演示" httpbin_base: "https://httpbin.org" diff --git a/src/server/checker/config-loader.ts b/src/server/checker/config-loader.ts index ba399ee..931f0db 100644 --- a/src/server/checker/config-loader.ts +++ b/src/server/checker/config-loader.ts @@ -3,7 +3,6 @@ import { dirname, resolve } from "node:path"; import type { ConfigValidationIssue } from "./schema/issues"; import type { - DefaultsConfig, ExecutionConfig, LoggingConfig, LogLevel, @@ -90,7 +89,6 @@ export async function loadConfig(configPath: string): Promise { const server = validated.server ?? {}; const listen = server.listen ?? {}; const storage = server.storage ?? {}; - const defaults = validated.defaults ?? {}; const host = listen.host ?? DEFAULT_HOST; const port = listen.port ?? DEFAULT_PORT; @@ -109,11 +107,11 @@ export async function loadConfig(configPath: string): Promise { throwConfigIssues(dedupeIssues(allRuntimeIssues)); } - const defaultIntervalMs = parseDuration(defaults.interval ?? DEFAULT_INTERVAL); - const defaultTimeoutMs = parseDuration(defaults.timeout ?? DEFAULT_TIMEOUT); + const defaultIntervalMs = parseDuration(DEFAULT_INTERVAL); + const defaultTimeoutMs = parseDuration(DEFAULT_TIMEOUT); const targets: ResolvedTargetBase[] = validated.targets.map((target) => - resolveTarget(target, defaults, defaultIntervalMs, defaultTimeoutMs, configDir), + resolveTarget(target, defaultIntervalMs, defaultTimeoutMs, configDir), ); return { configDir, dataDir, host, logging, maxConcurrentChecks, port, retentionMs, targets }; @@ -193,16 +191,15 @@ function resolveRetention(storage: ServerStorageConfig): number { function resolveTarget( target: RawTargetConfig, - defaults: DefaultsConfig, defaultIntervalMs: number, defaultTimeoutMs: number, configDir: string, ): ResolvedTargetBase { - const intervalMs = parseDuration(target.interval ?? defaults.interval ?? DEFAULT_INTERVAL); - const timeoutMs = parseDuration(target.timeout ?? defaults.timeout ?? DEFAULT_TIMEOUT); + const intervalMs = parseDuration(target.interval ?? DEFAULT_INTERVAL); + const timeoutMs = parseDuration(target.timeout ?? DEFAULT_TIMEOUT); const checker = checkerRegistry.get(target.type); - const result = checker.resolve(target, { configDir, defaultIntervalMs, defaults, defaultTimeoutMs }); + const result = checker.resolve(target, { configDir, defaultIntervalMs, defaultTimeoutMs }); result.intervalMs = intervalMs; result.timeoutMs = timeoutMs; @@ -275,11 +272,9 @@ function validateConfig(config: RawProbeConfig): ConfigValidationIssue[] { } for (const checker of checkerRegistry.definitions) { - issues.push(...checker.validate({ defaults: config.defaults ?? {}, targets: config.targets })); + issues.push(...checker.validate({ targets: config.targets })); } - validateDurationValue(config.defaults?.interval, "defaults.interval", issues); - validateDurationValue(config.defaults?.timeout, "defaults.timeout", issues); validateDurationValue( isString(config.server?.storage?.retention) ? config.server.storage.retention : undefined, "server.storage.retention", diff --git a/src/server/checker/runner/cmd/execute.ts b/src/server/checker/runner/cmd/execute.ts index 0fc05e1..7ca339f 100644 --- a/src/server/checker/runner/cmd/execute.ts +++ b/src/server/checker/runner/cmd/execute.ts @@ -209,12 +209,11 @@ export class CommandChecker implements CheckerDefinition resolve(target: RawTargetConfig, context: ResolveContext): ResolvedCommandTarget { const t = target as RawTargetConfig & { cmd: CommandTargetConfig; type: "cmd" }; - const cmdDefaults = context.defaults["cmd"] as undefined | { cwd?: string; maxOutputBytes?: string }; - const cwd = t.cmd.cwd ?? cmdDefaults?.cwd ?? "."; + const cwd = t.cmd.cwd ?? "."; const resolvedCwd = resolve(context.configDir, cwd); - const maxOutputBytes = parseSize(t.cmd.maxOutputBytes ?? cmdDefaults?.maxOutputBytes ?? "100MB"); + const maxOutputBytes = parseSize(t.cmd.maxOutputBytes ?? "100MB"); const env = { ...process.env, ...(t.cmd.env ?? {}) } as Record; diff --git a/src/server/checker/runner/cmd/schema.ts b/src/server/checker/runner/cmd/schema.ts index 317491b..cf5f550 100644 --- a/src/server/checker/runner/cmd/schema.ts +++ b/src/server/checker/runner/cmd/schema.ts @@ -20,13 +20,6 @@ export const commandCheckerSchemas: CheckerSchemas = { }, { additionalProperties: false }, ), - defaults: Type.Object( - { - cwd: Type.Optional(Type.String()), - maxOutputBytes: Type.Optional(sizeSchema), - }, - { additionalProperties: false }, - ), expect: Type.Object( { durationMs: Type.Optional(createRawValueExpectationSchema()), diff --git a/src/server/checker/runner/cmd/types.ts b/src/server/checker/runner/cmd/types.ts index 0b2d42c..716e4e8 100644 --- a/src/server/checker/runner/cmd/types.ts +++ b/src/server/checker/runner/cmd/types.ts @@ -6,11 +6,6 @@ import type { } from "../../expect/types"; import type { ResolvedTargetBase } from "../../types"; -export interface CommandDefaultsConfig { - cwd?: string; - maxOutputBytes?: string; -} - export interface CommandTargetConfig { args?: string[]; cwd?: string; diff --git a/src/server/checker/runner/cmd/validate.ts b/src/server/checker/runner/cmd/validate.ts index ee4505e..8a3becb 100644 --- a/src/server/checker/runner/cmd/validate.ts +++ b/src/server/checker/runner/cmd/validate.ts @@ -9,12 +9,6 @@ import { parseSize } from "../../utils"; export function validateCommandConfig(input: CheckerValidationInput): ConfigValidationIssue[] { const issues: ConfigValidationIssue[] = []; - const defaults = - isPlainRecord(input.defaults) && isPlainRecord(input.defaults["cmd"]) ? input.defaults["cmd"] : undefined; - - if (isSizeInput(defaults?.["maxOutputBytes"])) { - issues.push(...validateSizeValue(defaults["maxOutputBytes"], "defaults.cmd.maxOutputBytes")); - } for (let i = 0; i < input.targets.length; i++) { const target = input.targets[i] as unknown; diff --git a/src/server/checker/runner/db/schema.ts b/src/server/checker/runner/db/schema.ts index 750fdbd..7e7b6f9 100644 --- a/src/server/checker/runner/db/schema.ts +++ b/src/server/checker/runner/db/schema.ts @@ -20,7 +20,6 @@ export const dbCheckerSchemas: CheckerSchemas = { }, { additionalProperties: false }, ), - defaults: Type.Object({}, { additionalProperties: false }), expect: Type.Object( { durationMs: Type.Optional(createRawValueExpectationSchema()), diff --git a/src/server/checker/runner/http/execute.ts b/src/server/checker/runner/http/execute.ts index c9ad06e..eb691af 100644 --- a/src/server/checker/runner/http/execute.ts +++ b/src/server/checker/runner/http/execute.ts @@ -175,12 +175,9 @@ export class HttpChecker implements CheckerDefinition { resolve(target: RawTargetConfig, context: ResolveContext): ResolvedHttpTarget { const t = target as RawTargetConfig & { http: HttpTargetConfig; type: "http" }; - const httpDefaults = context.defaults["http"] as - | undefined - | { headers?: Record; maxBodyBytes?: string }; const method = t.http.method ?? "GET"; - const maxBodyBytes = parseSize(t.http.maxBodyBytes ?? httpDefaults?.maxBodyBytes ?? "100MB"); + const maxBodyBytes = parseSize(t.http.maxBodyBytes ?? "100MB"); const rawExpect = target.expect as RawHttpExpectConfig | undefined; const resolvedExpect: ResolvedHttpExpectConfig = rawExpect @@ -198,7 +195,7 @@ export class HttpChecker implements CheckerDefinition { group: target.group ?? "default", http: { body: t.http.body, - headers: { ...(httpDefaults?.headers ?? {}), ...(t.http.headers ?? {}) }, + headers: { ...(t.http.headers ?? {}) }, ignoreSSL: t.http.ignoreSSL ?? false, maxBodyBytes, maxRedirects: t.http.maxRedirects ?? 0, diff --git a/src/server/checker/runner/http/schema.ts b/src/server/checker/runner/http/schema.ts index 5c9910d..081b25a 100644 --- a/src/server/checker/runner/http/schema.ts +++ b/src/server/checker/runner/http/schema.ts @@ -25,13 +25,6 @@ export const httpCheckerSchemas: CheckerSchemas = { }, { additionalProperties: false }, ), - defaults: Type.Object( - { - headers: Type.Optional(stringMapSchema), - maxBodyBytes: Type.Optional(sizeSchema), - }, - { additionalProperties: false }, - ), expect: Type.Object( { body: Type.Optional(createRawContentExpectationsSchema()), diff --git a/src/server/checker/runner/http/types.ts b/src/server/checker/runner/http/types.ts index a51a973..040f464 100644 --- a/src/server/checker/runner/http/types.ts +++ b/src/server/checker/runner/http/types.ts @@ -8,12 +8,6 @@ import type { } from "../../expect/types"; import type { ResolvedTargetBase } from "../../types"; -export interface HttpDefaultsConfig { - headers?: Record; - maxBodyBytes?: string; - method?: string; -} - export interface HttpTargetConfig { body?: string; headers?: Record; diff --git a/src/server/checker/runner/http/validate.ts b/src/server/checker/runner/http/validate.ts index dd80c9d..df4e834 100644 --- a/src/server/checker/runner/http/validate.ts +++ b/src/server/checker/runner/http/validate.ts @@ -16,12 +16,6 @@ const ALLOWED_PROTOCOLS = new Set(["http:", "https:"]); export function validateHttpConfig(input: CheckerValidationInput): ConfigValidationIssue[] { const issues: ConfigValidationIssue[] = []; - const defaults = - isPlainRecord(input.defaults) && isPlainRecord(input.defaults["http"]) ? input.defaults["http"] : undefined; - - if (isSizeInput(defaults?.["maxBodyBytes"])) { - issues.push(...validateSizeValue(defaults["maxBodyBytes"], "defaults.http.maxBodyBytes")); - } for (let i = 0; i < input.targets.length; i++) { const target = input.targets[i] as unknown; diff --git a/src/server/checker/runner/icmp/schema.ts b/src/server/checker/runner/icmp/schema.ts index 7ad2d55..b0aa631 100644 --- a/src/server/checker/runner/icmp/schema.ts +++ b/src/server/checker/runner/icmp/schema.ts @@ -13,7 +13,6 @@ export const icmpCheckerSchemas: CheckerSchemas = { }, { additionalProperties: false }, ), - defaults: Type.Object({}, { additionalProperties: false }), expect: Type.Object( { alive: Type.Optional(Type.Boolean()), diff --git a/src/server/checker/runner/icmp/validate.ts b/src/server/checker/runner/icmp/validate.ts index b7c317b..3f21a90 100644 --- a/src/server/checker/runner/icmp/validate.ts +++ b/src/server/checker/runner/icmp/validate.ts @@ -9,19 +9,6 @@ import { issue, joinPath } from "../../schema/issues"; export function validatePingConfig(input: CheckerValidationInput): ConfigValidationIssue[] { const issues: ConfigValidationIssue[] = []; - const defaults = input.defaults["icmp"]; - if (defaults !== undefined && defaults !== null) { - const targetName = "defaults.icmp"; - if (!isPlainRecord(defaults)) { - issues.push(issue("invalid-type", "defaults.icmp", "必须为对象", targetName)); - } else { - const icmpDefaults = defaults; - for (const key of Object.keys(icmpDefaults)) { - issues.push(issue("unknown-field", joinPath("defaults.icmp", key), "是未知字段", targetName)); - } - } - } - for (let i = 0; i < input.targets.length; i++) { const target = input.targets[i] as unknown; if (!isPlainRecord(target)) continue; diff --git a/src/server/checker/runner/llm/execute.ts b/src/server/checker/runner/llm/execute.ts index 3875024..448c1f2 100644 --- a/src/server/checker/runner/llm/execute.ts +++ b/src/server/checker/runner/llm/execute.ts @@ -1,5 +1,3 @@ -import type { JSONObject } from "@ai-sdk/provider"; - import { APICallError, generateText, streamText } from "ai"; import { isError } from "es-toolkit"; @@ -133,43 +131,27 @@ export class LlmChecker implements CheckerDefinition { resolve(target: RawTargetConfig, context: ResolveContext): ResolvedLlmTarget { const t = target as RawTargetConfig & { llm: LlmTargetConfig; type: "llm" }; - const llmDefaults = context.defaults["llm"] as - | undefined - | { - headers?: Record; - ignoreSSL?: boolean; - mode?: string; - options?: Record; - providerOptions?: Record>; - }; const resolvedConfig = { authToken: t.llm.authToken, - headers: { ...(llmDefaults?.headers ?? {}), ...(t.llm.headers ?? {}) }, - ignoreSSL: t.llm.ignoreSSL ?? llmDefaults?.ignoreSSL ?? false, + headers: { ...(t.llm.headers ?? {}) }, + ignoreSSL: t.llm.ignoreSSL ?? false, key: t.llm.key ?? "", - mode: (t.llm.mode ?? llmDefaults?.mode ?? "http") as "http" | "stream", + mode: t.llm.mode ?? "http", model: t.llm.model, options: { - frequencyPenalty: - t.llm.options?.frequencyPenalty ?? (llmDefaults?.options?.["frequencyPenalty"] as number | undefined), - maxOutputTokens: - t.llm.options?.maxOutputTokens ?? (llmDefaults?.options?.["maxOutputTokens"] as number | undefined) ?? 16, - presencePenalty: - t.llm.options?.presencePenalty ?? (llmDefaults?.options?.["presencePenalty"] as number | undefined), - seed: t.llm.options?.seed ?? (llmDefaults?.options?.["seed"] as number | undefined), - stopSequences: - t.llm.options?.stopSequences ?? (llmDefaults?.options?.["stopSequences"] as string[] | undefined), - temperature: t.llm.options?.temperature ?? (llmDefaults?.options?.["temperature"] as number | undefined) ?? 0, - topK: t.llm.options?.topK ?? (llmDefaults?.options?.["topK"] as number | undefined), - topP: t.llm.options?.topP ?? (llmDefaults?.options?.["topP"] as number | undefined), + frequencyPenalty: t.llm.options?.frequencyPenalty, + maxOutputTokens: t.llm.options?.maxOutputTokens ?? 16, + presencePenalty: t.llm.options?.presencePenalty, + seed: t.llm.options?.seed, + stopSequences: t.llm.options?.stopSequences, + temperature: t.llm.options?.temperature ?? 0, + topK: t.llm.options?.topK, + topP: t.llm.options?.topP, }, prompt: t.llm.prompt, provider: t.llm.provider, - providerOptions: { - ...((llmDefaults?.providerOptions ?? {}) as Record), - ...(t.llm.providerOptions ?? {}), - }, + providerOptions: { ...(t.llm.providerOptions ?? {}) }, url: t.llm.url, }; diff --git a/src/server/checker/runner/llm/schema.ts b/src/server/checker/runner/llm/schema.ts index f6a758b..6e7c27c 100644 --- a/src/server/checker/runner/llm/schema.ts +++ b/src/server/checker/runner/llm/schema.ts @@ -43,16 +43,6 @@ export const llmCheckerSchemas: CheckerSchemas = { }, { additionalProperties: false }, ), - defaults: Type.Object( - { - headers: Type.Optional(stringMapSchema), - ignoreSSL: Type.Optional(Type.Boolean()), - mode: Type.Optional(Type.Union([Type.Literal("http"), Type.Literal("stream")])), - options: Type.Optional(createLlmOptionsSchema()), - providerOptions: Type.Optional(Type.Record(Type.String(), Type.Object({}, { additionalProperties: true }))), - }, - { additionalProperties: false }, - ), expect: Type.Object( { durationMs: Type.Optional(createRawValueExpectationSchema()), diff --git a/src/server/checker/runner/llm/types.ts b/src/server/checker/runner/llm/types.ts index 4532289..eb83112 100644 --- a/src/server/checker/runner/llm/types.ts +++ b/src/server/checker/runner/llm/types.ts @@ -23,14 +23,6 @@ export interface LlmCheckObservation { warnings: string[]; } -export interface LlmDefaultsConfig { - headers?: Record; - ignoreSSL?: boolean; - mode?: LlmMode; - options?: LlmOptions; - providerOptions?: Record; -} - export interface LlmHttpMetadata { headers: Record; status: number; diff --git a/src/server/checker/runner/llm/validate.ts b/src/server/checker/runner/llm/validate.ts index 61a8572..8bf9b3a 100644 --- a/src/server/checker/runner/llm/validate.ts +++ b/src/server/checker/runner/llm/validate.ts @@ -17,12 +17,6 @@ const ALLOWED_PROVIDERS = new Set(["anthropic", "openai", "openai-responses"]); export function validateLlmConfig(input: CheckerValidationInput): ConfigValidationIssue[] { const issues: ConfigValidationIssue[] = []; - const defaults = - isPlainRecord(input.defaults) && isPlainRecord(input.defaults["llm"]) ? input.defaults["llm"] : undefined; - - if (defaults) { - issues.push(...validateLlmDefaults(defaults, "defaults.llm")); - } for (let i = 0; i < input.targets.length; i++) { const target = input.targets[i] as unknown; @@ -39,28 +33,6 @@ function getTargetName(target: Record): string | undefined { return isString(target["id"]) ? target["id"] : undefined; } -function validateLlmDefaults(defaults: Record, path: string): ConfigValidationIssue[] { - const issues: ConfigValidationIssue[] = []; - - if (defaults["mode"] !== undefined && !ALLOWED_MODES.has(defaults["mode"] as string)) { - issues.push(issue("invalid-type", joinPath(path, "mode"), "必须为 http 或 stream")); - } - if (defaults["ignoreSSL"] !== undefined && !isBoolean(defaults["ignoreSSL"])) { - issues.push(issue("invalid-type", joinPath(path, "ignoreSSL"), "必须为布尔值")); - } - if (defaults["headers"] !== undefined) { - issues.push(...validateStringMap(defaults["headers"], joinPath(path, "headers"))); - } - if (defaults["options"] !== undefined) { - issues.push(...validateLlmOptions(defaults["options"], joinPath(path, "options"))); - } - if (defaults["providerOptions"] !== undefined) { - issues.push(...validateProviderOptions(defaults["providerOptions"], joinPath(path, "providerOptions"))); - } - - return issues; -} - function validateLlmExpect( target: Record, path: string, diff --git a/src/server/checker/runner/tcp/execute.ts b/src/server/checker/runner/tcp/execute.ts index e6d02d6..cb534b9 100644 --- a/src/server/checker/runner/tcp/execute.ts +++ b/src/server/checker/runner/tcp/execute.ts @@ -206,12 +206,9 @@ export class TcpChecker implements CheckerDefinition { resolve(target: RawTargetConfig, context: ResolveContext): ResolvedTcpTarget { const t = target as RawTargetConfig & { tcp: TcpTargetConfig; type: "tcp" }; - const tcpDefaults = context.defaults["tcp"] as - | undefined - | { bannerReadTimeout?: number; maxBannerBytes?: number | string }; - const maxBannerBytes = parseSize(t.tcp.maxBannerBytes ?? tcpDefaults?.maxBannerBytes ?? DEFAULT_MAX_BANNER_BYTES); - const bannerReadTimeout = t.tcp.bannerReadTimeout ?? tcpDefaults?.bannerReadTimeout ?? DEFAULT_BANNER_READ_TIMEOUT; + const maxBannerBytes = parseSize(t.tcp.maxBannerBytes ?? DEFAULT_MAX_BANNER_BYTES); + const bannerReadTimeout = t.tcp.bannerReadTimeout ?? DEFAULT_BANNER_READ_TIMEOUT; const rawExpect = target.expect as RawTcpExpectConfig | undefined; const resolvedExpect: ResolvedTcpExpectConfig = rawExpect diff --git a/src/server/checker/runner/tcp/schema.ts b/src/server/checker/runner/tcp/schema.ts index 73c89f2..d1832ce 100644 --- a/src/server/checker/runner/tcp/schema.ts +++ b/src/server/checker/runner/tcp/schema.ts @@ -19,13 +19,6 @@ export const tcpCheckerSchemas: CheckerSchemas = { }, { additionalProperties: false }, ), - defaults: Type.Object( - { - bannerReadTimeout: Type.Optional(Type.Number({ minimum: 0 })), - maxBannerBytes: Type.Optional(sizeSchema), - }, - { additionalProperties: false }, - ), expect: Type.Object( { banner: Type.Optional(createRawContentExpectationsSchema()), diff --git a/src/server/checker/runner/tcp/types.ts b/src/server/checker/runner/tcp/types.ts index cd62a1d..e0a4f2d 100644 --- a/src/server/checker/runner/tcp/types.ts +++ b/src/server/checker/runner/tcp/types.ts @@ -37,11 +37,6 @@ export interface ResolvedTcpTarget extends ResolvedTargetBase { type: "tcp"; } -export interface TcpDefaultsConfig { - bannerReadTimeout?: number; - maxBannerBytes?: number | string; -} - export interface TcpTargetConfig { bannerReadTimeout?: number; host: string; diff --git a/src/server/checker/runner/tcp/validate.ts b/src/server/checker/runner/tcp/validate.ts index 2c1add4..30951d0 100644 --- a/src/server/checker/runner/tcp/validate.ts +++ b/src/server/checker/runner/tcp/validate.ts @@ -9,8 +9,6 @@ import { issue, joinPath } from "../../schema/issues"; export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[] { const issues: ConfigValidationIssue[] = []; - issues.push(...validateTcpDefaults(input)); - for (let i = 0; i < input.targets.length; i++) { const target = input.targets[i] as unknown; if (!isPlainRecord(target)) continue; @@ -30,40 +28,6 @@ function isNonNegativeFiniteNumber(value: unknown): boolean { return isNumber(value) && Number.isFinite(value) && value >= 0; } -function validateTcpDefaults(input: CheckerValidationInput): ConfigValidationIssue[] { - const issues: ConfigValidationIssue[] = []; - const defaults = input.defaults["tcp"]; - if (defaults === undefined || defaults === null || !isPlainRecord(defaults)) return issues; - - const targetName = "defaults.tcp"; - - if (defaults["bannerReadTimeout"] !== undefined && !isNonNegativeFiniteNumber(defaults["bannerReadTimeout"])) { - issues.push(issue("invalid-type", "defaults.tcp.bannerReadTimeout", "必须为非负有限数字", targetName)); - } - - if (defaults["maxBannerBytes"] !== undefined) { - if ( - !isString(defaults["maxBannerBytes"]) && - !( - isNumber(defaults["maxBannerBytes"]) && - Number.isFinite(defaults["maxBannerBytes"]) && - defaults["maxBannerBytes"] >= 0 - ) - ) { - issues.push(issue("invalid-value", "defaults.tcp.maxBannerBytes", "必须为合法 size 值", targetName)); - } - } - - const allowedKeys = new Set(["bannerReadTimeout", "maxBannerBytes"]); - for (const key of Object.keys(defaults)) { - if (!allowedKeys.has(key)) { - issues.push(issue("unknown-field", joinPath("defaults.tcp", key), "是未知字段", targetName)); - } - } - - return issues; -} - function validateTcpExpect( target: Record, path: string, @@ -159,5 +123,3 @@ function validateTcpTarget(target: Record, path: string): Confi return issues; } - -export { validateTcpDefaults }; diff --git a/src/server/checker/runner/types.ts b/src/server/checker/runner/types.ts index 762ccca..6dd04d3 100644 --- a/src/server/checker/runner/types.ts +++ b/src/server/checker/runner/types.ts @@ -1,7 +1,7 @@ import type { TSchema } from "@sinclair/typebox"; import type { ConfigValidationIssue } from "../schema/issues"; -import type { CheckResult, DefaultsConfig, RawTargetConfig, ResolvedTargetBase } from "../types"; +import type { CheckResult, RawTargetConfig, ResolvedTargetBase } from "../types"; export type Checker = CheckerDefinition; @@ -22,18 +22,15 @@ export interface CheckerDefinition { resolve(target: RawTargetConfig, context: ResolveContext): ResolvedUdpTarget { const t = target as RawTargetConfig & { type: "udp"; udp: UdpTargetConfig }; - const udpDefaults = context.defaults["udp"] as UdpDefaultsConfig | undefined; - const encoding = t.udp.encoding ?? udpDefaults?.encoding ?? "text"; - const responseEncoding = t.udp.responseEncoding ?? udpDefaults?.responseEncoding ?? "text"; - const maxResponseBytes = parseSize( - t.udp.maxResponseBytes ?? udpDefaults?.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES, - ); + const encoding = t.udp.encoding ?? "text"; + const responseEncoding = t.udp.responseEncoding ?? "text"; + const maxResponseBytes = parseSize(t.udp.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES); const rawExpect = target.expect as RawUdpExpectConfig | undefined; const resolvedExpect: ResolvedUdpExpectConfig = rawExpect diff --git a/src/server/checker/runner/udp/schema.ts b/src/server/checker/runner/udp/schema.ts index b3985dc..6f7b999 100644 --- a/src/server/checker/runner/udp/schema.ts +++ b/src/server/checker/runner/udp/schema.ts @@ -20,14 +20,6 @@ export const udpCheckerSchemas: CheckerSchemas = { }, { additionalProperties: false }, ), - defaults: Type.Object( - { - encoding: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("hex"), Type.Literal("base64")])), - maxResponseBytes: Type.Optional(sizeSchema), - responseEncoding: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("hex"), Type.Literal("base64")])), - }, - { additionalProperties: false }, - ), expect: Type.Object( { durationMs: Type.Optional(createRawValueExpectationSchema()), diff --git a/src/server/checker/runner/udp/types.ts b/src/server/checker/runner/udp/types.ts index e2890ad..1cff281 100644 --- a/src/server/checker/runner/udp/types.ts +++ b/src/server/checker/runner/udp/types.ts @@ -44,12 +44,6 @@ export interface ResolvedUdpTarget extends ResolvedTargetBase { udp: ResolvedUdpConfig; } -export interface UdpDefaultsConfig { - encoding?: UdpEncoding; - maxResponseBytes?: number | string; - responseEncoding?: UdpEncoding; -} - export type UdpEncoding = "base64" | "hex" | "text"; export interface UdpTargetConfig { diff --git a/src/server/checker/runner/udp/validate.ts b/src/server/checker/runner/udp/validate.ts index d3eb095..c50655a 100644 --- a/src/server/checker/runner/udp/validate.ts +++ b/src/server/checker/runner/udp/validate.ts @@ -11,8 +11,6 @@ const VALID_ENCODINGS = new Set(["base64", "hex", "text"]); export function validateUdpConfig(input: CheckerValidationInput): ConfigValidationIssue[] { const issues: ConfigValidationIssue[] = []; - issues.push(...validateUdpDefaults(input)); - for (let i = 0; i < input.targets.length; i++) { const target = input.targets[i] as unknown; if (!isPlainRecord(target)) continue; @@ -44,29 +42,6 @@ function validateSize(value: unknown, path: string, targetName: string | undefin return []; } -function validateUdpDefaults(input: CheckerValidationInput): ConfigValidationIssue[] { - const issues: ConfigValidationIssue[] = []; - const defaults = input.defaults["udp"]; - if (defaults === undefined || defaults === null || !isPlainRecord(defaults)) return issues; - - const targetName = "defaults.udp"; - - issues.push(...validateEncoding(defaults["encoding"], joinPath("defaults.udp", "encoding"), targetName)); - issues.push( - ...validateEncoding(defaults["responseEncoding"], joinPath("defaults.udp", "responseEncoding"), targetName), - ); - issues.push(...validateSize(defaults["maxResponseBytes"], joinPath("defaults.udp", "maxResponseBytes"), targetName)); - - const allowedKeys = new Set(["encoding", "maxResponseBytes", "responseEncoding"]); - for (const key of Object.keys(defaults)) { - if (!allowedKeys.has(key)) { - issues.push(issue("unknown-field", joinPath("defaults.udp", key), "是未知字段", targetName)); - } - } - - return issues; -} - function validateUdpExpect(target: Record, path: string): ConfigValidationIssue[] { const targetName = getTargetName(target); const expect = target["expect"]; diff --git a/src/server/checker/schema/builder.ts b/src/server/checker/schema/builder.ts index b48ad95..cef9a9a 100644 --- a/src/server/checker/schema/builder.ts +++ b/src/server/checker/schema/builder.ts @@ -34,7 +34,6 @@ export function createExternalProbeConfigSchema(checkers: CheckerDefinition[]): export function createProbeConfigSchema(checkers: CheckerDefinition[], external = false): TSchema { return Type.Object( { - defaults: Type.Optional(createDefaultsSchema(checkers)), probes: Type.Optional(createProbesSchema()), server: Type.Optional(createServerSchema()), targets: Type.Array(external ? createExternalTargetSchema(checkers) : createBaseTargetSchema(checkers), { @@ -80,17 +79,6 @@ function createBaseTargetSchema(checkers: CheckerDefinition[]): TSchema { ); } -function createDefaultsSchema(checkers: CheckerDefinition[]): TSchema { - const properties: Record = { - interval: Type.Optional(durationSchema), - timeout: Type.Optional(durationSchema), - }; - for (const checker of checkers) { - properties[checker.configKey] = Type.Optional(checker.schemas.defaults); - } - return Type.Object(properties, { additionalProperties: false }); -} - function createExternalTargetSchema(checkers: CheckerDefinition[]): TSchema { return Type.Union(checkers.map((checker) => createTargetSchema(checker)) as [TSchema, ...TSchema[]]); } diff --git a/src/server/checker/types.ts b/src/server/checker/types.ts index 87c7e91..b340fc7 100644 --- a/src/server/checker/types.ts +++ b/src/server/checker/types.ts @@ -4,12 +4,6 @@ export interface CheckResult extends ApiCheckResult { targetId: string; } -export interface DefaultsConfig { - [checkerKey: string]: unknown; - interval?: string; - timeout?: string; -} - export interface ExecutionConfig { maxConcurrentChecks?: number; } @@ -41,7 +35,6 @@ export interface LoggingFileRotationConfig { export type LogLevel = "debug" | "error" | "fatal" | "info" | "trace" | "warn"; export interface ProbeConfig { - defaults?: DefaultsConfig; probes?: ProbesConfig; server?: ServerConfig; targets: RawTargetConfig[]; diff --git a/tests/server/checker/config-loader.test.ts b/tests/server/checker/config-loader.test.ts index 5e98c4c..cf75bfa 100644 --- a/tests/server/checker/config-loader.test.ts +++ b/tests/server/checker/config-loader.test.ts @@ -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 () => { diff --git a/tests/server/checker/runner/cmd/runner.test.ts b/tests/server/checker/runner/cmd/runner.test.ts index 7e49283..920d56e 100644 --- a/tests/server/checker/runner/cmd/runner.test.ts +++ b/tests/server/checker/runner/cmd/runner.test.ts @@ -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( diff --git a/tests/server/checker/runner/db/validate.test.ts b/tests/server/checker/runner/db/validate.test.ts index 0bfa81d..9ad6c60 100644 --- a/tests/server/checker/runner/db/validate.test.ts +++ b/tests/server/checker/runner/db/validate.test.ts @@ -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" }, diff --git a/tests/server/checker/runner/http/runner.test.ts b/tests/server/checker/runner/http/runner.test.ts index 6abf368..1afecbd 100644 --- a/tests/server/checker/runner/http/runner.test.ts +++ b/tests/server/checker/runner/http/runner.test.ts @@ -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, }; } diff --git a/tests/server/checker/runner/icmp/execute.test.ts b/tests/server/checker/runner/icmp/execute.test.ts index 65aed15..d4dc420 100644 --- a/tests/server/checker/runner/icmp/execute.test.ts +++ b/tests/server/checker/runner/icmp/execute.test.ts @@ -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"); diff --git a/tests/server/checker/runner/icmp/validate.test.ts b/tests/server/checker/runner/icmp/validate.test.ts index 98c5bcb..1c9d5a8 100644 --- a/tests/server/checker/runner/icmp/validate.test.ts +++ b/tests/server/checker/runner/icmp/validate.test.ts @@ -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", () => { diff --git a/tests/server/checker/runner/llm/registry.test.ts b/tests/server/checker/runner/llm/registry.test.ts index f110722..348cf59 100644 --- a/tests/server/checker/runner/llm/registry.test.ts +++ b/tests/server/checker/runner/llm/registry.test.ts @@ -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); diff --git a/tests/server/checker/runner/llm/schema-validate-resolve.test.ts b/tests/server/checker/runner/llm/schema-validate-resolve.test.ts index af3af31..33510bf 100644 --- a/tests/server/checker/runner/llm/schema-validate-resolve.test.ts +++ b/tests/server/checker/runner/llm/schema-validate-resolve.test.ts @@ -42,7 +42,6 @@ function makeResolveContext(overrides?: Partial): 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 } }); - }); }); diff --git a/tests/server/checker/runner/registry.test.ts b/tests/server/checker/runner/registry.test.ts index 83c4e0e..3818ea4 100644 --- a/tests/server/checker/runner/registry.test.ts +++ b/tests/server/checker/runner/registry.test.ts @@ -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", () => { diff --git a/tests/server/checker/runner/shared/duplicate-header-key.test.ts b/tests/server/checker/runner/shared/duplicate-header-key.test.ts index 6030cab..35161d4 100644 --- a/tests/server/checker/runner/shared/duplicate-header-key.test.ts +++ b/tests/server/checker/runner/shared/duplicate-header-key.test.ts @@ -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): 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", () => { diff --git a/tests/server/checker/runner/shared/value-matcher-shorthand.test.ts b/tests/server/checker/runner/shared/value-matcher-shorthand.test.ts index f0a1484..9bd9703 100644 --- a/tests/server/checker/runner/shared/value-matcher-shorthand.test.ts +++ b/tests/server/checker/runner/shared/value-matcher-shorthand.test.ts @@ -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): CheckerValidationInput { - return { defaults: {}, targets: [target as CheckerValidationInput["targets"][number]] }; + return { targets: [target as CheckerValidationInput["targets"][number]] }; } describe("ValueMatcher primitive shorthand in checker validators", () => { diff --git a/tests/server/checker/runner/tcp/execute.test.ts b/tests/server/checker/runner/tcp/execute.test.ts index 9e5a1fa..c2e3a33 100644 --- a/tests/server/checker/runner/tcp/execute.test.ts +++ b/tests/server/checker/runner/tcp/execute.test.ts @@ -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"); diff --git a/tests/server/checker/runner/tcp/validate.test.ts b/tests/server/checker/runner/tcp/validate.test.ts index 25bdc3f..4a55621 100644 --- a/tests/server/checker/runner/tcp/validate.test.ts +++ b/tests/server/checker/runner/tcp/validate.test.ts @@ -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): 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); - }); }); diff --git a/tests/server/checker/runner/udp/execute.test.ts b/tests/server/checker/runner/udp/execute.test.ts index 58aa0f6..065dc58 100644 --- a/tests/server/checker/runner/udp/execute.test.ts +++ b/tests/server/checker/runner/udp/execute.test.ts @@ -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"); }); diff --git a/tests/server/checker/runner/udp/validate.test.ts b/tests/server/checker/runner/udp/validate.test.ts index 6de8c51..ab4f70e 100644 --- a/tests/server/checker/runner/udp/validate.test.ts +++ b/tests/server/checker/runner/udp/validate.test.ts @@ -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; - targets?: Array>; - }): CheckerValidationInput => ({ - defaults: overrides.defaults ?? {}, + const makeInput = (overrides: { targets?: Array> }): 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({