1
0
Files
DiAL/openspec/changes/archive/2026-05-10-enhance-expect-rules/design.md

113 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Context
当前 expect 校验通过 `checkExpect()` 函数(`src/server/checker/fetcher.ts`)实现,仅支持 status 白名单、bodyContains 子串匹配、maxLatencyMs 延迟阈值。body 校验能力薄弱,无法处理 JSON 结构化数据和 HTML/XML 页面内容校验。
本次设计将 body 校验扩展为五种可组合方法,并引入操作符系统统一提取值的比较逻辑。同时新增响应头校验。
项目约束Bun 1.3.13 运行时、TypeScript、SQLite 持久化、YAML 配置格式。
## Goals / Non-Goals
**Goals:**
- body 校验支持五大方法contains、regex、json、css、xpath任意 AND 组合
- 操作符系统equals默认、contains、match、empty、exists、gte、lte、gt、lt
- 响应头校验 headers
- 保持 matched/success 两层判定模型不变
- 所有新逻辑有完整单元测试
**Non-Goals:**
- 不支持 json/csv/xpath 的 OR 组合(当前全 AND
- 不支持 JSONPath 的通配符/过滤器(`.items[*].name``.items[?(@.price>10)]`
- 不支持 CSS 伪类选择器(如 `:nth-child`
- 不改变前端 Dashboard UI
- 不做告警通知
## Decisions
### D1: body 分组嵌套结构
选择 `expect.body.<method>` 而非平铺 `expect.bodyXxx`
**理由**:五种 body 方法语义上同属一层嵌套结构比平铺更清晰YAML 可读性更好。代价是将 `bodyContains``ExpectConfig` 顶层迁移至 `body.contains`,属于 **BREAKING** 变更,但项目尚在早期,影响极小。
**替代方案**:平铺 `expect.bodyContains``expect.bodyRegex` 等。不选,因随着方法增多字段名会越来越长且缺乏层次。
### D2: 操作符采用"标量=equals对象=显式"的二态模型
```yaml
json:
$.status: ok # 标量 → equals
$.data.count: # 对象 → 显式操作符
gte: 1
```
**理由**90% 的拨测场景只需要等值比较,标量语法最简洁。需要复杂比较时展开为对象,二态在同一个 map 中共存,无需额外字段指示意图。
**替代方案**:每个规则必须是 `{ path, operator, value }` 对象。过于冗长,不如二态模型灵活。
### D3: CSS 选择器通过 `attr` 切换提取维度
```yaml
css:
"div.status": OK # 默认 textContent
"meta[name=build-hash]": # 提取属性
attr: content
empty: false
```
**理由**99% 的 CSS 选择器场景只需要 textContent。通过可选的 `attr` 字段覆盖属性提取场景,保持常见用法最简。
**替代方案**:在选择器字符串中编码(如 `meta[name=build]@content`)。不选,语法污染。
### D4: 依赖选型 cheerio + xpath + @xmldom/xmldom
| 包 | 用途 | 选型理由 |
|----|------|---------|
| cheerio | CSS 选择器 HTML 解析 | npm 27M+ 周下载jQuery API 熟悉度高,依赖树由同一组织维护 |
| xpath | XPath 1.0 引擎 | npm 600K+ 周下载,轻量,业界标准 |
| @xmldom/xmldom | xpath 的 DOM 实现 | 2M+ 周下载xmldom 官方维护 |
**替代方案**jsdom体积大~200KB、linkedom不支持 XPath。不选。
cheerio 和 xpath 使用不同的 DOM 模型,同一个 HTML body 需要各自解析。拨测场景(秒级频率,非高并发 HTML 解析)性能开销可忽略。
### D5: body 方法按需解析,短路 AND 执行
整体 checkExpect 执行顺序为 `status → headers → body → maxLatencyMs`,均为 AND 短路。body 内部执行顺序:
```
body 内部:
1. contains → 文本匹配,失败立即返回
2. regex → 文本匹配,失败立即返回
3. json → 仅当 json 配置存在时解析 JSON否则跳过
4. css → 仅当 css 配置存在时解析 HTMLcheerio
5. xpath → 仅当 xpath 配置存在时解析 HTML/XMLxmldom
解析失败JSON.parse 异常、cheerio 加载失败)→ matched=false
```
**理由**:避免不必要的解析开销(例如只配了 contains 时不解析 JSON/HTML。AND 短路语义与现有 expect 规则保持一致。
### D6: 操作符的类型转换策略
| 操作符 | 提取值类型 | 转换逻辑 |
|--------|-----------|---------|
| equals | 保留原类型 | strict === 比较 |
| contains | 强制 toString() | actual.toString().includes(expected) |
| match | 强制 toString() | new RegExp(pattern).test(actual.toString()) |
| empty | - | null、undefined、""、[]、{} → 判定为空 |
| exists | - | undefined vs 非 undefined |
| gte/lte/gt/lt | 强制 Number() | Number(actual) >= expected |
CSS/XPath 提取的值始终是 string数字比较时自动 Number() 转换。JSON 提取的值保留原类型number/boolean/null/string
单个提取值可配置多个操作符(如 `{gte: 10, lte: 100}`),所有操作符全部通过才算该字段通过,语义为 AND。
## Risks / Trade-offs
- **[兼容性风险]** `bodyContains``body.contains` 是 BREAKING 变更 → 通过 README 和示例配置文件说明,现有用户量极小,影响可控
- **[性能风险]** cheerio 和 xpath 各自解析 HTML → 同一 body 可能解析两次 → 拨测场景下无需缓存,单次解析耗时 <5ms整体影响可忽略
- **[JSONPath 功能局限]** 自实现简易路径解析不支持通配符和过滤器 → 通过文档说明限制,后续可按需增强
- **[XPath 浏览器兼容]** xpath 使用 xmldom 而非浏览器原生 evaluate → 语义上等价,测试覆盖保证行为正确
- **[依赖体积]** 新增 3 个包增加约 95KB → 这是 executable 构建Bun compile 会打包进二进制,对最终产物影响有限