1
0

refactor: expect 类型模型重构,Raw/Resolved 双层分离与断言基础设施内聚

- 重命名 ContentRules→ContentExpectations, KeyValueExpect→KeyedExpectations
- 新增 Raw/Resolved 双层模型:resolve 阶段物化为执行计划,store 持久化 Raw 快照
- HTTP body 按需读取:status/headers 失败或无 body expectation 时不读取 body
- 新增 displayValueExpectation() 解包 failure.expected 用户可读展示
- 修复 checkEarlyTimeout 独立 lte/lt 检查,修复 KeyedExpectations JSON Schema
- 新增 expect/value.ts(resolve/check/display)、keyed.ts、content.ts、headers.ts、status.ts
- 删除旧 normalize.ts/matcher.ts/validate-matcher.ts/key-value.ts
- 更新 DEVELOPMENT.md:expect 五层管线表、displayValueExpectation、1.7↔1.10 交叉引用
- 同步 13 个 main specs,归档 refactor-expect-type-model 变更(62/62 tasks)
This commit is contained in:
2026-05-20 16:12:48 +08:00
parent 6098be2d9e
commit 60a54b483f
90 changed files with 2487 additions and 1493 deletions

View File

@@ -47,30 +47,30 @@
- **THEN** 该 checker 专属的断言函数 SHALL 定义在 `expect.ts`
### Requirement: 断言基础设施目录
系统 SHALL 在 `src/server/checker/expect/` 目录中提供所有 checker 共享的断言基础设施。
系统 SHALL 在 `src/server/checker/expect/` 目录中提供所有 checker 共享的断言基础设施。共享 expect 目录 SHALL 使用 Raw/Resolved expectation 术语和 value/content/keyed/status/headers 模块边界。
#### Scenario: expect 共享类型位置
- **WHEN** 任何 checker 需要使用断言相关的共享类型(如 `ExpectResult`
- **WHEN** 任何 checker 需要使用断言相关的共享类型(如 `ExpectationResult``ValueExpectation``ContentExpectations``KeyedExpectations`
- **THEN** 这些类型 SHALL 从 `src/server/checker/expect/types.ts` 导入
#### Scenario: operator 断言引擎位置
- **WHEN** 任何 checker 需要使用 `applyOperator``evaluateJsonPath``checkExpectValue`
- **THEN** 这些函数 SHALL 从 `src/server/checker/expect/operator.ts` 导入
#### Scenario: value 断言引擎位置
- **WHEN** 任何 checker 需要使用 `applyValueMatcher``evaluateJsonPath``resolveValueExpectation``checkValueExpectation`
- **THEN** 这些函数 SHALL 从 `src/server/checker/expect/value.ts` 导入
#### Scenario: duration 断言位置
- **WHEN** 任何 checker 需要使用 `checkDuration`
- **THEN** 该函数 SHALL 从 `src/server/checker/expect/duration.ts` 导入
#### Scenario: content 和 keyed 断言位置
- **WHEN** 任何 checker 需要执行内容数组或键值表 expectation
- **THEN** SHALL 分别`src/server/checker/expect/content.ts``src/server/checker/expect/keyed.ts` 导入共享函数
#### Scenario: failure 构造器位置
- **WHEN** 任何 checker 需要使用 `errorFailure``mismatchFailure`
- **THEN** 这些函数 SHALL 从 `src/server/checker/expect/failure.ts` 导入
#### Scenario: operator 校验位置
- **WHEN** 任何 checker 的 validate 需要使用 `validateOperatorObject`
- **THEN** 函数 SHALL 从 `src/server/checker/expect/validate-operator.ts` 导入
#### Scenario: expectation 校验位置
- **WHEN** 任何 checker 的 validate 需要校验 Raw value、Raw content 或 Raw keyed expectation
- **THEN** 对应函数 SHALL 从 `src/server/checker/expect/validate.ts` 导入
#### Scenario: ExpectResult 类型位置
- **WHEN** 任何 checker 需要使用 `ExpectResult` 类型
#### Scenario: ExpectationResult 类型位置
- **WHEN** 任何 checker 需要使用 `ExpectationResult` 类型
- **THEN** 该类型 SHALL 从 `src/server/checker/expect/types.ts` 导入
### Requirement: Schema 目录结构
@@ -130,15 +130,15 @@ checker 系统内的模块依赖 SHALL 遵循严格的分层方向。
- **THEN** 开发者 SHALL 在 `runner/index.ts` 中添加一行 import 和一行数组项,无需修改其他文件
### Requirement: 公共类型文件瘦身
顶层 `src/server/checker/types.ts` SHALL 仅保留跨 checker 共享的 base 类型和存储相关类型。
顶层 `src/server/checker/types.ts` SHALL 仅保留跨 checker 共享的 base 类型和存储相关类型expect 专属类型 SHALL 放在 `src/server/checker/expect/types.ts`
#### Scenario: types.ts 不包含 checker 专属类型
#### Scenario: types.ts 不包含 checker 或 expect 专属类型
- **WHEN** 开发者查看顶层 `types.ts`
- **THEN** 该文件 SHALL NOT 包含 `HttpTargetConfig``ResolvedHttpTarget``CommandExpectConfig``BodyRule``TextRule` 等 checker 专属类型
- **THEN** 该文件 SHALL NOT 包含 `HttpTargetConfig``ResolvedHttpTarget``RawCommandExpectConfig``ContentExpectation` 等 checker 专属或 expect 专属类型
#### Scenario: types.ts 保留 base 类型
- **WHEN** 开发者查看顶层 `types.ts`
- **THEN** 该文件 SHALL 包含 `ResolvedTargetBase``RawTargetConfig``DefaultsConfig``CheckResult``ExpectOperator``CheckFailure``StoredTarget``StoredCheckResult``JsonValue` 等公共类型
- **THEN** 该文件 SHALL 包含 `ResolvedTargetBase``RawTargetConfig``DefaultsConfig``CheckResult``CheckFailure``StoredTarget``StoredCheckResult``JsonValue` 等公共类型
#### Scenario: ResolvedTargetBase 替代联合类型
- **WHEN** engine、store、config-loader 需要引用 resolved target 类型

View File

@@ -146,38 +146,57 @@
- **THEN** HttpChecker 的语义校验 SHALL 抛出校验错误,提示 URL 格式不合法
### Requirement: 存储序列化通过 registry 获取展示格式
系统 SHALL 在 `ProbeStore.syncTargets()` 中通过 `checkerRegistry.get(t.type).serialize(t)` 获取每个 target 的展示摘要(`target` 列)和配置 JSON`config` 列),替代 `buildTargetDisplay()` / `buildTargetConfig()` 中的类型分支。
系统 SHALL 在 `ProbeStore.syncTargets()` 中通过 `checkerRegistry.get(t.type).serialize(t)` 获取每个 target 的展示摘要(`target` 列)和配置 JSON`config` 列),替代 `buildTargetDisplay()` / `buildTargetConfig()` 中的类型分支。系统 SHALL 将 `targets.expect` 持久化为 resolved target 上的 Raw expect 快照,而不是运行期 Resolved expect 执行计划。
#### Scenario: 序列化委托 checker
- **WHEN** store 同步 targets 表
- **THEN** store SHALL 对每个 target 调用对应 checker 的 `serialize()` 方法获取 `{ target, config }`
#### Scenario: expect 持久化使用 rawExpect
- **WHEN** store 同步带 expect 的 target 到 targets 表
- **THEN** store SHALL 将 `rawExpect` 序列化写入 `targets.expect`MUST NOT 将包含 `kind` 的 Resolved content expectation 写入该列
### Requirement: Checker resolve 生成 Raw 与 Resolved expect
每个 checker 的 `resolve()` SHALL 在解析 checker 专属 target 配置时,同时保留变量替换后的 Raw expect 快照并生成运行期 Resolved expect 执行计划。Raw expect SHALL 用于配置快照持久化Resolved expect SHALL 用于 checker `execute()``config-loader` SHALL 继续通过 registry 委托 checker resolveMUST NOT 在中间层理解 checker 专属 expect 字段。
#### Scenario: resolve 输出双 expect 模型
- **WHEN** config-loader 解析一个带 `expect.durationMs: 1000` 的 target
- **THEN** 对应 checker 的 resolved target SHALL 包含 Raw expect 中的 `durationMs: 1000`,并在 Resolved expect 中包含 `{equals: 1000}` 形式的运行期 matcher
#### Scenario: 中间层不感知 checker expect 字段
- **WHEN** 新增 checker 定义自己的 Raw/Resolved expect 字段
- **THEN** config-loader SHALL 只调用该 checker 的 `validate()``resolve()`,不新增 checker 类型分支
### Requirement: 共享 expect 断言函数
系统 SHALL 在 `src/server/checker/expect/` 中提供可被多个 checker 复用的 expect 基础设施。共享基础设施 SHALL 包含 matcher、content rules、key-value expect、failure 构造和 ReDoS 校验。checker 专用的状态类断言 SHALL 保留在对应 checker 目录,或在多个 checker 复用时移动到共享模块。仅被单个 checker 使用且不属于通用 matcher/content/key-value 模型的断言模块 SHALL 位于该 checker 目录内。
系统 SHALL 在 `src/server/checker/expect/` 中提供可被多个 checker 复用的 expect 基础设施。共享基础设施 SHALL 包含 value expectation、content expectations、keyed expectations、status code 断言、headers keyed 断言、failure 构造和 ReDoS 校验。checker 专用的状态类断言 SHALL 保留在对应 checker 目录,或在多个 checker 复用时移动到共享模块。仅被单个 checker 使用且不属于通用 value/content/keyed/status/header 模型的断言模块 SHALL 位于该 checker 目录内。
#### Scenario: 共享 ValueMatcher 断言
#### Scenario: 共享 ValueExpectation 断言
- **WHEN** 任何 checker 需要对数字、字符串、布尔或 JSON value 执行 matcher 匹配
- **THEN** SHALL 调用共享 matcher 工具执行 `equals``contains``regex``exists``empty``gt``gte``lt``lte` 语义
- **THEN** SHALL 调用共享 value expectation 工具执行 `equals``contains``regex``exists``empty``gt``gte``lt``lte` 语义
#### Scenario: 共享 ContentRules 断言
#### Scenario: 共享 ContentExpectations 断言
- **WHEN** HTTP body、LLM output、Cmd stdout/stderr、UDP response 或 TCP banner 需要执行返回内容校验
- **THEN** SHALL 调用共享 content rules 工具,而不是在 checker 目录内复制 contains/regex/json/css/xpath 逻辑
- **THEN** SHALL 调用共享 content expectations 工具,而不是在 checker 目录内复制 contains/regex/json/css/xpath 逻辑
#### Scenario: 共享 KeyValueExpect 断言
#### Scenario: 共享 KeyedExpectations 断言
- **WHEN** HTTP 或 LLM checker 需要校验响应 headers或 DB checker 需要校验 rows 中的列值
- **THEN** SHALL 调用共享 key-value expect 工具,并按调用方规则决定 key 是否大小写敏感
- **THEN** SHALL 调用共享 keyed expectations 工具,并按调用方规则决定 key 是否大小写敏感
#### Scenario: 共享 headers 断言
- **WHEN** HTTP 或 LLM checker 需要校验响应 headers
- **THEN** SHALL 调用共享 header expectation 包装函数,确保 header key 大小写不敏感
#### Scenario: 共享 regex ReDoS 校验
- **WHEN** 任一 matcher 或 content rule 配置 `regex`
- **WHEN** 任一 matcher 或 content expectation 配置 `regex`
- **THEN** SHALL 调用共享 ReDoS 校验工具在启动期拒绝危险正则
#### Scenario: 共享 failure 构造
- **WHEN** 任何 checker 需要构造 CheckFailure 对象
- **THEN** SHALL 调用 `expect/failure.ts` 中的 `errorFailure()``mismatchFailure()`,并保留 actual 截断策略
#### Scenario: HTTP 专用 status 断言
#### Scenario: 共享 status 断言
- **WHEN** HTTP 或 LLM checker 需要校验响应状态码
- **THEN** SHALL 复用同一 status 断言函数,支持精确状态码和 `1xx``5xx` 范围模式
- **THEN** SHALL 复用共享 status code 断言函数,支持精确状态码和 `1xx``5xx` 范围模式
### Requirement: 超时控制由引擎注入 signal
Checker 实现的 `execute()` MUST 使用 `ctx.signal` 感知超时,不得自行创建 `AbortController``setTimeout` 用于超时控制。Cmd checker 和 ping checker 可在 signal abort 时 `proc.kill()` 以确保子进程被终止。

View File

