1
0
Files
DiAL/openspec/specs/probe-engine/spec.md
lanyuanxiaoyao 60a54b483f 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)
2026-05-20 16:12:48 +08:00

15 KiB
Raw Blame History

Purpose

定义拨测调度引擎的行为:按 interval 分组定时、组内并发拨测、expect 结果校验和结果持久化。

Requirements

Requirement: 按 interval 分组调度

系统 SHALL 将拨测目标按 interval 值分组,每组使用独立的定时器进行调度。

Scenario: 相同 interval 的目标共享定时器

  • WHEN 多个 target 配置了相同的 interval如 30s
  • THEN 系统 SHALL 使用同一个 setInterval 定时器,每次 tick 并发拨测所有该组目标

Scenario: 不同 interval 的目标各自调度

  • WHEN target A 配置 15s intervaltarget B 配置 30s interval
  • THEN 系统 SHALL 创建两个独立定时器,分别按各自频率调度

Requirement: 组内并发拨测

系统 SHALL 在每次调度 tick 时并发执行同组内目标的检查,但实际同时运行的检查数 MUST 受全局 runtime.maxConcurrentChecks 限制。当某个目标的 checker 执行 rejected非正常 CheckResult 返回,而是 Promise reject系统 SHALL 将该异常记录为 matched: false 的 check_result而非仅 console.warn。

Scenario: 同组目标并发执行

  • WHEN 调度器触发一次 tick该组有 3 个目标,且全局并发余量至少为 3
  • THEN 系统 SHALL 同时执行 3 个 checker而非顺序执行

Scenario: 单个目标失败不影响同组其他目标

  • WHEN 同组中某个目标的检查请求超时或失败checker 正常返回 CheckResult
  • THEN 其他目标的检查 SHALL 正常完成并记录结果

Scenario: 同组中某个目标的 checker 执行 rejected

  • WHEN 同组中某个目标的 checker 执行抛出未捕获异常Promise rejected
  • THEN 系统 SHALL 为该目标写入一条 matched: false 的 check_resultfailure 为 { kind: "error", phase: "internal", path: "engine", message: <rejected reason> },其他目标的检查 SHALL 不受影响

Scenario: rejected 结果通过索引关联 targetName

  • WHEN checker 执行 rejected
  • THEN 系统 SHALL 通过 Promise.allSettled 的索引关联回 target 数组,获取对应的 targetName 用于写入 check_result

Scenario: 全局并发限制生效

  • WHEN 调度器同时触发 10 个目标且 runtime.maxConcurrentChecks 为 3
  • THEN 系统 MUST 同时最多运行 3 个检查,其余检查等待并发槽位释放

Requirement: HTTP 拨测执行

系统 SHALL 对 type: http 的目标执行 HTTP 请求,支持 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS 方法,并携带 http.headershttp.body。系统 SHALL 支持 http.ignoreSSL 配置跳过 SSL 证书校验,支持 http.maxRedirects 配置控制重定向行为。HTTP response body 读取 SHALL 受 http.maxBodyBytes 流式限制,重定向跟随 SHALL 释放被跟随响应的 body。

Scenario: 执行 GET 请求

  • WHEN 目标配置 method 为 GET
  • THEN 系统 SHALL 发送 GET 请求到目标 URL

Scenario: 执行 POST 请求带 body

  • WHEN 目标配置 method 为 POST 且指定了 body 和 Content-Type header
  • THEN 系统 SHALL 发送带指定 body 的 POST 请求

Scenario: 携带自定义 headers

  • WHEN 目标配置了 headers如 Authorization
  • THEN 系统 SHALL 在请求中包含所有配置的 headers

Scenario: HTTP body 读取上限

  • WHEN HTTP response body 超过该 target 的 maxBodyBytes
  • THEN 系统 MUST 停止继续读取 response body并记录 matched=falsefailure.kind="error"failure.phase="body" 的结构化输出超限错误

Scenario: HTTP body 大小等于上限

  • WHEN HTTP response body 的字节数等于该 target 的 maxBodyBytes
  • THEN 系统 SHALL 允许该 body 进入后续解码和 expect 校验

Scenario: HTTP body 上限为 0

  • WHEN HTTP target 配置 maxBodyBytes 为 0 且响应体非空
  • THEN 系统 SHALL 停止读取并记录 body 超限错误

Scenario: 忽略 SSL 证书校验

  • WHEN 目标配置 http.ignoreSSL: true 且目标 URL 为 HTTPS
  • THEN 系统 SHALL 跳过 SSL 证书校验,即使证书无效也正常完成请求

