1
0
Files
DiAL/openspec/specs/probe-config/spec.md
lanyuanxiaoyao cfca03b4d6 refactor: 规范审查与重组,合并细粒度规范,清理过时内容
- 合并 20+ 细粒度 spec 为粗粒度主题规范:dashboard、data-store、probe-engine、probe-api、probe-config 等
- 删除完全冗余规范:data-retention(被 probe-engine+data-store 覆盖)、backend-code-quality(DEVELOPMENT.md 已记录)
- 补充 http-checker 规范至完整标准(配置+执行+expect+校验+observation),匹配代码 440 行实现
- 清理 tcp/udp/llm checker 规范中已废弃 defaults 配置段的残留 Scenario
- 清理 checker-cohesion-structure 中的实现路径引用(src/server/...)
- 统一所有 spec 格式(## Purpose 开头,去除 # Capability/Title 形式)
- 更新 prompt-spec-review.md 审查提示文档
2026-05-22 18:55:18 +08:00

729 lines
49 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Purpose
定义拨测工具的 YAML 配置文件格式、解析校验规则和 CLI 启动流程。
## Requirements
### Requirement: 旧配置入口拒绝
系统 SHALL 拒绝旧版顶层 `runtime` 和顶层 `logging` 配置入口。系统 SHALL 拒绝旧版 `server.host``server.port``server.dataDir` 入口,并要求使用 `server.listen``server.storage` 下的新路径。
#### Scenario: 顶层 runtime 被拒绝
- **WHEN** 配置文件声明顶层 `runtime`
- **THEN** 系统 SHALL 以配置错误退出并提示存在未知字段 `runtime`
#### Scenario: 顶层 logging 被拒绝
- **WHEN** 配置文件声明顶层 `logging`
- **THEN** 系统 SHALL 以配置错误退出并提示存在未知字段 `logging`
#### Scenario: server 旧监听字段被拒绝
- **WHEN** 配置文件声明 `server.host``server.port`
- **THEN** 系统 SHALL 以配置错误退出并提示 `server` 中存在未知字段
#### Scenario: server 旧 dataDir 字段被拒绝
- **WHEN** 配置文件声明 `server.dataDir`
- **THEN** 系统 SHALL 以配置错误退出并提示 `server` 中存在未知字段 `dataDir`
### Requirement: YAML 配置文件格式
系统 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 和 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 和 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 使用其自身的值,不受对应内置默认值影响
#### Scenario: HTTP target 配置 ignoreSSL
- **WHEN** YAML 配置中 HTTP target 设置 `http.ignoreSSL: true`
- **THEN** 系统 SHALL 解析该字段并在执行时跳过 SSL 证书校验
#### Scenario: HTTP target 配置 maxRedirects
- **WHEN** YAML 配置中 HTTP target 设置 `http.maxRedirects: 5`
- **THEN** 系统 SHALL 解析该字段并在执行时允许最多跟随 5 次重定向
#### Scenario: 最简 db 配置文件解析
- **WHEN** 系统读取只包含一个 `type: db` target`id``db.url`)的 YAML 配置文件
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段interval=30s, timeout=10s, group="default"),并保留 name=null、description=null
#### 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: per-target http.method 仍然有效
- **WHEN** HTTP target 配置 `http.method: POST`
- **THEN** 系统 SHALL 使用 POST 作为该 target 的请求方法
#### Scenario: 未配置 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
#### 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: 最简 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
### Requirement: CLI 参数
系统 SHALL 通过单一命令行参数接受 YAML 配置文件路径。
#### Scenario: 指定配置文件启动
- **WHEN** 用户执行 `./dial-server ./probes.yaml`
- **THEN** 系统 SHALL 读取并解析指定路径的 YAML 文件作为配置
#### Scenario: 未提供配置文件路径
- **WHEN** 用户启动程序时未提供任何命令行参数
- **THEN** 系统 SHALL 以错误退出并提示需要指定配置文件路径
#### Scenario: 配置文件不存在
- **WHEN** 用户指定的配置文件路径不存在
- **THEN** 系统 SHALL 以错误退出并提示文件不存在
### Requirement: 配置校验
系统 SHALL 在启动时对 YAML 配置进行完整校验,校验失败时以非零状态退出并输出清晰的错误信息。系统 SHALL 使用 TypeBox 定义 Authoring Config 和 Normalized Config 的配置契约,并使用 Ajv 校验 TypeBox 生成的 JSON Schema。配置加载流程 SHALL 明确区分 `AuthoringProbeConfig``NormalizedProbeConfig``ValidatedProbeConfig``ResolvedConfig` 生命周期,并在 YAML 解析之后、Normalized schema 校验之前执行 `normalizeAuthoringConfig()`。该 normalizer SHALL 只负责去糖变量替换、expect primitive/keyed/content 简写展开、移除 `variables` 段。该 normalizer MUST NOT 注入默认值、解析 duration/size/path/env或合并 `cmd.env`
JSON Schema 契约 SHALL 覆盖业务无关的结构规则包括字段类型、必填字段、枚举、数组与对象形状、数值范围和未知字段。Authoring schema SHALL 用于导出的 `probe-config.schema.json`,描述用户 YAML 可书写形式,包括变量引用和 expect 简写。Normalized schema SHALL 用于运行时 AJV 校验,描述 normalizer 输出结果,不接受变量引用、不接受 expect primitive 简写、不包含 `variables` 段。语义 validator SHALL 覆盖契约不适合表达的业务规则,包括 target id 唯一性、id 命名规则校验、checker type 注册状态、时长和大小解析、HTTP URL、正则可编译、JSONPath 子集和 XPath 可编译。
契约校验、normalizer 和语义 validator SHALL 统一产出 `ConfigValidationIssue`,最终由配置加载流程统一渲染为中文错误信息。除 `headers``env`、Authoring `variables` 等明确声明为动态键值表的对象外,配置中的未知字段 SHALL 导致启动期配置错误。系统 MUST NOT 静默忽略未知字段。
#### Scenario: target 缺少必填字段
- **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 字段
#### Scenario: cmd target 缺少 exec
- **WHEN** YAML 中某个 target 配置 `type: cmd` 但缺少 `cmd.exec`
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 cmd.exec 字段
#### Scenario: target type 非法
- **WHEN** YAML 中某个 target 的 type 不是已注册 checker 类型
- **THEN** 系统 SHALL 以错误退出,提示不支持的 target type 和当前支持的 type 列表
#### Scenario: target id 重复
- **WHEN** YAML 中存在两个 id 相同的 target
- **THEN** 系统 SHALL 以错误退出,提示重复的 id
#### Scenario: target id 不合法
- **WHEN** YAML 中某个 target 的 id 不符合 `[a-zA-Z0-9][a-zA-Z0-9_-]*` 规则
- **THEN** 系统 SHALL 以错误退出,提示 id 命名不合法
#### Scenario: group 字段类型校验
- **WHEN** YAML 中某个 target 的 `group` 字段不是字符串
- **THEN** 系统 SHALL 以错误退出并提示 group 字段类型错误
#### Scenario: interval 格式非法
- **WHEN** interval 或 timeout 值不是有效的时长格式(如 `30s``5m``500ms`
- **THEN** 系统 SHALL 以错误退出并提示格式错误
#### Scenario: maxConcurrentChecks 非法
- **WHEN** Normalized Config 中 probes.execution.maxConcurrentChecks 不是正整数
- **THEN** 系统 SHALL 以错误退出并提示 probes.execution.maxConcurrentChecks 格式错误
#### Scenario: interval 或 timeout 解析结果非法
- **WHEN** interval 或 timeout 解析结果不是正整数毫秒(如 `0ms``1.5ms`
- **THEN** 系统 SHALL 以错误退出并提示必须为正整数毫秒
#### Scenario: size 格式非法
- **WHEN** maxBodyBytes 或 maxOutputBytes 值不是有效的 size 格式
- **THEN** 系统 SHALL 以错误退出并提示支持 B、KB、MB、GB 格式
#### Scenario: size 解析结果非法
- **WHEN** maxBodyBytes 或 maxOutputBytes 解析结果不是非负安全整数字节数(如 `1.5B`
- **THEN** 系统 SHALL 以错误退出并提示必须为非负安全整数字节数
#### Scenario: HTTP method 非法
- **WHEN** Normalized Config 中某个 HTTP target 的 `http.method` 不是 GET、HEAD、POST、PUT、PATCH、DELETE、OPTIONS 之一
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 method 不合法
#### Scenario: HTTP method 小写非法
- **WHEN** YAML 中某个 HTTP target 配置 `http.method: get`
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 method 必须为大写枚举值
#### Scenario: URL 格式非法
- **WHEN** YAML 中某个 HTTP target 的 `http.url` 不是合法 URL或协议不是 `http:` / `https:`
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 URL 格式不合法
#### Scenario: maxRedirects 非法
- **WHEN** YAML 中某个 HTTP target 的 `http.maxRedirects` 为负数
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 maxRedirects 必须为非负整数
#### Scenario: maxRedirects 非整数非法
- **WHEN** Normalized Config 中某个 HTTP target 的 `http.maxRedirects` 不是非负整数(如 `1.5``"5"`
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 maxRedirects 必须为非负整数
#### Scenario: ignoreSSL 类型非法
- **WHEN** YAML 中某个 HTTP target 的 `http.ignoreSSL` 不是布尔值
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 ignoreSSL 必须为布尔值
#### Scenario: HTTP headers 类型非法
- **WHEN** YAML 中某个 HTTP target 的 `http.headers` 不是对象,或任一 header 值不是字符串
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 http.headers 格式错误
#### Scenario: HTTP body 类型非法
- **WHEN** YAML 中某个 HTTP target 的 `http.body` 已配置但不是字符串
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 http.body 必须为字符串
#### Scenario: maxBodyBytes 数字非法
- **WHEN** YAML 中某个 HTTP target 的 `http.maxBodyBytes` 是负数、非整数或非安全整数
- **THEN** 系统 SHALL 以错误退出,提示 maxBodyBytes 必须为非负安全整数字节数或合法 size 字符串
#### Scenario: status 模式非法
- **WHEN** YAML 中某个 HTTP target 的 `expect.status` 包含不符合 `1xx``5xx` 格式的字符串(如 `"abc"``"2x"``"6xx"`
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 status 模式不合法
#### Scenario: status 数字非法
- **WHEN** YAML 中某个 HTTP target 的 `expect.status` 包含非整数或不在 100-599 范围内的数字
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 status 数字不合法
#### Scenario: durationMs matcher 非法
- **WHEN** Normalized Config 中某个 target 的 `expect.durationMs` 不是合法 `ValueMatcher` 对象
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 expect.durationMs 格式错误
#### Scenario: durationMs 原始值简写在 Authoring schema 合法
- **WHEN** 使用 Authoring schema 校验配置文件中 `expect.durationMs: 5000`
- **THEN** JSON Schema 校验 SHALL 通过,因为 Authoring schema 接受 primitive 简写
#### Scenario: durationMs 原始值简写在 Normalized schema 非法
- **WHEN** 使用 Normalized schema 校验配置对象中 `expect.durationMs: 5000`
- **THEN** JSON Schema 校验 SHALL 失败,因为 Normalized schema 只接受 `ValueMatcher` 对象
#### Scenario: 变量引用在 Authoring schema 合法
- **WHEN** 使用 Authoring schema 校验配置文件中 `server.listen.port: "${PORT|3000}"`
- **THEN** JSON Schema 校验 SHALL 通过,因为 Authoring schema 面向用户可书写 YAML
#### Scenario: 变量引用在 Normalized schema 非法
- **WHEN** 使用 Normalized schema 校验配置对象中 `server.listen.port: "${PORT|3000}"`
- **THEN** JSON Schema 校验 SHALL 失败,因为 Normalized schema 只接受变量替换后的 integer
#### Scenario: Authoring schema 对 integer/boolean/enum 字段接受变量引用
- **WHEN** 使用 Authoring schema 校验配置文件中 `http.maxRedirects: "${MAX_REDIRECTS|5}"``http.ignoreSSL: "${IGNORE_SSL|false}"``llm.provider: "${PROVIDER|openai}"`
- **THEN** JSON Schema 校验 SHALL 通过,因为 Authoring schema 对支持变量替换的 integer/boolean/enum/pattern-string 字段使用 `anyOf: [originalType, {type: "string", pattern: "^\\$\\{[^}]+\\}$"}]` 额外接受完整变量引用字符串
#### Scenario: icmp target 缺少 host
- **WHEN** YAML 中某个 target 配置 `type: icmp` 但缺少 `icmp.host`
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 icmp.host 字段
#### Scenario: icmp expect 未知字段
- **WHEN** YAML 中 icmp target 的 expect 包含非 icmp expect 字段
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
#### Scenario: HTTP expect headers 非法
- **WHEN** YAML 中某个 HTTP target 的 `expect.headers` 不是对象,或某个 header 期望不是合法 `RawValueExpectation`
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 expect.headers 格式错误
#### Scenario: HTTP expect body 必须为数组
- **WHEN** YAML 中某个 HTTP target 的 `expect.body` 已配置但不是数组
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 expect.body 必须为数组
#### Scenario: HTTP body expectation 缺少支持字段
- **WHEN** YAML 中某个 HTTP target 的 `expect.body` 数组项未包含 contains、regex、json、css、xpath 任一支持字段
- **THEN** 系统 SHALL 以错误退出,提示该 body expectation 缺少支持的 expectation 类型
#### Scenario: HTTP body expectation 同时配置多个支持字段
- **WHEN** YAML 中某个 HTTP target 的同一条 body expectation 同时包含 contains、regex、json、css、xpath 中的多个支持字段
- **THEN** 系统 SHALL 以错误退出,提示每条 body expectation 只能配置一种 expectation 类型
#### Scenario: HTTP body regex 非法
- **WHEN** YAML 中某个 HTTP target 的 body regex expectation 不是字符串或不是可编译正则表达式
- **THEN** 系统 SHALL 以错误退出,提示该 body regex 不合法
#### Scenario: HTTP body json path 非法
- **WHEN** YAML 中某个 HTTP target 的 body json expectation 缺少 path或 path 不符合系统支持的 JSONPath 子集
- **THEN** 系统 SHALL 以错误退出,提示该 body json path 不合法
#### Scenario: HTTP body css selector 非法
- **WHEN** YAML 中某个 HTTP target 的 body css expectation 缺少 selector或 selector 不是非空字符串
- **THEN** 系统 SHALL 以错误退出,提示该 body css selector 不合法
#### Scenario: HTTP body xpath path 非法
- **WHEN** YAML 中某个 HTTP target 的 body xpath expectation 缺少 path或 path 不是非空字符串,或可被现有 XPath 库静态判定为语法错误
- **THEN** 系统 SHALL 以错误退出,提示该 body xpath path 不合法
#### Scenario: expect matcher 类型非法
- **WHEN** YAML 中某个 expect matcher 的 regex 不是可编译正则字符串empty/exists 不是布尔值,或 gt/gte/lt/lte 不是有限数字
- **THEN** 系统 SHALL 以错误退出,提示对应 matcher 配置不合法
#### Scenario: expect match 字段不再支持
- **WHEN** YAML 中某个 expect matcher 配置 `match` 字段
- **THEN** 系统 SHALL 以错误退出,提示 `match` 是未知字段,请使用 `regex`
#### Scenario: unknown 字段失败
- **WHEN** YAML 中任一结构化配置对象包含契约未声明的字段,且该对象不是明确允许动态键的对象
- **THEN** 系统 SHALL 以错误退出,提示未知字段所在路径
#### Scenario: 动态 headers 字段允许
- **WHEN** YAML 中 `http.headers``expect.headers` 包含任意 header 名称,且对应值符合契约
- **THEN** 系统 SHALL 接受这些动态 header 名称
#### Scenario: 动态 env 字段允许
- **WHEN** YAML 中 `cmd.env` 包含任意环境变量名称,且对应值为字符串
- **THEN** 系统 SHALL 接受这些动态 env 名称
#### Scenario: JSON Schema 不修改输入
- **WHEN** 系统执行 JSON Schema 契约校验
- **THEN** 系统 MUST NOT 通过契约校验器强制转换类型、注入默认值或删除未知字段
#### Scenario: 变量替换后字段超长由 Normalized schema 的 maxLength 校验拦截
- **WHEN** Authoring Config 中 target 的 `description` 通过 `${...}` 变量替换后超过 500 个字符
- **THEN** Normalized schema SHALL 在 AJV 校验阶段以错误退出,提示 description 字段长度错误
#### Scenario: 配置生命周期分离
- **WHEN** 系统加载配置文件
- **THEN** 系统 SHALL 按 `unknown -> AuthoringProbeConfig -> NormalizedProbeConfig -> ValidatedProbeConfig -> ResolvedConfig` 的顺序执行 YAML 解析、配置去糖、契约校验、语义校验和运行期配置解析
#### Scenario: Normalized 不补默认值
- **WHEN** Authoring Config 中 HTTP target 未配置 `http.method``expect.status`
- **THEN** Normalized Config SHALL 仍不包含这些默认值checker.resolve() SHALL 在 ResolvedConfig 阶段物化默认 method 和 status 语义
#### Scenario: 结构化校验 issue
- **WHEN** 契约校验、normalizer、语义 validator 或变量替换阶段发现非法配置
- **THEN** 系统 SHALL 先生成包含 code、path、message 和可选 targetName 的结构化 `ConfigValidationIssue`,再统一渲染为中文错误信息
#### Scenario: 导出配置 JSON Schema
- **WHEN** 仓库生成或检查配置契约
- **THEN** 根目录 SHALL 存在 draft-07 `probe-config.schema.json`,且其内容 SHALL 与当前 Authoring fragments 和已注册 checker Authoring fragments 组装出的完整 schema 一致(包含 variables 段和 target 的 id/name/description 字段,且不包含顶层 defaults
系统 SHALL 支持使用单位字符串配置读取上限,单位包括 `B``KB``MB``GB`
#### Scenario: 解析 MB
- **WHEN** YAML 中配置 `maxBodyBytes: "100MB"`
- **THEN** 系统 SHALL 将其解析为 104857600 bytes
#### Scenario: 解析 KB
- **WHEN** YAML 中配置 `maxOutputBytes: "512KB"`
- **THEN** 系统 SHALL 将其解析为 524288 bytes
### Requirement: runtime 并发配置
系统 SHALL 支持 `probes.execution.maxConcurrentChecks` 配置全局最大并发检查数。`probes``probes.execution` 配置段均 SHALL 为可选,省略时使用默认值。
#### Scenario: 使用默认并发限制
- **WHEN** YAML 中未配置 `probes` 或未配置 `probes.execution.maxConcurrentChecks`
- **THEN** 系统 SHALL 使用默认值 20
#### Scenario: 配置并发限制
- **WHEN** YAML 中配置 `probes.execution.maxConcurrentChecks: 5`
- **THEN** 系统 SHALL 将全局最大并发检查数设置为 5
### Requirement: YAML 配置使用 Bun 内置解析
系统 SHALL 使用 Bun 内置的 `Bun.YAML.parse()` 解析配置文件,不引入外部 YAML 解析库。
#### Scenario: 解析 YAML 内容
- **WHEN** 系统读取 YAML 文件内容
- **THEN** 系统 SHALL 调用 `Bun.YAML.parse()` 将内容解析为配置对象
### Requirement: expect 配置增强
系统 SHALL 支持 typed target 的领域专用 expect 配置,并通过共享 `ValueMatcher``ContentExpectations``KeyedExpectations` 表达可复用断言能力。状态类字段 SHALL 保持枚举或布尔语义,包括 HTTP/LLM 的 `status`支持精确数字和范围模式、cmd 的 `exitCode`、tcp 的 `connected`、icmp 的 `alive` 和 udp 的 `responded`。Authoring value 类指标字段 SHALL 使用 `RawValueExpectation` 输入,并在 Normalized 阶段归一化为运行期 `ValueExpectation`,包括通用 `durationMs`、db 的 `rowCount`、udp 的 `responseSize`/`sourceHost`/`sourcePort`、icmp 的 `packetLossPercent`/`avgLatencyMs`/`maxLatencyMs`、llm 的 usage token 与 stream 首 token 耗时。Authoring 内容类字段 MUST 使用 `RawContentExpectations` 数组表达配置顺序,并在 Normalized 阶段转换为带 `kind``ContentExpectation` 数组,包括 HTTP `body`、cmd `stdout`/`stderr`、tcp `banner`、udp `response`、llm `output` 和 db `result`。Authoring 键值类字段 SHALL 使用动态对象,并在 Normalized 阶段转换为 `KeyedExpectations` 数组,包括 HTTP/LLM `headers` 和 db `rows` 中的列值断言。
配置加载流程 MUST NOT 保留变量替换后的 Raw expect 作为执行路径依赖。语义校验 SHALL 读取 Normalized expect 并报告问题。Store 持久化 MUST NOT 依赖 Raw expectchecker execute SHALL 只消费 Resolved expect。
#### Scenario: 解析 HTTP expect 配置
- **WHEN** YAML 配置文件中 HTTP target 的 expect 包含 status、headers、body expectation 数组和 durationMs matcher
- **THEN** Normalized Config SHALL 包含 normalized keyed headers、normalized content body 和 normalized durationMschecker.resolve() SHALL 生成包含默认 status 的 HTTP Resolved expect
#### Scenario: 解析 cmd expect 配置
- **WHEN** YAML 配置文件中 cmd target 的 expect 包含 exitCode、stdout、stderr 和 durationMs matcher
- **THEN** Normalized Config SHALL 包含 normalized stdout/stderr content expectations 和 normalized durationMschecker.resolve() SHALL 生成包含默认 exitCode 的 cmd Resolved expect
#### Scenario: 解析 db expect 配置
- **WHEN** YAML 配置文件中 db target 的 expect 包含 durationMs、rowCount、rows 和 result
- **THEN** Normalized Config SHALL 包含 normalized rowCount、rows keyed expectations 和 result content expectations
#### Scenario: 解析 tcp expect 配置
- **WHEN** YAML 配置文件中 tcp target 的 expect 包含 connected、banner expectation 数组和 durationMs matcher
- **THEN** 系统 SHALL 保留 Raw tcp expect 快照,并生成包含默认 connected、resolved banner content expectations 和 resolved durationMs 的 tcp Resolved expect
#### Scenario: 解析 icmp expect 配置
- **WHEN** YAML 配置文件中 icmp target 的 expect 包含 alive、packetLossPercent、avgLatencyMs、maxLatencyMs 和 durationMs matcher
- **THEN** 系统 SHALL 保留 Raw icmp expect 快照,并生成包含默认 alive 和 resolved 数值 expectations 的 icmp Resolved expect
#### Scenario: 解析 udp expect 配置
- **WHEN** YAML 配置文件中 udp target 的 expect 包含 responded、response、responseSize、sourceHost、sourcePort 和 durationMs matcher
- **THEN** 系统 SHALL 保留 Raw udp expect 快照,并生成包含默认 responded、resolved response content expectations 和 resolved value expectations 的 udp Resolved expect
#### Scenario: 解析 llm expect 配置
- **WHEN** YAML 配置文件中 llm target 的 expect 包含 status、headers、output、finishReason、rawFinishReason、usage、stream 和 durationMs matcher
- **THEN** 系统 SHALL 保留 Raw llm expect 快照,并生成包含默认 status、resolved headers、output、finishReason、rawFinishReason、usage、stream 和 durationMs 的 llm Resolved expect并保留 output 内容 expectation 数组顺序
#### Scenario: 解析有序 ContentExpectations 数组
- **WHEN** YAML 中任一内容类 expect 配置 contains、json、regex 三个数组项
- **THEN** 系统 SHALL 在 Normalized expect 中保留执行顺序,供执行阶段按配置顺序快速失败
#### Scenario: 不配置 HTTP status
- **WHEN** HTTP target 未配置 `expect.status`
- **THEN** Normalized Config SHALL 不注入 statuschecker.resolve() SHALL 在 Resolved HTTP expect 中物化默认 `status: [200]` 语义
#### Scenario: 配置 HTTP status 范围模式
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]`
- **THEN** 系统 SHALL 在执行 expect 时匹配所有 200-299 状态码
#### Scenario: 不配置 cmd exitCode
- **WHEN** cmd target 未配置 `expect.exitCode`
- **THEN** Normalized Config SHALL 不注入 exitCodechecker.resolve() SHALL 在 Resolved cmd expect 中物化默认 `exitCode: [0]` 语义
#### Scenario: 不配置 expect
- **WHEN** target 未配置任何 expect 规则
- **THEN** 系统 SHALL 正常处理Normalized Config 不包含 expectResolved expect 由各 checker 物化自身默认状态语义
#### Scenario: Raw expect 不再保留
- **WHEN** YAML 中配置 `expect.durationMs: 1000`
- **THEN** Normalized Config SHALL 包含 `expect.durationMs: {equals: 1000}`ResolvedTarget MUST NOT 携带 `rawExpect`
#### Scenario: 旧 maxDurationMs 字段不再支持
- **WHEN** YAML 中任一 target 配置 `expect.maxDurationMs`
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该字段未知,并要求使用 `expect.durationMs`
#### Scenario: 旧 match 字段不再支持
- **WHEN** YAML 中任一 matcher 或内容 expectation 配置 `match`
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该字段未知或不支持,并要求使用 `regex`
#### Scenario: durationMs matcher 配置
- **WHEN** YAML 中任一 target 配置 `expect.durationMs: {lte: 1000}`
- **THEN** 系统 SHALL 接受该配置并在运行期使用完整执行耗时进行 matcher 校验
#### Scenario: 动态 headers 字段允许
- **WHEN** YAML 中 `http.headers``llm.headers``expect.headers` 包含任意 header 名称,且对应值符合契约
- **THEN** 系统 SHALL 接受这些动态 header 名称
#### Scenario: ContentExpectations 字段必须为数组
- **WHEN** YAML 中任一内容类 expect 字段配置为非数组
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该内容字段必须为 expectation 数组
#### Scenario: regex 字段非法
- **WHEN** YAML 中任一 `regex` 不是字符串、不是可编译正则或存在 ReDoS 风险
- **THEN** 系统 SHALL 在启动期配置校验失败,提示对应 regex 不合法
### Requirement: 数据保留配置字段
配置 schema 的 `server.storage` 段 SHALL 支持 `retention` 字段,类型为字符串,格式为 `<数字><单位>`(单位:`d` 天、`h` 小时、`m` 分钟),用于指定历史数据保留时长。
#### Scenario: retention 字段校验通过
- **WHEN** 配置文件中 `server.storage.retention` 为合法格式(如 `"7d"``"24h"``"30m"`
- **THEN** 配置校验 SHALL 通过
#### Scenario: retention 字段格式非法
- **WHEN** 配置文件中 `server.storage.retention` 为非法格式(如 `"abc"``"7x"``""`
- **THEN** 配置校验 SHALL 失败并报告格式错误
#### Scenario: retention 字段缺省
- **WHEN** 配置文件中未指定 `server.storage.retention`
- **THEN** 系统 SHALL 使用默认值 `"7d"`
### Requirement: 数据目录路径解析
配置加载流程 SHALL 将 `server.storage.dataDir` 相对路径基于配置文件所在目录configDir解析为绝对路径。绝对路径 SHALL 保持不变。
#### Scenario: dataDir 为相对路径
- **WHEN** 配置文件位于 `/opt/dial/probes.yaml`,且 `server.storage.dataDir` 配置为 `./data`
- **THEN** 系统 SHALL 将 dataDir 解析为 `/opt/dial/data`,而非依赖进程 cwd
#### Scenario: dataDir 为绝对路径
- **WHEN** `server.storage.dataDir` 配置为 `/var/lib/dial/data`
- **THEN** 系统 SHALL 直接使用该绝对路径,不做额外解析
#### Scenario: dataDir 使用默认值
- **WHEN** 未配置 `server.storage.dataDir`(使用默认值 `./data`
- **THEN** 系统 SHALL 将默认值 `./data` 基于 configDir 解析为绝对路径
### Requirement: target 通用元信息字段约束
系统 SHALL 在 YAML target 通用字段中对 `id``name``description` 执行契约校验。`id` MUST 为 1 到 30 个字符,符合 `[a-zA-Z0-9][a-zA-Z0-9_-]*` 命名规则MUST 在所有 targets 中全局唯一MUST NOT 参与变量替换。`name` MUST 为 null 或 1 到 30 个字符的字符串支持变量替换MUST NOT 要求全局唯一MUST NOT 参与 target 唯一性判定,语义校验 SHALL 拒绝仅包含空白字符的 name。`description` MUST 为 null 或不超过 500 个字符的字符串,支持变量替换,且 MAY 为空字符串MUST NOT 参与 target 唯一性判定。`name` 为 null 时前端展示 SHALL 使用 `name ?? id` 作为目标名称文案,但该 fallback MUST NOT 改变 target 本身的 name 值。
#### Scenario: description 字段解析
- **WHEN** 系统读取包含 `description: "检查生产 API 健康状态"` 的 target
- **THEN** 系统 SHALL 将该字段解析为 target 的目标说明
#### Scenario: id 包含下划线和连字符
- **WHEN** target 配置 `id: "db_check-01"`
- **THEN** 系统 SHALL 使用该 id 作为 target 的唯一标识符
#### Scenario: id 不合法报错
- **WHEN** target 配置 `id: "_invalid"``id: "-start"``id: "has space"`
- **THEN** 系统 SHALL 以配置错误退出,提示 id 不符合命名规则
#### Scenario: id 重复报错
- **WHEN** 两个 target 配置相同的 `id: "api-health"`
- **THEN** 系统 SHALL 以配置错误退出,提示 id 重复
#### Scenario: id 不参与变量替换
- **WHEN** target 配置 `id: "${VAR}"` 形式的变量引用
- **THEN** 系统 SHALL NOT 对 id 执行变量替换
#### Scenario: name 使用变量
- **WHEN** target 配置 `name: "${env} API 健康检查"` 且 variables 中 `env: "生产"`
- **THEN** 系统 SHALL 将 name 解析为 "生产 API 健康检查"
#### Scenario: name 显式 null
- **WHEN** target 配置 `id: "api-health"``name: null`
- **THEN** 系统 SHALL 接受该配置,并在解析、存储和 API 响应中保留 name 为 null
#### Scenario: name 为 null 时前端展示 fallback
- **WHEN** 前端展示 name 为 null 的 target
- **THEN** 前端 SHALL 显示该 target 的 id 作为目标名称文案
#### Scenario: name 为 null 通过校验
- **WHEN** 系统读取包含 `name: null` 或省略 `name` 的 target
- **THEN** 系统 SHALL 接受该配置
#### Scenario: name 仅包含空白字符报错
- **WHEN** 系统读取包含 `name: " "` 的 target
- **THEN** 系统 SHALL 以配置错误退出,提示 name 不能为空白
#### Scenario: description 为空字符串
- **WHEN** 系统读取包含 `description: ""` 的 target
- **THEN** 系统 SHALL 接受该配置,且不触发长度错误
#### Scenario: description 为 null 通过校验
- **WHEN** 系统读取包含 `description: null` 或省略 `description` 的 target
- **THEN** 系统 SHALL 接受该配置
#### Scenario: description 类型非法
- **WHEN** YAML 中某个 target 的 `description` 字段不是字符串也不是 null
- **THEN** 系统 SHALL 以错误退出,提示 description 字段类型错误
#### Scenario: description 超过最大长度
- **WHEN** YAML 中某个 target 的 `description` 字段超过 500 个字符
- **THEN** 系统 SHALL 以错误退出,提示 description 字段长度错误
#### Scenario: id 超过最大长度
- **WHEN** YAML 中某个 target 的 `id` 字段超过 30 个字符
- **THEN** 系统 SHALL 以错误退出,提示 id 字段长度错误
#### Scenario: name 超过最大长度
- **WHEN** YAML 中某个 target 的 `name` 字段超过 30 个字符
- **THEN** 系统 SHALL 以错误退出,提示 name 字段长度错误
#### Scenario: 变量替换后 description 超长
- **WHEN** target 的 `description` 通过变量替换后超过 500 个字符
- **THEN** 系统 SHALL 在契约校验阶段以错误退出,提示 description 字段长度错误
### Requirement: target 分组
系统 SHALL 支持在每个 target 上配置可选的 `group` 字段用于将目标归类到不同分组。未指定 `group` 的 target SHALL 归入 `"default"` 分组。系统 SHALL 保证 "default" 分组始终排在最前面,其余分组按配置文件中首次出现的顺序排列。系统 SHALL 在 API 响应中返回每个 target 的分组信息。系统 SHALL 在数据库 targets 表中持久化每个 target 的分组信息。
#### Scenario: 配置分组名称
- **WHEN** YAML 配置中某个 target 指定 `group: "搜索引擎"`
- **THEN** 系统 SHALL 将该 target 归类到 "搜索引擎" 分组
#### Scenario: 不配置分组
- **WHEN** YAML 配置中某个 target 未指定 `group` 字段
- **THEN** 系统 SHALL 将该 target 归类到 "default" 分组
#### Scenario: default 分组排最前
- **WHEN** 配置中存在多个分组(包括 "default" 和自定义分组)
- **THEN** API 返回的目标列表中 "default" 分组的目标 SHALL 排在其他分组之前
#### Scenario: 自定义分组按出现顺序
- **WHEN** 配置中 "搜索引擎" 分组在 "后端服务" 分组之前首次出现
- **THEN** API 返回中 "搜索引擎" 分组 SHALL 排在 "后端服务" 分组之前
#### Scenario: targets 列表包含分组
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 响应中每个目标 SHALL 包含 `group` 字段,值为该目标所属的分组名称
#### Scenario: 持久化分组信息
- **WHEN** 系统同步 targets 到数据库
- **THEN** 每个 target 的 `grp` 列 SHALL 存储其分组名称,未配置分组的存储 `"default"`
### Requirement: 配置 schema 导出包含 target 元信息约束
系统 SHALL 在导出的 Authoring `probe-config.schema.json` 中包含 target `id``name``description` 的长度约束和可空类型,用于编辑器提示和外部校验。导出 schema SHALL 面向用户可书写规则文件,因此还 SHALL 接受支持变量替换字段中的完整变量引用字符串和 expect 简写。
#### Scenario: schema 导出 description
- **WHEN** 系统导出 `probe-config.schema.json`
- **THEN** target schema SHALL 包含可选的 `description` 字段,类型为 string 或 null字符串最大长度为 500并允许完整变量引用字符串
#### Scenario: schema 导出 id 和 name
- **WHEN** 系统导出 `probe-config.schema.json`
- **THEN** target schema SHALL 声明 `id` 的 minLength 为 1、maxLength 为 30并声明 `name` 为可选字段,类型为 string 或 null字符串的 minLength 为 1、maxLength 为 30
#### Scenario: schema 导出面向 Authoring Config
- **WHEN** 系统导出 `probe-config.schema.json`
- **THEN** 导出 schema SHALL 接受 `server.listen.port: "${PORT|3000}"``expect.durationMs: 5000` 这类 Authoring 写法
### Requirement: TCP 配置校验
系统 SHALL 在启动期对 tcp checker 的配置契约和语义执行严格校验。Tcp target 的 `tcp` 分组 SHALL 只允许 `host``port``readBanner``bannerReadTimeout``maxBannerBytes` 字段Tcp expect SHALL 只允许 `connected``durationMs``banner` 字段。`banner` MUST 为 `RawContentExpectations` 数组,`durationMs` SHALL 为 `RawValueExpectation`。未知字段、非法类型、非法端口、非法 size、非法 ContentExpectations 和不可编译正则 MUST 导致启动期配置错误。语义校验 MUST NOT 修改 Raw tcp expect 输入。
#### Scenario: tcp host 类型非法
- **WHEN** YAML 中 tcp target 的 `tcp.host` 不是非空字符串
- **THEN** 系统 SHALL 以配置错误退出,提示 tcp.host 必须为非空字符串
#### Scenario: tcp port 类型非法
- **WHEN** YAML 中 tcp target 的 `tcp.port` 不是整数
- **THEN** 系统 SHALL 以配置错误退出,提示 tcp.port 必须为整数端口
#### Scenario: tcp readBanner 类型非法
- **WHEN** YAML 中 tcp target 的 `tcp.readBanner` 不是布尔值
- **THEN** 系统 SHALL 以配置错误退出,提示 tcp.readBanner 必须为布尔值
#### Scenario: tcp bannerReadTimeout 非法
- **WHEN** YAML 中 tcp target 的 `bannerReadTimeout` 不是非负有限数字
- **THEN** 系统 SHALL 以配置错误退出,提示 bannerReadTimeout 格式错误
#### Scenario: tcp maxBannerBytes 非法
- **WHEN** YAML 中 tcp target 的 `maxBannerBytes` 不是合法 size 值
- **THEN** 系统 SHALL 以配置错误退出,提示 maxBannerBytes 格式错误
#### Scenario: tcp expect connected 类型非法
- **WHEN** YAML 中 tcp target 的 `expect.connected` 不是布尔值
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.connected 必须为布尔值
#### Scenario: tcp expect banner 非法
- **WHEN** YAML 中 tcp target 的 `expect.banner` 不是合法 `RawContentExpectations` 数组
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.banner 格式错误
#### Scenario: tcp expect banner regex 正则非法
- **WHEN** YAML 中 tcp target 配置 `expect.banner: [{ regex: "[invalid" }]`
- **THEN** 系统 SHALL 在启动期配置校验失败,而不是延迟到运行期抛错
#### Scenario: tcp 分组未知字段失败
- **WHEN** YAML 中 tcp target 的 `tcp` 分组包含 `tls: true` 等未知字段
- **THEN** 系统 SHALL 以配置错误退出,提示 tcp 分组包含未知字段
### Requirement: LLM 配置校验
系统 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`
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.provider 不合法
#### Scenario: llm url 非法
- **WHEN** YAML 中 llm target 的 `llm.url` 不是 `http://``https://` URL
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.url 格式不合法
#### Scenario: llm model 为空
- **WHEN** YAML 中 llm target 的 `llm.model` 不是非空字符串
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.model 必须为非空字符串
#### Scenario: llm prompt 为空
- **WHEN** YAML 中 llm target 的 `llm.prompt` 不是非空字符串
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.prompt 必须为非空字符串
#### Scenario: llm mode 非法
- **WHEN** YAML 中 llm target 的 `mode` 不是 `http``stream`
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.mode 不合法
#### Scenario: llm headers 类型非法
- **WHEN** YAML 中 llm target 的 `headers` 不是对象,或任一 header 值不是字符串
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.headers 格式错误
#### Scenario: llm ignoreSSL 类型非法
- **WHEN** YAML 中 llm target 的 `ignoreSSL` 不是布尔值
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.ignoreSSL 必须为布尔值
#### Scenario: llm authToken provider 非法
- **WHEN** YAML 中 `provider: openai``provider: openai-responses` 的 llm target 配置 `authToken`
- **THEN** 系统 SHALL 以配置错误退出,提示 authToken 仅支持 anthropic provider
#### Scenario: Anthropic key 与 authToken 冲突
- **WHEN** YAML 中 `provider: anthropic` 的 llm target 同时配置非空 `key` 和非空 `authToken`
- **THEN** 系统 SHALL 以配置错误退出,提示 key 与 authToken 不能同时配置
#### Scenario: llm options 非法
- **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 的 `providerOptions` 不是 JSON object
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.providerOptions 格式错误
#### Scenario: llm 禁止字段失败
- **WHEN** YAML 中 llm target 配置 `api``providerName``baseURL``apiKey``messages``maxRetries``request``maxBodyBytes``maxStreamBytes`
- **THEN** 系统 SHALL 以配置错误退出,提示 llm 分组包含未知字段
#### Scenario: llm output expectation 缺少支持字段
- **WHEN** YAML 中 llm target 的 `expect.output` 数组项未包含任何合法 ValueMatcher 字段或 extractor
- **THEN** 系统 SHALL 以配置错误退出,提示 output expectation 缺少支持的 expectation 类型
#### Scenario: llm output expectation 同时配置多个 extractor
- **WHEN** YAML 中 llm target 的同一条 output expectation 同时包含 json、css、xpath 中的多个 extractor
- **THEN** 系统 SHALL 以配置错误退出,提示每条 output expectation 只能配置一种 extractor
#### Scenario: llm output regex 非法
- **WHEN** YAML 中 llm target 的 output regex expectation 不是字符串、不是可编译正则表达式或存在 ReDoS 风险
- **THEN** 系统 SHALL 以配置错误退出,提示该 output regex 不合法
#### Scenario: llm output json path 非法
- **WHEN** YAML 中 llm target 的 output json expectation 缺少 path或 path 不符合系统支持的 JSONPath 子集
- **THEN** 系统 SHALL 以配置错误退出,提示该 output json path 不合法
#### Scenario: llm expect usage 非法
- **WHEN** YAML 中 llm target 的 `expect.usage.inputTokens``expect.usage.outputTokens``expect.usage.totalTokens` 不是合法 `RawValueExpectation`
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.usage 格式错误
#### Scenario: llm expect stream 仅允许 stream mode
- **WHEN** YAML 中 llm target 配置 `llm.mode: http` 且配置 `expect.stream`
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.stream 仅支持 stream mode
#### Scenario: llm expect stream firstTokenMs 非法
- **WHEN** YAML 中 llm target 的 `expect.stream.firstTokenMs` 不是合法 `RawValueExpectation`
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.stream.firstTokenMs 格式错误
### Requirement: 日志配置格式
系统 SHALL 支持可选的 `server.logging` 配置,用于定义运行时日志等级、命令行日志等级、文件日志等级、文件路径和滚动策略。`server.logging` 未配置时 SHALL 使用内置默认值。系统 SHALL NOT 支持 `server.logging.console.enabled``server.logging.console.format``server.logging.file.enabled``server.logging.file.format``server.logging.file.rotation.enabled` 字段。
#### Scenario: 未配置 logging 使用默认值
- **WHEN** 配置文件未声明 `server.logging`
- **THEN** 系统 SHALL 使用 `server.logging.level=info``server.logging.console.level=info``server.logging.file.level=info``server.logging.file.path=<resolved dataDir>/logs/dial.log``server.logging.file.rotation.size=50MB``server.logging.file.rotation.frequency=daily``server.logging.file.rotation.maxFiles=14`
#### Scenario: console 和 file level 继承全局 level
- **WHEN** 配置声明 `server.logging.level: warn` 且未声明 `server.logging.console.level``server.logging.file.level`
- **THEN** 系统 SHALL 将 console 和 file 的日志等级均解析为 `warn`
#### Scenario: 显式配置文件日志路径
- **WHEN** 配置声明 `server.logging.file.path`
- **THEN** 系统 SHALL 使用该路径作为文件日志路径,而不是默认 `<resolved dataDir>/logs/dial.log`
#### Scenario: 相对日志路径
- **WHEN** `server.logging.file.path` 是相对路径
- **THEN** 系统 SHALL 基于配置文件所在目录解析为绝对路径
#### Scenario: 绝对日志路径
- **WHEN** `server.logging.file.path` 是绝对路径
- **THEN** 系统 SHALL 原样使用该绝对路径,并允许该路径位于 `dataDir` 之外
#### Scenario: 不支持日志开关和格式字段
- **WHEN** 配置声明 `server.logging.console.enabled``server.logging.console.format``server.logging.file.enabled``server.logging.file.format``server.logging.file.rotation.enabled`
- **THEN** 系统 SHALL 以配置错误退出并提示存在未知字段
### Requirement: 日志配置校验
系统 SHALL 在启动期校验 `server.logging` 配置。日志等级 SHALL 只能是 `trace``debug``info``warn``error``fatal``rotation.size` SHALL 使用有效 size 格式且解析为正整数字节数。`rotation.frequency` SHALL 只能是 `hourly``daily``weekly``rotation.maxFiles` SHALL 是正整数。
#### Scenario: 非法日志等级
- **WHEN** 配置声明 `server.logging.level: verbose`
- **THEN** 系统 SHALL 以配置错误退出并提示日志等级非法
#### Scenario: 非法滚动大小
- **WHEN** 配置声明 `server.logging.file.rotation.size: "large"`
- **THEN** 系统 SHALL 以配置错误退出并提示 size 格式非法
#### Scenario: 非法滚动频率
- **WHEN** 配置声明 `server.logging.file.rotation.frequency: monthly`
- **THEN** 系统 SHALL 以配置错误退出并提示 frequency 非法
#### Scenario: 非法归档数量
- **WHEN** 配置声明 `server.logging.file.rotation.maxFiles: 0`
- **THEN** 系统 SHALL 以配置错误退出并提示 maxFiles 必须为正整数
#### Scenario: 非法日志路径
- **WHEN** 配置声明 `server.logging.file.path` 为空字符串或空白字符串
- **THEN** 系统 SHALL 以配置错误退出并提示日志路径非法