@@ -55,11 +55,11 @@
- **THEN** 系统 MUST 停止收集输出并终止该检查,记录 `matched=false`,并在 failure 中写入输出超限信息observation SHALL 包含已截断输出预览和 error
### Requirement: cmd expect 校验
系统 SHALL 支持 cmd 专用 expect包括 `exitCode``durationMs``stdout``stderr`,并按 exitCode、durationMs、stdout、stderr 的阶段顺序快速失败。`exitCode` SHALL 保持有限整数数组语义,未配置时默认 `[0]``durationMs` SHALL 使用共享 `ValueMatcher` 校验完整命令执行耗时。`stdout``stderr` MUST 使用共享 `ContentRules` 数组,直接 matcher 作用于对应输出文本,`json` extractor SHALL 支持对 JSON CLI 输出执行 JSONPath 断言。
系统 SHALL 支持 cmd 专用 expect包括 `exitCode``durationMs``stdout``stderr`,并按 exitCode、durationMs、stdout、stderr 的阶段顺序快速失败。`exitCode` SHALL 保持有限整数数组语义,未配置时在 Resolved expect 中默认 `[0]``durationMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation` 校验完整命令执行耗时。`stdout``stderr` MUST 使用共享 `RawContentExpectations` 数组输入并在运行期使用 `ContentExpectations`,直接 matcher 作用于对应输出文本,`json` extractor SHALL 支持对 JSON CLI 输出执行 JSONPath 断言。
#### Scenario: 默认 exitCode 成功语义
- **WHEN** cmd target 未显式配置 `expect.exitCode`
- **THEN** 系统 SHALL 使用默认 `expect.exitCode: [0]` 进行校验
- **THEN** 系统 SHALL 在 Resolved cmd expect 中使用默认 `exitCode: [0]` 进行校验
#### Scenario: 显式 exitCode 校验
- **WHEN** cmd target 配置 `expect.exitCode: [0, 2]` 且实际 exit code 为 2
@@ -74,8 +74,8 @@
- **THEN** 系统 SHALL 返回 `matched=false`failure 的 phase 为 `duration`
#### Scenario: stdout 按配置顺序校验
- **WHEN** cmd target 配置 `expect.stdout` 为两个 ContentRules第一条通过且第二条失败
- **THEN** 系统 SHALL 先执行第一条 stdout 规则,再执行第二条,并将 failure.path 指向失败的 `stdout[1]`
- **WHEN** cmd target 配置 `expect.stdout` 为两个 ContentExpectations第一条通过且第二条失败
- **THEN** 系统 SHALL 先执行第一条 stdout expectation,再执行第二条,并将 failure.path 指向失败的 `stdout[1]`
#### Scenario: stderr 校验为空
- **WHEN** cmd target 配置 `expect.stderr: [{empty: true}]` 且实际 stderr 为空字符串
@@ -86,11 +86,11 @@
- **THEN** 系统 SHALL 判定 stdout 阶段通过
#### Scenario: stdout 失败后不检查 stderr
- **WHEN** cmd target 同时配置 stdout 和 stderr 规则,且 stdout 规则失败
- **THEN** 系统 SHALL 快速失败并 MUST NOT 继续执行 stderr 规则
- **WHEN** cmd target 同时配置 stdout 和 stderr expectation且 stdout expectation 失败
- **THEN** 系统 SHALL 快速失败并 MUST NOT 继续执行 stderr expectation
### Requirement: cmd checker 启动期配置校验
系统 SHALL 在启动期对 cmd checker 的配置契约和语义执行严格校验。Cmd target 的 `cmd` 分组 SHALL 只允许 `exec``args``cwd``env``maxOutputBytes` 字段。Cmd expect SHALL 只允许 `exitCode``durationMs``stdout``stderr` 字段。未知字段、非法类型、不可编译正则和 ReDoS 风险正则 MUST 导致启动期配置错误。`expect.exitCode` SHALL 保留原有有限整数数组语义,不限制到特定平台范围。
系统 SHALL 在启动期对 cmd checker 的配置契约和语义执行严格校验。Cmd target 的 `cmd` 分组 SHALL 只允许 `exec``args``cwd``env``maxOutputBytes` 字段。Cmd expect SHALL 只允许 `exitCode``durationMs``stdout``stderr` 字段。未知字段、非法类型、不可编译正则和 ReDoS 风险正则 MUST 导致启动期配置错误。`expect.exitCode` SHALL 保留原有有限整数数组语义,不限制到特定平台范围。语义校验 MUST NOT 修改 Raw cmd expect 输入。
#### Scenario: cmd args 类型非法
- **WHEN** YAML 中 cmd target 配置 `cmd.args` 不是字符串数组
@@ -105,24 +105,24 @@
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.exitCode 必须为整数数组
#### Scenario: cmd expect durationMs 非法
- **WHEN** YAML 中 cmd target 配置 `expect.durationMs` 不是合法 `ValueMatcher`
- **WHEN** YAML 中 cmd target 配置 `expect.durationMs` 不是合法 `RawValueExpectation`
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.durationMs 格式错误
#### Scenario: stdout 必须为 ContentRules 数组
#### Scenario: stdout 必须为 ContentExpectations 数组
- **WHEN** YAML 中 cmd target 配置 `expect.stdout` 但其值不是数组
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.stdout 必须为数组
#### Scenario: stderr 必须为 ContentRules 数组
#### Scenario: stderr 必须为 ContentExpectations 数组
- **WHEN** YAML 中 cmd target 配置 `expect.stderr` 但其值不是数组
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.stderr 必须为数组
#### Scenario: stdout text rule 空对象非法
#### Scenario: stdout text expectation 空对象非法
- **WHEN** YAML 中 cmd target 配置 `expect.stdout: [{}]`
- **THEN** 系统 SHALL 以配置错误退出,提示 stdout 规则必须包含至少一个合法 matcher 或 extractor
- **THEN** 系统 SHALL 以配置错误退出,提示 stdout expectation 必须包含至少一个合法 matcher 或 extractor
#### Scenario: stderr text rule 未知字段非法
#### Scenario: stderr text expectation 未知字段非法
- **WHEN** YAML 中 cmd target 配置 `expect.stderr: [{foo: "bar"}]`
- **THEN** 系统 SHALL 以配置错误退出,提示 stderr 规则包含未知 matcher 或未知 extractor
- **THEN** 系统 SHALL 以配置错误退出,提示 stderr expectation 包含未知 matcher 或未知 extractor
#### Scenario: stdout regex 正则非法
- **WHEN** YAML 中 cmd target 配置 `expect.stdout: [{regex: "[invalid"}]`

View File