Scenario: 不忽略 SSL 证书校验

  • WHEN 目标未配置 http.ignoreSSL 或配置为 false,且目标 URL 使用自签名证书
  • THEN 系统 SHALL 因 SSL 证书校验失败而记录请求错误

Scenario: 默认不跟随重定向

  • WHEN 目标未配置 http.maxRedirects 或配置为 0且服务端返回 301/302
  • THEN 系统 SHALL 不跟随重定向,直接返回 301/302 的响应状态码和响应头

Scenario: 配置跟随重定向

  • WHEN 目标配置 http.maxRedirects: 5 且服务端返回重定向
  • THEN 系统 SHALL 跟随重定向,最多跟随 5 次,并在跟随前释放当前重定向响应的 body

Scenario: 超过最大重定向次数

  • WHEN 目标配置 http.maxRedirects: 1 且服务端连续返回两次重定向
  • THEN 系统 SHALL 只跟随第一次重定向,并返回第二次重定向响应的状态码和响应头

Scenario: POST 重定向改 GET

  • WHEN POST 请求遇到 301/302 或任意方法请求遇到 303且系统决定按 GET 跟随重定向
  • THEN 系统 SHALL 移除请求 body并清理 content-type、content-length 等 body 相关 headers 后发起后续 GET 请求

Scenario: 跨 origin 重定向敏感 header

  • WHEN HTTP 请求跟随重定向到不同 origin
  • THEN 系统 SHALL NOT 将 authorization、cookie 等敏感 headers 转发到新的 origin

Scenario: 响应体编码自动检测

  • WHEN HTTP 响应的 Content-Type header 包含 charset=gbkcharset="gbk"
  • THEN 系统 SHALL 使用 GBK 编码解码响应体,而非硬编码 UTF-8

Scenario: 响应体编码回退 UTF-8

  • WHEN HTTP 响应的 Content-Type header 未指定 charset
  • THEN 系统 SHALL 使用 UTF-8 编码解码响应体

Scenario: 响应体编码不支持

  • WHEN HTTP 响应的 Content-Type header 指定了当前运行时不支持的 charset
  • THEN 系统 SHALL 记录 matched=falsefailure.kind="error"failure.phase="body" 的解码失败结果

Requirement: 请求超时控制

系统 SHALL 对每次 checker 执行实施超时控制,超时时间使用目标配置的 timeout 值。

Scenario: HTTP 请求超时

  • WHEN HTTP 请求在 timeout 时间内未收到响应
  • THEN 系统 SHALL 中止该请求,记录为失败并标注超时错误

Scenario: cmd 执行超时

  • WHEN cmd 进程在 timeout 时间内未退出
  • THEN 系统 MUST 终止该子进程,记录为失败并标注超时错误

Scenario: 请求在超时前完成

  • WHEN checker 在超时前完成执行
  • THEN 系统 SHALL 正常记录执行结果并进入 expect 校验

Requirement: 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 在 Resolved HTTP expect 中物化默认 status: [200] 并按该语义校验响应状态码

Scenario: 校验 HTTP 状态码精确值

  • WHEN HTTP target 配置了 expect.status: [200, 201]
  • THEN 系统 SHALL 检查响应状态码是否在列表中,将匹配结果记录到 matched 字段

Scenario: 校验 HTTP 状态码范围模式

  • WHEN HTTP target 配置了 expect.status: ["2xx"]
  • THEN 系统 SHALL 检查响应状态码是否在 200-299 范围内

Scenario: 校验 HTTP 状态码混合模式

  • WHEN HTTP target 配置了 expect.status: ["2xx", 301],且响应状态码为 204
  • THEN 系统 SHALL 判定状态码匹配204 属于 2xx 范围)

Scenario: 校验 HTTP 响应头

  • WHEN HTTP target 配置了 expect.headers: {"Content-Type": {contains: "application/json"}}
  • THEN 系统 SHALL 检查响应头是否符合指定规则,全部匹配时继续后续阶段

Scenario: 校验 HTTP 响应体

  • WHEN HTTP target 配置了有序 expect.body ContentExpectations 数组
  • THEN 系统 SHALL 按数组顺序执行 body expectations任一失败立即记录 failure 并停止后续 expectation

Scenario: 校验 HTTP 完整耗时阈值

  • WHEN 目标配置了 expect.durationMs: {lte: 1000},且 HTTP checker 完整执行含重定向、body 读取、解码和 expect后的 durationMs 超过阈值
  • THEN 系统 SHALL 判定 duration 不匹配,记录完整 durationMs 和 duration failure

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.durationMs: {lte: 1000}body 阶段存在失败,且完整执行后 duration 也超过阈值
  • THEN 系统 SHALL 返回 body 阶段的失败首个失败为准durationMs SHALL 记录完整耗时

