- 新增 server.listen (host/port)、server.storage (dataDir/retention)、 server.logging 分组 - 新增 probes.execution (maxConcurrentChecks) 分组,替代顶层 runtime - 旧配置入口 (runtime/logging/server.host/server.port/server.dataDir) 启动期拒绝 - 更新 types.ts、builder.ts、config-loader.ts 适配新路径 - 更新 probe-config.schema.json、probes.example.yaml、README.md、 DEVELOPMENT.md - 补充 config-loader 和 variables 测试覆盖新路径和旧入口拒绝 - 同步 5 个 delta specs 到主规范 (probe-config, config-variables, data-retention, probe-engine, runtime-logging) - 归档 openspec change reorganize-config-layout
15 KiB
Purpose
定义拨测调度引擎的行为:按 interval 分组定时、组内并发拨测、expect 结果校验和结果持久化。
Requirements
Requirement: 按 interval 分组调度
系统 SHALL 将拨测目标按 interval 值分组,每组使用独立的定时器进行调度。
Scenario: 相同 interval 的目标共享定时器
- WHEN 多个 target 配置了相同的 interval(如 30s)
- THEN 系统 SHALL 使用同一个
setInterval定时器,每次 tick 并发拨测所有该组目标
Scenario: 不同 interval 的目标各自调度
- WHEN target A 配置 15s interval,target B 配置 30s interval
- THEN 系统 SHALL 创建两个独立定时器,分别按各自频率调度
Requirement: 组内并发拨测
系统 SHALL 在每次调度 tick 时并发执行同组内目标的检查,但实际同时运行的检查数 MUST 受全局 probes.execution.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_result,failure 为{ 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 个目标且 probes.execution.maxConcurrentChecks 为 3
- THEN 系统 MUST 同时最多运行 3 个检查,其余检查等待并发槽位释放
Requirement: HTTP 拨测执行
系统 SHALL 对 type: http 的目标执行 HTTP 请求,支持 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS 方法,并携带 http.headers 和 http.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=false、failure.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-Typeheader 包含charset=gbk或charset="gbk" - THEN 系统 SHALL 使用 GBK 编码解码响应体,而非硬编码 UTF-8
Scenario: 响应体编码回退 UTF-8
- WHEN HTTP 响应的
Content-Typeheader 未指定 charset - THEN 系统 SHALL 使用 UTF-8 编码解码响应体
Scenario: 响应体编码不支持
- WHEN HTTP 响应的
Content-Typeheader 指定了当前运行时不支持的 charset - THEN 系统 SHALL 记录
matched=false、failure.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.bodyContentExpectations 数组 - 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.stdoutContentExpectations 数组 - 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=false、failure.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、observation,failure 为 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 不注册清理定时器