@@ -75,7 +75,7 @@
- **THEN** 系统 SHALL 立即关闭数据库连接
### Requirement: db expect 校验
系统 SHALL 支持 db 专用 expect包括 `durationMs``rowCount``rows``result`,按 durationMs、rowCount、rows、result 的阶段顺序快速失败。`durationMs``rowCount` SHALL 使用共享 `ValueMatcher``rows` SHALL 保留按行索引匹配列值的语义,类型为 `Array<KeyValueExpect>`(外层数组按行索引,内层每个元素为一个 `KeyValueExpect` 表达该行的列值断言),每个行规则中列值字面量等价于 `{equals: <literal>}``result` MUST 使用共享 `ContentRules` 数组,对查询结果对象 `{ rows, rowCount }` 执行断言。
系统 SHALL 支持 db 专用 expect包括 `durationMs``rowCount``rows``result`,按 durationMs、rowCount、rows、result 的阶段顺序快速失败。`durationMs``rowCount` SHALL 使用共享 `RawValueExpectation` 输入,并在 resolve 阶段转换为运行期 `ValueExpectation``rows` SHALL 保留按行索引匹配列值的语义,Raw 类型为 `Array<RawKeyedExpectations>`外层数组按行索引内层每个元素表达该行的列值断言Resolved 类型为 `Array<KeyedExpectations>`。每个行规则中列值 primitive 字面量等价于 `{equals: <literal>}``result` MUST 使用共享 `RawContentExpectations` 数组输入,并在运行期以 `ContentExpectations` 对查询结果对象 `{ rows, rowCount }` 执行断言。
#### Scenario: durationMs 校验
- **WHEN** db target 配置 `expect.durationMs: {lte: 3000}` 且实际执行耗时 4000ms
@@ -95,7 +95,7 @@
#### Scenario: rows 按索引匹配列值字面量形式
- **WHEN** db target 配置 `expect.rows: [{ status: "active" }]` 且查询首行 status 列值为 `"active"`
- **THEN** 系统 SHALL 判定该行该列通过(字面量等价于 `{ equals: "active" }`
- **THEN** 系统 SHALL 在 resolve 阶段将该列值解析为 `{equals: "active"}` 并判定该行该列通过
#### Scenario: rows 只检查声明的列
- **WHEN** db target 配置 `expect.rows: [{ cnt: { gte: 1 } }]` 且查询首行包含 cnt、name、age 三列
@@ -122,14 +122,14 @@
- **THEN** 系统 SHALL 按 durationMs → rowCount → rows → result 顺序校验,任一阶段失败立即返回
### Requirement: db checker 启动期配置校验
系统 SHALL 在启动期对 db checker 的配置契约和语义执行严格校验。Db target 的 `db` 分组 SHALL 只允许 `url``query` 字段。Db expect SHALL 只允许 `durationMs``rowCount``rows``result` 字段。未知字段、非法 matcher、非法 ContentRules、非法 regex 和 ReDoS 风险正则 MUST 导致启动期配置错误。
系统 SHALL 在启动期对 db checker 的配置契约和语义执行严格校验。Db target 的 `db` 分组 SHALL 只允许 `url``query` 字段。Db expect SHALL 只允许 `durationMs``rowCount``rows``result` 字段。未知字段、非法 ValueExpectation、非法 ContentExpectations、非法 regex 和 ReDoS 风险正则 MUST 导致启动期配置错误。语义校验 MUST NOT 修改 Raw db expect 输入。
#### Scenario: db expect durationMs 非法
- **WHEN** YAML 中 db target 配置 `expect.durationMs` 不是合法 `ValueMatcher`
- **WHEN** YAML 中 db target 配置 `expect.durationMs` 不是合法 `RawValueExpectation`
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.durationMs 格式错误
#### Scenario: db expect rowCount 非法
- **WHEN** YAML 中 db target 配置 `expect.rowCount` 不是合法 `ValueMatcher`
- **WHEN** YAML 中 db target 配置 `expect.rowCount` 不是合法 `RawValueExpectation`
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.rowCount 格式错误
#### Scenario: db expect rows 非法
@@ -140,8 +140,12 @@
- **WHEN** YAML 中 db target 配置 `expect.rows: [{ cnt: { foo: 1 } }]`,其中 foo 不是合法 matcher
- **THEN** 系统 SHALL 以配置错误退出,提示 rows 中包含未知 matcher
#### Scenario: db expect rows 对象值必须显式 equals
- **WHEN** YAML 中 db target 配置 `expect.rows: [{ payload: { status: "ok" } }]` 且 payload 值不是合法 matcher 对象
- **THEN** 系统 SHALL 以配置错误退出,提示对象值必须显式写成 `{equals: {status: "ok"}}`
#### Scenario: db expect result 非法
- **WHEN** YAML 中 db target 配置 `expect.result` 不是合法 ContentRules 数组
- **WHEN** YAML 中 db target 配置 `expect.result` 不是合法 ContentExpectations 数组
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.result 格式错误
#### Scenario: db expect 未知字段失败

View File

@@ -5,54 +5,54 @@
## Requirements
### Requirement: 响应体多种校验方法
系统 SHALL 支持通过共享 `ContentRules` 对 HTTP 响应体进行有序内容校验。`expect.body` MUST 为规则数组。每个规则 SHALL 为直接 `ValueMatcher`,或 `json``css``xpath` extractor 规则之一。直接 matcher SHALL 作用于完整响应体文本。`json` SHALL 解析响应体为 JSON 并用 JSONPath 子集提取值。`css` SHALL 使用 CSS selector 从 HTML 中提取元素文本或属性。`xpath` SHALL 使用 XPath 从 XML/HTML 中提取节点值。Extractor 未配置 matcher 时 SHALL 等价于 `exists: true`
系统 SHALL 支持通过共享 `ContentExpectations` 对 HTTP 响应体进行有序内容校验。`expect.body` MUST 为 expectation 数组。每个 expectation SHALL 为直接 `ValueMatcher`,或 `json``css``xpath` extractor expectation 之一。直接 matcher SHALL 作用于完整响应体文本。`json` SHALL 解析响应体为 JSON 并用 JSONPath 子集提取值。`css` SHALL 使用 CSS selector 从 HTML 中提取元素文本或属性。`xpath` SHALL 使用 XPath 从 XML/HTML 中提取节点值。Extractor 未配置 matcher 时resolve 阶段 SHALL 将其 matcher 物化为 `{exists: true}`
#### Scenario: contains 子串匹配
- **WHEN** HTTP target 配置 `expect.body: [{contains: "healthy"}]`,且响应体包含 `"healthy"`
- **THEN** 系统 SHALL 判定该 body 规则通过
- **THEN** 系统 SHALL 判定该 body expectation 通过
#### Scenario: regex 正则匹配
- **WHEN** HTTP target 配置 `expect.body: [{regex: '"status"\\s*:\\s*"ok"'}]`,且响应体匹配该正则
- **THEN** 系统 SHALL 判定该 body 规则通过
- **THEN** 系统 SHALL 判定该 body expectation 通过
#### Scenario: json JSONPath 等值匹配
- **WHEN** HTTP target 配置 `expect.body: [{json: {path: "$.status", equals: "ok"}}]`,且响应 JSON 中 `$.status` 值为 `"ok"`
- **THEN** 系统 SHALL 判定该 body 规则通过
- **THEN** 系统 SHALL 判定该 body expectation 通过
#### Scenario: json JSONPath 存在性匹配
- **WHEN** HTTP target 配置 `expect.body: [{json: {path: "$.status"}}]`,且响应 JSON 中存在 `$.status`
- **THEN** 系统 SHALL 将该规则`exists: true` 语义判定通过
- **THEN** resolve 阶段 SHALL 将该 expectation `exists: true` 语义物化并在运行期判定通过
#### Scenario: css 选择器匹配
- **WHEN** HTTP target 配置 `expect.body: [{css: {selector: "div#health", equals: "OK"}}]`,且 HTML 中存在 `div#health` 元素文本为 `"OK"`
- **THEN** 系统 SHALL 判定该 body 规则通过
- **THEN** 系统 SHALL 判定该 body expectation 通过
#### Scenario: css 选择器匹配属性值
- **WHEN** HTTP target 配置 css 规则`attr: "content"` 用于提取属性,且属性值匹配期望
- **THEN** 系统 SHALL 判定该 body 规则通过
- **WHEN** HTTP target 配置 css expectation `attr: "content"` 用于提取属性,且属性值匹配期望
- **THEN** 系统 SHALL 判定该 body expectation 通过
#### Scenario: xpath 表达式匹配
- **WHEN** HTTP target 配置 `expect.body: [{xpath: {path: "/root/status/text()", equals: "ok"}}]`,且 XML 中 `/root/status` 节点文本为 `"ok"`
- **THEN** 系统 SHALL 判定该 body 规则通过
- **THEN** 系统 SHALL 判定该 body expectation 通过
#### Scenario: 提取器无匹配目标失败
- **WHEN** HTTP target 配置了 json、css 或 xpath 规则且对应路径、元素或节点不存在,并且规则未配置 `exists: false`
- **WHEN** HTTP target 配置了 json、css 或 xpath expectation 且对应路径、元素或节点不存在,并且 expectation 未配置 `exists: false`
- **THEN** 系统 SHALL 判定 matched 为 false
### Requirement: 多种 body 校验方法 AND 组合
系统 SHALL 支持在 `expect.body` 数组中同时配置多条内容规则,所有规则均通过时 matched 方为 true。系统 SHALL 按数组顺序执行规则,任一规则失败后 MUST NOT 继续执行后续规则
系统 SHALL 支持在 `expect.body` 数组中同时配置多条内容 expectation所有 expectation 均通过时 matched 方为 true。系统 SHALL 按数组顺序执行 expectation任一 expectation 失败后 MUST NOT 继续执行后续 expectation
#### Scenario: 多种方法全部通过
- **WHEN** HTTP target 的 `expect.body` 数组依次配置 contains、json、regex且全部通过
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: 多种方法任一失败
- **WHEN** HTTP target 的 `expect.body` 数组第一条 contains 不通过,后续还有 json 规则
- **THEN** 系统 SHALL 判定 matched 为 false且不再检查后续 json 规则
- **WHEN** HTTP target 的 `expect.body` 数组第一条 contains 不通过,后续还有 json expectation
- **THEN** 系统 SHALL 判定 matched 为 false且不再检查后续 json expectation
#### Scenario: 直接 matcher 多字段组合
- **WHEN** HTTP target 配置 `expect.body: [{contains: "ok", regex: "status"}]`,且响应体同时满足 contains 和 regex
- **THEN** 系统 SHALL 判定该规则通过
- **THEN** 系统 SHALL 判定该 expectation 通过
### Requirement: 操作符系统
系统 SHALL 支持通过共享 `ValueMatcher` 对提取值和文本值进行比较:`equals`(深度等值)、`contains`(子串包含)、`regex`(正则匹配)、`empty`(空值判断)、`exists`(存在性判断)、`gte`/`lte`/`gt`/`lt`(数值比较)。系统 MUST NOT 支持旧 `match` 字段。
@@ -82,7 +82,7 @@
- **THEN** 系统 SHALL 对同一字段进行多 matcher 复合比较,全部通过则该规则通过
### Requirement: 响应头校验
系统 SHALL 支持通过共享 `KeyValueExpect` 配置 `expect.headers` 对 HTTP 响应头进行键值规则校验header 名称匹配 MUST 不区分大小写。header 期望值 MAY 为字符串字面量或 `ValueMatcher`。字符串字面量 SHALL 等价 `{equals: <value>}`
系统 SHALL 支持通过共享 `KeyedExpectations` 配置 `expect.headers` 对 HTTP 响应头进行键值断言header 名称匹配 MUST 不区分大小写。header 期望值 MUST 为 `RawValueExpectation`primitive 字面量 SHALL 在 resolve 阶段等价 `{equals: <value>}`
#### Scenario: 响应头字面量匹配
- **WHEN** HTTP target 配置 `expect.headers: {"Content-Type": "application/json"}`,且响应包含该 header 且值精确匹配
@@ -155,18 +155,18 @@
- **THEN** 系统 SHALL 在启动期配置校验失败
### Requirement: HTTP expect 规则启动期校验
系统 SHALL 在启动期校验 HTTP expect 中已支持字段的类型、格式、未知字段和可编译表达式。HTTP expect SHALL 只允许 `status``headers``body``durationMs` 字段。`expect.body` MUST 为 `ContentRules` 数组。直接 `ValueMatcher` 对象 MUST 至少包含一个合法 matcher。Extractor 规则 MUST 只包含 `json``css``xpath` 中的一种 extractor。Extractor 内部可以不配置 matcher并 SHALL 在运行期以存在性作为通过语义。`equals` matcher SHALL 支持任意 JSON value包括数组和对象。系统 SHALL 在启动期对所有 `regex` 执行静态 ReDoS 检测。
系统 SHALL 在启动期校验 HTTP expect 中已支持字段的类型、格式、未知字段和可编译表达式。HTTP expect SHALL 只允许 `status``headers``body``durationMs` 字段。`expect.body` MUST 为 `RawContentExpectations` 数组。直接 `ValueMatcher` 对象 MUST 至少包含一个合法 matcher。Extractor expectation MUST 只包含 `json``css``xpath` 中的一种 extractor。Extractor 内部可以不配置 matcher并 SHALL 在 resolve 阶段以存在性 matcher 作为通过语义。`equals` matcher SHALL 支持任意 JSON value包括数组和对象。系统 SHALL 在启动期对所有 `regex` 执行静态 ReDoS 检测。语义校验 MUST NOT 修改 Raw HTTP expect 输入。
#### Scenario: body rule 使用 regex 字段
- **WHEN** HTTP target 配置 `expect.body: [{regex: "ok|healthy"}]` 且 regex 可编译且无 ReDoS 风险
- **THEN** 系统 SHALL 接受该配置,并在运行期按 regex 规则匹配响应体
- **THEN** 系统 SHALL 接受该配置,并在运行期按 regex expectation 匹配响应体
#### Scenario: body rule 不支持 match 字段
- **WHEN** HTTP target 配置 `expect.body: [{match: "ok"}]`
- **THEN** 系统 SHALL 在启动期配置校验失败,提示 `match` 是未知字段或不支持字段
#### Scenario: body rule 多 extractor 非法
- **WHEN** HTTP target 的同一条 body rule 同时配置 `json``css`
- **WHEN** HTTP target 的同一条 body expectation 同时配置 `json``css`
- **THEN** 系统 SHALL 在启动期配置校验失败
#### Scenario: matcher regex 正则非法
@@ -182,7 +182,7 @@
- **THEN** 系统 SHALL 在启动期配置校验失败
#### Scenario: JSONPath 子集非法
- **WHEN** HTTP target 的 json body rule path 不符合系统支持的 JSONPath 子集
- **WHEN** HTTP target 的 json body expectation path 不符合系统支持的 JSONPath 子集
- **THEN** 系统 SHALL 在启动期配置校验失败
#### Scenario: matcher 未知字段非法
@@ -190,20 +190,20 @@
- **THEN** 系统 SHALL 在启动期配置校验失败
#### Scenario: durationMs matcher 非法
- **WHEN** HTTP target 配置 `expect.durationMs` 不是合法 `ValueMatcher` 或其中数值 matcher 不是有限数字
- **WHEN** HTTP target 配置 `expect.durationMs` 不是合法 `RawValueExpectation` 或其中数值 matcher 不是有限数字
- **THEN** 系统 SHALL 在启动期配置校验失败
### Requirement: HTTP body 运行期失败结构化
系统 SHALL 将 HTTP body 运行期失败记录为结构化 CheckFailure并保留与具体规则相关的 phase 和 path。响应内容不符合配置 SHALL 记录为 mismatch响应内容无法按配置解析或解码 SHALL 记录为 error。
系统 SHALL 将 HTTP body 运行期失败记录为结构化 CheckFailure并保留与具体 expectation 相关的 phase 和 path。响应内容不符合配置 SHALL 记录为 mismatch响应内容无法按配置解析或解码 SHALL 记录为 error。
#### Scenario: JSON 响应不是合法 JSON
- **WHEN** HTTP target 配置 json body rule,但响应体不是合法 JSON
- **THEN** 系统 SHALL 记录 `failure.kind="error"``failure.phase="body"`,且 failure.path SHALL 指向对应 json 规则
- **WHEN** HTTP target 配置 json body expectation,但响应体不是合法 JSON
- **THEN** 系统 SHALL 记录 `failure.kind="error"``failure.phase="body"`,且 failure.path SHALL 指向对应 json expectation
#### Scenario: CSS selector 无匹配元素
- **WHEN** HTTP target 配置 css body rule,但响应 HTML 中无匹配元素
- **THEN** 系统 SHALL 记录 `failure.kind="mismatch"``failure.phase="body"`,且 failure.path SHALL 指向对应 css 规则
- **WHEN** HTTP target 配置 css body expectation,但响应 HTML 中无匹配元素
- **THEN** 系统 SHALL 记录 `failure.kind="mismatch"``failure.phase="body"`,且 failure.path SHALL 指向对应 css expectation
#### Scenario: XPath 无匹配节点
- **WHEN** HTTP target 配置 xpath body rule,但响应 XML/HTML 中无匹配节点
- **THEN** 系统 SHALL 记录 `failure.kind="mismatch"``failure.phase="body"`,且 failure.path SHALL 指向对应 xpath 规则
- **WHEN** HTTP target 配置 xpath body expectation,但响应 XML/HTML 中无匹配节点
- **THEN** 系统 SHALL 记录 `failure.kind="mismatch"``failure.phase="body"`,且 failure.path SHALL 指向对应 xpath expectation

View File

@@ -1,13 +1,13 @@
## Purpose
定义共享 expect 断言规则系统的核心概念和基础设施ValueMatcher 统一匹配器、ContentRules 内容规则数组、KeyValueExpect 键值规则、以及相关的启动期校验和失败路径规范。
定义共享 expect 断言规则系统的核心概念和基础设施ValueMatcher 统一匹配器、ContentExpectations 内容断言数组、KeyedExpectations 键控断言数组、以及相关的启动期校验和失败路径规范。
## Requirements
### Requirement: ValueMatcher 统一匹配器
系统 SHALL 提供共享 `ValueMatcher` 作为所有非状态类 expect 的基础匹配结构。`ValueMatcher` SHALL 支持 `equals``contains``regex``exists``empty``gt``gte``lt``lte` 字段。`equals` MUST 支持任意 JSON value并使用深度相等比较。`contains``regex` SHALL 将实际值转换为字符串后匹配。`gt``gte``lt``lte` SHALL 将实际值转换为有限数字后比较,无法转换为有限数字时 SHALL 判定不匹配。一个 `ValueMatcher` 对象包含多个 matcher 字段时,系统 SHALL 要求全部 matcher 均通过。
系统 SHALL 提供共享 `ValueMatcher` 作为所有非状态类 value expectation 的基础匹配结构。`ValueMatcher` SHALL 支持 `equals``contains``regex``exists``empty``gt``gte``lt``lte` 字段。`equals` MUST 支持任意 JSON value并使用深度相等比较。`contains``regex` SHALL 将实际值转换为字符串后匹配。`gt``gte``lt``lte` SHALL 将实际值转换为有限数字后比较,无法转换为有限数字时 SHALL 判定不匹配。一个 `ValueMatcher` 对象包含多个 matcher 字段时,系统 SHALL 要求全部 matcher 均通过。
所有类型为 `ValueMatcher` 的 expect 字段 SHALL 同时接受 primitive 原始值string / number / boolean / null作为简写形式。原始值简写 SHALL 等价于 `{ equals: value }`。系统 SHALL 在语义校验入口将 primitive 原始值归一化为 `{ equals: value }` 对象形式,后续 resolve 和运行期逻辑 SHALL 仅处理 ValueMatcher 对象形式。数组和对象 MUST NOT 作为原始值简写;需要对数组或对象执行 equals 匹配时,配置 MUST 显式写成 `{ equals: value }`
所有类型为 `RawValueExpectation` 的 expect 字段 SHALL 同时接受 primitive 原始值string / number / boolean / null作为简写形式。原始值简写 SHALL 等价于 `{ equals: value }`。系统 SHALL 在 resolve 阶段将 primitive 原始值归一化为 `{ equals: value }` 对象形式,运行期逻辑 SHALL 仅处理 `ValueMatcher` 对象形式。数组和对象 MUST NOT 作为原始值简写;需要对数组或对象执行 equals 匹配时,配置 MUST 显式写成 `{ equals: value }`
#### Scenario: equals 匹配对象
- **WHEN** 实际值为 `{status: "ok", count: 1}` 且 matcher 为 `{equals: {status: "ok", count: 1}}`
@@ -27,26 +27,26 @@
#### Scenario: 字符串原始值简写等价 equals
- **WHEN** expect 字段配置为 `finishReason: "stop"` 且实际值为 `"stop"`
- **THEN** 系统 SHALL 将 `"stop"` 归一化为 `{equals: "stop"}` 并判定通过
- **THEN** 系统 SHALL 在 resolve 阶段`"stop"` 归一化为 `{equals: "stop"}` 并判定通过
#### Scenario: 数字原始值简写等价 equals
- **WHEN** expect 字段配置为 `rowCount: 1` 且实际值为 `1`
- **THEN** 系统 SHALL 将 `1` 归一化为 `{equals: 1}` 并判定通过
- **THEN** 系统 SHALL 在 resolve 阶段`1` 归一化为 `{equals: 1}` 并判定通过
#### Scenario: 布尔原始值简写等价 equals
- **WHEN** expect 字段配置为 ValueMatcher 类型且值为 `true`,实际值为 `true`
- **THEN** 系统 SHALL 将 `true` 归一化为 `{equals: true}` 并判定通过
- **WHEN** expect 字段配置为 RawValueExpectation 类型且值为 `true`,实际值为 `true`
- **THEN** 系统 SHALL 在 resolve 阶段`true` 归一化为 `{equals: true}` 并判定通过
#### Scenario: null 原始值简写等价 equals
- **WHEN** expect 字段配置为 ValueMatcher 类型且值为 `null`,实际值为 `null`
- **THEN** 系统 SHALL 将 `null` 归一化为 `{equals: null}` 并判定通过
- **WHEN** expect 字段配置为 RawValueExpectation 类型且值为 `null`,实际值为 `null`
- **THEN** 系统 SHALL 在 resolve 阶段`null` 归一化为 `{equals: null}` 并判定通过
#### Scenario: 原始值简写不匹配
- **WHEN** expect 字段配置为 `finishReason: "stop"` 且实际值为 `"error"`
- **THEN** 系统 SHALL 判定不通过并生成 mismatch failure
### Requirement: ValueMatcher 启动期校验
系统 SHALL 在启动期对所有 `ValueMatcher` 字段执行严格的类型和语义校验。校验 SHALL 同时接受 primitive 原始值和 ValueMatcher 对象两种形式。当输入为 primitive 原始值时,系统 SHALL 视为合法配置(等价于 `{equals: value}`),无需进一步校验 matcher 字段。当输入为 ValueMatcher 对象时,`contains` MUST 为 string。`equals` MAY 为任意 JSON value。`exists``empty` MUST 为 boolean。`gt``gte``lt``lte` MUST 为有限数字(`Number.isFinite`)。`regex` MUST 为可编译的 string pattern并通过 ReDoS 风险校验。ValueMatcher 对象 MUST 至少包含一个合法 matcher 字段,空对象 `{}` SHALL 导致启动期配置错误。ValueMatcher 对象 MUST NOT 包含未知字段,任何不属于 `equals``contains``regex``exists``empty``gt``gte``lt``lte` 的字段 SHALL 导致启动期配置错误。
系统 SHALL 在启动期对所有 `RawValueExpectation` 字段执行严格的类型和语义校验。校验 SHALL 同时接受 primitive 原始值和 `ValueMatcher` 对象两种形式,但 MUST NOT 修改输入对象。当输入为 primitive 原始值时,系统 SHALL 视为合法配置(等价于 `{equals: value}`),无需进一步校验 matcher 字段。当输入为 `ValueMatcher` 对象时,`contains` MUST 为 string。`equals` MAY 为任意 JSON value。`exists``empty` MUST 为 boolean。`gt``gte``lt``lte` MUST 为有限数字(`Number.isFinite`)。`regex` MUST 为可编译的 string pattern并通过 ReDoS 风险校验。`ValueMatcher` 对象 MUST 至少包含一个合法 matcher 字段,空对象 `{}` SHALL 导致启动期配置错误。`ValueMatcher` 对象 MUST NOT 包含未知字段,任何不属于 `equals``contains``regex``exists``empty``gt``gte``lt``lte` 的字段 SHALL 导致启动期配置错误。
#### Scenario: 空 matcher 对象被拒绝
- **WHEN** YAML 配置中任一 matcher 对象为空 `{}`
@@ -65,23 +65,23 @@
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该字段必须为布尔值
#### Scenario: 字符串原始值校验通过
- **WHEN** YAML 配置中 ValueMatcher 字段值为字符串 `"stop"`
- **THEN** 系统 SHALL 接受该配置,视为 `{equals: "stop"}`
- **WHEN** YAML 配置中 RawValueExpectation 字段值为字符串 `"stop"`
- **THEN** 系统 SHALL 接受该配置,但 SHALL NOT 在语义校验阶段修改输入对象
#### Scenario: 数字原始值校验通过
- **WHEN** YAML 配置中 ValueMatcher 字段值为数字 `5000`
- **THEN** 系统 SHALL 接受该配置,视为 `{equals: 5000}`
- **WHEN** YAML 配置中 RawValueExpectation 字段值为数字 `5000`
- **THEN** 系统 SHALL 接受该配置,但 SHALL NOT 在语义校验阶段修改输入对象
#### Scenario: null 原始值校验通过
- **WHEN** YAML 配置中 ValueMatcher 字段值为 `null`
- **THEN** 系统 SHALL 接受该配置,视为 `{equals: null}`
- **WHEN** YAML 配置中 RawValueExpectation 字段值为 `null`
- **THEN** 系统 SHALL 接受该配置,但 SHALL NOT 在语义校验阶段修改输入对象
#### Scenario: 数组原始值被拒绝
- **WHEN** YAML 配置中 ValueMatcher 字段值为数组 `[1, 2]`
- **WHEN** YAML 配置中 RawValueExpectation 字段值为数组 `[1, 2]`
- **THEN** 系统 SHALL 在启动期配置校验失败,提示必须为 primitive 原始值或 matcher 对象;如需数组 equals 匹配应写成 `{equals: [1, 2]}`
#### Scenario: 对象原始值必须显式 equals
- **WHEN** YAML 配置中 ValueMatcher 字段值为对象 `{foo: "bar"}`,且 `foo` 不是合法 matcher 字段
- **WHEN** YAML 配置中 RawValueExpectation 字段值为对象 `{foo: "bar"}`,且 `foo` 不是合法 matcher 字段
- **THEN** 系统 SHALL 在启动期配置校验失败,提示 `foo` 是未知 matcher如需对象 equals 匹配应写成 `{equals: {foo: "bar"}}`
### Requirement: empty matcher 语义
@@ -141,92 +141,108 @@
- **WHEN** YAML 配置中任一 `regex``"(a+)+$"`
- **THEN** 系统 SHALL 在启动期配置校验失败,提示正则存在 ReDoS 风险
### Requirement: ContentRules 内容规则数组
系统 SHALL 提供共享 `ContentRules` 表达返回内容断言。`ContentRules` MUST 为有序数组,数组项 SHALL 为直接 `ValueMatcher`,或 `json``css``xpath` 三类 extractor 规则之一。系统 SHALL 按数组顺序执行全部规则,任一规则失败时 SHALL 立即停止并返回该规则的 failure。系统 MUST NOT 支持内容字段的非数组对象快捷写法。
### Requirement: ContentExpectations 内容断言数组
系统 SHALL 提供共享 `ContentExpectations` 表达返回内容断言。`ContentExpectations` MUST 为有序数组,数组项 SHALL 为直接 `ValueMatcher`,或 `json``css``xpath` 三类 extractor expectation 之一。系统 SHALL 在 resolve 阶段将 Raw content DSL 解析为带 `kind` 字段的 Resolved `ContentExpectation` 执行计划,并按数组顺序执行全部 expectation任一 expectation 失败时 SHALL 立即停止并返回该 expectation 的 failure。系统 MUST NOT 支持内容字段的非数组对象快捷写法。
#### Scenario: 直接 matcher 内容规则
- **WHEN** 内容字段配置 `[{contains: "ready"}, {regex: "listening on \\d+"}]` 且原始内容同时满足两条规则
#### Scenario: 直接 matcher 内容 expectation
- **WHEN** 内容字段配置 `[{contains: "ready"}, {regex: "listening on \\d+"}]` 且原始内容同时满足两条 expectation
- **THEN** 系统 SHALL 判定该内容字段通过
#### Scenario: 内容规则数组快速失败
- **WHEN** 内容字段配置三条规则且第二条规则失败
- **THEN** 系统 SHALL 返回第二条规则的 failure并 MUST NOT 执行第三条规则
#### Scenario: 内容 expectation 数组快速失败
- **WHEN** 内容字段配置三条 expectation 且第二条 expectation 失败
- **THEN** 系统 SHALL 返回第二条 expectation 的 failure并 MUST NOT 执行第三条 expectation
#### Scenario: 内容字段必须为数组
- **WHEN** YAML 中内容字段配置为 `{contains: "ok"}` 而不是数组
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该内容字段必须为规则数组
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该内容字段必须为 expectation 数组
### Requirement: ContentRule 互斥性约束
一条 `ContentRule` MUST 为直接 `ValueMatcher` 或恰好一个 extractor`json``css``xpath` 之一)。系统 MUST NOT 允许同一条规则同时包含多个 extractor。直接 `ValueMatcher` 规则 MUST NOT 包含 `json``css``xpath` 字段。系统 SHALL 在启动期对违反互斥性的规则报错。
#### Scenario: Resolved content expectation 使用 kind
- **WHEN** Raw 内容字段包含直接 matcher、json extractor、css extractor 和 xpath extractor
- **THEN** resolve 阶段 SHALL 分别生成 `kind="value"``kind="json"``kind="css"``kind="xpath"` 的 Resolved `ContentExpectation`
### Requirement: ContentExpectation 互斥性约束
一条 Raw `ContentExpectation` MUST 为直接 `ValueMatcher` 或恰好一个 extractor`json``css``xpath` 之一)。系统 MUST NOT 允许同一条 Raw expectation 同时包含多个 extractor。直接 `ValueMatcher` expectation MUST NOT 包含 `json``css``xpath` 字段。系统 SHALL 在启动期对违反互斥性的 Raw expectation 报错。
#### Scenario: 多 extractor 被拒绝
- **WHEN** YAML 中内容规则`{json: {path: "$.a"}, css: {selector: "div"}}`
- **THEN** 系统 SHALL 在启动期配置校验失败,提示一条规则不能同时包含多个 extractor
- **WHEN** YAML 中内容 expectation `{json: {path: "$.a"}, css: {selector: "div"}}`
- **THEN** 系统 SHALL 在启动期配置校验失败,提示一条 expectation 不能同时包含多个 extractor
#### Scenario: 直接 matcher 混入 extractor 被拒绝
- **WHEN** YAML 中内容规则`{contains: "ok", json: {path: "$.a"}}`
- **WHEN** YAML 中内容 expectation `{contains: "ok", json: {path: "$.a"}}`
- **THEN** 系统 SHALL 在启动期配置校验失败,提示直接 matcher 不能与 extractor 混用
### Requirement: 空 ContentRules 数组语义
`ContentRules` 空数组 `[]` SHALL 被系统接受为合法配置。运行期空数组 SHALL 等价于无规则,即该内容字段的断言直接通过。
### Requirement: 空 ContentExpectations 数组语义
`ContentExpectations` 空数组 `[]` SHALL 被系统接受为合法配置。运行期空数组 SHALL 等价于无内容 expectation,即该内容字段的断言直接通过。
#### Scenario: 空 body 数组通过
- **WHEN** HTTP target 配置 `expect.body: []` 且响应体为任意内容
- **THEN** 系统 SHALL 判定 body 阶段通过
### Requirement: ContentRules 非字符串值序列化
`ContentRules` 的观测源为非字符串值(如对象或数组)时,直接 `ValueMatcher``contains``regex` SHALL 先将值 JSON 序列化为字符串后匹配。`equals` SHALL 直接在原始结构化值上使用深度相等比较,不进行序列化。
### Requirement: ContentExpectations 非字符串值序列化
`ContentExpectations` 的观测源为非字符串值(如对象或数组)时,直接 `ValueMatcher``contains``regex` SHALL 先将值 JSON 序列化为字符串后匹配。`equals` SHALL 直接在原始结构化值上使用深度相等比较,不进行序列化。
#### Scenario: 对象序列化后 contains 匹配
- **WHEN** ContentRules 观测源为 `{status: "ok"}`规则`{contains: "ok"}`
- **WHEN** ContentExpectations 观测源为 `{status: "ok"}` expectation `{contains: "ok"}`
- **THEN** 系统 SHALL 将对象 JSON 序列化后执行 contains 匹配
#### Scenario: 对象 equals 不序列化
- **WHEN** ContentRules 观测源为 `{status: "ok"}`规则`{equals: {status: "ok"}}`
- **WHEN** ContentExpectations 观测源为 `{status: "ok"}` expectation `{equals: {status: "ok"}}`
- **THEN** 系统 SHALL 直接在结构化值上使用深度相等比较
### Requirement: ContentRules 提取器
系统 SHALL 支持在 `ContentRules` 中使用 `json``css``xpath` extractor。`json.path` MUST 使用现有 JSONPath 子集。`css.selector` MUST 为非空字符串,并 MAY 配置 `attr` 提取属性值。`xpath.path` MUST 为非空字符串,并 SHALL 在启动期进行可编译校验。Extractor 内部 MAY 包含任意 `ValueMatcher` 字段。Extractor 规则未配置任何 matcher 时 SHALL 等价于 `exists: true`
### Requirement: ContentExpectations 提取器
系统 SHALL 支持在 `ContentExpectations` 中使用 `json``css``xpath` extractor。`json.path` MUST 使用现有 JSONPath 子集。`css.selector` MUST 为非空字符串,并 MAY 配置 `attr` 提取属性值。`xpath.path` MUST 为非空字符串,并 SHALL 在启动期进行可编译校验。Extractor 内部 MAY 包含任意 `ValueMatcher` 字段。Extractor expectation 未配置任何 matcher 时resolve 阶段 SHALL 将其 Resolved matcher 物化为 `{ exists: true }`
#### Scenario: json extractor 数字比较
- **WHEN** 原始内容为 JSON 字符串 `{"count": 2}`规则`{json: {path: "$.count", gte: 1}}`
- **THEN** 系统 SHALL 解析 JSON、提取 `$.count` 并判定该规则通过
- **WHEN** 原始内容为 JSON 字符串 `{"count": 2}` expectation `{json: {path: "$.count", gte: 1}}`
- **THEN** 系统 SHALL 解析 JSON、提取 `$.count` 并判定该 expectation 通过
#### Scenario: json extractor 存在性默认语义
- **WHEN** 原始内容为 JSON 字符串 `{"user": {"id": null}}`规则`{json: {path: "$.user.id"}}`
- **THEN** 系统 SHALL 将该规则视为 `{json: {path: "$.user.id", exists: true}}` 并判定通过
- **WHEN** 原始内容为 JSON 字符串 `{"user": {"id": null}}` expectation `{json: {path: "$.user.id"}}`
- **THEN** resolve 阶段 SHALL 将该 expectation 的 matcher 物化为 `{exists: true}`在运行期判定通过
#### Scenario: css attr 存在性默认语义
- **WHEN** 原始内容包含 `<meta name="status" content="ok">`规则`{css: {selector: "meta[name=status]", attr: "content"}}`
- **THEN** 系统 SHALL 在属性存在时判定该规则通过
- **WHEN** 原始内容包含 `<meta name="status" content="ok">` expectation `{css: {selector: "meta[name=status]", attr: "content"}}`
- **THEN** resolve 阶段 SHALL 将该 expectation 的 matcher 物化为 `{exists: true}`在属性存在时判定通过
#### Scenario: xpath 无匹配节点失败
- **WHEN** XML 内容中不存在 XPath 指向的节点,且规则`{xpath: {path: "/root/status"}}`
- **THEN** 系统 SHALL 判定该规则不通过并生成 phase 对应内容字段的 mismatch failure
- **WHEN** XML 内容中不存在 XPath 指向的节点,且 expectation `{xpath: {path: "/root/status"}}`
- **THEN** 系统 SHALL 判定该 expectation 不通过并生成 phase 对应内容字段的 mismatch failure
### Requirement: KeyValueExpect 键值规则
系统 SHALL 提供共享 `KeyValueExpect` 表达键值型观测值断言。`KeyValueExpect` SHALL 为动态键对象,每个键对应的值 MAY`ValueMatcher` 或 JSON 字面量。字面量值 SHALL 等价于 `{equals: <literal>}`。调用方 MAY 指定 key 规范化策略HTTP 与 LLM headers MUST 使用大小写不敏感的 key 匹配。
### Requirement: KeyedExpectations 键控断言数组
系统 SHALL 提供共享 `KeyedExpectations` 表达键值型观测值断言。Raw `KeyedExpectations` SHALL 为动态键对象,每个键对应的值 MUST`RawValueExpectation`;数组和对象简写 MUST 被拒绝,数组或对象 equals 匹配 MUST 显式写为 `{equals: <value>}`。Resolved `KeyedExpectations` SHALL 为有序数组,每个元素包含原始 key 和已归一化的 `ValueExpectation` matcher。调用方 MAY 指定 key 规范化策略HTTP 与 LLM headers MUST 使用大小写不敏感的 key 匹配,并 MUST 在启动期校验或 resolve 阶段拒绝归一化后重复的 header key。DB rows 不做 key 归一化,按查询结果列名大小写敏感匹配。
#### Scenario: headers 字面量快捷写法
- **WHEN** 响应 headers 中 `content-type``application/json`,且配置为 `headers: {Content-Type: "application/json"}`
- **THEN** 系统 SHALL 按大小写不敏感 key 匹配并使用 equals 语义判定通过
- **THEN** resolve 阶段 SHALL 将该项解析为 keyed expectation `{key: "Content-Type", matcher: {equals: "application/json"}}`,运行期按大小写不敏感 key 匹配并判定通过
#### Scenario: headers matcher 写法
- **WHEN** 响应 headers 中 `content-type``application/json; charset=utf-8`,且配置为 `headers: {Content-Type: {contains: "application/json"}}`
- **THEN** 系统 SHALL 判定该 header 规则通过
- **THEN** 系统 SHALL 判定该 header expectation 通过
#### Scenario: 缺失键 exists false
- **WHEN** 观测键值表中不存在 `x-debug`,且配置为 `{x-debug: {exists: false}}`
- **THEN** 系统 SHALL 判定该键规则通过
- **THEN** 系统 SHALL 判定该 keyed expectation 通过
#### Scenario: keyed 对象值必须显式 equals
- **WHEN** Raw keyed expectation 的某个值是对象 `{foo: "bar"}` 且未写在 `equals`
- **THEN** 系统 SHALL 在启动期配置校验失败,提示对象 equals 必须显式写成 `{equals: {foo: "bar"}}`
#### Scenario: keyed 数组值必须显式 equals
- **WHEN** Raw keyed expectation 的某个值是数组 `["a"]` 且未写在 `equals`
- **THEN** 系统 SHALL 在启动期配置校验失败,提示数组 equals 必须显式写成 `{equals: ["a"]}`
#### Scenario: header 归一化重复 key 被拒绝
- **WHEN** HTTP 或 LLM `expect.headers` 同时配置 `Content-Type``content-type`
- **THEN** 系统 SHALL 在启动期配置校验失败,提示 header key 归一化后重复
### Requirement: 结构化失败路径
系统 SHALL 在共享 matcher、content 和 key-value 断言失败时生成结构化 `CheckFailure`。failure SHALL 包含 `kind``phase``path``message`,并在 mismatch 场景包含 `expected``actual`。内容规则 failure path SHALL 包含数组下标key-value failure path SHALL 包含键名extractor failure path SHALL 包含 extractor 类型和 path/selector 信息。
系统 SHALL 在共享 matcher、content 和 keyed expectation 断言失败时生成结构化 `CheckFailure`。failure SHALL 包含 `kind``phase``path``message`,并在 mismatch 场景包含 `expected``actual`。内容 expectation failure path SHALL 包含数组下标keyed expectation failure path SHALL 包含键名extractor failure path SHALL 包含 extractor 类型和 path/selector 信息。failure.expected SHOULD 使用用户可理解的 matcher 或 expectation 片段MUST NOT 直接暴露 Resolved `kind` 执行计划;单字段 `equals` 包装 SHOULD 展示为原始 expected 值。
#### Scenario: ContentRules 失败路径
- **WHEN** `expect.body[1].json` 规则失败
#### Scenario: ContentExpectations 失败路径
- **WHEN** `expect.body[1].json` expectation 失败
- **THEN** failure.path SHALL 指向 `body[1].json($.path)` 或等价可定位路径failure.phase SHALL 为 `body`
#### Scenario: KeyValueExpect 失败路径
#### Scenario: KeyedExpectations 失败路径
- **WHEN** `expect.headers.Content-Type` 不匹配
- **THEN** failure.path SHALL 指向 `headers.Content-Type`failure.phase SHALL 为 `headers`

