- 引入共享 ValueMatcher(equals/contains/regex/exists/empty/gt/gte/lt/lte) - 引入共享 ContentRules 数组(direct/json/css/xpath 提取器) - 引入共享 KeyValueExpect(动态键值断言,字面量等价 equals) - maxDurationMs → durationMs: ValueMatcher(所有 checker) - match → regex(固定无 flags) - Ping max* → packetLossPercent/avgLatencyMs/maxLatencyMs(ValueMatcher) - LLM finishReason/rawFinishReason → ValueMatcher - DB 新增 result: ContentRules - TCP banner → ContentRules 数组 - 删除旧模块:operator.ts、validate-operator.ts、duration.ts、body.ts、text.ts、output.ts - 更新全部 checker schema/validate/expect/execute - 更新 probe-config.schema.json、probes.example.yaml - 更新 README.md、DEVELOPMENT.md(含 expect 字段选择规范) - 同步 10 个 delta specs 到主 specs,归档 change
194 lines
13 KiB
Markdown
194 lines
13 KiB
Markdown
## Purpose
|
||
|
||
定义共享 expect 断言规则系统的核心概念和基础设施:ValueMatcher 统一匹配器、ContentRules 内容规则数组、KeyValueExpect 键值规则、以及相关的启动期校验和失败路径规范。
|
||
|
||
## 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 均通过。
|
||
|
||
#### Scenario: equals 匹配对象
|
||
- **WHEN** 实际值为 `{status: "ok", count: 1}` 且 matcher 为 `{equals: {status: "ok", count: 1}}`
|
||
- **THEN** 系统 SHALL 使用深度相等判定该 matcher 通过
|
||
|
||
#### Scenario: contains 字符串化匹配
|
||
- **WHEN** 实际值为 `"service ready"` 且 matcher 为 `{contains: "ready"}`
|
||
- **THEN** 系统 SHALL 判定该 matcher 通过
|
||
|
||
#### Scenario: 数字范围组合匹配
|
||
- **WHEN** 实际值为 `50` 且 matcher 为 `{gte: 0, lte: 100}`
|
||
- **THEN** 系统 SHALL 判定该 matcher 通过
|
||
|
||
#### Scenario: 多 matcher 快速失败
|
||
- **WHEN** 实际值为 `"healthy"` 且 matcher 为 `{contains: "health", regex: "^ready$"}`
|
||
- **THEN** 系统 SHALL 在任一 matcher 不满足时判定该 matcher 对象不通过
|
||
|
||
### Requirement: ValueMatcher 启动期校验
|
||
系统 SHALL 在启动期对所有 `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 对象为空 `{}`
|
||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示 matcher 必须包含至少一个合法字段
|
||
|
||
#### Scenario: 未知 matcher 字段被拒绝
|
||
- **WHEN** YAML 配置中任一 matcher 对象包含 `foo: "bar"` 等未知字段
|
||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该字段未知
|
||
|
||
#### Scenario: 数值 matcher 非有限数字被拒绝
|
||
- **WHEN** YAML 配置中任一 matcher 的 `gt`、`gte`、`lt` 或 `lte` 值为 `NaN`、`Infinity` 或非数字类型
|
||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示数值 matcher 必须为有限数字
|
||
|
||
#### Scenario: 布尔 matcher 非布尔值被拒绝
|
||
- **WHEN** YAML 配置中任一 matcher 的 `exists` 或 `empty` 值不是布尔类型
|
||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该字段必须为布尔值
|
||
|
||
### Requirement: empty matcher 语义
|
||
`empty: true` SHALL 在以下情况判定通过:实际值为 `null`、`undefined`、空字符串 `""`、空数组 `[]` 或空对象 `{}`。`empty: false` SHALL 在以上条件均不满足时判定通过。数字 `0` 和布尔 `false` SHALL NOT 被视为 empty。
|
||
|
||
#### Scenario: null 视为 empty
|
||
- **WHEN** 实际值为 `null` 且 matcher 为 `{empty: true}`
|
||
- **THEN** 系统 SHALL 判定该 matcher 通过
|
||
|
||
#### Scenario: 空字符串视为 empty
|
||
- **WHEN** 实际值为 `""` 且 matcher 为 `{empty: true}`
|
||
- **THEN** 系统 SHALL 判定该 matcher 通过
|
||
|
||
#### Scenario: 空数组视为 empty
|
||
- **WHEN** 实际值为 `[]` 且 matcher 为 `{empty: true}`
|
||
- **THEN** 系统 SHALL 判定该 matcher 通过
|
||
|
||
#### Scenario: 空对象视为 empty
|
||
- **WHEN** 实际值为 `{}` 且 matcher 为 `{empty: true}`
|
||
- **THEN** 系统 SHALL 判定该 matcher 通过
|
||
|
||
#### Scenario: 数字 0 不视为 empty
|
||
- **WHEN** 实际值为 `0` 且 matcher 为 `{empty: true}`
|
||
- **THEN** 系统 SHALL 判定该 matcher 不通过
|
||
|
||
#### Scenario: 布尔 false 不视为 empty
|
||
- **WHEN** 实际值为 `false` 且 matcher 为 `{empty: true}`
|
||
- **THEN** 系统 SHALL 判定该 matcher 不通过
|
||
|
||
### Requirement: exists 与其他 matcher 的组合语义
|
||
当 `ValueMatcher` 同时包含 `exists: false` 和其他非存在性 matcher(如 `contains`、`regex`、`equals` 等)时,系统 SHALL 在启动期配置校验失败,提示 `exists: false` 不能与其他 matcher 组合使用。`exists: true` MAY 与其他 matcher 组合,语义为先确认存在再执行其他 matcher。
|
||
|
||
#### Scenario: exists false 与 contains 组合被拒绝
|
||
- **WHEN** YAML 配置中 matcher 为 `{exists: false, contains: "foo"}`
|
||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示 `exists: false` 不能与其他 matcher 组合
|
||
|
||
#### Scenario: exists true 与 contains 组合允许
|
||
- **WHEN** 实际值为 `"hello foo"` 且 matcher 为 `{exists: true, contains: "foo"}`
|
||
- **THEN** 系统 SHALL 判定该 matcher 通过
|
||
|
||
### Requirement: regex 字段语义
|
||
系统 SHALL 使用 `regex` 作为唯一正则 matcher 字段。`regex` 值 MUST 为可编译的字符串 pattern。运行期 SHALL 固定使用无 flags 的 `new RegExp(pattern).test(String(actual))` 执行匹配。系统 MUST NOT 支持旧 `match` 字段。系统 SHALL 在启动期对所有 `regex` pattern 执行可编译校验和 ReDoS 风险校验。
|
||
|
||
#### Scenario: regex 任意位置匹配
|
||
- **WHEN** 实际值为 `"api status ok"` 且 matcher 为 `{regex: "status"}`
|
||
- **THEN** 系统 SHALL 判定该 matcher 通过,因为无 flags 的 JavaScript 正则仍会搜索整个字符串中的第一次匹配
|
||
|
||
#### Scenario: regex 完整匹配由用户声明锚点
|
||
- **WHEN** 实际值为 `"OK\n"` 且 matcher 为 `{regex: "^OK$"}`
|
||
- **THEN** 系统 SHALL 判定该 matcher 不通过,因为系统 MUST NOT 默认启用 multiline flags
|
||
|
||
#### Scenario: match 字段启动失败
|
||
- **WHEN** YAML 配置中任一 matcher 对象包含 `match: "ok"`
|
||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示 `match` 是未知字段或不支持字段
|
||
|
||
#### Scenario: regex ReDoS 风险启动失败
|
||
- **WHEN** YAML 配置中任一 `regex` 为 `"(a+)+$"`
|
||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示正则存在 ReDoS 风险
|
||
|
||
### Requirement: ContentRules 内容规则数组
|
||
系统 SHALL 提供共享 `ContentRules` 表达返回内容断言。`ContentRules` MUST 为有序数组,数组项 SHALL 为直接 `ValueMatcher`,或 `json`、`css`、`xpath` 三类 extractor 规则之一。系统 SHALL 按数组顺序执行全部规则,任一规则失败时 SHALL 立即停止并返回该规则的 failure。系统 MUST NOT 支持内容字段的非数组对象快捷写法。
|
||
|
||
#### Scenario: 直接 matcher 内容规则
|
||
- **WHEN** 内容字段配置 `[{contains: "ready"}, {regex: "listening on \\d+"}]` 且原始内容同时满足两条规则
|
||
- **THEN** 系统 SHALL 判定该内容字段通过
|
||
|
||
#### Scenario: 内容规则数组快速失败
|
||
- **WHEN** 内容字段配置三条规则且第二条规则失败
|
||
- **THEN** 系统 SHALL 返回第二条规则的 failure,并 MUST NOT 执行第三条规则
|
||
|
||
#### Scenario: 内容字段必须为数组
|
||
- **WHEN** YAML 中内容字段配置为 `{contains: "ok"}` 而不是数组
|
||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该内容字段必须为规则数组
|
||
|
||
### Requirement: ContentRule 互斥性约束
|
||
一条 `ContentRule` MUST 为直接 `ValueMatcher` 或恰好一个 extractor(`json`、`css`、`xpath` 之一)。系统 MUST NOT 允许同一条规则同时包含多个 extractor。直接 `ValueMatcher` 规则 MUST NOT 包含 `json`、`css`、`xpath` 字段。系统 SHALL 在启动期对违反互斥性的规则报错。
|
||
|
||
#### Scenario: 多 extractor 被拒绝
|
||
- **WHEN** YAML 中内容规则为 `{json: {path: "$.a"}, css: {selector: "div"}}`
|
||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示一条规则不能同时包含多个 extractor
|
||
|
||
#### Scenario: 直接 matcher 混入 extractor 被拒绝
|
||
- **WHEN** YAML 中内容规则为 `{contains: "ok", json: {path: "$.a"}}`
|
||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示直接 matcher 不能与 extractor 混用
|
||
|
||
### Requirement: 空 ContentRules 数组语义
|
||
`ContentRules` 空数组 `[]` SHALL 被系统接受为合法配置。运行期空数组 SHALL 等价于无规则,即该内容字段的断言直接通过。
|
||
|
||
#### Scenario: 空 body 数组通过
|
||
- **WHEN** HTTP target 配置 `expect.body: []` 且响应体为任意内容
|
||
- **THEN** 系统 SHALL 判定 body 阶段通过
|
||
|
||
### Requirement: ContentRules 非字符串值序列化
|
||
当 `ContentRules` 的观测源为非字符串值(如对象或数组)时,直接 `ValueMatcher` 的 `contains` 和 `regex` SHALL 先将值 JSON 序列化为字符串后匹配。`equals` SHALL 直接在原始结构化值上使用深度相等比较,不进行序列化。
|
||
|
||
#### Scenario: 对象序列化后 contains 匹配
|
||
- **WHEN** ContentRules 观测源为 `{status: "ok"}` 且规则为 `{contains: "ok"}`
|
||
- **THEN** 系统 SHALL 将对象 JSON 序列化后执行 contains 匹配
|
||
|
||
#### Scenario: 对象 equals 不序列化
|
||
- **WHEN** ContentRules 观测源为 `{status: "ok"}` 且规则为 `{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`。
|
||
|
||
#### Scenario: json extractor 数字比较
|
||
- **WHEN** 原始内容为 JSON 字符串 `{"count": 2}` 且规则为 `{json: {path: "$.count", gte: 1}}`
|
||
- **THEN** 系统 SHALL 解析 JSON、提取 `$.count` 并判定该规则通过
|
||
|
||
#### Scenario: json extractor 存在性默认语义
|
||
- **WHEN** 原始内容为 JSON 字符串 `{"user": {"id": null}}` 且规则为 `{json: {path: "$.user.id"}}`
|
||
- **THEN** 系统 SHALL 将该规则视为 `{json: {path: "$.user.id", exists: true}}` 并判定通过
|
||
|
||
#### Scenario: css attr 存在性默认语义
|
||
- **WHEN** 原始内容包含 `<meta name="status" content="ok">` 且规则为 `{css: {selector: "meta[name=status]", attr: "content"}}`
|
||
- **THEN** 系统 SHALL 在属性存在时判定该规则通过
|
||
|
||
#### Scenario: xpath 无匹配节点失败
|
||
- **WHEN** XML 内容中不存在 XPath 指向的节点,且规则为 `{xpath: {path: "/root/status"}}`
|
||
- **THEN** 系统 SHALL 判定该规则不通过并生成 phase 对应内容字段的 mismatch failure
|
||
|
||
### Requirement: KeyValueExpect 键值规则
|
||
系统 SHALL 提供共享 `KeyValueExpect` 表达键值型观测值断言。`KeyValueExpect` SHALL 为动态键对象,每个键对应的值 MAY 为 `ValueMatcher` 或 JSON 字面量。字面量值 SHALL 等价于 `{equals: <literal>}`。调用方 MAY 指定 key 规范化策略;HTTP 与 LLM headers MUST 使用大小写不敏感的 key 匹配。
|
||
|
||
#### Scenario: headers 字面量快捷写法
|
||
- **WHEN** 响应 headers 中 `content-type` 为 `application/json`,且配置为 `headers: {Content-Type: "application/json"}`
|
||
- **THEN** 系统 SHALL 按大小写不敏感 key 匹配并使用 equals 语义判定通过
|
||
|
||
#### Scenario: headers matcher 写法
|
||
- **WHEN** 响应 headers 中 `content-type` 为 `application/json; charset=utf-8`,且配置为 `headers: {Content-Type: {contains: "application/json"}}`
|
||
- **THEN** 系统 SHALL 判定该 header 规则通过
|
||
|
||
#### Scenario: 缺失键 exists false
|
||
- **WHEN** 观测键值表中不存在 `x-debug`,且配置为 `{x-debug: {exists: false}}`
|
||
- **THEN** 系统 SHALL 判定该键规则通过
|
||
|
||
### 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 信息。
|
||
|
||
#### Scenario: ContentRules 失败路径
|
||
- **WHEN** `expect.body[1].json` 规则失败
|
||
- **THEN** failure.path SHALL 指向 `body[1].json($.path)` 或等价可定位路径,failure.phase SHALL 为 `body`
|
||
|
||
#### Scenario: KeyValueExpect 失败路径
|
||
- **WHEN** `expect.headers.Content-Type` 不匹配
|
||
- **THEN** failure.path SHALL 指向 `headers.Content-Type`,failure.phase SHALL 为 `headers`
|
||
|
||
#### Scenario: actual 截断
|
||
- **WHEN** matcher 失败时 actual 字符串长度超过 200 字符
|
||
- **THEN** 系统 SHALL 使用现有截断策略保存 failure.actual,避免历史记录写入过长内容
|