Scenario: HTTP 慢响应体计入耗时

  • WHEN HTTP target 配置了 body 校验和 expect.durationMs: {lte: 1000},且响应头很快返回但响应体读取导致完整执行耗时超过阈值
  • THEN 系统 SHALL 判定 duration 不匹配并记录完整 durationMs

Scenario: 多条 expect 规则

  • WHEN 目标同时配置状态、duration、元数据和内容 expectations
  • THEN 系统 SHALL 所有 expectations 全部通过时 matched 为 true任一不通过则为 false 并记录首个失败原因

Scenario: cmd 默认 exitCode

  • WHEN cmd target 未配置 expect.exitCode
  • THEN 系统 SHALL 在 Resolved cmd expect 中物化默认 exitCode: [0] 并按该语义校验命令退出码

Scenario: 校验 cmd stdout

  • WHEN cmd target 配置了有序 expect.stdout ContentExpectations 数组
  • THEN 系统 SHALL 按数组顺序执行 stdout expectations任一失败立即记录 failure 并停止后续 expectation

Requirement: 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 阶段不匹配
  • THEN 系统 SHALL 立即返回 matched=false且 MUST NOT 读取 response body

Scenario: headers 失败时不读取 body

  • WHEN HTTP target 的 status 阶段匹配但 headers 阶段不匹配
  • THEN 系统 SHALL 立即返回 matched=false且 MUST NOT 读取 response 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 expectation 而未配置 json/css/xpath expectation
  • THEN 系统 SHALL 不执行 JSON.parse 或 HTML/XML 解析

Scenario: 配置 json 时解析 JSON 失败

  • 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 阶段。

Scenario: 请求错误归属 request

  • WHEN HTTP 请求因为网络、TLS 或 timeout 失败
  • THEN 系统 SHALL 记录 matched=falsefailure.kind="error"failure.phase="request"

Scenario: body 超限归属 body

  • WHEN HTTP response body 超过 maxBodyBytes
  • THEN 系统 SHALL 记录 failure.kind="error"failure.phase="body"failure.path="body"

Scenario: body 解析错误归属 body

  • WHEN HTTP response body 已读取但解码、JSON 解析、CSS 解析或 XPath 解析失败
  • THEN 系统 SHALL 记录 failure.phase="body",且 SHALL NOT 将该失败记录为 request 错误

Requirement: 拨测结果记录

系统 SHALL 在每次 checker 完成后,将结果写入 SQLite 数据存储,包含 target_id、timestamp、matched、duration_ms、observation、failure 字段。detail SHALL 为 API 层派生字段,不写入存储层;系统 SHALL NOT 写入 status_detail 字段。

Scenario: 成功检查结果记录

  • WHEN checker 成功执行且 expect 全部匹配
  • THEN 系统 SHALL 记录 matched=true、duration_ms、observationfailure 为 null

Scenario: 执行失败结果记录

  • WHEN checker 执行失败(网络错误、超时、命令启动失败、输出超限等)
  • THEN 系统 SHALL 记录 matched=false、failure.kind="error" 和具体错误信息,并在可收集领域观测数据时记录 observation

Scenario: expect 不匹配结果记录

  • WHEN checker 执行成功但 expect 不匹配
  • THEN 系统 SHALL 记录 matched=false、observation、failure.kind="mismatch" 和具体不匹配信息

Requirement: runner 选择

系统 SHALL 根据 target.type 选择对应 runner 执行检查。

Scenario: 选择 HTTP runner

  • WHEN target.type 为 http
  • THEN 系统 SHALL 使用 HTTP runner 执行该目标

Scenario: 选择 cmd runner

  • WHEN target.type 为 cmd
  • THEN 系统 SHALL 使用 cmd runner 执行该目标

Requirement: 定期数据清理

ProbeEngine SHALL 在启动时注册数据清理定时器,定期调用 ProbeStore.prune() 清理过期数据和空壳非活跃目标。

Scenario: 引擎启动注册清理

  • WHEN ProbeEngine.start() 被调用且 retentionMs > 0
  • THEN 系统 SHALL 立即执行一次 prune然后每隔 1 小时再次执行

Scenario: 引擎停止清除定时器

  • WHEN ProbeEngine.stop() 被调用
  • THEN 系统 SHALL 清除清理定时器,不再执行后续清理

Scenario: retentionMs 为 0 不注册清理

  • WHEN ProbeEngine 构造时 retentionMs 为 0
  • THEN 系统 SHALL 不注册清理定时器