View File

@@ -110,11 +110,11 @@
- **THEN** 系统 SHALL 记录 `matched=false`failure 的 kind 为 `error`phase 为 `icmp`path 为 `parse`message 包含 "无法解析 icmp 输出"
### Requirement: icmp expect 校验
系统 SHALL 支持 icmp 专属 expect包括 `alive``packetLossPercent``avgLatencyMs``maxLatencyMs``durationMs`,并按 alive、packetLossPercent、avgLatencyMs、maxLatencyMs、durationMs 的阶段顺序快速失败。`alive` SHALL 保持布尔状态语义,未配置时默认 `true``packetLossPercent` SHALL 表示 0 到 100 的丢包率百分比,并使用共享 `ValueMatcher``avgLatencyMs``maxLatencyMs``durationMs` SHALL 使用共享 `ValueMatcher`
系统 SHALL 支持 icmp 专属 expect包括 `alive``packetLossPercent``avgLatencyMs``maxLatencyMs``durationMs`,并按 alive、packetLossPercent、avgLatencyMs、maxLatencyMs、durationMs 的阶段顺序快速失败。`alive` SHALL 保持布尔状态语义,未配置时在 Resolved expect 中默认 `true``packetLossPercent` SHALL 表示 0 到 100 的丢包率百分比,并使用共享 `RawValueExpectation` 输入、运行期 `ValueExpectation` 执行`avgLatencyMs``maxLatencyMs``durationMs` SHALL 使用共享 `RawValueExpectation` 输入、运行期 `ValueExpectation` 执行
#### Scenario: 默认 alive 成功语义
- **WHEN** icmp target 未显式配置 `expect.alive`
- **THEN** 系统 SHALL 使用默认 `expect.alive: true` 进行校验
- **THEN** 系统 SHALL 在 Resolved icmp expect 中使用默认 `alive: true` 进行校验
#### Scenario: alive 校验通过
- **WHEN** icmp target 配置 `expect.alive: true`,且目标主机可达
@@ -165,17 +165,21 @@
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
#### Scenario: packetLossPercent 类型非法
- **WHEN** YAML 中 icmp target 的 `expect.packetLossPercent` 不是合法 `ValueMatcher`,或其数值范围无法用于 0 到 100 的百分比断言
- **WHEN** YAML 中 icmp target 的 `expect.packetLossPercent` 不是合法 `RawValueExpectation`,或其数值范围无法用于 0 到 100 的百分比断言
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.packetLossPercent 格式错误
#### Scenario: avgLatencyMs 类型非法
- **WHEN** YAML 中 icmp target 的 `expect.avgLatencyMs` 不是合法 `ValueMatcher`
- **WHEN** YAML 中 icmp target 的 `expect.avgLatencyMs` 不是合法 `RawValueExpectation`
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.avgLatencyMs 格式错误
#### Scenario: maxLatencyMs 类型非法
- **WHEN** YAML 中 icmp target 的 `expect.maxLatencyMs` 不是合法 `ValueMatcher`
- **WHEN** YAML 中 icmp target 的 `expect.maxLatencyMs` 不是合法 `RawValueExpectation`
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.maxLatencyMs 格式错误
#### Scenario: Raw icmp expect 不被校验阶段修改
- **WHEN** YAML 中 icmp target 配置 `expect.durationMs: 5000`
- **THEN** 语义校验 SHALL 接受该 Raw primitive 简写且 MUST NOT 将输入对象原地改写为 `{equals: 5000}`
### Requirement: icmp detail 摘要
系统 SHALL 在 icmp API 序列化时从 observation 动态生成结构化 detail 摘要展示关键指标。API registry type SHALL 仍为 `icmp`

