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

5.5 KiB
Raw Blame History

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 可读性更好。代价是将 bodyContainsExpectConfig 顶层迁移至 body.contains,属于 BREAKING 变更,但项目尚在早期,影响极小。

替代方案:平铺 expect.bodyContainsexpect.bodyRegex 等。不选,因随着方法增多字段名会越来越长且缺乏层次。

D2: 操作符采用"标量=equals对象=显式"的二态模型

json:
  $.status: ok               # 标量 → equals
  $.data.count:               # 对象 → 显式操作符
    gte: 1

理由90% 的拨测场景只需要等值比较,标量语法最简洁。需要复杂比较时展开为对象,二态在同一个 map 中共存,无需额外字段指示意图。

替代方案:每个规则必须是 { path, operator, value } 对象。过于冗长,不如二态模型灵活。

D3: CSS 选择器通过 attr 切换提取维度

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

  • [兼容性风险] bodyContainsbody.contains 是 BREAKING 变更 → 通过 README 和示例配置文件说明,现有用户量极小,影响可控
  • [性能风险] cheerio 和 xpath 各自解析 HTML → 同一 body 可能解析两次 → 拨测场景下无需缓存,单次解析耗时 <5ms整体影响可忽略
  • [JSONPath 功能局限] 自实现简易路径解析不支持通配符和过滤器 → 通过文档说明限制,后续可按需增强
  • [XPath 浏览器兼容] xpath 使用 xmldom 而非浏览器原生 evaluate → 语义上等价,测试覆盖保证行为正确
  • [依赖体积] 新增 3 个包增加约 95KB → 这是 executable 构建Bun compile 会打包进二进制,对最终产物影响有限