feat: 结构化 observation 替代 statusDetail,API 层动态构造 detail
- CheckResult: statusDetail -> observation (持久化) + detail (API 动态派生) - 存储: status_detail 列 -> observation TEXT (JSON) - CheckerDefinition: 新增 buildDetail(observation) 方法 - 各 checker 返回结构化 observation,API 层通过 registry 调用 buildDetail - HTTP: bodyPreview 在 status/header 失败时也提前采集 - UDP: observation 包含 durationMs,未响应归为 error failure - CMD: 超时/输出超限时保留已收集 observation - TCP: connectTimeMs 仅含连接建立耗时,不含 banner 等待 - 新增 buildDetail 单测和 mapCheckResult 覆盖测试 - 同步 openspec 主规范,归档 checker-observation 变更
This commit is contained in:
158
openspec/specs/checker-observation/spec.md
Normal file
158
openspec/specs/checker-observation/spec.md
Normal file
@@ -0,0 +1,158 @@
|
||||
## Purpose
|
||||
|
||||
定义 CheckResult 的 observation 数据模型、各 checker 类型 observation 结构、截断策略、序列化规则和 detail 动态构造机制。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Observation 数据模型
|
||||
CheckResult SHALL 包含 `observation: Record<string, unknown> | null` 字段,用于承载 checker 执行过程中收集的结构化观测数据。observation 为 null 表示执行过程中无法形成有意义的领域观测数据(如进程 spawn 失败、内部异常、请求在拿到响应前失败且无可记录元数据等场景)。各 checker SHALL 自行定义 observation 的内部结构,不做跨 checker 类型的统一约束。
|
||||
|
||||
#### Scenario: 正常执行返回 observation
|
||||
- **WHEN** checker 执行成功、expect 断言失败或产生可收集上下文的负向结果
|
||||
- **THEN** CheckResult.observation SHALL 包含该 checker 类型定义的完整观测数据
|
||||
|
||||
#### Scenario: 异常执行返回 null observation
|
||||
- **WHEN** checker 执行过程中发生无法收集领域观测数据的异常
|
||||
- **THEN** CheckResult.observation SHALL 为 null
|
||||
|
||||
### Requirement: HTTP Checker Observation
|
||||
HTTP checker 的 observation SHALL 包含 statusCode(number)、headers(Record<string, string>,截断)、bodyPreview(string | null,截断)、contentType(string | null)、contentLength(number | null)。
|
||||
|
||||
#### Scenario: 正常 HTTP 响应
|
||||
- **WHEN** HTTP 请求成功返回
|
||||
- **THEN** observation SHALL 包含响应状态码、截断后的响应 headers、响应体预览、Content-Type 和 Content-Length,即使未配置 body expect 也 SHALL 采集 bodyPreview
|
||||
|
||||
#### Scenario: HTTP 请求失败
|
||||
- **WHEN** HTTP 请求因网络错误或超时失败
|
||||
- **THEN** observation SHALL 为 null
|
||||
|
||||
### Requirement: TCP Checker Observation
|
||||
TCP checker 的 observation SHALL 包含 connected(boolean)、connectTimeMs(number | null)、banner(string | null,截断)、error(string | null)。
|
||||
|
||||
#### Scenario: TCP 连接成功且读取 banner
|
||||
- **WHEN** TCP 连接成功并读取到 banner
|
||||
- **THEN** observation SHALL 包含 connected=true、连接耗时、截断后的 banner 内容
|
||||
|
||||
#### Scenario: TCP 连接失败
|
||||
- **WHEN** TCP 连接失败
|
||||
- **THEN** observation SHALL 包含 connected=false 和错误信息
|
||||
|
||||
### Requirement: UDP Checker Observation
|
||||
UDP checker 的 observation SHALL 包含 responded(boolean)、durationMs(number)、responseSize(number | null)、responsePreview(string | null,截断)、sourceAddress(string | null)、sourcePort(number | null)、error(string | null)。durationMs 用于 API 序列化层生成包含耗时的 UDP detail 摘要。
|
||||
|
||||
#### Scenario: UDP 收到响应
|
||||
- **WHEN** UDP 发送数据后收到响应
|
||||
- **THEN** observation SHALL 包含 responded=true、响应大小、截断后的响应预览、来源地址和端口
|
||||
|
||||
#### Scenario: UDP 未收到响应
|
||||
- **WHEN** UDP 发送数据后超时未收到响应
|
||||
- **THEN** observation SHALL 包含 responded=false
|
||||
|
||||
### Requirement: Ping Checker Observation
|
||||
Ping/ICMP checker 的 observation SHALL 包含 alive(boolean)、transmitted(number)、received(number)、packetLoss(number)、avgLatencyMs(number | null)、maxLatencyMs(number | null)、minLatencyMs(number | null)、error(string | null)。API registry type SHALL 仍为 `ping`。
|
||||
|
||||
#### Scenario: Ping 正常返回统计
|
||||
- **WHEN** ping 命令成功执行并解析出统计数据
|
||||
- **THEN** observation SHALL 包含完整的 PingStats 字段
|
||||
|
||||
#### Scenario: Ping 命令失败
|
||||
- **WHEN** ping 命令未找到或超时
|
||||
- **THEN** observation SHALL 为 null 或包含 error 字段
|
||||
|
||||
### Requirement: DB Checker Observation
|
||||
DB checker 的 observation SHALL 包含 connected(boolean)、rowCount(number | null)、rowsPreview(unknown[] | null,截断前 N 行)、error(string | null)。
|
||||
|
||||
#### Scenario: 数据库查询成功
|
||||
- **WHEN** 数据库连接和查询成功
|
||||
- **THEN** observation SHALL 包含 connected=true、行数、截断后的行预览
|
||||
|
||||
#### Scenario: 仅探活无查询
|
||||
- **WHEN** 数据库配置仅探活连接(无 query)
|
||||
- **THEN** observation SHALL 包含 connected=true,rowCount 和 rowsPreview 为 null
|
||||
|
||||
### Requirement: CMD Checker Observation
|
||||
CMD checker 的 observation SHALL 包含 exitCode(number | null)、stdoutPreview(string | null,截断)、stderrPreview(string | null,截断)、error(string | null)。
|
||||
|
||||
#### Scenario: 命令正常执行
|
||||
- **WHEN** 命令执行完成
|
||||
- **THEN** observation SHALL 包含退出码、截断后的 stdout 和 stderr 预览
|
||||
|
||||
#### Scenario: 命令 spawn 失败
|
||||
- **WHEN** 命令进程无法启动
|
||||
- **THEN** observation SHALL 为 null
|
||||
|
||||
### Requirement: LLM Checker Observation
|
||||
LLM checker SHALL 保留执行期 `LlmCheckObservation.outputText` 完整文本用于 expect 校验,并从执行期 observation 派生持久化 observation。持久化 observation SHALL 包含 provider(string)、mode(string)、model(string)、http({ status, statusText, headers } | null,headers 截断)、finishReason(string | null)、rawFinishReason(string | null)、outputPreview(string | null,截断)、outputLength(number | null)、usage({ inputTokens, outputTokens, totalTokens } | null)、stream({ completed, firstTokenMs } | null)、warnings(string[])。
|
||||
|
||||
#### Scenario: LLM HTTP 模式成功
|
||||
- **WHEN** LLM 以 HTTP 模式成功执行
|
||||
- **THEN** observation SHALL 包含 provider、mode、model、HTTP 元数据、finish 原因、截断后的输出预览、完整输出长度和 token 用量
|
||||
|
||||
#### Scenario: LLM Stream 模式成功
|
||||
- **WHEN** LLM 以 stream 模式成功执行
|
||||
- **THEN** observation SHALL 额外包含 stream 观测数据(completed、firstTokenMs)
|
||||
|
||||
### Requirement: Observation 截断策略
|
||||
各 checker SHALL 对 observation 中的大文本和集合字段执行截断,防止存储膨胀。
|
||||
|
||||
#### Scenario: HTTP body 截断
|
||||
- **WHEN** HTTP 响应体超过 1024 字符
|
||||
- **THEN** bodyPreview SHALL 截断为前 1024 字符
|
||||
|
||||
#### Scenario: HTTP headers 截断
|
||||
- **WHEN** HTTP 响应 headers 超过 20 个
|
||||
- **THEN** headers SHALL 仅保留前 20 个
|
||||
|
||||
#### Scenario: TCP banner 截断
|
||||
- **WHEN** TCP banner 内容超过 256 字符
|
||||
- **THEN** banner SHALL 截断为前 256 字符
|
||||
|
||||
#### Scenario: UDP response 截断
|
||||
- **WHEN** UDP 响应预览超过 512 字符
|
||||
- **THEN** responsePreview SHALL 截断为前 512 字符
|
||||
|
||||
#### Scenario: DB rows 截断
|
||||
- **WHEN** 查询返回超过 5 行
|
||||
- **THEN** rowsPreview SHALL 仅保留前 5 行
|
||||
|
||||
#### Scenario: CMD stdout/stderr 截断
|
||||
- **WHEN** stdout 或 stderr 超过 1024 字符
|
||||
- **THEN** 对应 Preview 字段 SHALL 截断为前 1024 字符
|
||||
|
||||
#### Scenario: LLM output 截断
|
||||
- **WHEN** LLM 输出文本超过 512 字符
|
||||
- **THEN** outputPreview SHALL 截断为前 512 字符
|
||||
|
||||
#### Scenario: LLM headers 截断
|
||||
- **WHEN** LLM 响应 headers 超过 20 个
|
||||
- **THEN** headers SHALL 仅保留前 20 个
|
||||
|
||||
### Requirement: Observation 序列化规则
|
||||
observation SHALL 使用 JSON.stringify 序列化为 TEXT 格式存入 SQLite,使用 JSON.parse 反序列化读出。不引入额外的序列化依赖。
|
||||
|
||||
#### Scenario: 写入 observation
|
||||
- **WHEN** 存储 CheckResult 到数据库
|
||||
- **THEN** observation 字段 SHALL 使用 JSON.stringify 序列化后存入 TEXT 列;observation 为 null 时 SHALL 存入 SQL NULL
|
||||
|
||||
#### Scenario: 读取 observation
|
||||
- **WHEN** 从数据库读取 CheckResult
|
||||
- **THEN** observation 字段 SHALL 使用 JSON.parse 反序列化;SQL NULL 值 SHALL 映射为 null
|
||||
|
||||
#### Scenario: 使用 SQLite CLI 直接查看
|
||||
- **WHEN** 开发者使用 sqlite3 CLI 工具查看 check_results 表
|
||||
- **THEN** observation 列 SHALL 为人可读的 JSON 文本
|
||||
|
||||
### Requirement: Detail 动态构造
|
||||
CheckResult SHALL 包含 `detail: string | null` 字段(替代原 statusDetail),该字段 SHALL 不持久化到数据库,而是在 API 序列化层根据 target type 从 observation 动态构造。每个 checker SHALL 提供 `buildDetail(observation)` 方法,定义该 checker 类型的人可读摘要格式。
|
||||
|
||||
#### Scenario: API 返回 detail 字段
|
||||
- **WHEN** API 序列化 CheckResult 返回给前端
|
||||
- **THEN** 系统 SHALL 根据 target type 调用对应 checker 的 buildDetail 方法,从 observation 动态生成 detail 字段
|
||||
|
||||
#### Scenario: observation 为 null 时
|
||||
- **WHEN** observation 为 null
|
||||
- **THEN** detail SHALL 为 null
|
||||
|
||||
#### Scenario: 各 checker 的 detail 格式
|
||||
- **WHEN** 各 checker 的 buildDetail 被调用
|
||||
- **THEN** HTTP SHALL 返回 `"HTTP {statusCode}"` 格式;TCP SHALL 返回连接状态和 banner 摘要;UDP SHALL 返回响应状态和大小摘要;Ping SHALL 返回存活状态、平均延迟和丢包率摘要;DB SHALL 返回连接状态或行数摘要;CMD SHALL 返回 `"exitCode={N}"` 格式;LLM SHALL 返回 provider、mode、状态码、finish 原因、输出长度和 token 用量摘要
|
||||
@@ -50,11 +50,11 @@
|
||||
- **THEN** checker SHALL 返回 `ConfigValidationIssue`,而不是直接抛出最终用户错误字符串
|
||||
|
||||
### Requirement: Checker 接口定义
|
||||
系统 SHALL 在 `src/server/checker/runner/types.ts` 中定义面向扩展的泛型 `CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase>`,包含 `type`、`configKey`、TypeBox 配置契约、启动期语义校验、`resolve`、`execute`、`serialize` 成员。泛型参数 SHALL 约束 `execute` 和 `serialize` 方法的 target 参数类型,使 checker 实现内部获得编译期类型安全。默认泛型参数 `= ResolvedTargetBase` 保证中间层(registry、engine、config-loader)无需指定泛型。
|
||||
系统 SHALL 在 `src/server/checker/runner/types.ts` 中定义面向扩展的泛型 `CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase>`,包含 `type`、`configKey`、TypeBox 配置契约、启动期语义校验、`resolve`、`execute`、`serialize`、`buildDetail` 成员。泛型参数 SHALL 约束 `execute` 和 `serialize` 方法的 target 参数类型,使 checker 实现内部获得编译期类型安全。默认泛型参数 `= ResolvedTargetBase` 保证中间层(registry、engine、config-loader)无需指定泛型。
|
||||
|
||||
#### Scenario: Checker 接口包含必要方法
|
||||
- **WHEN** 开发者实现一个新的 Checker
|
||||
- **THEN** 该实现 MUST 提供 `type`(字符串标识)、`configKey`(配置分组名)、TypeBox 配置契约、启动期语义校验、`resolve(target, context)`(解析配置并填充默认值)、`execute(target, ctx)`(执行探测返回 CheckResult)和 `serialize(target)`(返回 target 展示文本和 config JSON)
|
||||
- **THEN** 该实现 MUST 提供 `type`(字符串标识)、`configKey`(配置分组名)、TypeBox 配置契约、启动期语义校验、`resolve(target, context)`(解析配置并填充默认值)、`execute(target, ctx)`(执行探测返回 CheckResult)、`serialize(target)`(返回 target 展示文本和 config JSON)和 `buildDetail(observation)`(从 observation 构造人可读摘要)
|
||||
|
||||
#### Scenario: CheckerContext 注入 signal
|
||||
- **WHEN** 引擎调用 `checker.execute(target, ctx)`
|
||||
@@ -80,6 +80,14 @@
|
||||
- **WHEN** CheckerRegistry 存储和返回 checker 实例
|
||||
- **THEN** registry 内部 SHALL 使用 `CheckerDefinition`(等价于 `CheckerDefinition<ResolvedTargetBase>`),实现类型擦除
|
||||
|
||||
#### Scenario: buildDetail 方法签名
|
||||
- **WHEN** 开发者实现 buildDetail 方法
|
||||
- **THEN** 方法签名 SHALL 为 `buildDetail(observation: Record<string, unknown>): string | null`,接收 observation 对象并返回人可读摘要字符串或 null
|
||||
|
||||
#### Scenario: buildDetail 由 API 层调用
|
||||
- **WHEN** API 序列化 CheckResult
|
||||
- **THEN** API 层 SHALL 通过 registry 获取对应 checker 并调用 buildDetail,而非由 execute 方法直接生成 detail
|
||||
|
||||
### Requirement: CheckerRegistry 注册中心
|
||||
系统 SHALL 在 `src/server/checker/runner/registry.ts` 中提供 `CheckerRegistry` 类,支持 `register(checker)`、`get(type)` 和 `supportedTypes`。重复注册同一 type SHALL 抛出错误。registry 内部 SHALL 存储 `CheckerDefinition`(使用默认泛型参数),对外提供类型擦除后的接口。
|
||||
|
||||
|
||||
@@ -32,27 +32,27 @@
|
||||
- **THEN** 系统 MUST NOT 向子进程 stdin 写入数据,避免命令因等待输入而阻塞
|
||||
|
||||
### Requirement: cmd checker 执行
|
||||
系统 SHALL 按 cmd target 配置执行本地命令,记录执行耗时、退出码、stdout 和 stderr,并在执行失败时产生结构化错误信息。
|
||||
系统 SHALL 按 cmd target 配置执行本地命令,记录执行耗时、退出码、stdout 和 stderr observation,并在执行失败时产生结构化错误信息。
|
||||
|
||||
#### Scenario: 命令正常退出
|
||||
- **WHEN** cmd target 执行的进程正常退出且 exit code 为 0
|
||||
- **THEN** 系统 SHALL 记录 `durationMs`、`statusDetail="exitCode=0"`,并进入 expect 校验
|
||||
- **THEN** 系统 SHALL 记录 `durationMs` 和包含 exitCode、stdoutPreview、stderrPreview 的 observation,并进入 expect 校验;API detail SHALL 为 `exitCode=0`
|
||||
|
||||
#### Scenario: 命令非零退出
|
||||
- **WHEN** cmd target 执行的进程正常退出但 exit code 为 1
|
||||
- **THEN** 系统 SHALL 记录 `statusDetail="exitCode=1"`,并由 expect.exitCode 决定 matched 结果
|
||||
- **THEN** 系统 SHALL 记录包含 exitCode、stdoutPreview、stderrPreview 的 observation,并由 expect.exitCode 决定 matched 结果;API detail SHALL 为 `exitCode=1`
|
||||
|
||||
#### Scenario: 命令启动失败
|
||||
- **WHEN** cmd target 的 exec 不存在或无法启动
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,并在 failure 中写入 kind=`error` 和可读错误信息
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,observation SHALL 为 null,并在 failure 中写入 kind=`error` 和可读错误信息
|
||||
|
||||
#### Scenario: 命令超时
|
||||
- **WHEN** cmd target 在 timeout 时间内未结束
|
||||
- **THEN** 系统 MUST 终止该子进程,记录 `matched=false`,并在 failure 中写入命令超时信息
|
||||
- **THEN** 系统 MUST 终止该子进程,记录 `matched=false`,并在 failure 中写入命令超时信息;如已收集输出片段,observation SHALL 包含 stdoutPreview、stderrPreview 和 error
|
||||
|
||||
#### Scenario: 命令输出超限
|
||||
- **WHEN** cmd target 的 stdout 和 stderr 合计输出超过 `maxOutputBytes`
|
||||
- **THEN** 系统 MUST 停止收集输出并终止该检查,记录 `matched=false`,并在 failure 中写入输出超限信息
|
||||
- **THEN** 系统 MUST 停止收集输出并终止该检查,记录 `matched=false`,并在 failure 中写入输出超限信息;observation SHALL 包含已截断输出预览和 error
|
||||
|
||||
### Requirement: cmd expect 校验
|
||||
系统 SHALL 支持 cmd 专用 expect,包括 `exitCode`、`durationMs`、`stdout` 和 `stderr`,并按 exitCode、durationMs、stdout、stderr 的阶段顺序快速失败。`exitCode` SHALL 保持有限整数数组语义,未配置时默认 `[0]`。`durationMs` SHALL 使用共享 `ValueMatcher` 校验完整命令执行耗时。`stdout` 和 `stderr` MUST 使用共享 `ContentRules` 数组,直接 matcher 作用于对应输出文本,`json` extractor SHALL 支持对 JSON CLI 输出执行 JSONPath 断言。
|
||||
|
||||
@@ -172,17 +172,17 @@
|
||||
- **WHEN** YAML 中 ping target 的 `expect.maxLatencyMs` 不是合法 `ValueMatcher`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.maxLatencyMs 格式错误
|
||||
|
||||
### Requirement: ping statusDetail 摘要
|
||||
系统 SHALL 在 ping 执行成功后生成结构化 statusDetail 摘要,展示关键指标。
|
||||
### Requirement: ping detail 摘要
|
||||
系统 SHALL 在 ping API 序列化时从 observation 动态生成结构化 detail 摘要,展示关键指标。API registry type SHALL 仍为 `ping`。
|
||||
|
||||
#### Scenario: 目标可达无丢包
|
||||
- **WHEN** ping 结果为 alive=true, avg=12ms, packetLoss=0%, transmitted=3, received=3
|
||||
- **THEN** statusDetail SHALL 为 `alive, avg 12ms, loss 0% (3/3)`
|
||||
- **WHEN** ping observation 为 alive=true, avgLatencyMs=12, packetLoss=0%, transmitted=3, received=3
|
||||
- **THEN** detail SHALL 为 `alive, avg 12ms, loss 0% (3/3)`
|
||||
|
||||
#### Scenario: 目标可达有丢包
|
||||
- **WHEN** ping 结果为 alive=true, avg=156ms, max=340ms, packetLoss=33%, transmitted=3, received=2
|
||||
- **THEN** statusDetail SHALL 包含 avg、max 和 loss 信息
|
||||
- **WHEN** ping observation 为 alive=true, avgLatencyMs=156, maxLatencyMs=340, packetLoss=33%, transmitted=3, received=2
|
||||
- **THEN** detail SHALL 包含 avg、max 和 loss 信息
|
||||
|
||||
#### Scenario: 目标不可达
|
||||
- **WHEN** ping 结果为 alive=false, transmitted=3, received=0
|
||||
- **THEN** statusDetail SHALL 为 `unreachable (0/3 received)`
|
||||
- **WHEN** ping observation 为 alive=false, transmitted=3, received=0
|
||||
- **THEN** detail SHALL 为 `unreachable (0/3 received)`
|
||||
|
||||
@@ -205,7 +205,7 @@ LLM checker SHALL 仅允许 `mode: stream` 使用 `expect.stream`。`expect.stre
|
||||
- **THEN** 默认 `stream.completed=true` SHALL NOT 阻断基于 status 和 headers 的 APICallError 状态探测
|
||||
|
||||
### Requirement: LLM Failure Phase 与状态摘要
|
||||
LLM checker SHALL 使用 `request`、`status`、`headers`、`stream`、`output`、`finishReason`、`rawFinishReason`、`usage`、`duration` 作为第一版 failure phase。成功结果的 `statusDetail` SHALL 简短描述 provider、mode、HTTP status、finish reason、raw finish reason、first token、输出长度和 token usage 中可用的信息。`statusDetail` MUST NOT 写入完整 prompt、完整输出或 key。
|
||||
LLM checker SHALL 使用 `request`、`status`、`headers`、`stream`、`output`、`finishReason`、`rawFinishReason`、`usage`、`duration` 作为第一版 failure phase。成功结果的 API detail SHALL 从持久化 observation 动态构造,简短描述 provider、mode、HTTP status、finish reason、raw finish reason、first token、输出长度和 token usage 中可用的信息。observation 和 detail MUST NOT 写入完整 prompt、完整输出或 key。
|
||||
|
||||
#### Scenario: request failure
|
||||
- **WHEN** 模型请求因网络错误、认证调用异常、AbortSignal 或无 HTTP metadata 的 SDK 错误失败
|
||||
@@ -217,11 +217,11 @@ LLM checker SHALL 使用 `request`、`status`、`headers`、`stream`、`output`
|
||||
|
||||
#### Scenario: 非流式成功摘要
|
||||
- **WHEN** `provider: openai` 的非流式检查成功
|
||||
- **THEN** `statusDetail` SHALL 使用类似 `LLM openai http 200 finish=stop, output=2 chars, usage=12/2 tokens` 的简短格式
|
||||
- **THEN** detail SHALL 使用类似 `LLM openai http 200 finish=stop, output=2 chars, usage=12/2 tokens` 的简短格式
|
||||
|
||||
#### Scenario: 流式成功摘要
|
||||
- **WHEN** `provider: anthropic` 的流式检查成功且存在 raw finish reason
|
||||
- **THEN** `statusDetail` SHALL 使用类似 `LLM anthropic stream 200 finish=stop raw=end_turn, firstToken=624ms, output=2 chars` 的简短格式
|
||||
- **THEN** detail SHALL 使用类似 `LLM anthropic stream 200 finish=stop raw=end_turn, firstToken=624ms, output=2 chars` 的简短格式
|
||||
|
||||
#### Scenario: serialize 展示文本
|
||||
- **WHEN** store 同步 LLM target
|
||||
@@ -236,7 +236,7 @@ LLM checker 的自动化测试 MUST 不访问真实外部模型服务。测试 S
|
||||
|
||||
#### Scenario: 本地 mock provider 测试错误路径
|
||||
- **WHEN** 测试运行 401、429、500、超时、stream error、stream abort、缺 usage 或无文本输出路径
|
||||
- **THEN** 测试 SHALL 断言 LLM checker 返回符合 spec 的 matched、failure phase、actual 和 statusDetail
|
||||
- **THEN** 测试 SHALL 断言 LLM checker 返回符合 spec 的 matched、failure phase、actual、detail 和 observation
|
||||
|
||||
#### Scenario: 质量检查覆盖 LLM checker
|
||||
- **WHEN** 实现完成后执行质量检查
|
||||
|
||||
@@ -105,7 +105,7 @@ Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态
|
||||
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
|
||||
|
||||
### Requirement: 新增共享类型
|
||||
系统 SHALL 在 `src/shared/api.ts` 中定义 Dashboard 和 Metrics 相关共享类型。
|
||||
系统 SHALL 在 `src/shared/api.ts` 中定义 Dashboard 和 Metrics 相关共享类型。CheckResult SHALL 包含 durationMs(null | number)、failure(CheckFailure | null)、matched(boolean)、detail(null | string)、observation(Record<string, unknown> | null)、timestamp(string)。其中 detail 替代原 statusDetail 字段名。
|
||||
|
||||
#### Scenario: DashboardResponse 类型
|
||||
- **WHEN** 前后端共享 `DashboardResponse` 类型
|
||||
@@ -123,9 +123,9 @@ Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态
|
||||
- **WHEN** 前后端共享 `TrendPoint` 类型
|
||||
- **THEN** 该类型 SHALL 包含 bucketStart、avgDurationMs、minDurationMs、maxDurationMs、availability、totalChecks、upChecks、downChecks 字段
|
||||
|
||||
#### Scenario: CheckResult 类型
|
||||
- **WHEN** 前后端共享 `CheckResult` 类型
|
||||
- **THEN** 该类型 SHALL 包含 `timestamp: string`、`matched: boolean`、`durationMs: number | null`、`statusDetail: string | null`、`failure` 字段,不包含 success 字段
|
||||
#### Scenario: CheckResult 类型变更
|
||||
- **WHEN** 前端或后端引用 CheckResult 类型
|
||||
- **THEN** 该类型 SHALL 包含 `timestamp: string`、`matched: boolean`、`durationMs: number | null`、`detail: string | null`、`observation: Record<string, unknown> | null`、`failure` 字段,不包含 statusDetail 字段,不包含 success 字段
|
||||
|
||||
#### Scenario: RecentSample 类型
|
||||
- **WHEN** 前后端共享 `RecentSample` 类型
|
||||
@@ -135,6 +135,22 @@ Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态
|
||||
- **WHEN** 前后端共享 `HistoryResponse` 类型
|
||||
- **THEN** 该类型 SHALL 包含 `items: CheckResult[]`、`total: number`、`page: number`、`pageSize: number` 字段
|
||||
|
||||
#### Scenario: API 序列化构造 detail
|
||||
- **WHEN** API 路由序列化 StoredCheckResult 为 API 响应
|
||||
- **THEN** 系统 SHALL 从 StoredCheckResult 中反序列化 observation,根据 target type 通过 checkerRegistry 获取对应 checker 并调用 buildDetail(observation) 动态生成 detail 字段
|
||||
|
||||
#### Scenario: mapCheckResult 接收 type 参数
|
||||
- **WHEN** 序列化辅助函数 mapCheckResult 被调用
|
||||
- **THEN** 函数 SHALL 接收 target type 参数,用于从 registry 获取对应 checker 调用 buildDetail
|
||||
|
||||
#### Scenario: Dashboard API 传递 type
|
||||
- **WHEN** Dashboard 路由序列化 latestCheck
|
||||
- **THEN** 路由 SHALL 将 target.type 传递给 mapCheckResult
|
||||
|
||||
#### Scenario: History API 传递 type
|
||||
- **WHEN** History 路由序列化历史记录列表
|
||||
- **THEN** 路由 SHALL 将已查询的 target.type 传递给 mapCheckResult
|
||||
|
||||
### Requirement: 保留健康检查端点
|
||||
系统 SHALL 保留 `GET /health` 端点,不受拨测功能影响。
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
#### Scenario: 首次启动创建数据库
|
||||
- **WHEN** 指定的数据目录下不存在数据库文件
|
||||
- **THEN** 系统 SHALL 创建数据库文件并初始化 targets 表和 check_results 表,check_results 表包含 id(INTEGER PRIMARY KEY AUTOINCREMENT)、target_id(INTEGER NOT NULL)、timestamp(TEXT NOT NULL)、matched(INTEGER NOT NULL)、duration_ms(REAL)、status_detail(TEXT)、failure(TEXT),不包含 success 列
|
||||
- **THEN** 系统 SHALL 创建数据库文件并初始化 targets 表和 check_results 表,check_results 表包含 id(INTEGER PRIMARY KEY AUTOINCREMENT)、target_id(TEXT NOT NULL)、timestamp(TEXT NOT NULL)、matched(INTEGER NOT NULL)、duration_ms(REAL)、observation(TEXT)、failure(TEXT),不包含 status_detail 列,不包含 success 列
|
||||
|
||||
#### Scenario: targets name 列允许 NULL
|
||||
- **WHEN** 系统首次创建 targets 表
|
||||
@@ -61,7 +61,11 @@
|
||||
|
||||
#### Scenario: 写入检查结果
|
||||
- **WHEN** 一次 checker 执行完成
|
||||
- **THEN** 系统 SHALL 插入一条包含 target_id、timestamp、matched、duration_ms、status_detail、failure 的记录
|
||||
- **THEN** 系统 SHALL 插入一条包含 target_id、timestamp、matched、duration_ms、observation、failure 的记录,其中 observation 使用 JSON.stringify 序列化为 TEXT
|
||||
|
||||
#### Scenario: 查询检查结果
|
||||
- **WHEN** 系统查询 latest check 或历史 check_results
|
||||
- **THEN** 存储层 SHALL 返回 observation 字段而非 status_detail 字段,供 API 序列化层反序列化并构造 detail
|
||||
|
||||
#### Scenario: 写入结构化失败信息
|
||||
- **WHEN** checker 执行失败或 expect 不匹配
|
||||
|
||||
@@ -214,19 +214,19 @@ HTTP checker SHALL 将运行期失败归属到实际失败阶段。请求、网
|
||||
- **THEN** 系统 SHALL 记录 `failure.phase="body"`,且 SHALL NOT 将该失败记录为 request 错误
|
||||
|
||||
### Requirement: 拨测结果记录
|
||||
系统 SHALL 在每次 checker 完成后,将结果写入 SQLite 数据存储,包含 target_id、timestamp、matched、duration_ms、status_detail、failure 字段。
|
||||
系统 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、status_detail,failure 为 null
|
||||
- **THEN** 系统 SHALL 记录 matched=true、duration_ms、observation,failure 为 null
|
||||
|
||||
#### Scenario: 执行失败结果记录
|
||||
- **WHEN** checker 执行失败(网络错误、超时、命令启动失败、输出超限等)
|
||||
- **THEN** 系统 SHALL 记录 matched=false、failure.kind="error" 和具体错误信息
|
||||
- **THEN** 系统 SHALL 记录 matched=false、failure.kind="error" 和具体错误信息,并在可收集领域观测数据时记录 observation
|
||||
|
||||
#### Scenario: expect 不匹配结果记录
|
||||
- **WHEN** checker 执行成功但 expect 不匹配
|
||||
- **THEN** 系统 SHALL 记录 matched=false、failure.kind="mismatch" 和具体不匹配信息
|
||||
- **THEN** 系统 SHALL 记录 matched=false、observation、failure.kind="mismatch" 和具体不匹配信息
|
||||
|
||||
### Requirement: runner 选择
|
||||
系统 SHALL 根据 target.type 选择对应 runner 执行检查。
|
||||
|
||||
@@ -303,7 +303,7 @@ Drawer 顶部的时间范围快捷按钮和日期范围选择器 SHALL 在同一
|
||||
|
||||
#### Scenario: 检查结果表格
|
||||
- **WHEN** 记录面板渲染且数据可用
|
||||
- **THEN** 面板 SHALL 使用 TDesign PrimaryTable 展示检查结果,列包含:状态(StatusDot 圆点)、时间(YYYY-MM-DD HH:mm:ss 格式)、耗时(标题含 ms 单位,单元格仅显示数值,居中对齐)、详情(statusDetail 和 failure.message 用冒号拼接)
|
||||
- **THEN** 面板 SHALL 使用 TDesign PrimaryTable 展示检查结果,列包含:状态(StatusDot 圆点)、时间(YYYY-MM-DD HH:mm:ss 格式)、耗时(标题含 ms 单位,单元格仅显示数值,居中对齐)、详情(detail 和 failure.message 用冒号拼接)
|
||||
|
||||
#### Scenario: 服务端分页
|
||||
- **WHEN** 检查结果总数超过一页
|
||||
|
||||
@@ -36,27 +36,27 @@
|
||||
- **THEN** `target` 展示摘要 SHALL 为 `<host>:<port>`,`config` JSON SHALL 包含 resolved 后的 host、port、readBanner、bannerReadTimeout 和 maxBannerBytes
|
||||
|
||||
### Requirement: tcp checker 执行
|
||||
系统 SHALL 按 tcp target 配置建立 TCP 连接,记录完整执行耗时,并在连接失败、超时或资源超限时产生结构化失败信息。
|
||||
系统 SHALL 按 tcp target 配置建立 TCP 连接,记录完整执行耗时和 TCP observation,并在连接失败、超时或资源超限时产生结构化失败信息。
|
||||
|
||||
#### Scenario: TCP 连接成功
|
||||
- **WHEN** tcp target 指向可连接的 TCP 服务,且未配置 expect 或 `expect.connected` 为 `true`
|
||||
- **THEN** 系统 SHALL 记录 `matched=true`、`durationMs` 和 `statusDetail`,并关闭 socket
|
||||
- **THEN** 系统 SHALL 记录 `matched=true`、`durationMs` 和包含 connected、connectTimeMs、banner 的 observation,并关闭 socket
|
||||
|
||||
#### Scenario: TCP 连接失败
|
||||
- **WHEN** tcp target 指向不可连接的 host/port,且未配置 expect 或 `expect.connected` 为 `true`
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `connect`,message 包含可读连接失败原因
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,observation SHALL 包含 connected=false 和错误信息,failure 的 kind 为 `error`,phase 为 `connect`,message 包含可读连接失败原因
|
||||
|
||||
#### Scenario: 期望端口不可达且连接失败
|
||||
- **WHEN** tcp target 配置 `expect.connected: false`,且 TCP 连接失败
|
||||
- **THEN** 系统 SHALL 记录 `matched=true`,statusDetail SHALL 展示实际连接失败原因摘要
|
||||
- **THEN** 系统 SHALL 记录 `matched=true`,observation SHALL 包含 connected=false 和实际连接失败原因,API detail SHALL 展示实际连接失败原因摘要
|
||||
|
||||
#### Scenario: 期望端口不可达但连接成功
|
||||
- **WHEN** tcp target 配置 `expect.connected: false`,但 TCP 连接成功
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `connected`
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,observation SHALL 包含 connected=true,failure 的 kind 为 `mismatch`,phase 为 `connected`
|
||||
|
||||
#### Scenario: TCP 执行超时
|
||||
- **WHEN** 引擎注入的 `ctx.signal` 在 TCP 连接或 banner 读取过程中 abort
|
||||
- **THEN** 系统 SHALL best-effort 关闭 socket,记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `connect` 或 `banner`,message 包含超时信息
|
||||
- **THEN** 系统 SHALL best-effort 关闭 socket,记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `connect` 或 `banner`,message 包含超时信息,并在可收集时记录 observation
|
||||
|
||||
#### Scenario: duration 包含 banner 读取
|
||||
- **WHEN** tcp target 开启 `readBanner` 且服务端延迟发送 banner
|
||||
@@ -81,9 +81,9 @@
|
||||
- **WHEN** 服务端发送的 banner 数据超过 `maxBannerBytes`
|
||||
- **THEN** 系统 SHALL 停止读取并记录 `matched=false`、failure.kind=`error`、failure.phase=`banner` 的结构化错误
|
||||
|
||||
#### Scenario: banner statusDetail 截断展示
|
||||
#### Scenario: banner detail 截断展示
|
||||
- **WHEN** tcp target 成功读取到较长 banner
|
||||
- **THEN** `statusDetail` SHALL 展示截断后的 banner 摘要,避免 UI 和历史记录写入过长文本
|
||||
- **THEN** observation.banner SHALL 保存截断后的 banner 摘要,API detail SHALL 展示截断后的 banner 摘要,避免 UI 展示过长文本
|
||||
|
||||
### Requirement: tcp expect 校验
|
||||
系统 SHALL 支持 tcp 专属 expect,包括 `connected`、`banner` 和 `durationMs`,并按 connected、banner、durationMs 的阶段顺序快速失败。`connected` SHALL 保持布尔状态语义,未配置时默认 `true`。`banner` MUST 使用共享 `ContentRules` 数组,并仅在 `tcp.readBanner: true` 时允许配置。`durationMs` SHALL 使用共享 `ValueMatcher` 校验包含连接和 banner 读取在内的完整执行耗时。
|
||||
|
||||
@@ -75,11 +75,11 @@
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 udp.payload 与 udp.encoding 不匹配
|
||||
|
||||
### Requirement: udp checker 执行
|
||||
系统 SHALL 使用 Bun connected UDP socket 向目标发送单个 datagram,等待第一个 UDP 响应 datagram,记录完整执行耗时,并在发送失败、超时、资源超限或底层 socket 错误时产生结构化失败信息。
|
||||
系统 SHALL 使用 Bun connected UDP socket 向目标发送单个 datagram,等待第一个 UDP 响应 datagram,记录完整执行耗时和 UDP observation,并在发送失败、超时、资源超限或底层 socket 错误时产生结构化失败信息。
|
||||
|
||||
#### Scenario: UDP 请求响应成功
|
||||
- **WHEN** udp target 指向会返回 `PONG` 的 UDP 服务,且未配置 expect 或 `expect.responded` 为 `true`
|
||||
- **THEN** 系统 SHALL 记录 `matched=true`、`durationMs` 和包含响应大小的 `statusDetail`,并关闭 socket
|
||||
- **THEN** 系统 SHALL 记录 `matched=true`、`durationMs` 和包含响应大小的 observation,并关闭 socket
|
||||
|
||||
#### Scenario: 使用 hostname 执行 UDP 探测
|
||||
- **WHEN** udp target 的 `udp.host` 为可解析域名或 `localhost`
|
||||
@@ -91,19 +91,19 @@
|
||||
|
||||
#### Scenario: UDP 无响应且默认期望响应
|
||||
- **WHEN** udp target 指向在 timeout 内不返回 UDP datagram 的服务,且未配置 expect 或 `expect.responded` 为 `true`
|
||||
- **THEN** 系统 SHALL 在 `ctx.signal` abort 后记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `response`,message 包含超时或未响应信息
|
||||
- **THEN** 系统 SHALL 在 `ctx.signal` abort 后记录 `matched=false`,observation SHALL 包含 responded=false 和 error,failure 的 kind 为 `error`,phase 为 `response`,message 包含超时或未响应信息
|
||||
|
||||
#### Scenario: 期望无响应且实际无响应
|
||||
- **WHEN** udp target 配置 `expect.responded: false`,且 timeout 内未收到 UDP datagram
|
||||
- **THEN** 系统 SHALL 记录 `matched=true`,statusDetail SHALL 表示未收到响应
|
||||
- **THEN** 系统 SHALL 记录 `matched=true`,observation SHALL 包含 responded=false,API detail SHALL 表示未收到响应
|
||||
|
||||
#### Scenario: 期望无响应但实际收到响应
|
||||
- **WHEN** udp target 配置 `expect.responded: false`,但收到了 UDP datagram
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `responded`
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,observation SHALL 包含 responded=true 和响应摘要,failure 的 kind 为 `mismatch`,phase 为 `responded`
|
||||
|
||||
#### Scenario: UDP socket 底层错误
|
||||
- **WHEN** Bun UDP socket 在发送或接收过程中触发 error 事件
|
||||
- **THEN** 系统 SHALL best-effort 关闭 socket,并记录 `matched=false`、failure.kind=`error` 和可读错误信息
|
||||
- **THEN** 系统 SHALL best-effort 关闭 socket,并记录 `matched=false`、failure.kind=`error` 和可读错误信息,并在可收集时记录 observation
|
||||
|
||||
#### Scenario: ICMP unreachable 不作为 UDP 响应
|
||||
- **WHEN** 底层系统因目标端口不可达产生 ICMP unreachable
|
||||
@@ -187,17 +187,17 @@
|
||||
- **WHEN** YAML 中 udp target 的 expect 包含 `status: [200]`、`maxDurationMs: 1000` 或其他非 udp expect 字段
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
|
||||
|
||||
### Requirement: udp statusDetail 摘要
|
||||
系统 SHALL 在 udp 执行后生成简短 statusDetail 摘要,展示关键结果并避免写入过长响应内容。
|
||||
### Requirement: udp detail 摘要
|
||||
系统 SHALL 在 udp API 序列化时从 observation 动态生成简短 detail 摘要,展示关键结果和执行耗时并避免返回过长响应内容。UDP observation SHALL 包含 durationMs 以支持 detail 构造。
|
||||
|
||||
#### Scenario: 收到响应的摘要
|
||||
- **WHEN** udp target 收到 4 字节响应且完整执行耗时为 12ms
|
||||
- **THEN** statusDetail SHALL 包含 `responded in 12ms` 和 `4 bytes`
|
||||
- **THEN** detail SHALL 包含 `responded in 12ms` 和 `4 bytes`
|
||||
|
||||
#### Scenario: 未收到响应的摘要
|
||||
- **WHEN** udp target 配置 `expect.responded: false` 且 timeout 内未收到 UDP datagram
|
||||
- **THEN** statusDetail SHALL 包含 `no response` 和执行耗时
|
||||
- **THEN** detail SHALL 包含 `no response` 和执行耗时
|
||||
|
||||
#### Scenario: 响应内容摘要截断
|
||||
- **WHEN** udp target 收到较长响应内容
|
||||
- **THEN** statusDetail SHALL 只展示按 `responseEncoding` 转换并截断后的响应摘要
|
||||
- **THEN** detail SHALL 只展示按 `responseEncoding` 转换并截断后的响应摘要
|
||||
|
||||
Reference in New Issue
Block a user