View File

@@ -108,18 +108,18 @@ LLM checker SHALL 在 SDK 调用结果和 expect 断言之间构建 `LlmCheckObs
- **THEN** LLM checker SHALL 返回 `phase: "request"` 的 error failure
### Requirement: LLM Expect 断言
LLM checker SHALL 支持 `expect.status``expect.headers``expect.output``expect.finishReason``expect.rawFinishReason``expect.usage.inputTokens``expect.usage.outputTokens``expect.usage.totalTokens``expect.stream.completed``expect.stream.firstTokenMs``expect.durationMs``expect.status` SHALL 保持 HTTP 状态码数组语义并复用 HTTP status 断言。`expect.headers` SHALL 使用共享 `KeyValueExpect`header key 大小写不敏感。`expect.output` MUST 使用共享 `ContentRules``expect.finishReason``expect.rawFinishReason` SHALL 使用共享 `ValueMatcher``expect.usage.*``expect.stream.firstTokenMs``expect.durationMs` SHALL 使用共享 `ValueMatcher`。LLM checker MUST 按固定顺序快速失败,非流式顺序为 status、headers、output、finishReason、rawFinishReason、usage、durationMs流式顺序为 status、headers、stream.completed、stream.firstTokenMs、output、finishReason、rawFinishReason、usage、durationMs。
LLM checker SHALL 支持 `expect.status``expect.headers``expect.output``expect.finishReason``expect.rawFinishReason``expect.usage.inputTokens``expect.usage.outputTokens``expect.usage.totalTokens``expect.stream.completed``expect.stream.firstTokenMs``expect.durationMs``expect.status` SHALL 保持 HTTP 状态码数组语义并复用共享 status code 断言,未配置时在 Resolved expect 中物化默认 `[200]``expect.headers` SHALL 使用共享 `RawKeyedExpectations` 输入并在运行期使用 `KeyedExpectations`header key 大小写不敏感。`expect.output` MUST 使用共享 `RawContentExpectations` 输入并在运行期使用 `ContentExpectations``expect.finishReason``expect.rawFinishReason` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation``expect.usage.*``expect.stream.firstTokenMs``expect.durationMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation`。LLM checker MUST 按固定顺序快速失败,非流式顺序为 status、headers、output、finishReason、rawFinishReason、usage、durationMs流式顺序为 status、headers、stream.completed、stream.firstTokenMs、output、finishReason、rawFinishReason、usage、durationMs。
#### Scenario: 默认 status 断言
- **WHEN** LLM target 未配置 `expect.status`
- **THEN** LLM checker SHALL 使用默认 `status: [200]` 语义
- **THEN** LLM checker SHALL 在 Resolved expect 中使用默认 `status: [200]` 语义
#### Scenario: expect headers 通过
- **WHEN** observing fetch 捕获的响应 headers 满足 `expect.headers` 配置
- **THEN** LLM checker SHALL 判定 headers 断言通过
- **THEN** LLM checker SHALL 通过共享 header expectation 包装函数判定 headers 断言通过
#### Scenario: output ContentRules 通过
- **WHEN** LLM 输出文本满足 `expect.output` 中配置的全部 ContentRules
#### Scenario: output ContentExpectations 通过
- **WHEN** LLM 输出文本满足 `expect.output` 中配置的全部 ContentExpectations
- **THEN** LLM checker SHALL 判定 output 阶段通过
#### Scenario: finishReason ValueMatcher 通过
@@ -175,18 +175,22 @@ LLM checker SHALL 使用共享 `ContentRules` 校验 `expect.output`。每个 ou
#### Scenario: output JSONPath 存在性默认语义
- **WHEN** `outputText` 是 JSON 字符串且 target 配置 `expect.output: [{json: {path: "$.status"}}]`
- **THEN** LLM checker SHALL 将该规则按 `exists: true` 语义执行
- **THEN** resolve 阶段 SHALL 将该 expectation 的 matcher 物化为 `{exists: true}`,运行期按存在性语义执行
#### Scenario: output 规则按顺序快速失败
- **WHEN** `expect.output` 包含多个规则且第一条规则失败
- **THEN** LLM checker SHALL 返回第一条失败规则的 mismatch failure不继续校验后续 output 规则
- **WHEN** `expect.output` 包含多个 expectation 且第一条 expectation 失败
- **THEN** LLM checker SHALL 返回第一条失败 expectation 的 mismatch failure不继续校验后续 output expectation
### Requirement: LLM Stream 断言
LLM checker SHALL 仅允许 `mode: stream` 使用 `expect.stream``expect.stream.completed` 未配置时LLM checker SHALL 在 stream observation 路径使用默认 `true` 语义`expect.stream.firstTokenMs` SHALL 使用共享 `ValueMatcher`仅统计第一个非空 `text-delta` 事件耗时,不统计 reasoning、tool call 或 source 事件。
LLM checker SHALL 仅允许 `mode: stream` 使用 `expect.stream`仅当用户配置了 `expect.stream` 且未配置 `expect.stream.completed`resolve 阶段 SHALL 在 Resolved expect 中物化默认 `completed: true`LLM checker MUST NOT 因为 `llm.mode: stream` 自动添加 `stream.completed` 断言`expect.stream.firstTokenMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation`仅统计第一个非空 `text-delta` 事件耗时,不统计 reasoning、tool call 或 source 事件。
#### Scenario: stream completed 默认值
- **WHEN** target 配置 `llm.mode: stream`配置 `expect.stream.completed`
- **THEN** LLM checker SHALL 要求 SDK stream 正常完成
- **WHEN** target 配置 `llm.mode: stream` 且配置 `expect.stream: {}`
- **THEN** resolve 阶段 SHALL 在 Resolved expect 中物化 `stream.completed: true`要求 SDK stream 正常完成
#### Scenario: 未配置 expect.stream 不添加 completed
- **WHEN** target 配置 `llm.mode: stream` 但未配置 `expect.stream`
- **THEN** resolve 阶段 SHALL NOT 自动添加 `stream.completed` 断言
#### Scenario: stream error
- **WHEN** `fullStream` 产生 error part

