refactor: 统一 expect 断言体系,引入共享 ValueMatcher/ContentRules/KeyValueExpect 模型
- 引入共享 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
This commit is contained in:
163
DEVELOPMENT.md
163
DEVELOPMENT.md
@@ -39,7 +39,7 @@ src/
|
||||
variables.ts 配置 variables 提取、target 字符串变量替换和 unresolved-variable issue 生成
|
||||
schema/ TypeBox + Ajv 配置契约、schema fragments、issue 渲染和 schema 导出入口
|
||||
builder.ts 全量 JSON Schema 组装(遍历 registry 生成)
|
||||
fragments.ts 共享 TypeBox schema 片段(duration、size、operator 等)
|
||||
fragments.ts 共享 TypeBox schema 片段(duration、size、ValueMatcher、ContentRules、KeyValueExpect 等)
|
||||
validate.ts Ajv 契约校验入口
|
||||
issues.ts 校验问题类型与渲染
|
||||
types.ts schema 层类型
|
||||
@@ -48,22 +48,24 @@ src/
|
||||
engine.ts 调度引擎(按 interval 分组的 es-toolkit groupBy + Semaphore 并发控制 + 数据清理)
|
||||
utils.ts 共享工具函数(parseSize、parseDuration)
|
||||
expect/ 共享 expect 断言基础设施(跨 checker 复用)
|
||||
types.ts ExpectResult 共享断言类型
|
||||
types.ts ExpectResult、ValueMatcher、ContentRules、KeyValueExpect 类型
|
||||
failure.ts 失败信息构造(errorFailure、mismatchFailure、truncateActual)
|
||||
operator.ts 操作符系统(applyOperator、evaluateJsonPath)
|
||||
duration.ts 耗时断言(checkDuration)
|
||||
validate-operator.ts 操作符语义校验(validateOperatorObject、isJsonValue、isPlainRecord)
|
||||
matcher.ts ValueMatcher 执行、JSONPath 提取、字面量 equals 快捷语义
|
||||
content.ts ContentRules 执行(direct/json/css/xpath)
|
||||
key-value.ts KeyValueExpect 执行(动态键与 key 规范化)
|
||||
validate-matcher.ts matcher/content/key-value 语义校验
|
||||
redos.ts regex ReDoS 风险检测
|
||||
runner/ Checker 统一抽象与注册机制
|
||||
types.ts CheckerDefinition、CheckerContext、CheckerSchemas、ResolveContext
|
||||
registry.ts CheckerRegistry 注册中心
|
||||
index.ts 注册入口(显式数组 + 循环注册)
|
||||
http/ HTTP Checker(自包含模块,含 types/schema/execute/expect/validate/body)
|
||||
cmd/ Cmd Checker(自包含模块,含 types/schema/execute/expect/validate/text)
|
||||
http/ HTTP Checker(自包含模块,含 types/schema/execute/expect/validate)
|
||||
cmd/ Cmd Checker(自包含模块,含 types/schema/execute/expect/validate)
|
||||
db/ DB Checker(自包含模块,含 types/schema/execute/expect/validate)
|
||||
tcp/ TCP Checker(自包含模块,含 types/schema/execute/expect/validate)
|
||||
icmp/ Ping Checker(自包含模块,含 types/schema/execute/expect/validate/parse)
|
||||
udp/ UDP Checker(自包含模块,含 types/schema/execute/expect/validate/encoding)
|
||||
llm/ LLM Checker(自包含模块,含 types/schema/execute/expect/validate/output/provider/observation)
|
||||
llm/ LLM Checker(自包含模块,含 types/schema/execute/expect/validate/provider/observation)
|
||||
shared/
|
||||
api.ts 前后端共享 TypeScript 类型
|
||||
web/ React 前端 Dashboard(通过 Bun HTML import 集成)
|
||||
@@ -271,7 +273,7 @@ checkerRegistry(单例)
|
||||
| `validate.ts` | 启动期语义校验(JSON Schema 无法表达的规则) |
|
||||
| `execute.ts` | Checker 类:resolve(默认值合并 + 解析)、execute(执行检查)、serialize(DB 持久化) |
|
||||
| `expect.ts` | Checker 专用断言函数 |
|
||||
| `*.ts` | 其他 checker 专属逻辑(如 http/body.ts、cmd/text.ts) |
|
||||
| `*.ts` | 其他 checker 专属逻辑(如协议解析、编码、provider 适配、平台命令封装) |
|
||||
|
||||
#### 1.7.2 步骤一:创建 Checker 目录与类型
|
||||
|
||||
@@ -290,16 +292,16 @@ checkerRegistry(单例)
|
||||
|
||||
**可复用的共享 fragments**(来自 `schema/fragments.ts`):
|
||||
|
||||
| Fragment | 用途 |
|
||||
| ---------------------------- | -------------------------------------------------------- |
|
||||
| `durationSchema` | 时长字符串(`"30s"`、`"5m"`、`"2h"`、`"7d"`、`"500ms"`) |
|
||||
| `sizeSchema` | 大小单位(字符串如 `"10MB"` 或整数) |
|
||||
| `statusCodePatternSchema` | 状态码(`100`-`599` 或 `"2xx"`) |
|
||||
| `stringMapSchema` | `Record<string, string>`(用于 headers / env) |
|
||||
| `createBodyRulesSchema()` | body 规则数组(json/css/xpath/contains/regex) |
|
||||
| `createTextRulesSchema()` | 文本规则数组(stdout/stderr) |
|
||||
| `createPureOperatorSchema()` | 操作符对象 |
|
||||
| `operatorProperties()` | 所有操作符字段的 Record |
|
||||
| Fragment | 用途 |
|
||||
| ------------------------------ | -------------------------------------------------------- |
|
||||
| `durationSchema` | 时长字符串(`"30s"`、`"5m"`、`"2h"`、`"7d"`、`"500ms"`) |
|
||||
| `sizeSchema` | 大小单位(字符串如 `"10MB"` 或整数) |
|
||||
| `statusCodePatternSchema` | 状态码(`100`-`599` 或 `"2xx"`) |
|
||||
| `stringMapSchema` | `Record<string, string>`(用于 headers / env) |
|
||||
| `createValueMatcherSchema()` | `ValueMatcher` 对象(equals/contains/regex/数值比较等) |
|
||||
| `createContentRulesSchema()` | `ContentRules` 数组(direct/json/css/xpath 内容规则) |
|
||||
| `createKeyValueExpectSchema()` | 动态键 `KeyValueExpect`(headers、DB rows 列值) |
|
||||
| `matcherProperties()` | matcher 字段 Record,供 extractor schema 复用 |
|
||||
|
||||
**注意**:默认对象策略为 `additionalProperties: false`。只有明确的动态键值表(如 `http.headers`、`cmd.env`)可以开放任意键名。
|
||||
|
||||
@@ -311,12 +313,15 @@ checkerRegistry(单例)
|
||||
export function validateTcpConfig(input: CheckerValidationInput): ConfigValidationIssue[];
|
||||
```
|
||||
|
||||
**共享校验工具**(`expect/validate-operator.ts`):
|
||||
**共享校验工具**(`expect/validate-matcher.ts`):
|
||||
|
||||
| 函数 | 用途 |
|
||||
| --------------------------------------------------------- | ---------------------- |
|
||||
| `validateOperatorObject(ops, path, targetName, options?)` | 校验操作符对象 |
|
||||
| `isJsonValue(value)` | 判断是否为合法 JSON 值 |
|
||||
| 函数 | 用途 |
|
||||
| ------------------------------------------------------ | ----------------------------------------- |
|
||||
| `validateValueMatcher(value, path, targetName, opts?)` | 校验 matcher 字段、类型、regex 和组合语义 |
|
||||
| `validateContentRules(rules, path, targetName)` | 校验 ContentRules 数组、extractor 互斥性 |
|
||||
| `validateKeyValueExpect(value, path, targetName)` | 校验动态键值断言 |
|
||||
| `validateJsonPath(path, rulePath, targetName)` | 校验项目支持的 JSONPath 子集 |
|
||||
| `isJsonValue(value)` | 判断是否为合法 JSON value |
|
||||
|
||||
#### 1.7.5 步骤四:实现 Checker 类
|
||||
|
||||
@@ -351,15 +356,18 @@ TcpChecker implements Checker
|
||||
|
||||
**可用的共享断言工具**(`checker/expect/`):
|
||||
|
||||
| 模块 | 函数 | 用途 |
|
||||
| ---------------------- | ----------------------------------------------------- | ------------------------------------- |
|
||||
| `failure.ts` | `errorFailure(phase, path, msg)` | 构造错误类型 failure |
|
||||
| `failure.ts` | `mismatchFailure(phase, path, expected, actual, msg)` | 构造不匹配类型 failure |
|
||||
| `failure.ts` | `truncateActual(value, maxLen?)` | 截断过长的 actual 值(默认 200 字符) |
|
||||
| `duration.ts` | `checkDuration(ms, maxMs?)` | 耗时断言 |
|
||||
| `operator.ts` | `applyOperator(actual, operator)` | 执行单个操作符比较 |
|
||||
| `operator.ts` | `evaluateJsonPath(json, path)` | JSONPath 提取 |
|
||||
| `validate-operator.ts` | `validateOperatorObject(ops, path, name)` | 操作符语义校验 |
|
||||
| 模块 | 函数 | 用途 |
|
||||
| --------------------- | ----------------------------------------------------- | ------------------------------------- |
|
||||
| `failure.ts` | `errorFailure(phase, path, msg)` | 构造错误类型 failure |
|
||||
| `failure.ts` | `mismatchFailure(phase, path, expected, actual, msg)` | 构造不匹配类型 failure |
|
||||
| `failure.ts` | `truncateActual(value, maxLen?)` | 截断过长的 actual 值(默认 200 字符) |
|
||||
| `matcher.ts` | `applyMatcher(actual, matcher, options?)` | 执行 ValueMatcher AND 匹配 |
|
||||
| `matcher.ts` | `checkValueMatcher(actual, matcher, options)` | 执行 matcher 并返回 `ExpectResult` |
|
||||
| `matcher.ts` | `checkExpectValue(actual, expected)` | 执行字面量 equals 或 matcher |
|
||||
| `matcher.ts` | `evaluateJsonPath(json, path)` | JSONPath 提取 |
|
||||
| `content.ts` | `checkContentRules(source, rules, options)` | 执行 ContentRules 数组 |
|
||||
| `key-value.ts` | `checkKeyValueExpect(actual, expected, options)` | 执行动态键值断言 |
|
||||
| `validate-matcher.ts` | `validateValueMatcher/ContentRules/KeyValueExpect` | matcher/content/key-value 语义校验 |
|
||||
|
||||
**Checker 专属断言**(如需要)放在同目录的 `expect.ts` 中,参考 `http/expect.ts`(checkStatus、checkHeaders)和 `cmd/expect.ts`(checkExitCode)。
|
||||
|
||||
@@ -486,39 +494,77 @@ TcpChecker implements Checker
|
||||
|
||||
### 1.10 expect 断言系统
|
||||
|
||||
两层模型:**观测值收集** → **规则校验**。共享断言基础设施位于 `checker/expect/`,checker 专属断言位于各自目录。
|
||||
两层模型:**观测值收集** → **规则校验**。共享断言基础设施位于 `checker/expect/`,checker 专属状态断言位于各自目录。
|
||||
|
||||
**HTTP 校验流程**:
|
||||
**共享模型**:
|
||||
|
||||
| 模型 | 用途 | 典型字段 |
|
||||
| ---------------- | ---------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| `ValueMatcher` | 单个值、数字指标和字符串元数据断言 | `durationMs`、`rowCount`、`usage.totalTokens`、`finishReason` |
|
||||
| `ContentRules` | 返回内容或半结构化内容断言,必须是数组 | `body`、`stdout`、`stderr`、`banner`、`response`、`output`、`result` |
|
||||
| `KeyValueExpect` | 动态键值断言,字面量等价于 `{ equals: value }` | `headers`、DB `rows[]` 中的列值 |
|
||||
|
||||
`ValueMatcher` 支持 `equals`、`contains`、`regex`、`empty`、`exists`、`gte`、`lte`、`gt`、`lt`。一个 matcher 对象内多个字段为 AND 语义;`exists: false` 不能和其他 matcher 组合;`equals` 使用 `es-toolkit/isEqual` 做 JSON 深度相等;`regex` 固定为无 flags 的 `new RegExp(pattern).test(String(actual))`。
|
||||
|
||||
`ContentRules` 数组按顺序快速失败。数组项可以是直接 matcher,也可以是 `{ json: {...} }`、`{ css: {...} }`、`{ xpath: {...} }` 提取器规则;一条规则不能混用直接 matcher 和 extractor,多个 extractor 也不能共存。Extractor 未配置 matcher 时等价于 `exists: true`。对对象或数组源执行直接 `contains`/`regex` 时会先 JSON 序列化,`equals` 仍对原始结构做深度相等。
|
||||
|
||||
启动期语义校验统一由 `expect/validate-matcher.ts` 负责,会校验空 matcher、未知字段、字段类型、`exists:false` 组合、ContentRules 互斥性、JSONPath 子集、XPath 可编译性、regex 可编译性和 ReDoS 风险。旧字段 `match`、`maxDurationMs`、Ping 的 `max*` 阈值字段不再支持。
|
||||
|
||||
**快速失败顺序**:
|
||||
|
||||
| Checker | 顺序 |
|
||||
| ---------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| HTTP | `status → headers → body → durationMs` |
|
||||
| Cmd | `exitCode → durationMs → stdout → stderr` |
|
||||
| DB | `durationMs → rowCount → rows → result` |
|
||||
| TCP | `connected → banner → durationMs` |
|
||||
| UDP | `responded → responseSize → response → sourceHost → sourcePort → durationMs` |
|
||||
| Ping | `alive → packetLossPercent → avgLatencyMs → maxLatencyMs → durationMs` |
|
||||
| LLM http | `status → headers → output → finishReason → rawFinishReason → usage → durationMs` |
|
||||
| LLM stream | `status → headers → stream.completed → stream.firstTokenMs → output → finishReason → rawFinishReason → usage → durationMs` |
|
||||
|
||||
HTTP checker 的 `durationMs` 覆盖完整执行(含重定向、响应体读取、解码和 expect 校验)。status 或 headers 失败时不读取 body;有 body 规则时,在读取 body 前可先检查是否已超过 `durationMs` 阈值,避免无意义读取。
|
||||
|
||||
**expect 字段选择规范**:
|
||||
|
||||
新增或修改 checker 的 expect 字段时,按以下决策树选择合适的断言模型:
|
||||
|
||||
```
|
||||
HttpChecker.execute → 收集观测(statusCode/headers)
|
||||
→ status → headers → (early duration) → body(按需) → (final duration)
|
||||
→ 首个失败即停止,返回 CheckFailure
|
||||
expect 字段
|
||||
│
|
||||
├─ 状态类结果,结果集合小且稳定
|
||||
│ └─ enum / boolean
|
||||
│ HTTP/LLM status、Cmd exitCode、TCP connected、
|
||||
│ UDP responded、Ping alive
|
||||
│
|
||||
├─ 数字指标 / 字符串元数据
|
||||
│ └─ ValueMatcher
|
||||
│ durationMs、rowCount、responseSize、sourceHost、sourcePort、
|
||||
│ packetLossPercent、avgLatencyMs、maxLatencyMs、
|
||||
│ finishReason、rawFinishReason、usage.*、stream.firstTokenMs
|
||||
│
|
||||
└─ 返回内容 / 半结构化内容 / 不完全确定的值
|
||||
├─ 内容断言 → ContentRules(数组)
|
||||
│ HTTP body、Cmd stdout/stderr、TCP banner、
|
||||
│ UDP response、LLM output、DB result
|
||||
│
|
||||
└─ 键值断言 → KeyValueExpect(动态键对象)
|
||||
HTTP/LLM headers、DB rows[] 中的列值
|
||||
```
|
||||
|
||||
HTTP checker 的 `durationMs` 覆盖完整执行(含重定向、响应体读取、解码和 expect 校验)。status 或 headers 失败时不读取 body;进入 body 前若已超过 `maxDurationMs`,直接返回 duration failure。
|
||||
选择原则:
|
||||
|
||||
**Cmd 校验流程**:
|
||||
1. **状态类字段使用 enum 或 boolean**。结果集合小且稳定时(如 HTTP status 200/2xx、exitCode 0),枚举和布尔比 matcher 更贴近协议语义,配置也更直观。不要为了统一而把状态类字段改成 ValueMatcher。
|
||||
|
||||
```
|
||||
CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
|
||||
→ exitCode → duration → stdout → stderr
|
||||
→ 首个失败即停止
|
||||
```
|
||||
2. **单值数字指标和字符串元数据使用 ValueMatcher**。观测值是一个明确的标量(耗时、行数、丢包率、finish reason),但阈值不确定时,使用 `{ lte: 100 }` 或 `{ regex: "^(stop|end)$" }` 等 matcher 表达。
|
||||
|
||||
**Body 规则类型**(`runner/http/body.ts`):
|
||||
3. **返回内容使用 ContentRules 数组**。观测值是文本、JSON、HTML 或 XML 内容,且可能需要多步提取或多条规则时,使用 ContentRules。即使只有一条规则也必须写成数组形式(`[{ contains: "ok" }]`),不支持对象快捷写法。
|
||||
|
||||
- `contains`:文本包含匹配
|
||||
- `regex`:正则表达式匹配(注意:body 正则字段为 `regex`,不是 `match`,启动期会拒绝嵌套量词等 ReDoS 风险模式)
|
||||
- `json`:JSONPath 提取 + 操作符比较(使用 `es-toolkit/isPlainObject` 区分纯值和操作符)
|
||||
- `css`:cheerio CSS 选择器 + 操作符比较
|
||||
- `xpath`:XPath 节点提取 + 操作符比较
|
||||
4. **键值对使用 KeyValueExpect**。观测值是动态键值表(如 headers),且需要对每个键独立断言时使用。字面量值自动等价于 `{ equals: value }`。
|
||||
|
||||
**文本规则**(`runner/cmd/text.ts`):stdout/stderr 文本匹配,支持 `contains`、`match`(正则)、操作符比较
|
||||
5. **不要混用模型**。一个 expect 字段只能对应一种断言模型。例如 `finishReason` 是单值字符串元数据,用 ValueMatcher 而非 ContentRules(ContentRules 的 json/css/xpath 提取器对单字符串无意义,且会增加数组包装的配置冗余)。
|
||||
|
||||
**操作符**(`expect/operator.ts`):`equals`(深度比较,`es-toolkit/isEqual`)、`contains`、`match`(正则,启动期通过 `expect/redos.ts` 拒绝 ReDoS 风险模式)、`empty`(`isNil`+`isEmptyObject`)、`exists`、`gte`/`lte`/`gt`/`lt`
|
||||
|
||||
启动期语义校验会对 HTTP body `regex` 规则和所有 `match` operator 执行静态 ReDoS 检测,常见的嵌套量词模式如 `(a+)+`、`(\\d+)*x` 会被拒绝,避免运行期正则在大响应体上阻塞事件循环。
|
||||
6. **failure phase 命名遵循去单位后缀规则**。数字指标字段的 phase 去掉单位后缀(`durationMs` → `duration`、`packetLossPercent` → `packetLoss`、`avgLatencyMs` → `avgLatency`),不带单位后缀的字段直接使用字段名(`rowCount` → `rowCount`、`finishReason` → `finishReason`)。
|
||||
|
||||
### 1.11 错误模式
|
||||
|
||||
@@ -529,12 +575,7 @@ CommandChecker.execute → 收集观测(exitCode/stdout/stderr/durationMs)
|
||||
|
||||
### 1.12 测试规范
|
||||
|
||||
- 测试目录 `tests/` 镜像 `src/` 目录结构,但共享模块的测试集中放在 `tests/server/checker/runner/shared/` 下
|
||||
- `tests/server/checker/runner/shared/failure.test.ts` ↔ `src/server/checker/expect/failure.ts`
|
||||
- `tests/server/checker/runner/shared/duration.test.ts` ↔ `src/server/checker/expect/duration.ts`
|
||||
- `tests/server/checker/runner/shared/operator.test.ts` ↔ `src/server/checker/expect/operator.ts`
|
||||
- `tests/server/checker/runner/shared/body.test.ts` ↔ `src/server/checker/runner/http/body.ts`
|
||||
- `tests/server/checker/runner/shared/text.test.ts` ↔ `src/server/checker/runner/cmd/text.ts`
|
||||
- 测试目录 `tests/` 镜像 `src/` 目录结构,但共享 expect 模块的测试集中放在 `tests/server/checker/runner/shared/` 下,覆盖 `failure.ts`、`matcher.ts`、`content.ts`、`key-value.ts`、`validate-matcher.ts` 和 `redos.ts`
|
||||
- 使用 `bun:test` 框架(`describe`/`test`/`expect`),测试数据库用临时目录 + `tmpdir()`
|
||||
- 新增 store 方法必须编写单元测试;新增 API 端点必须在 `app.test.ts` 中添加集成测试
|
||||
- 测试后清理:`afterAll` 中 `store.close()` + `rm(tempDir, { recursive: true })`
|
||||
|
||||
Reference in New Issue
Block a user