View File

@@ -101,7 +101,7 @@
`headers``env``variables` 等明确声明为动态键值表的对象外,配置中的未知字段 SHALL 导致启动期配置错误。系统 MUST NOT 静默忽略未知字段。
所有 ValueMatcher 类型的 expect 字段 SHALL 在 JSON Schema 契约中声明为 `anyOf: [primitiveValue, matcherObject]` 联合类型,同时接受 primitive 原始值string / number / boolean / null和 ValueMatcher 对象。语义 validator SHALL 在校验 ValueMatcher 字段之前执行归一化,将 primitive 原始值转换为 `{equals: value}` 对象形式。数组和对象 MUST NOT 作为 ValueMatcher 原始值简写;需要对数组或对象执行 equals 匹配时,配置 MUST 显式写成 `{equals: value}`
所有 `RawValueExpectation` 类型的 expect 字段 SHALL 在 JSON Schema 契约中声明为 `anyOf: [primitiveValue, matcherObject]` 联合类型,同时接受 primitive 原始值string / number / boolean / null`ValueMatcher` 对象。语义 validator SHALL 在不修改输入的前提下校验 `RawValueExpectation`primitive 原始值 SHALL 被视为合法 Raw 输入,并在 checker resolve 阶段转换为 `{equals: value}` 对象形式。数组和对象 MUST NOT 作为 `RawValueExpectation` 原始值简写;需要对数组或对象执行 equals 匹配时,配置 MUST 显式写成 `{equals: value}``RawKeyedExpectations` 的动态键值 schema SHALL 复用 `RawValueExpectation`MUST NOT 通过额外 JSON value 分支接受数组或对象简写。
#### Scenario: target 缺少必填字段
- **WHEN** YAML 中某个 target 缺少 id 或 type 字段
@@ -196,20 +196,20 @@
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 status 数字不合法
#### Scenario: durationMs matcher 非法
- **WHEN** YAML 中某个 target 的 `expect.durationMs` 不是合法 `ValueMatcher` 也不是 primitive 原始值
- **WHEN** YAML 中某个 target 的 `expect.durationMs` 不是合法 `RawValueExpectation`
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 expect.durationMs 格式错误
#### Scenario: durationMs 原始值简写合法
- **WHEN** YAML 中某个 target 配置 `expect.durationMs: 5000`
- **THEN** 系统 SHALL 接受该配置,归一化`{equals: 5000}` 后校验通过
- **THEN** 语义校验 SHALL 接受该 Raw primitive 简写且 MUST NOT 修改输入checker resolve 阶段 SHALL 将其解析`{equals: 5000}`
#### Scenario: ValueMatcher 字段字符串简写合法
- **WHEN** YAML 中某个 target 配置 `expect.finishReason: "stop"`
- **THEN** 系统 SHALL 接受该配置,归一化`{equals: "stop"}` 后校验通过
- **THEN** 语义校验 SHALL 接受该 Raw primitive 简写且 MUST NOT 修改输入checker resolve 阶段 SHALL 将其解析`{equals: "stop"}`
#### Scenario: ValueMatcher 字段 null 简写合法
- **WHEN** YAML 中某个 target 配置 ValueMatcher 字段值为 `null`
- **THEN** 系统 SHALL 接受该配置,归一化`{equals: null}` 后校验通过
- **THEN** 语义校验 SHALL 接受该 Raw primitive 简写且 MUST NOT 修改输入checker resolve 阶段 SHALL 将其解析`{equals: null}`
#### Scenario: ValueMatcher 字段数组简写非法
- **WHEN** YAML 中某个 target 配置 ValueMatcher 字段值为数组 `[1, 2]`
@@ -223,40 +223,40 @@
- **WHEN** YAML 中某个 target 配置 `type: icmp` 但缺少 `icmp.host`
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 icmp.host 字段
#### Scenario: ping expect 未知字段
- **WHEN** YAML 中 ping target 的 expect 包含非 ping expect 字段
#### Scenario: icmp expect 未知字段
- **WHEN** YAML 中 icmp target 的 expect 包含非 icmp expect 字段
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
#### Scenario: HTTP expect headers 非法
- **WHEN** YAML 中某个 HTTP target 的 `expect.headers` 不是对象,或某个 header 期望既不是字符串也不是合法 operator
- **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 rule 缺少支持字段
#### Scenario: HTTP body expectation 缺少支持字段
- **WHEN** YAML 中某个 HTTP target 的 `expect.body` 数组项未包含 contains、regex、json、css、xpath 任一支持字段
- **THEN** 系统 SHALL 以错误退出,提示该 body rule 缺少支持的规则类型
- **THEN** 系统 SHALL 以错误退出,提示该 body expectation 缺少支持的 expectation 类型
#### Scenario: HTTP body rule 同时配置多个支持字段
- **WHEN** YAML 中某个 HTTP target 的同一条 body rule 同时包含 contains、regex、json、css、xpath 中的多个支持字段
- **THEN** 系统 SHALL 以错误退出,提示每条 body rule 只能配置一种规则类型
#### 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 规则不是字符串或不是可编译正则表达式
- **WHEN** YAML 中某个 HTTP target 的 body regex expectation 不是字符串或不是可编译正则表达式
- **THEN** 系统 SHALL 以错误退出,提示该 body regex 不合法
#### Scenario: HTTP body json path 非法
- **WHEN** YAML 中某个 HTTP target 的 body json 规则缺少 path或 path 不符合系统支持的 JSONPath 子集
- **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 规则缺少 selector或 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 规则缺少 path或 path 不是非空字符串,或可被现有 XPath 库静态判定为语法错误
- **WHEN** YAML 中某个 HTTP target 的 body xpath expectation 缺少 path或 path 不是非空字符串,或可被现有 XPath 库静态判定为语法错误
- **THEN** 系统 SHALL 以错误退出,提示该 body xpath path 不合法
#### Scenario: expect matcher 类型非法
@@ -293,23 +293,27 @@
#### Scenario: 导出配置 JSON Schema
- **WHEN** 仓库生成或检查配置契约
- **THEN** 根目录 SHALL 存在 draft-07 `probe-config.schema.json`,且其内容 SHALL 与当前公共 fragments 和已注册 checker fragments 组装出的完整 schema 一致(包含 variables 段和 target 的 id/name 字段)。所有 ValueMatcher 字段的 schema SHALL 声明为 `anyOf: [primitiveValue, matcherObject]` 联合类型
- **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`
#### Scenario: JSON Schema ValueMatcher 接受原始值
- **WHEN** 使用 JSON Schema 校验配置文件中 ValueMatcher 字段值为数字 `5000`
- **THEN** JSON Schema 校验 SHALL 通过,因为 ValueMatcher schema 声明为 `anyOf: [primitiveValue, matcherObject]`
#### Scenario: JSON Schema RawValueExpectation 接受原始值
- **WHEN** 使用 JSON Schema 校验配置文件中 RawValueExpectation 字段值为数字 `5000`
- **THEN** JSON Schema 校验 SHALL 通过,因为 RawValueExpectation schema 声明为 `anyOf: [primitiveValue, matcherObject]`
#### Scenario: JSON Schema ValueMatcher 接受 matcher 对象
- **WHEN** 使用 JSON Schema 校验配置文件中 ValueMatcher 字段值为 `{lte: 5000}`
#### Scenario: JSON Schema RawValueExpectation 接受 matcher 对象
- **WHEN** 使用 JSON Schema 校验配置文件中 RawValueExpectation 字段值为 `{lte: 5000}`
- **THEN** JSON Schema 校验 SHALL 通过
#### Scenario: JSON Schema ValueMatcher 拒绝数组原始值
- **WHEN** 使用 JSON Schema 校验配置文件中 ValueMatcher 字段值为数组 `[1, 2]`
#### Scenario: JSON Schema RawValueExpectation 拒绝数组原始值
- **WHEN** 使用 JSON Schema 校验配置文件中 RawValueExpectation 字段值为数组 `[1, 2]`
- **THEN** JSON Schema 校验 SHALL 失败,因为数组不属于 primitive 原始值或 matcher 对象
#### Scenario: JSON Schema ValueMatcher 接受 equals 数组对象
- **WHEN** 使用 JSON Schema 校验配置文件中 ValueMatcher 字段值为 `{equals: [1, 2]}``{equals: {status: "ok"}}`
#### Scenario: JSON Schema RawValueExpectation 接受 equals 数组对象
- **WHEN** 使用 JSON Schema 校验配置文件中 RawValueExpectation 字段值为 `{equals: [1, 2]}``{equals: {status: "ok"}}`
- **THEN** JSON Schema 校验 SHALL 通过,因为 `equals` 支持任意 JSON value
#### Scenario: JSON Schema RawKeyedExpectations 拒绝数组对象简写
- **WHEN** 使用 JSON Schema 校验配置文件中 `expect.headers.X` 或 DB row 列值为数组 `["a"]` 或对象 `{nested: "ok"}`
- **THEN** JSON Schema 校验 SHALL 失败,除非该值显式写在 `{equals: ...}`
系统 SHALL 支持使用单位字符串配置读取上限,单位包括 `B``KB``MB``GB`
#### Scenario: 解析 MB
@@ -339,43 +343,45 @@
- **THEN** 系统 SHALL 调用 `Bun.YAML.parse()` 将内容解析为配置对象
### Requirement: expect 配置增强
系统 SHALL 支持 typed target 的领域专用 expect 配置,并通过共享 `ValueMatcher``ContentRules``KeyValueExpect` 表达可复用断言能力。状态类字段 SHALL 保持枚举或布尔语义,包括 HTTP/LLM 的 `status`支持精确数字和范围模式、cmd 的 `exitCode`、tcp 的 `connected`ping`alive` 和 udp 的 `responded`数字指标字段 SHALL 使用 `ValueMatcher`,包括通用 `durationMs`、db 的 `rowCount`、udp 的 `responseSize`/`sourceHost`/`sourcePort`ping`packetLossPercent`/`avgLatencyMs`/`maxLatencyMs`、llm 的 usage token 与 stream 首 token 耗时。内容类字段 MUST 使用 `ContentRules` 数组表达配置顺序,包括 HTTP `body`、cmd `stdout`/`stderr`、tcp `banner`、udp `response`、llm `output` 和 db `result`。LLM `finishReason``rawFinishReason` SHALL 使用 `ValueMatcher`(非 ContentRules因为它们是单值字符串元数据。键值类字段 SHALL 使用 `KeyValueExpect`,包括 HTTP/LLM `headers` 和 db `rows` 中的列值断言db `rows` 的类型为 `Array<KeyValueExpect>`,外层数组按行索引,内层每个元素为 KeyValueExpect)。
系统 SHALL 支持 typed target 的领域专用 expect 配置,并通过共享 `ValueMatcher``ContentExpectations``KeyedExpectations` 表达可复用断言能力。状态类字段 SHALL 保持枚举或布尔语义,包括 HTTP/LLM 的 `status`支持精确数字和范围模式、cmd 的 `exitCode`、tcp 的 `connected`icmp`alive` 和 udp 的 `responded`value 类指标字段 SHALL 使用 `RawValueExpectation` 输入,并在 resolve 阶段归一化为运行期 `ValueExpectation`,包括通用 `durationMs`、db 的 `rowCount`、udp 的 `responseSize`/`sourceHost`/`sourcePort`icmp`packetLossPercent`/`avgLatencyMs`/`maxLatencyMs`、llm 的 usage token 与 stream 首 token 耗时。内容类字段 MUST 使用 `RawContentExpectations` 数组表达配置顺序,包括 HTTP `body`、cmd `stdout`/`stderr`、tcp `banner`、udp `response`、llm `output` 和 db `result`。LLM `finishReason``rawFinishReason` SHALL 使用 `RawValueExpectation`(非 ContentExpectations因为它们是单值字符串元数据。键值类字段 SHALL 使用 `RawKeyedExpectations`,包括 HTTP/LLM `headers` 和 db `rows` 中的列值断言db `rows` 的类型为 `Array<RawKeyedExpectations>`,外层数组按行索引,内层每个元素表达该行的列值断言)。
配置加载流程 SHALL 保留变量替换后的 Raw expect 作为用户配置快照,同时生成 Resolved expect 作为运行期执行计划。语义校验 SHALL 只读取 Raw expect 并报告问题MUST NOT 原地归一化或修改 Raw expect。Store 持久化 SHALL 写入 Raw expectchecker execute SHALL 只消费 Resolved expect。
#### Scenario: 解析 HTTP expect 配置
- **WHEN** YAML 配置文件中 HTTP target 的 expect 包含 status、headers、body 规则数组和 durationMs matcher
- **THEN** 系统 SHALL 正确解析并存储为 HTTP target 的 expect 字段
- **WHEN** YAML 配置文件中 HTTP target 的 expect 包含 status、headers、body expectation 数组和 durationMs matcher
- **THEN** 系统 SHALL 保留 Raw HTTP expect 快照,并生成包含默认 status、resolved keyed headers、resolved content body 和 resolved durationMs 的 HTTP Resolved expect
#### Scenario: 解析 cmd expect 配置
- **WHEN** YAML 配置文件中 cmd target 的 expect 包含 exitCode、stdout、stderr 和 durationMs matcher
- **THEN** 系统 SHALL 正确解析并存储为 cmd target 的 expect 字段
- **THEN** 系统 SHALL 保留 Raw cmd expect 快照,并生成包含默认 exitCode、resolved stdout/stderr content expectations 和 resolved durationMs 的 cmd Resolved expect
#### Scenario: 解析 db expect 配置
- **WHEN** YAML 配置文件中 db target 的 expect 包含 durationMs、rowCount、rows 和 result
- **THEN** 系统 SHALL 正确解析并存储为 db target 的 expect 字段
- **THEN** 系统 SHALL 保留 Raw db expect 快照,并生成包含 resolved rowCount、rows keyed expectations 和 result content expectations 的 db Resolved expect
#### Scenario: 解析 tcp expect 配置
- **WHEN** YAML 配置文件中 tcp target 的 expect 包含 connected、banner 规则数组和 durationMs matcher
- **THEN** 系统 SHALL 正确解析并存储为 tcp target 的 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: 解析 ping expect 配置
- **WHEN** YAML 配置文件中 ping target 的 expect 包含 alive、packetLossPercent、avgLatencyMs、maxLatencyMs 和 durationMs matcher
- **THEN** 系统 SHALL 正确解析并存储为 ping target 的 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 正确解析并存储为 udp target 的 expect 字段
- **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 正确解析并存储为 llm target 的 expect 字段,并保留 output 内容规则数组顺序
- **THEN** 系统 SHALL 保留 Raw llm expect 快照,并生成包含默认 status、resolved headers、output、finishReason、rawFinishReason、usage、stream 和 durationMs 的 llm Resolved expect并保留 output 内容 expectation 数组顺序
#### Scenario: 解析有序 ContentRules 数组
#### Scenario: 解析有序 ContentExpectations 数组
- **WHEN** YAML 中任一内容类 expect 配置 contains、json、regex 三个数组项
- **THEN** 系统 SHALL 保留数组顺序,供执行阶段按配置顺序快速失败
- **THEN** 系统 SHALL 在 Raw expect 中保留数组顺序,并在 Resolved expect 中保留执行顺序,供执行阶段按配置顺序快速失败
#### Scenario: 不配置 HTTP status
- **WHEN** HTTP target 未配置 `expect.status`
- **THEN** 系统 SHALL 在执行 expect 时使用默认 `status: [200]` 语义
- **THEN** 系统 SHALL 在 Resolved HTTP expect 中物化默认 `status: [200]` 语义
#### Scenario: 配置 HTTP status 范围模式
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]`
@@ -383,19 +389,23 @@
#### Scenario: 不配置 cmd exitCode
- **WHEN** cmd target 未配置 `expect.exitCode`
- **THEN** 系统 SHALL 在执行 expect 时使用默认 `exitCode: [0]` 语义
- **THEN** 系统 SHALL 在 Resolved cmd expect 中物化默认 `exitCode: [0]` 语义
#### Scenario: 不配置 expect
- **WHEN** target 未配置任何 expect 规则
- **THEN** 系统 SHALL 正常处理expect 字段为 undefined由各 checker 使用自身默认状态语义
- **THEN** 系统 SHALL 正常处理,Raw expect 快照为 undefinedResolved expect 由各 checker 物化自身默认状态语义
#### Scenario: Raw expect 不被语义校验修改
- **WHEN** YAML 中配置 `expect.durationMs: 1000`
- **THEN** 语义校验 SHALL 接受该 Raw primitive 简写且 MUST NOT 将 Raw 输入原地修改为 `{equals: 1000}`
#### Scenario: 旧 maxDurationMs 字段不再支持
- **WHEN** YAML 中任一 target 配置 `expect.maxDurationMs`
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该字段未知,并要求使用 `expect.durationMs`
#### Scenario: 旧 match 字段不再支持
- **WHEN** YAML 中任一 matcher 或内容规则配置 `match`
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该字段未知,并要求使用 `regex`
- **WHEN** YAML 中任一 matcher 或内容 expectation 配置 `match`
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该字段未知或不支持,并要求使用 `regex`
#### Scenario: durationMs matcher 配置
- **WHEN** YAML 中任一 target 配置 `expect.durationMs: {lte: 1000}`
@@ -405,14 +415,18 @@
- **WHEN** YAML 中 `http.headers``defaults.http.headers``llm.headers``defaults.llm.headers``expect.headers` 包含任意 header 名称,且对应值符合契约
- **THEN** 系统 SHALL 接受这些动态 header 名称
#### Scenario: ContentRules 字段必须为数组
#### Scenario: ContentExpectations 字段必须为数组
- **WHEN** YAML 中任一内容类 expect 字段配置为非数组
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该字段必须为规则数组
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该字段必须为 expectation 数组
#### Scenario: regex 字段非法
- **WHEN** YAML 中任一 `regex` 不是字符串、不是可编译正则或存在 ReDoS 风险
- **THEN** 系统 SHALL 在启动期配置校验失败,提示对应 regex 不合法
#### Scenario: Store 持久化 Raw expect
- **WHEN** 系统同步已解析 target 到 targets 表
- **THEN** `targets.expect` SHALL 存储变量替换后的 Raw expect JSON而不是包含 `kind` 或 resolved matcher 的运行期执行计划
### Requirement: 数据保留配置字段
配置 schema 的 `runtime` 段 SHALL 支持 `retention` 字段,类型为字符串,格式为 `<数字><单位>`(单位:`d` 天、`h` 小时、`m` 分钟),用于指定历史数据保留时长。
@@ -498,7 +512,7 @@
- **THEN** target schema SHALL 声明 `id` 的 minLength 为 1、maxLength 为 30并声明 `name` 为可选字段,类型为 string 或 null字符串的 minLength 为 1、maxLength 为 30
### Requirement: TCP 配置校验
系统 SHALL 在启动期对 tcp checker 的配置契约和语义执行严格校验。Tcp target 的 `tcp` 分组 SHALL 只允许 `host``port``readBanner``bannerReadTimeout``maxBannerBytes` 字段Tcp expect SHALL 只允许 `connected``durationMs``banner` 字段。`banner` MUST 为 `ContentRules` 数组。未知字段、非法类型、非法端口、非法 size 和不可编译正则 MUST 导致启动期配置错误
系统 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` 不是非空字符串
@@ -525,7 +539,7 @@
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.connected 必须为布尔值
#### Scenario: tcp expect banner 非法
- **WHEN** YAML 中 tcp target 的 `expect.banner` 不是合法 ContentRules 数组
- **WHEN** YAML 中 tcp target 的 `expect.banner` 不是合法 `RawContentExpectations` 数组
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.banner 格式错误
#### Scenario: tcp expect banner regex 正则非法
@@ -541,7 +555,7 @@
- **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 为 `ContentRules` 数组。`expect.finishReason``expect.rawFinishReason` SHALL 使用 `ValueMatcher``expect.usage.*``expect.stream.firstTokenMs` SHALL 使用 `ValueMatcher`。未知字段、非法 provider、非法 URL、非法 mode、非法认证组合、非法 options、非法 output 规则`mode: http` 下配置 `expect.stream` MUST 导致启动期配置错误。
系统 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 输入。
#### Scenario: llm provider 非法
- **WHEN** YAML 中 llm target 的 `llm.provider` 不是 `openai``openai-responses``anthropic`
@@ -591,24 +605,24 @@
- **WHEN** YAML 中 llm target 配置 `api``providerName``baseURL``apiKey``messages``maxRetries``request``maxBodyBytes``maxStreamBytes`
- **THEN** 系统 SHALL 以配置错误退出,提示 llm 分组包含未知字段
#### Scenario: llm output 规则缺少支持字段
#### Scenario: llm output expectation 缺少支持字段
- **WHEN** YAML 中 llm target 的 `expect.output` 数组项未包含任何合法 ValueMatcher 字段或 extractor
- **THEN** 系统 SHALL 以配置错误退出,提示 output rule 缺少支持的规则类型
- **THEN** 系统 SHALL 以配置错误退出,提示 output expectation 缺少支持的 expectation 类型
#### Scenario: llm output 规则同时配置多个 extractor
- **WHEN** YAML 中 llm target 的同一条 output rule 同时包含 json、css、xpath 中的多个 extractor
- **THEN** 系统 SHALL 以配置错误退出,提示每条 output rule 只能配置一种 extractor
#### 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 规则不是字符串、不是可编译正则表达式或存在 ReDoS 风险
- **WHEN** YAML 中 llm target 的 output regex expectation 不是字符串、不是可编译正则表达式或存在 ReDoS 风险
- **THEN** 系统 SHALL 以配置错误退出,提示该 output regex 不合法
#### Scenario: llm output json path 非法
- **WHEN** YAML 中 llm target 的 output json 规则缺少 path或 path 不符合系统支持的 JSONPath 子集
- **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` 不是合法 `ValueMatcher`
- **WHEN** YAML 中 llm target 的 `expect.usage.inputTokens``expect.usage.outputTokens``expect.usage.totalTokens` 不是合法 `RawValueExpectation`
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.usage 格式错误
#### Scenario: llm expect stream 仅允许 stream mode
@@ -616,5 +630,5 @@
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.stream 仅支持 stream mode
#### Scenario: llm expect stream firstTokenMs 非法
- **WHEN** YAML 中 llm target 的 `expect.stream.firstTokenMs` 不是合法 `ValueMatcher`
- **WHEN** YAML 中 llm target 的 `expect.stream.firstTokenMs` 不是合法 `RawValueExpectation`
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.stream.firstTokenMs 格式错误

View File

@@ -30,11 +30,11 @@
- **THEN** check_results 表的外键约束 SHALL 使用 `ON DELETE RESTRICT`,确保删除 target 时数据库层面阻止操作而非级联删除关联记录
### Requirement: targets 表同步
系统 SHALL 在启动时将 YAML 配置中的目标列表同步到 SQLite targets 表,并持久化 target 类型、展示名称元信息、展示摘要、领域配置、调度配置、expect 配置、分组信息和目标说明。配置中不存在的 target SHALL 被标记为非活跃而非删除。
系统 SHALL 在启动时将 YAML 配置中的目标列表同步到 SQLite targets 表,并持久化 target 类型、展示名称元信息、展示摘要、领域配置、调度配置、变量替换后的 Raw expect 配置快照、分组信息和目标说明。配置中不存在的 target SHALL 被标记为非活跃而非删除。`targets.expect` SHALL 存储 Raw expect JSON系统 MUST NOT 将 Resolved expect 执行计划、`ContentExpectation.kind` union 或已归一化 matcher 包装结构写入该列。
#### Scenario: 首次同步目标
- **WHEN** 数据库为空且 YAML 中定义了 N 个 typed target
- **THEN** 系统 SHALL 将所有目标插入 targets 表,包含 name、description、type、target、config、interval_ms、timeout_ms、expect、grp 和 active=1其中 name 和 description 均可为 NULL
- **THEN** 系统 SHALL 将所有目标插入 targets 表,包含 name、description、type、target、config、interval_ms、timeout_ms、expect、grp 和 active=1其中 name 和 description 均可为 NULLexpect 列保存变量替换后的 Raw expect JSON
#### Scenario: 配置变更后重新同步
- **WHEN** YAML 配置发生变更(新增、删除或修改目标)后重启
@@ -64,6 +64,14 @@
- **WHEN** YAML target 配置 `description: null`
- **THEN** targets 表 SHALL 将该目标的 description 存储为 NULL
#### Scenario: expect 列保存 Raw expect
- **WHEN** target 配置 `expect.body: [{json: {path: "$.status"}}]``expect.durationMs: 1000`
- **THEN** targets 表的 expect 列 SHALL 保存变量替换后的 Raw JSON包含原始 `json.path``durationMs: 1000`MUST NOT 保存 resolved `kind` 字段或 `{equals: 1000}` 执行计划
#### Scenario: 未配置 expect 写入 NULL
- **WHEN** target 未配置任何 expect
- **THEN** targets 表的 expect 列 SHALL 写入 NULL即使 Resolved expect 中存在 checker 默认状态语义
### Requirement: check_results 表追加写入
系统 SHALL 将每次检查结果追加写入 check_results 表,不更新或删除已有记录。

View File

@@ -121,11 +121,11 @@
- **THEN** 系统 SHALL 正常记录执行结果并进入 expect 校验
### Requirement: expect 校验
系统 SHALL 在 checker 执行完成后根据目标类型的 expect 配置校验观测结果,校验结果和首个失败原因记入 check result。HTTP checker 的 `durationMs` SHALL 表示完整 checker 执行耗时,包括重定向、响应体读取、响应体解码和 expect 校验。
系统 SHALL 在 checker 执行完成后根据目标类型的 Resolved expect 执行计划校验观测结果,校验结果和首个失败原因记入 check result。HTTP checker 的 `durationMs` SHALL 表示完整 checker 执行耗时,包括重定向、响应体读取、响应体解码和 expect 校验。HTTP `expect.durationMs` SHALL 使用 `RawValueExpectation` 输入并在 resolve 阶段转换为运行期 `ValueExpectation`;旧 `expect.maxDurationMs` MUST NOT 再作为运行期耗时阈值使用。
#### Scenario: HTTP 默认状态码
- **WHEN** HTTP target 未配置 `expect.status`
- **THEN** 系统 SHALL 默认 `status: [200]` 校验响应状态码
- **THEN** 系统 SHALL 在 Resolved HTTP expect 中物化默认 `status: [200]` 并按该语义校验响应状态码
#### Scenario: 校验 HTTP 状态码精确值
- **WHEN** HTTP target 配置了 `expect.status: [200, 201]`
@@ -144,39 +144,39 @@
- **THEN** 系统 SHALL 检查响应头是否符合指定规则,全部匹配时继续后续阶段
#### Scenario: 校验 HTTP 响应体
- **WHEN** HTTP target 配置了有序 `expect.body` 规则数组
- **THEN** 系统 SHALL 按数组顺序执行 body 规则,任一失败立即记录 failure 并停止后续规则
- **WHEN** HTTP target 配置了有序 `expect.body` ContentExpectations 数组
- **THEN** 系统 SHALL 按数组顺序执行 body expectations,任一失败立即记录 failure 并停止后续 expectation
#### Scenario: 校验 HTTP 完整耗时阈值
- **WHEN** 目标配置了 `expect.maxDurationMs`,且 HTTP checker 完整执行含重定向、body 读取、解码和 expect后的 durationMs 超过阈值
- **WHEN** 目标配置了 `expect.durationMs: {lte: 1000}`,且 HTTP checker 完整执行含重定向、body 读取、解码和 expect后的 durationMs 超过阈值
- **THEN** 系统 SHALL 判定 duration 不匹配,记录完整 durationMs 和 duration failure
#### Scenario: HTTP body 前耗时已超阈值
- **WHEN** HTTP target 配置了 body 校验和 `expect.maxDurationMs`,且进入 body 读取前的已耗时已超过阈值
#### Scenario: HTTP body 前耗时已不可能满足 durationMs 上界
- **WHEN** HTTP target 配置了 body 校验和 `expect.durationMs` 上界 matcher`{lte: 1000}`,且进入 body 读取前的已耗时已使该 matcher 不可能通过
- **THEN** 系统 SHALL 直接返回 duration failure且 MUST NOT 读取 response body
#### Scenario: HTTP body 失败优先于后续 duration 检查
- **WHEN** HTTP target 配置了 body 校验和 `expect.maxDurationMs`body 阶段存在失败,且完整执行后 duration 也超过阈值
- **WHEN** HTTP target 配置了 body 校验和 `expect.durationMs: {lte: 1000}`body 阶段存在失败,且完整执行后 duration 也超过阈值
- **THEN** 系统 SHALL 返回 body 阶段的失败首个失败为准durationMs SHALL 记录完整耗时
#### Scenario: HTTP 慢响应体计入耗时
- **WHEN** HTTP target 配置了 body 校验和 `expect.maxDurationMs`,且响应头很快返回但响应体读取导致完整执行耗时超过阈值
- **WHEN** HTTP target 配置了 body 校验和 `expect.durationMs: {lte: 1000}`,且响应头很快返回但响应体读取导致完整执行耗时超过阈值
- **THEN** 系统 SHALL 判定 duration 不匹配并记录完整 durationMs
#### Scenario: 多条 expect 规则
- **WHEN** 目标同时配置状态、duration、元数据和内容规则
- **THEN** 系统 SHALL 所有规则全部通过时 matched 为 true任一不通过则为 false 并记录首个失败原因
- **WHEN** 目标同时配置状态、duration、元数据和内容 expectations
- **THEN** 系统 SHALL 所有 expectations 全部通过时 matched 为 true任一不通过则为 false 并记录首个失败原因
#### Scenario: cmd 默认 exitCode
- **WHEN** cmd target 未配置 `expect.exitCode`
- **THEN** 系统 SHALL 默认 `exitCode: [0]` 校验命令退出码
- **THEN** 系统 SHALL 在 Resolved cmd expect 中物化默认 `exitCode: [0]` 并按该语义校验命令退出码
#### Scenario: 校验 cmd stdout
- **WHEN** cmd target 配置了有序 `expect.stdout` 规则数组
- **THEN** 系统 SHALL 按数组顺序执行 stdout 规则,任一失败立即记录 failure 并停止后续规则
- **WHEN** cmd target 配置了有序 `expect.stdout` ContentExpectations 数组
- **THEN** 系统 SHALL 按数组顺序执行 stdout expectations,任一失败立即记录 failure 并停止后续 expectation
### Requirement: Body 校验按需解析
系统 SHALL 仅在 HTTP target 配置了 body 校验,且 status、headers 阶段均通过,并且进入 body 前未确定 duration 已失败时才读取并解析响应体避免不必要的读取和解析开销。HTTP target 未配置 body 校验时,系统 SHALL NOT 读取 response body。
系统 SHALL 仅在 HTTP target 配置了 body 校验,且 status、headers 阶段均通过,并且进入 body 前未确定 `expect.durationMs` 已失败时才读取并解析响应体避免不必要的读取和解析开销。HTTP target 未配置 body 校验时,系统 SHALL NOT 读取 response body。仅当 Resolved `durationMs` 包含上界 matcher 且当前已耗时已经使其不可能通过时,系统 MAY 在读取 body 前返回 duration failure其他 duration matcher SHALL 在完整执行耗时可用后校验。
#### Scenario: status 失败时不读取 body
- **WHEN** HTTP target 的 status 阶段不匹配
@@ -186,17 +186,17 @@
- **WHEN** HTTP target 的 status 阶段匹配但 headers 阶段不匹配
- **THEN** 系统 SHALL 立即返回 matched=false且 MUST NOT 读取 response body
#### Scenario: 进入 body 前 duration 已失败时不读取 body
- **WHEN** HTTP target 已配置 `expect.maxDurationMs`,且进入 body 读取前的已耗时已经超过阈值
#### Scenario: 进入 body 前 durationMs 上界已失败时不读取 body
- **WHEN** HTTP target 已配置 `expect.durationMs` 上界 matcher,且进入 body 读取前的已耗时已经使该 matcher 不可能通过
- **THEN** 系统 SHALL 返回 duration failure且 MUST NOT 读取 response body
#### Scenario: 仅配置 contains 时不解析 JSON
- **WHEN** HTTP target 仅配置 body contains 规则而未配置 json/css/xpath 规则
- **WHEN** HTTP target 仅配置 body contains expectation 而未配置 json/css/xpath expectation
- **THEN** 系统 SHALL 不执行 JSON.parse 或 HTML/XML 解析
#### Scenario: 配置 json 时解析 JSON 失败
- **WHEN** HTTP target 配置了 body json 规则但响应体不是合法 JSON
- **THEN** 系统 SHALL 判定 matched 为 false并记录 json 规则对应的 failure.path
- **WHEN** HTTP target 配置了 body json expectation 但响应体不是合法 JSON
- **THEN** 系统 SHALL 判定 matched 为 false并记录 json expectation 对应的 failure.path
### Requirement: HTTP 运行期错误归属
HTTP checker SHALL 将运行期失败归属到实际失败阶段。请求、网络、TLS 和 timeout 错误 SHALL 记录为 request 阶段错误body 超限、响应体解码失败、响应内容解析失败 SHALL 记录为 body 阶段错误expect 不匹配 SHALL 记录为对应 mismatch 阶段。

View File

@@ -86,27 +86,27 @@
- **THEN** observation.banner SHALL 保存截断后的 banner 摘要API detail SHALL 展示截断后的 banner 摘要,避免 UI 展示过长文本
### Requirement: tcp expect 校验
系统 SHALL 支持 tcp 专属 expect包括 `connected``banner``durationMs`,并按 connected、banner、durationMs 的阶段顺序快速失败。`connected` SHALL 保持布尔状态语义,未配置时默认 `true``banner` MUST 使用共享 `ContentRules` 数组,并仅在 `tcp.readBanner: true` 时允许配置。`durationMs` SHALL 使用共享 `ValueMatcher` 校验包含连接和 banner 读取在内的完整执行耗时。
系统 SHALL 支持 tcp 专属 expect包括 `connected``banner``durationMs`,并按 connected、banner、durationMs 的阶段顺序快速失败。`connected` SHALL 保持布尔状态语义,未配置时在 Resolved expect 中默认 `true``banner` MUST 使用共享 `RawContentExpectations` 数组输入并在运行期使用 `ContentExpectations`,且仅在 `tcp.readBanner: true` 时允许配置。`durationMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation` 校验包含连接和 banner 读取在内的完整执行耗时。
#### Scenario: 默认 connected 成功语义
- **WHEN** tcp target 未显式配置 `expect.connected`
- **THEN** 系统 SHALL 使用默认 `expect.connected: true` 进行校验
- **THEN** 系统 SHALL 在 Resolved tcp expect 中使用默认 `connected: true` 进行校验
#### Scenario: durationMs 校验
- **WHEN** tcp target 配置 `expect.durationMs: {lte: 100}`,且完整执行耗时超过 100ms
- **THEN** 系统 SHALL 返回 `matched=false`failure 的 phase 为 `duration`
#### Scenario: banner ContentRules 校验通过
#### Scenario: banner ContentExpectations 校验通过
- **WHEN** tcp target 配置 `readBanner: true``expect.banner: [{contains: "ESMTP"}]`,且实际 banner 包含 `ESMTP`
- **THEN** 系统 SHALL 判定 banner 阶段通过
#### Scenario: banner regex 校验失败
- **WHEN** tcp target 配置 `readBanner: true``expect.banner: [{regex: "^SSH-2\\.0"}]`,且实际 banner 不匹配该正则
- **THEN** 系统 SHALL 返回 `matched=false`failure 的 kind 为 `mismatch`phase 为 `banner`path 指向失败的 banner 规则
- **THEN** 系统 SHALL 返回 `matched=false`failure 的 kind 为 `mismatch`phase 为 `banner`path 指向失败的 banner expectation
#### Scenario: banner 多规则快速失败
- **WHEN** tcp target 配置两条 banner 规则且第一条失败
- **THEN** 系统 SHALL 返回第一条失败规则的 failure并 MUST NOT 执行第二条规则
- **WHEN** tcp target 配置两条 banner expectation 且第一条失败
- **THEN** 系统 SHALL 返回第一条失败 expectation 的 failure并 MUST NOT 执行第二条 expectation
#### Scenario: expect.banner 未开启 readBanner
- **WHEN** tcp target 配置 `expect.banner`,但 `tcp.readBanner` 未配置为 `true`

View File

@@ -133,13 +133,13 @@
- **THEN** 系统 SHALL 以配置错误退出,提示 maxResponseBytes 格式错误
### Requirement: udp expect 校验
系统 SHALL 支持 udp 专属 expect包括 `responded``response``responseSize``sourceHost``sourcePort``durationMs`,并按 responded、responseSize、response、sourceHost、sourcePort、durationMs 的阶段顺序快速失败。`responded` SHALL 保持布尔状态语义,未配置时默认 `true``response` MUST 使用共享 `ContentRules` 数组,并作用于按 `udp.responseEncoding` 转换后的响应文本。`responseSize``sourceHost``sourcePort``durationMs` SHALL 使用共享 `ValueMatcher`
系统 SHALL 支持 udp 专属 expect包括 `responded``response``responseSize``sourceHost``sourcePort``durationMs`,并按 responded、responseSize、response、sourceHost、sourcePort、durationMs 的阶段顺序快速失败。`responded` SHALL 保持布尔状态语义,未配置时在 Resolved expect 中默认 `true``response` MUST 使用共享 `RawContentExpectations` 数组输入并在运行期使用 `ContentExpectations`,且作用于按 `udp.responseEncoding` 转换后的响应文本。`responseSize``sourceHost``sourcePort``durationMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation`
#### Scenario: 默认 responded 成功语义
- **WHEN** udp target 未显式配置 `expect.responded`
- **THEN** 系统 SHALL 使用默认 `expect.responded: true` 进行校验
- **THEN** 系统 SHALL 在 Resolved udp expect 中使用默认 `responded: true` 进行校验
#### Scenario: response ContentRules 校验通过
#### Scenario: response ContentExpectations 校验通过
- **WHEN** udp target 配置 `expect.response: [{ contains: "PONG" }]`,且按 `responseEncoding` 转换后的响应文本包含 `PONG`
- **THEN** 系统 SHALL 判定 response 阶段通过
@@ -147,13 +147,13 @@
- **WHEN** udp target 收到文本响应 `{"status":"ok"}` 且配置 `expect.response: [{json: {path: "$.status", equals: "ok"}}]`
- **THEN** 系统 SHALL 判定 response 阶段通过
#### Scenario: response ContentRules 校验失败
#### Scenario: response ContentExpectations 校验失败
- **WHEN** udp target 配置 `expect.response: [{ contains: "PONG" }]`,但按 `responseEncoding` 转换后的响应文本不包含 `PONG`
- **THEN** 系统 SHALL 返回 `matched=false`failure 的 kind 为 `mismatch`phase 为 `response`path 指向失败的 response 规则
- **THEN** 系统 SHALL 返回 `matched=false`failure 的 kind 为 `mismatch`phase 为 `response`path 指向失败的 response expectation
#### Scenario: responseEncoding 为 hex
- **WHEN** udp target 配置 `udp.responseEncoding: "hex"` 且收到字节内容 `PONG`
- **THEN** 系统 SHALL 将响应转换为小写 hex 字符串 `504f4e47` 后执行 `expect.response` 规则
- **THEN** 系统 SHALL 将响应转换为小写 hex 字符串 `504f4e47` 后执行 `expect.response` expectation
#### Scenario: responseSize matcher 校验通过
- **WHEN** udp target 配置 `expect.responseSize: { gte: 4 }`,且实际响应为 4 字节