refactor: 规范审查与重组,合并细粒度规范,清理过时内容
- 合并 20+ 细粒度 spec 为粗粒度主题规范:dashboard、data-store、probe-engine、probe-api、probe-config 等 - 删除完全冗余规范:data-retention(被 probe-engine+data-store 覆盖)、backend-code-quality(DEVELOPMENT.md 已记录) - 补充 http-checker 规范至完整标准(配置+执行+expect+校验+observation),匹配代码 440 行实现 - 清理 tcp/udp/llm checker 规范中已废弃 defaults 配置段的残留 Scenario - 清理 checker-cohesion-structure 中的实现路径引用(src/server/...) - 统一所有 spec 格式(## Purpose 开头,去除 # Capability/Title 形式) - 更新 prompt-spec-review.md 审查提示文档
This commit is contained in:
@@ -1,59 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义后端 API 路由的组织规范:按端点拆分为独立 handler、共享响应工具集中管理、路径参数由 Bun routes 解析,静态资源服务由 HTML import manifest 管理。
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
### Requirement: 路由按职责拆分
|
||||
系统 SHALL 将 HTTP 路由处理逻辑按 API 端点拆分为独立模块,每个模块导出 route handler 函数供 routes 对象统一注册。
|
||||
|
||||
#### Scenario: health 端点独立路由
|
||||
- **WHEN** 客户端请求 `GET /health`
|
||||
- **THEN** `routes/health.ts` 导出的 handler 负责处理,返回 HealthResponse JSON
|
||||
|
||||
#### Scenario: summary 端点独立路由
|
||||
- **WHEN** 客户端请求 `GET /api/summary`
|
||||
- **THEN** `routes/summary.ts` 导出的 handler 负责处理,委托 store 查询并返回 SummaryResponse JSON
|
||||
|
||||
#### Scenario: targets 端点独立路由
|
||||
- **WHEN** 客户端请求 `GET /api/targets`
|
||||
- **THEN** `routes/targets.ts` 导出的 handler 负责处理,委托 store 查询并返回 TargetStatus[] JSON
|
||||
|
||||
#### Scenario: history 端点独立路由
|
||||
- **WHEN** 客户端请求 `GET /api/targets/:id/history?from=ISO&to=ISO`
|
||||
- **THEN** `routes/history.ts` 导出的 handler 负责处理,通过 `req.params.id` 获取路径参数,包含参数校验、store 查询和 HistoryResponse 返回
|
||||
|
||||
#### Scenario: trend 端点独立路由
|
||||
- **WHEN** 客户端请求 `GET /api/targets/:id/trend?from=ISO&to=ISO`
|
||||
- **THEN** `routes/trend.ts` 导出的 handler 负责处理,通过 `req.params.id` 获取路径参数,包含参数校验、store 查询和 TrendPoint[] 返回
|
||||
|
||||
### Requirement: 共享辅助函数集中管理
|
||||
系统 SHALL 将跨路由共享的响应格式化函数抽取到 helpers.ts 模块,单一职责、集中管理。
|
||||
|
||||
#### Scenario: createApiError 集中定义
|
||||
- **WHEN** 任意路由需要返回 API 错误响应
|
||||
- **THEN** 从 `helpers.ts` 导入 `createApiError` 函数,提供错误信息和状态码
|
||||
|
||||
#### Scenario: jsonResponse 集中定义
|
||||
- **WHEN** 任意路由需要返回 JSON 响应
|
||||
- **THEN** 从 `helpers.ts` 导入 `jsonResponse` 函数,处理 HEAD 方法、Content-Type 和安全头
|
||||
|
||||
#### Scenario: mapCheckResult 集中定义
|
||||
- **WHEN** 需要将 StoredCheckResult 映射为 API CheckResult
|
||||
- **THEN** 从 `helpers.ts` 导入 `mapCheckResult` 函数,处理 failure JSON 解析和格式转换
|
||||
|
||||
### Requirement: 静态资源服务独立管理
|
||||
系统 SHALL 将 SPA fallback 逻辑交给 routes 对象中的 HTML import 通配符处理,静态资源服务由 Bun 内置 manifest 机制自动处理。
|
||||
|
||||
#### Scenario: 根路径返回 index.html
|
||||
- **WHEN** 客户端请求 `/`
|
||||
- **THEN** routes 中注册的 HTML import 自动返回 index.html
|
||||
|
||||
#### Scenario: 资源文件返回正确 Content-Type
|
||||
- **WHEN** 客户端请求构建后的静态资源
|
||||
- **THEN** Bun 内置 manifest 机制自动返回正确的 Content-Type 和缓存头
|
||||
|
||||
#### Scenario: SPA fallback
|
||||
- **WHEN** 客户端请求非 API、非资源的路径(如 `/dashboard`)
|
||||
- **THEN** routes 中注册的 `"/*"` HTML import 通配符返回 index.html 实现 SPA 的客户端路由
|
||||
@@ -1,136 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义后端代码中 es-toolkit 和 Bun 内置 API 的使用规范:类型判断、空值检测、深度比较、错误判断、并发控制、集合分组和 Web API 标准方法,替代手写实现落实库使用优先级规则。
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
### Requirement: 使用 es-toolkit 进行类型判断
|
||||
系统 SHALL 使用 es-toolkit 的 `isPlainObject` 替代手写的对象类型判断函数,用于 expect 校验中区分纯值(原始值)和操作符对象。
|
||||
|
||||
#### Scenario: 识别纯对象为操作符
|
||||
- **WHEN** body 校验规则中 expected 配置为 `{ equals: "value" }`(纯对象操作符)
|
||||
- **THEN** `isPlainObject(expected)` SHALL 返回 true,系统按操作符语义处理
|
||||
|
||||
#### Scenario: 排除非纯对象作为操作符
|
||||
- **WHEN** body 校验规则中 expected 为原始值如 `"value"` 或数字 `200`
|
||||
- **THEN** `isPlainObject(expected)` SHALL 返回 false,系统按 equals 默认操作符处理
|
||||
|
||||
### Requirement: 使用 es-toolkit 进行空值检测
|
||||
系统 SHALL 使用 es-toolkit 的 `isNil` 替代手写的 `actual === null || actual === undefined` 检测,用于 expect 中 `empty` 操作符的空值判断。
|
||||
|
||||
#### Scenario: null 值判定为空
|
||||
- **WHEN** 校验值为 null
|
||||
- **THEN** `isNil(null)` SHALL 返回 true
|
||||
|
||||
#### Scenario: undefined 值判定为空
|
||||
- **WHEN** 校验值为 undefined
|
||||
- **THEN** `isNil(undefined)` SHALL 返回 true
|
||||
|
||||
#### Scenario: 非空值判定为非空
|
||||
- **WHEN** 校验值为 0、"false"、空数组 `[]` 等非 nil 值
|
||||
- **THEN** `isNil(value)` SHALL 返回 false
|
||||
|
||||
### Requirement: 使用 es-toolkit 进行空对象检测
|
||||
系统 SHALL 使用 es-toolkit 的 `isEmptyObject` 替代手写的 `typeof actual === "object" && Object.keys(actual).length === 0` 检测,用于 expect 中 `empty` 操作符的空对象判断。
|
||||
|
||||
#### Scenario: 空对象判定为空
|
||||
- **WHEN** 校验值为 `{}`
|
||||
- **THEN** `isEmptyObject({})` SHALL 返回 true
|
||||
|
||||
#### Scenario: 非空对象判定为非空
|
||||
- **WHEN** 校验值为 `{ key: "val" }`
|
||||
- **THEN** `isEmptyObject({ key: "val" })` SHALL 返回 false
|
||||
|
||||
#### Scenario: null 不是空对象
|
||||
- **WHEN** 校验值为 null
|
||||
- **THEN** `isEmptyObject(null)` SHALL 返回 false(空值由 isNil 前置处理)
|
||||
|
||||
### Requirement: 使用 es-toolkit 进行深度相等比较
|
||||
系统 SHALL 使用 es-toolkit 的 `isEqual` 替代 `!==` 浅比较,用于 expect 中 `equals` 操作符的值比较,支持对象和数组的深度比较。
|
||||
|
||||
#### Scenario: 原始值浅比较
|
||||
- **WHEN** expected 和 actual 均为原始值(字符串、数字、布尔值、null)
|
||||
- **THEN** `isEqual(actual, expected)` 的行为 SHALL 与 `actual === expected` 一致
|
||||
|
||||
#### Scenario: 对象深度比较
|
||||
- **WHEN** expected 和 actual 均为对象(如从 JSONPath 提取的结构化数据)
|
||||
- **THEN** `isEqual(actual, expected)` SHALL 递归比较所有属性值,而非引用比较
|
||||
|
||||
### Requirement: 使用 es-toolkit 进行错误类型判断
|
||||
系统 SHALL 使用 es-toolkit 的 `isError` 替代 `error instanceof Error`,用于 HTTP runner 和 cmd runner 中的错误类型判断。
|
||||
|
||||
#### Scenario: Error 实例识别
|
||||
- **WHEN** 错误对象为 `new Error("msg")`
|
||||
- **THEN** `isError(error)` SHALL 返回 true
|
||||
|
||||
#### Scenario: Error 子类识别
|
||||
- **WHEN** 错误对象为继承 Error 的自定义类型
|
||||
- **THEN** `isError(error)` SHALL 返回 true
|
||||
|
||||
#### Scenario: 非 Error 对象识别
|
||||
- **WHEN** 错误对象为字符串或普通对象
|
||||
- **THEN** `isError(error)` SHALL 返回 false
|
||||
|
||||
### Requirement: 使用 es-toolkit Semaphore 实现并发控制
|
||||
系统 SHALL 使用 es-toolkit 的 `Semaphore` 类替代手写的信号量实现(计数器 + Promise 队列),用于 ProbeEngine 中的组内并发拨测控制。
|
||||
|
||||
#### Scenario: 获取并发槽位
|
||||
- **WHEN** 当前并发数未达上限
|
||||
- **THEN** `semaphore.acquire()` SHALL 立即返回,不阻塞
|
||||
|
||||
#### Scenario: 等待并发槽位
|
||||
- **WHEN** 当前并发数已达上限 maxConcurrentChecks
|
||||
- **THEN** `semaphore.acquire()` SHALL 阻塞等待,直到其他任务调用 `semaphore.release()`
|
||||
|
||||
#### Scenario: 释放并发槽位
|
||||
- **WHEN** 调用 `semaphore.release()`
|
||||
- **THEN** 系统 SHALL 唤醒一个等待中的 acquire() 调用
|
||||
|
||||
### Requirement: 使用 es-toolkit groupBy 实现 target 分组
|
||||
系统 SHALL 使用 es-toolkit 的 `groupBy` 函数替代手写的 Map 循环分组,用于 ProbeEngine 中按 interval 分组拨测目标。
|
||||
|
||||
#### Scenario: 按 interval 分组
|
||||
- **WHEN** 输入包含不同 intervalMs 值的多个 target
|
||||
- **THEN** `groupBy(targets, t => t.intervalMs)` SHALL 返回 key 为 intervalMs 值的分组对象,值为对应 target 数组
|
||||
|
||||
### Requirement: 使用原生 API 进行数组类型判断
|
||||
系统 SHALL 使用原生 `Array.isArray()` 替代 `es-toolkit/compat` 的 `isArray`,用于 checker 模块中所有数组类型判断场景。
|
||||
|
||||
#### Scenario: 数组值判定为数组
|
||||
- **WHEN** 校验值为数组(如 `[1, 2, 3]`)
|
||||
- **THEN** `Array.isArray(value)` SHALL 返回 true
|
||||
|
||||
#### Scenario: 非数组值判定为非数组
|
||||
- **WHEN** 校验值为对象 `{}`、字符串 `"abc"`、数字 `123`、null 等
|
||||
- **THEN** `Array.isArray(value)` SHALL 返回 false
|
||||
|
||||
### Requirement: 使用原生 API 进行对象类型判断
|
||||
系统 SHALL 使用原生 `typeof x === 'object' && x !== null` 替代 `es-toolkit/compat` 的 `isObject`,用于 checker 模块中需要判断值为对象类型(排除 null)的场景。
|
||||
|
||||
#### Scenario: 普通对象判定为对象
|
||||
- **WHEN** 值为普通对象 `{ key: "val" }`
|
||||
- **THEN** `typeof value === 'object' && value !== null` SHALL 返回 true
|
||||
|
||||
#### Scenario: 数组判定为对象
|
||||
- **WHEN** 值为数组 `[1, 2, 3]`
|
||||
- **THEN** `typeof value === 'object' && value !== null` SHALL 返回 true
|
||||
|
||||
#### Scenario: Headers 实例判定为对象
|
||||
- **WHEN** 值为 `Headers` 实例
|
||||
- **THEN** `typeof value === 'object' && value !== null` SHALL 返回 true
|
||||
|
||||
#### Scenario: null 判定为非对象
|
||||
- **WHEN** 值为 null
|
||||
- **THEN** `typeof value === 'object' && value !== null` SHALL 返回 false
|
||||
|
||||
#### Scenario: 原始值判定为非对象
|
||||
- **WHEN** 值为字符串、数字、布尔值、undefined
|
||||
- **THEN** `typeof value === 'object' && value !== null` SHALL 返回 false
|
||||
|
||||
### Requirement: 使用 Bun 内置 API 进行 Headers 转换
|
||||
系统 SHALL 使用 `Object.fromEntries(headers)` 标准 Web API 替代手写的 `headersToRecord` 函数,用于将 Fetch API 的 Headers 对象转换为键值对。
|
||||
|
||||
#### Scenario: 转换响应头
|
||||
- **WHEN** HTTP runner 获取到 response headers
|
||||
- **THEN** `Object.fromEntries(response.headers)` SHALL 返回以 header 名称为 key、header 值为 value 的对象
|
||||
@@ -1,92 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 ProbeStore 的批量查询方法:getLatestChecksMap、getAllTargetStats,以及 getSummary 和 createTargetsResponse 的 N+1 查询优化规范。同时约定单次查询操作使用 db.query() 利用内置缓存。
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
### Requirement: 批量查询最新检查结果
|
||||
系统 SHALL 提供 `getLatestChecksMap` 方法,通过单次 SQL 查询获取所有 target 的最新一次 check 结果,返回 Map 结构供调用方按 target_id 索引。
|
||||
|
||||
#### Scenario: 获取所有目标的最新检查
|
||||
- **WHEN** 调用 `getLatestChecksMap()`
|
||||
- **THEN** 系统 SHALL 执行子查询找到每个 target_id 的 MAX(timestamp),再 JOIN 回 check_results 获取完整行,返回 `Map<number, StoredCheckResult | null>`
|
||||
|
||||
#### Scenario: 目标无历史记录
|
||||
- **WHEN** 某 target 在 check_results 表中无任何记录
|
||||
- **THEN** 该 target_id 在返回的 Map 中 SHALL 不存在对应的 key
|
||||
|
||||
### Requirement: 批量查询目标统计
|
||||
系统 SHALL 提供 `getAllTargetStats` 方法,通过单次 SQL GROUP BY 聚合查询获取所有 target 的拨测统计(totalChecks 和 availability)。availability 计算精度 SHALL 与 `getTargetStats` 一致,统一使用 `Math.round(value * 100) / 100` 保留两位小数。
|
||||
|
||||
#### Scenario: 获取所有目标的聚合统计
|
||||
- **WHEN** 调用 `getAllTargetStats()`
|
||||
- **THEN** 系统 SHALL 执行 `SELECT target_id, COUNT(*), SUM(CASE WHEN matched=1 THEN 1 ELSE 0 END) FROM check_results GROUP BY target_id`,在内存中计算 availability 并返回 `Map<number, { totalChecks, availability }>`
|
||||
|
||||
#### Scenario: 目标无历史记录
|
||||
- **WHEN** 某 target 在 check_results 表中无任何记录
|
||||
- **THEN** 该 target_id 在返回的 Map 中 SHALL 不存在对应的 key
|
||||
|
||||
#### Scenario: availability 精度
|
||||
- **WHEN** 计算 availability(upCount / totalChecks * 100)
|
||||
- **THEN** 结果 SHALL 使用 `Math.round(value * 100) / 100` 四舍五入保留两位小数,与 `getTargetStats` 方法一致
|
||||
|
||||
### Requirement: summary 查询使用批量方法
|
||||
`getSummary` 方法 SHALL 使用 `getLatestChecksMap` 一次性获取所有 target 的最新检查结果,而非对每个 target 逐条查询。
|
||||
|
||||
#### Scenario: 统计总览使用批量查询
|
||||
- **WHEN** 调用 `store.getSummary()`
|
||||
- **THEN** 系统 SHALL 调用 `getLatestChecksMap()` 一次获取所有最新结果,在内存中遍历统计 up/down 数量,而非循环 N 次调用 `getLatestCheck()`
|
||||
|
||||
### Requirement: targets 列表使用批量方法
|
||||
`handleTargets`(routes/targets.ts 中生成 TargetStatus[] 的逻辑)SHALL 使用 `getLatestChecksMap`、`getAllTargetStats` 和 `getAllRecentSamples` 替代逐目标查询,消除 N+1 查询。
|
||||
|
||||
#### Scenario: 目标列表使用批量查询
|
||||
- **WHEN** 处理 `GET /api/targets` 请求
|
||||
- **THEN** 系统 SHALL 分别调用 `getLatestChecksMap()`、`getAllTargetStats()`、`getAllRecentSamples(30)` 批量获取数据,在内存中组装 TargetStatus 数组,而非对每个 target 逐条查询数据库
|
||||
|
||||
#### Scenario: 目标无采样数据
|
||||
- **WHEN** 某 target 在 getAllRecentSamples 返回的 Map 中不存在
|
||||
- **THEN** 该 target 的 recentSamples SHALL 为空数组
|
||||
|
||||
### Requirement: 批量查询所有目标的最近采样数据
|
||||
系统 SHALL 提供 `getAllRecentSamples(limit: number)` 方法,通过单次 SQL 查询获取所有 target 的最近 N 条采样数据,返回 `Map<number, Array<{ timestamp: string; duration_ms: number | null; matched: number }>>` 结构。
|
||||
|
||||
#### Scenario: 获取所有目标的最近采样
|
||||
- **WHEN** 调用 `getAllRecentSamples(30)`
|
||||
- **THEN** 系统 SHALL 通过单次 SQL 查询获取每个 target 最近 30 条记录,返回按 target_id 索引的 Map
|
||||
|
||||
#### Scenario: 目标无历史记录
|
||||
- **WHEN** 某 target 在 check_results 表中无任何记录
|
||||
- **THEN** 该 target_id 在返回的 Map 中 SHALL 不存在对应的 key
|
||||
|
||||
#### Scenario: 采样数据排序
|
||||
- **WHEN** 获取采样数据
|
||||
- **THEN** 每个 target 的记录 SHALL 按 timestamp 降序排列(最新在前)
|
||||
|
||||
### Requirement: prepared statement 使用 query() 缓存
|
||||
ProbeStore 中不涉及事务内复用的单次读/写操作 SHALL 使用 `this.db.query()` 而非 `this.db.prepare()`,利用 bun:sqlite 内置的 statement 缓存机制。
|
||||
|
||||
#### Scenario: insertCheckResult 使用 query
|
||||
- **WHEN** 写入一条检查结果
|
||||
- **THEN** `insertCheckResult` SHALL 使用 `this.db.query("INSERT INTO ...").run(...)` 而非 `this.db.prepare("INSERT INTO ...").run(...)`
|
||||
|
||||
#### Scenario: getHistory 查询使用 query
|
||||
- **WHEN** 查询历史记录(包括 COUNT 和分页查询)
|
||||
- **THEN** `getHistory` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
|
||||
|
||||
#### Scenario: getTargetStats 查询使用 query
|
||||
- **WHEN** 查询单目标统计
|
||||
- **THEN** `getTargetStats` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
|
||||
|
||||
#### Scenario: getTrend 查询使用 query
|
||||
- **WHEN** 查询趋势数据
|
||||
- **THEN** `getTrend` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
|
||||
|
||||
#### Scenario: getRecentSamples 查询使用 query
|
||||
- **WHEN** 查询采样数据
|
||||
- **THEN** `getRecentSamples` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
|
||||
|
||||
#### Scenario: syncTargets 事务保持 prepare(例外)
|
||||
- **WHEN** 同步 targets 配置(事务内多次复用 insertStmt/updateStmt/deleteStmt)
|
||||
- **THEN** `syncTargets` 方法 SHALL 保持使用 `this.db.prepare()`,因需要在事务闭包内持有引用
|
||||
@@ -1,49 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义基于 Bun.serve `routes` 对象的全栈声明式路由注册、路径参数、HTTP method 声明和 fallback 行为。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 声明式路由注册
|
||||
系统 SHALL 使用 Bun.serve 的 `routes` 对象以声明式方式注册 API 端点路由,非 API 请求由 `fetch` fallback 处理。
|
||||
|
||||
#### Scenario: API 端点路由注册
|
||||
- **WHEN** server 启动时
|
||||
- **THEN** 系统 SHALL 将所有 API 端点以 method handler 对象形式注册到 `routes` 对象
|
||||
|
||||
#### Scenario: 非 API 请求处理
|
||||
- **WHEN** 请求路径不匹配任何 `routes` 中注册的路由
|
||||
- **THEN** `fetch` fallback SHALL 将请求交给静态资源服务处理(production)或返回提示文本(development)
|
||||
|
||||
### Requirement: 路径参数支持
|
||||
系统 SHALL 使用 routes 对象的 `:param` 语法声明路径参数,替代手动 regex 匹配。
|
||||
|
||||
#### Scenario: 带路径参数的 API 路由
|
||||
- **WHEN** 客户端请求 `/api/targets/123/history`
|
||||
- **THEN** 系统 SHALL 通过 `routes` 中注册的 `/api/targets/:id/history` 匹配,并通过 `req.params.id` 获取参数值 `"123"`
|
||||
|
||||
#### Scenario: 路径参数类型
|
||||
- **WHEN** route handler 接收到路径参数
|
||||
- **THEN** 参数值 SHALL 为字符串类型,handler 负责进行类型转换和校验
|
||||
|
||||
### Requirement: HTTP Method 声明
|
||||
系统 SHALL 在 routes 对象中为每个 API 端点以 per-method handler 形式声明支持的 HTTP method;未匹配 method 的 API 请求 SHALL 落入 `/api/*` 通配符并返回 JSON 404。
|
||||
|
||||
#### Scenario: 单 method 端点
|
||||
- **WHEN** API 端点只支持 GET 方法
|
||||
- **THEN** 该端点 SHALL 以 `{ GET(req) { ... } }` 形式注册
|
||||
|
||||
#### Scenario: 不支持的 method 请求
|
||||
- **WHEN** 客户端使用未声明的 method 请求 API 端点
|
||||
- **THEN** `/api/*` 通配符 SHALL 返回 JSON 格式的 404 错误响应
|
||||
|
||||
### Requirement: Fetch Fallback 处理
|
||||
系统 SHALL 使用 `fetch` handler 作为非 API 请求的入口,负责静态资源服务和 SPA fallback。
|
||||
|
||||
#### Scenario: Production 模式 fetch fallback
|
||||
- **WHEN** production 模式下请求未匹配 routes 中的 API 路由
|
||||
- **THEN** `fetch` handler SHALL 调用 `serveStaticAsset` 返回对应静态资源或 SPA fallback
|
||||
|
||||
#### Scenario: Development 模式 fetch fallback
|
||||
- **WHEN** development 模式下请求未匹配 routes 中的 API 路由
|
||||
- **THEN** `fetch` handler SHALL 返回提示文本,引导开发者通过 Vite dev server 访问前端
|
||||
@@ -1,107 +1,25 @@
|
||||
## Purpose
|
||||
|
||||
定义 checker 模块的内聚化组织结构,确保每个 checker 以独立目录形式存在,包含其全部类型定义、schema 声明、语义校验、执行逻辑和断言逻辑。同时定义共享的 expect/ 和 schema/ 基础设施,以及严格的依赖方向约束。
|
||||
定义 checker 模块的内聚化组织结构,确保每个 checker 以独立目录形式存在,包含其全部类型定义、schema 声明、语义校验、执行逻辑和断言逻辑。同时定义严格的依赖方向约束、Checker 接口定义、CheckerRegistry 注册中心、配置契约片段、配置校验 issue、引擎调度和服务注册委托。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Checker 目录内聚结构
|
||||
每个 checker SHALL 以独立目录形式存在于 `src/server/checker/runner/<type>/`,目录内 SHALL 包含该 checker 的全部类型定义、schema 声明、语义校验、执行逻辑和断言逻辑。
|
||||
每个 checker SHALL 以独立目录形式存在于 checker runner 目录下,目录内 SHALL 包含该 checker 的全部类型定义、schema 声明、语义校验、执行逻辑和断言逻辑。
|
||||
|
||||
#### Scenario: HTTP checker 目录完整性
|
||||
- **WHEN** 开发者查看 `src/server/checker/runner/http/` 目录
|
||||
- **THEN** 该目录 SHALL 包含 `index.ts`、`types.ts`、`schema.ts`、`execute.ts`、`expect.ts`、`body.ts`、`validate.ts`
|
||||
|
||||
#### Scenario: Cmd checker 目录完整性
|
||||
- **WHEN** 开发者查看 `src/server/checker/runner/cmd/` 目录
|
||||
- **THEN** 该目录 SHALL 包含 `index.ts`、`types.ts`、`schema.ts`、`execute.ts`、`expect.ts`、`text.ts`、`validate.ts`
|
||||
#### Scenario: checker 目录完整性
|
||||
- **WHEN** 开发者查看某个 checker 目录
|
||||
- **THEN** 该目录 SHALL 包含该 checker 的全部类型定义、schema 声明、语义校验、执行逻辑和断言逻辑
|
||||
|
||||
#### Scenario: 新增 checker 最小改动
|
||||
- **WHEN** 开发者新增一个 checker 类型(如 dns)
|
||||
- **THEN** 开发者 SHALL 只需创建 `src/server/checker/runner/dns/` 目录及其内部文件,并在 `runner/index.ts` 注册列表中添加一行 import 和一行数组项
|
||||
- **THEN** 开发者 SHALL 只需创建 checker 目录及其内部文件,并在注册列表中添加一行 import 和一行数组项
|
||||
|
||||
### Requirement: Checker 目录文件职责
|
||||
每个 checker 目录内的文件 SHALL 遵循统一的职责划分。
|
||||
### Requirement: 断言基础设施
|
||||
系统 SHALL 提供所有 checker 共享的断言基础设施,使用 Raw/Resolved expectation 术语和 value/content/keyed/status/headers 模块边界。
|
||||
|
||||
#### Scenario: index.ts 仅做 re-export
|
||||
- **WHEN** 开发者查看某 checker 的 `index.ts`
|
||||
- **THEN** 该文件 SHALL 仅包含对 `execute.ts` 中 Checker 类的 re-export,不包含任何逻辑
|
||||
|
||||
#### Scenario: types.ts 包含该 checker 全部专属类型
|
||||
- **WHEN** 开发者需要该 checker 的配置类型、resolved 类型或 expect 类型
|
||||
- **THEN** 这些类型 SHALL 全部定义在该 checker 目录的 `types.ts` 中,不在顶层 `types.ts` 中
|
||||
|
||||
#### Scenario: schema.ts 包含 TypeBox schema 定义
|
||||
- **WHEN** 开发者需要该 checker 的 config 或 expect schema
|
||||
- **THEN** 这些 schema SHALL 定义在该 checker 目录的 `schema.ts` 中
|
||||
|
||||
#### Scenario: execute.ts 包含 Checker 类实现
|
||||
- **WHEN** 开发者需要查看该 checker 的执行逻辑
|
||||
- **THEN** Checker 类(实现 CheckerDefinition 接口)SHALL 定义在 `execute.ts` 中
|
||||
|
||||
#### Scenario: validate.ts 包含该 checker 全部语义校验
|
||||
- **WHEN** 开发者需要查看该 checker 的配置校验逻辑
|
||||
- **THEN** 该 checker 专属的语义校验函数 SHALL 全部定义在 `validate.ts` 中
|
||||
|
||||
#### Scenario: expect.ts 包含该 checker 专属断言
|
||||
- **WHEN** 开发者需要查看该 checker 的断言逻辑
|
||||
- **THEN** 该 checker 专属的断言函数 SHALL 定义在 `expect.ts` 中
|
||||
|
||||
### Requirement: 断言基础设施目录
|
||||
系统 SHALL 在 `src/server/checker/expect/` 目录中提供所有 checker 共享的断言基础设施。共享 expect 目录 SHALL 使用 Raw/Resolved expectation 术语和 value/content/keyed/status/headers 模块边界。
|
||||
|
||||
#### Scenario: expect 共享类型位置
|
||||
- **WHEN** 任何 checker 需要使用断言相关的共享类型(如 `ExpectationResult`、`ValueExpectation`、`ContentExpectations` 或 `KeyedExpectations`)
|
||||
- **THEN** 这些类型 SHALL 从 `src/server/checker/expect/types.ts` 导入
|
||||
|
||||
#### Scenario: value 断言引擎位置
|
||||
- **WHEN** 任何 checker 需要使用 `applyValueMatcher`、`evaluateJsonPath`、`resolveValueExpectation` 或 `checkValueExpectation`
|
||||
- **THEN** 这些函数 SHALL 从 `src/server/checker/expect/value.ts` 导入
|
||||
|
||||
#### Scenario: content 和 keyed 断言位置
|
||||
- **WHEN** 任何 checker 需要执行内容数组或键值表 expectation
|
||||
- **THEN** SHALL 分别从 `src/server/checker/expect/content.ts` 和 `src/server/checker/expect/keyed.ts` 导入共享函数
|
||||
|
||||
#### Scenario: failure 构造器位置
|
||||
- **WHEN** 任何 checker 需要使用 `errorFailure` 或 `mismatchFailure`
|
||||
- **THEN** 这些函数 SHALL 从 `src/server/checker/expect/failure.ts` 导入
|
||||
|
||||
#### Scenario: expectation 校验位置
|
||||
- **WHEN** 任何 checker 的 validate 需要校验 Raw value、Raw content 或 Raw keyed expectation
|
||||
- **THEN** 对应函数 SHALL 从 `src/server/checker/expect/validate.ts` 导入
|
||||
|
||||
#### Scenario: ExpectationResult 类型位置
|
||||
- **WHEN** 任何 checker 需要使用 `ExpectationResult` 类型
|
||||
- **THEN** 该类型 SHALL 从 `src/server/checker/expect/types.ts` 导入
|
||||
|
||||
### Requirement: Schema 目录结构
|
||||
系统 SHALL 在 `src/server/checker/schema/` 目录中组织配置 schema 体系,替代原 `config-contract/` 目录。
|
||||
|
||||
#### Scenario: schema 目录包含 builder
|
||||
- **WHEN** 系统需要从 registry 动态构建整体配置 schema
|
||||
- **THEN** 该逻辑 SHALL 位于 `src/server/checker/schema/builder.ts`
|
||||
|
||||
#### Scenario: schema 目录包含 fragments
|
||||
- **WHEN** checker 的 schema.ts 需要引用共享 schema 片段(如 durationSchema、sizeSchema)
|
||||
- **THEN** 这些片段 SHALL 从 `src/server/checker/schema/fragments.ts` 导入
|
||||
|
||||
#### Scenario: schema 目录包含 Ajv 校验入口
|
||||
- **WHEN** config-loader 需要执行契约校验
|
||||
- **THEN** 校验入口 SHALL 位于 `src/server/checker/schema/validate.ts`
|
||||
|
||||
#### Scenario: schema 目录包含 issue 工具
|
||||
- **WHEN** 任何校验逻辑需要构造 ConfigValidationIssue
|
||||
- **THEN** issue 类型和工具函数 SHALL 从 `src/server/checker/schema/issues.ts` 导入
|
||||
|
||||
### Requirement: 工具函数归集
|
||||
系统 SHALL 在 `src/server/checker/utils.ts` 中提供纯工具函数。
|
||||
|
||||
#### Scenario: parseSize 位置
|
||||
- **WHEN** 任何模块需要解析 size 字符串(如 "100MB")
|
||||
- **THEN** `parseSize` SHALL 从 `src/server/checker/utils.ts` 导入
|
||||
|
||||
#### Scenario: parseDuration 位置
|
||||
- **WHEN** 任何模块需要解析 duration 字符串(如 "30s")
|
||||
- **THEN** `parseDuration` SHALL 从 `src/server/checker/utils.ts` 导入
|
||||
### Requirement: Schema 体系
|
||||
系统 SHALL 通过 schema 体系组织配置校验、契约片段和 issue 报告,支持从 registry 动态构建整体配置 schema、共享 schema 片段引用、Ajv 校验入口和 ConfigValidationIssue 构造。
|
||||
|
||||
### Requirement: 依赖方向约束
|
||||
checker 系统内的模块依赖 SHALL 遵循严格的分层方向。
|
||||
@@ -119,35 +37,248 @@ checker 系统内的模块依赖 SHALL 遵循严格的分层方向。
|
||||
- **THEN** `schema/` 中的文件 SHALL 仅通过 `CheckerDefinition` 接口与 checker 交互,SHALL NOT 直接导入具体 checker 目录
|
||||
|
||||
### Requirement: 显式注册列表
|
||||
系统 SHALL 在 `src/server/checker/runner/index.ts` 中使用显式 import 列表注册所有 checker。
|
||||
系统 SHALL 在 checker 注册入口文件中使用显式 import 列表注册所有 checker。
|
||||
|
||||
#### Scenario: 注册入口结构
|
||||
- **WHEN** 开发者查看 `runner/index.ts`
|
||||
- **WHEN** 开发者查看 checker 注册入口文件
|
||||
- **THEN** 该文件 SHALL 包含所有 checker 的静态 import 和一个 checker 实例数组,通过循环调用 `registry.register()` 完成注册
|
||||
|
||||
#### Scenario: 新增 checker 注册
|
||||
- **WHEN** 开发者新增一个 checker
|
||||
- **THEN** 开发者 SHALL 在 `runner/index.ts` 中添加一行 import 和一行数组项,无需修改其他文件
|
||||
- **THEN** 开发者 SHALL 在注册入口文件中添加一行 import 和一行数组项,无需修改其他文件
|
||||
|
||||
### Requirement: 公共类型文件瘦身
|
||||
顶层 `src/server/checker/types.ts` SHALL 仅保留跨 checker 共享的 base 类型和存储相关类型;expect 专属类型 SHALL 放在 `src/server/checker/expect/types.ts`。
|
||||
### Requirement: Checker 配置契约片段
|
||||
系统 SHALL 支持 checker 提供自身 TypeBox 配置契约片段,用于描述该 checker 的 target 领域分组和 expect 分组。checker 契约 SHALL 区分 Authoring schema 与 Normalized schema。Authoring schema SHALL 描述用户 YAML 可书写形式,包括变量引用和 expect 简写;Normalized schema SHALL 描述 `normalizeAuthoringConfig()` 输出形式,不接受变量引用、不接受 expect primitive 简写。公共配置加载模块 SHALL 通过 registry 获取已注册 checker 的契约片段,并按用途组合为运行时 Ajv 契约校验流程和外部 `probe-config.schema.json` 导出流程。
|
||||
|
||||
#### Scenario: types.ts 不包含 checker 或 expect 专属类型
|
||||
- **WHEN** 开发者查看顶层 `types.ts`
|
||||
- **THEN** 该文件 SHALL NOT 包含 `HttpTargetConfig`、`ResolvedHttpTarget`、`RawCommandExpectConfig`、`ContentExpectation` 等 checker 专属或 expect 专属类型
|
||||
#### Scenario: HTTP checker 提供契约片段
|
||||
- **WHEN** HTTP checker 被注册
|
||||
- **THEN** registry SHALL 能提供 HTTP target 和 HTTP expect 的 TypeBox 契约片段
|
||||
|
||||
#### Scenario: types.ts 保留 base 类型
|
||||
- **WHEN** 开发者查看顶层 `types.ts`
|
||||
- **THEN** 该文件 SHALL 包含 `ResolvedTargetBase`、`RawTargetConfig`、`DefaultsConfig`、`CheckResult`、`CheckFailure`、`StoredTarget`、`StoredCheckResult`、`JsonValue` 等公共类型
|
||||
#### Scenario: Cmd checker 提供契约片段
|
||||
- **WHEN** Cmd checker 被注册
|
||||
- **THEN** registry SHALL 能提供 Cmd target 和 Cmd expect 的 TypeBox 契约片段
|
||||
|
||||
#### Scenario: ResolvedTargetBase 替代联合类型
|
||||
- **WHEN** engine、store、config-loader 需要引用 resolved target 类型
|
||||
- **THEN** 这些模块 SHALL 使用 `ResolvedTargetBase` interface,不再使用硬编码联合类型
|
||||
#### Scenario: 新 checker 只维护自身契约
|
||||
- **WHEN** 开发者新增一个 checker 类型
|
||||
- **THEN** 该 checker SHALL 提供自身 TypeBox 配置契约和语义 validator,而不需要把 checker 专属字段写入中央手工校验逻辑
|
||||
|
||||
#### Scenario: DefaultsConfig 为宽松 base 形式
|
||||
- **WHEN** 开发者查看顶层 `types.ts` 中的 `DefaultsConfig`
|
||||
- **THEN** 该 interface SHALL 仅包含公共字段(`interval?`、`timeout?`)和 index signature(`[checkerKey: string]: unknown`),SHALL NOT 包含 `cmd?`、`http?` 等 checker 专属字段
|
||||
#### Scenario: 外部 schema 通过 registry 生成
|
||||
- **WHEN** 系统生成 `probe-config.schema.json`
|
||||
- **THEN** 生成流程 SHALL 从 registry 获取已注册 checker 的 Authoring 契约片段,并将其组合进完整配置 schema
|
||||
|
||||
#### Scenario: 各 checker validate 自行 narrow defaults
|
||||
- **WHEN** checker 的 `validate()` 方法需要访问自身的 defaults 配置
|
||||
- **THEN** checker SHALL 从 `DefaultsConfig` 中通过 `defaults[configKey]` 获取并自行 narrow 为具体类型
|
||||
#### Scenario: 运行时 schema 通过 registry 生成
|
||||
- **WHEN** config-loader 执行运行时 AJV 契约校验
|
||||
- **THEN** 校验流程 SHALL 从 registry 获取已注册 checker 的 Normalized 契约片段,并将其组合进完整配置 schema
|
||||
|
||||
#### Scenario: 契约组装不依赖全局 singleton
|
||||
- **WHEN** 测试或 schema 生成流程需要组装配置契约
|
||||
- **THEN** 系统 SHALL 支持传入 fresh CheckerRegistry 实例完成契约组装,避免重复注册或全局状态污染
|
||||
|
||||
### Requirement: Checker 启动期语义校验
|
||||
系统 SHALL 支持 checker 提供启动期语义 validator,用于校验 TypeBox/Ajv 契约不适合表达或需要 checker 业务知识判断的配置规则。语义 validator MUST 在 resolver 填充最终 ResolvedTarget 之前执行,并 MUST 返回 `ConfigValidationIssue[]`。
|
||||
|
||||
#### Scenario: checker 语义校验先于 resolve
|
||||
- **WHEN** config-loader 准备解析一个 target
|
||||
- **THEN** 系统 SHALL 先完成该 target 的 checker 语义校验,再调用 checker.resolve()
|
||||
|
||||
#### Scenario: 语义校验失败阻止启动
|
||||
- **WHEN** checker 语义 validator 发现非法配置
|
||||
- **THEN** 系统 SHALL 以配置错误退出,不进入 checker 执行阶段
|
||||
|
||||
### Requirement: 结构化配置校验 issue
|
||||
系统 SHALL 使用统一 `ConfigValidationIssue` 表示配置校验问题,至少包含 `code`、`path`、`message`,并支持可选 `targetName`。契约校验和 checker 语义校验都 SHALL 产出该结构,由配置加载模块统一渲染为中文错误。
|
||||
|
||||
#### Scenario: Ajv 错误转换为 issue
|
||||
- **WHEN** Ajv 校验发现 required、type 或 additionalProperties 错误
|
||||
- **THEN** 系统 SHALL 将该错误转换为 `ConfigValidationIssue`,保留配置路径和可读 message
|
||||
|
||||
#### Scenario: checker validator 返回 issue
|
||||
- **WHEN** checker 语义 validator 发现非法 XPath 或正则表达式
|
||||
- **THEN** checker SHALL 返回 `ConfigValidationIssue`,而不是直接抛出最终用户错误字符串
|
||||
|
||||
### Requirement: Checker 接口定义
|
||||
系统 SHALL 定义面向扩展的泛型 `CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase>`,包含 `type`、`configKey`、Authoring/Normalized TypeBox 配置契约、启动期语义校验、`resolve`、`execute`、`serialize`、`buildDetail` 成员。泛型参数 SHALL 约束 `execute` 和 `serialize` 方法的 target 参数类型,使 checker 实现内部获得编译期类型安全。默认泛型参数 `= ResolvedTargetBase` 保证中间层(registry、engine、config-loader)无需指定泛型。
|
||||
|
||||
#### Scenario: Checker 接口包含必要方法
|
||||
- **WHEN** 开发者实现一个新的 Checker
|
||||
- **THEN** 该实现 MUST 提供 `type`(字符串标识)、`configKey`(配置分组名)、Authoring/Normalized TypeBox 配置契约、启动期语义校验、`resolve(target, context)`(解析配置并填充默认值)、`execute(target, ctx)`(执行探测返回 CheckResult)、`serialize(target)`(返回 target 展示文本和 config JSON)和 `buildDetail(observation)`(从 observation 构造人可读摘要)
|
||||
|
||||
#### Scenario: CheckerContext 注入 signal
|
||||
- **WHEN** 引擎调用 `checker.execute(target, ctx)`
|
||||
- **THEN** `ctx.signal` SHALL 是一个由引擎创建的 `AbortSignal`,在超时或引擎关闭时 abort
|
||||
|
||||
#### Scenario: resolve 不承担通用契约校验
|
||||
- **WHEN** config-loader 调用 checker.resolve()
|
||||
- **THEN** checker.resolve() SHALL 假定配置已经通过 Normalized TypeBox/Ajv 契约校验和启动期语义校验,只负责默认值填充、路径解析和领域配置转换
|
||||
|
||||
#### Scenario: resolve 接收 Normalized target
|
||||
- **WHEN** config-loader 调用 checker.resolve()
|
||||
- **THEN** 传入的 target SHALL 已通过 Normalized schema 和语义校验,且不包含变量引用、Authoring expect primitive 简写或 Raw content/keyed DSL
|
||||
|
||||
#### Scenario: type 与 configKey 默认一致
|
||||
- **WHEN** checker 定义 `type: "tcp"`
|
||||
- **THEN** checker 的 `configKey` SHALL 默认使用 `"tcp"`,对应 target 的 `tcp` 分组
|
||||
|
||||
#### Scenario: 接口方法使用泛型约束
|
||||
- **WHEN** 开发者查看 `CheckerDefinition<TResolved>` 接口签名
|
||||
- **THEN** `resolve` 的返回值 SHALL 为 `TResolved`;`execute` 的参数 SHALL 为 `TResolved`;`serialize` 的参数 SHALL 为 `TResolved`
|
||||
|
||||
#### Scenario: checker 实现无需手动断言
|
||||
- **WHEN** HttpChecker 实现 `CheckerDefinition<ResolvedHttpTarget>`
|
||||
- **THEN** `execute` 方法的 target 参数类型 SHALL 直接为 `ResolvedHttpTarget`,无需在方法内部使用 `as` 类型断言
|
||||
|
||||
#### Scenario: registry 使用默认泛型参数
|
||||
- **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 提供 `CheckerRegistry` 类,支持 `register(checker)`、`get(type)` 和 `supportedTypes`。重复注册同一 type SHALL 抛出错误。registry 内部 SHALL 存储 `CheckerDefinition`(使用默认泛型参数),对外提供类型擦除后的接口。
|
||||
|
||||
#### Scenario: 注册并获取 Checker
|
||||
- **WHEN** 调用 `registry.register(new HttpChecker())` 后再调用 `registry.get("http")`
|
||||
- **THEN** 返回的 SHALL 是之前注册的 HttpChecker 实例(类型为 `CheckerDefinition`)
|
||||
|
||||
#### Scenario: 获取未注册的 type
|
||||
- **WHEN** 调用 `registry.get("unknown")` 且未注册对应 type 的 checker
|
||||
- **THEN** 系统 SHALL 抛出错误,提示不支持的 probe type
|
||||
|
||||
#### Scenario: 重复注册
|
||||
- **WHEN** 同一 type 值被重复 `register()`
|
||||
- **THEN** 系统 SHALL 抛出错误,提示该 type 已注册
|
||||
|
||||
#### Scenario: 查询支持的 type 列表
|
||||
- **WHEN** 注册了 "http" 和 "cmd" 两个 checker 后查询 `registry.supportedTypes`
|
||||
- **THEN** 返回的数组 SHALL 包含 `["http", "cmd"]`(按注册顺序)
|
||||
|
||||
### Requirement: 引擎通过 registry 调度 checker
|
||||
系统 SHALL 在引擎执行检查时通过 `checkerRegistry.get(target.type).execute(target, ctx)` 调度检查,替代原有的 `switch/case` 分支。
|
||||
|
||||
#### Scenario: 引擎使用 registry 调度
|
||||
- **WHEN** engine 需要执行一个 type 为 "http" 的 target
|
||||
- **THEN** engine SHALL 从 `checkerRegistry` 中获取对应 checker 并调用其 `execute()` 方法,不再使用 `switch/case`
|
||||
|
||||
#### Scenario: 引擎注入超时 signal
|
||||
- **WHEN** engine 调度一次 checker 执行
|
||||
- **THEN** engine SHALL 创建 `AbortController`,设置超时定时器,将 `controller.signal` 注入 `CheckerContext`,执行完成后清理定时器
|
||||
|
||||
### Requirement: 配置解析通过 registry 委托 checker
|
||||
系统 SHALL 在配置加载流程中通过 `checkerRegistry` 发现已注册 checker,组合公共 TypeBox 契约与 checker 契约,并将 checker 专属语义校验和解析委托给对应 checker。公共配置校验 SHALL 仅保留公共语义校验(name 非空、name 不重复、group 类型、type 已注册等)和契约调度职责,不包含 checker 专属字段校验。
|
||||
|
||||
#### Scenario: 配置契约通过 registry 组合
|
||||
- **WHEN** config-loader 校验配置文件
|
||||
- **THEN** config-loader SHALL 从 `checkerRegistry` 获取已注册 checker 的契约片段,并用于校验 defaults 与 targets 中对应 checker 的配置形状
|
||||
|
||||
#### Scenario: Authoring 契约通过 registry 组合
|
||||
- **WHEN** 系统导出用户配置 JSON Schema
|
||||
- **THEN** 配置 builder SHALL 从 `checkerRegistry` 获取已注册 checker 的 Authoring 契约片段
|
||||
|
||||
#### Scenario: Normalized 契约通过 registry 组合
|
||||
- **WHEN** config-loader 校验 normalized 配置对象
|
||||
- **THEN** config-loader SHALL 从 `checkerRegistry` 获取已注册 checker 的 Normalized 契约片段
|
||||
|
||||
#### Scenario: 配置解析委托 checker
|
||||
- **WHEN** config-loader 解析一个 type 为 "cmd" 的 target
|
||||
- **THEN** config-loader SHALL 调用 `checkerRegistry.get("cmd")` 获取对应 checker,并委托该 checker 执行语义校验和 resolve
|
||||
|
||||
#### Scenario: 通用字段校验保留在 config-loader
|
||||
- **WHEN** YAML 配置中某个 target 缺少 name 或 type 字段
|
||||
- **THEN** config-loader 的公共校验流程 SHALL 仍负责校验这些通用字段
|
||||
|
||||
#### Scenario: type 专属校验下沉到 checker
|
||||
- **WHEN** YAML 配置中 HTTP target 缺少 `http.url`
|
||||
- **THEN** HTTP checker 的契约或语义校验 SHALL 抛出校验错误,提示缺少必填字段
|
||||
|
||||
#### Scenario: HTTP method 非法校验
|
||||
- **WHEN** YAML 配置中 HTTP target 的 `http.method` 不是大写合法方法枚举值
|
||||
- **THEN** HTTP checker 契约或语义校验 SHALL 抛出校验错误,提示 method 不合法
|
||||
|
||||
#### Scenario: URL 格式校验
|
||||
- **WHEN** YAML 配置中 HTTP target 的 `http.url` 不以 `http://` 或 `https://` 开头
|
||||
- **THEN** HttpChecker 的语义校验 SHALL 抛出校验错误,提示 URL 格式不合法
|
||||
|
||||
### Requirement: 存储序列化通过 registry 获取展示格式
|
||||
系统 SHALL 在存储同步 targets 时通过 `checkerRegistry.get(t.type).serialize(t)` 获取每个 target 的展示摘要和配置 JSON,替代函数中的类型分支。系统 SHALL 将 `targets.expect` 持久化为 null,不依赖 Raw expect 或 Resolved expect。
|
||||
|
||||
#### Scenario: 序列化委托 checker
|
||||
- **WHEN** store 同步 targets 表
|
||||
- **THEN** store SHALL 对每个 target 调用对应 checker 的 `serialize()` 方法获取 `{ target, config }`
|
||||
|
||||
#### Scenario: expect 持久化不依赖 rawExpect
|
||||
- **WHEN** store 同步带 expect 的 target 到 targets 表
|
||||
- **THEN** store SHALL 将 `targets.expect` 写入 NULL,MUST NOT 依赖 `rawExpect` 或 Raw expect 快照
|
||||
|
||||
### Requirement: Checker resolve 只接收已去糖配置
|
||||
每个 checker 的 `resolve()` SHALL 接收已通过 Normalized schema 和语义校验的配置,不再包含变量引用、Authoring expect primitive 简写或 Raw content/keyed DSL。`config-loader` SHALL 继续通过 registry 委托 checker resolve,MUST NOT 在中间层理解 checker 专属 expect 字段。
|
||||
|
||||
#### Scenario: resolve 不再展开 Raw expect
|
||||
- **WHEN** config-loader 解析一个带 `expect.durationMs: {equals: 1000}` 的 target
|
||||
- **THEN** 对应 checker 的 resolved target SHALL 直接使用 Normalized expect 中的 `{equals: 1000}`,resolve 只负责默认值和运行期配置转换
|
||||
|
||||
#### Scenario: 中间层不感知 checker expect 字段
|
||||
- **WHEN** 新增 checker 定义自己的 Raw/Resolved expect 字段
|
||||
- **THEN** config-loader SHALL 只调用该 checker 的 `validate()` 和 `resolve()`,不新增 checker 类型分支
|
||||
|
||||
### Requirement: 共享 expect 断言函数
|
||||
系统 SHALL 提供可被多个 checker 复用的 expect 基础设施。共享基础设施 SHALL 包含 value expectation、content expectations、keyed expectations、status code 断言、headers keyed 断言、failure 构造和 ReDoS 校验。checker 专用的状态类断言 SHALL 保留在对应 checker 目录,或在多个 checker 复用时移动到共享模块。仅被单个 checker 使用且不属于通用 value/content/keyed/status/header 模型的断言模块 SHALL 位于该 checker 目录内。
|
||||
|
||||
#### Scenario: 共享 ValueExpectation 断言
|
||||
- **WHEN** 任何 checker 需要对数字、字符串、布尔或 JSON value 执行 matcher 匹配
|
||||
- **THEN** SHALL 调用共享 value expectation 工具执行 `equals`、`contains`、`regex`、`exists`、`empty`、`gt`、`gte`、`lt` 和 `lte` 语义
|
||||
|
||||
#### Scenario: 共享 ContentExpectations 断言
|
||||
- **WHEN** HTTP body、LLM output、Cmd stdout/stderr、UDP response 或 TCP banner 需要执行返回内容校验
|
||||
- **THEN** SHALL 调用共享 content expectations 工具,而不是在 checker 目录内复制 contains/regex/json/css/xpath 逻辑
|
||||
|
||||
#### Scenario: 共享 KeyedExpectations 断言
|
||||
- **WHEN** HTTP 或 LLM checker 需要校验响应 headers,或 DB checker 需要校验 rows 中的列值
|
||||
- **THEN** SHALL 调用共享 keyed expectations 工具,并按调用方规则决定 key 是否大小写敏感
|
||||
|
||||
#### Scenario: 共享 headers 断言
|
||||
- **WHEN** HTTP 或 LLM checker 需要校验响应 headers
|
||||
- **THEN** SHALL 调用共享 header expectation 包装函数,确保 header key 大小写不敏感
|
||||
|
||||
#### Scenario: 共享 regex ReDoS 校验
|
||||
- **WHEN** 任一 matcher 或 content expectation 配置 `regex`
|
||||
- **THEN** SHALL 调用共享 ReDoS 校验工具在启动期拒绝危险正则
|
||||
|
||||
#### Scenario: 共享 failure 构造
|
||||
- **WHEN** 任何 checker 需要构造 CheckFailure 对象
|
||||
- **THEN** SHALL 调用共享的 `errorFailure()` 或 `mismatchFailure()` 构造 CheckFailure,并保留 actual 截断策略
|
||||
|
||||
#### Scenario: 共享 status 断言
|
||||
- **WHEN** HTTP 或 LLM checker 需要校验响应状态码
|
||||
- **THEN** SHALL 复用共享 status code 断言函数,支持精确状态码和 `1xx` 到 `5xx` 范围模式
|
||||
|
||||
### Requirement: 超时控制由引擎注入 signal
|
||||
Checker 实现的 `execute()` MUST 使用 `ctx.signal` 感知超时,不得自行创建 `AbortController` 或 `setTimeout` 用于超时控制。Cmd checker 和 ping checker 可在 signal abort 时 `proc.kill()` 以确保子进程被终止。
|
||||
|
||||
#### Scenario: HTTP checker 使用 signal
|
||||
- **WHEN** HttpChecker 执行 HTTP 请求
|
||||
- **THEN** SHALL 将 `ctx.signal` 传入 `fetch()` 的 `signal` 选项,不自行创建 `AbortController`
|
||||
|
||||
#### Scenario: Cmd checker 响应 signal
|
||||
- **WHEN** CommandChecker 执行命令且 signal 被 abort
|
||||
- **THEN** SHALL 调用 `proc.kill()` 终止子进程,并在 CheckResult 中记录超时错误
|
||||
|
||||
#### Scenario: Ping checker 响应 signal
|
||||
- **WHEN** IcmpChecker 执行 ping 命令且 signal 被 abort
|
||||
- **THEN** SHALL 调用 `proc.kill()` 终止 ping 子进程,并在 CheckResult 中记录超时错误
|
||||
|
||||
### Requirement: CheckFailure.phase 使用 string 类型
|
||||
`CheckFailure.phase` 的类型 SHALL 定义为 `string`,替代原有的硬编码联合类型 `"status" | "duration" | "headers" | "body" | "exitCode" | "stdout" | "stderr"`。
|
||||
|
||||
#### Scenario: phase 支持 checker 专用值
|
||||
- **WHEN** cmd checker 在执行失败(spawn error)时生成 failure
|
||||
- **THEN** `failure.phase` SHALL 可以是 `"spawn"` 等任意字符串值,类型系统 SHALL 不报错
|
||||
|
||||
#### Scenario: 前端展示 phase 不依赖硬编码类型
|
||||
- **WHEN** 前端收到任意 phase 字符串值
|
||||
- **THEN** 前端 SHALL 直接展示而不做类型判断
|
||||
|
||||
@@ -48,14 +48,14 @@ UDP checker 的 observation SHALL 包含 responded(boolean)、durationMs(n
|
||||
- **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`。
|
||||
### Requirement: ICMP Checker Observation
|
||||
ICMP checker 的 observation SHALL 包含 alive(boolean)、transmitted(number)、received(number)、packetLoss(number)、avgLatencyMs(number | null)、maxLatencyMs(number | null)、minLatencyMs(number | null)、error(string | null)。
|
||||
|
||||
#### Scenario: Ping 正常返回统计
|
||||
#### Scenario: ICMP 正常返回统计
|
||||
- **WHEN** ping 命令成功执行并解析出统计数据
|
||||
- **THEN** observation SHALL 包含完整的 PingStats 字段
|
||||
- **THEN** observation SHALL 包含完整的 ICMPStats 字段
|
||||
|
||||
#### Scenario: Ping 命令失败
|
||||
#### Scenario: ICMP 命令失败
|
||||
- **WHEN** ping 命令未找到或超时
|
||||
- **THEN** observation SHALL 为 null 或包含 error 字段
|
||||
|
||||
@@ -155,4 +155,4 @@ CheckResult SHALL 包含 `detail: string | null` 字段(替代原 statusDetail
|
||||
|
||||
#### 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 用量摘要
|
||||
- **THEN** HTTP SHALL 返回 `"HTTP {statusCode}"` 格式;TCP SHALL 返回连接状态和 banner 摘要;UDP SHALL 返回响应状态和大小摘要;Ping SHALL 返回存活状态、平均延迟和丢包率摘要;ICMP SHALL 返回存活状态、平均延迟和丢包率摘要;DB SHALL 返回连接状态或行数摘要;CMD SHALL 返回 `"exitCode={N}"` 格式;LLM SHALL 返回 provider、mode、状态码、finish 原因、输出长度和 token 用量摘要
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 Checker 接口规范、注册机制、CheckerContext 上下文注入,以及共享 expect 断言函数的职责边界。此 capability 是 checker 系统的架构基础,不定义任何具体 checker 类型的业务行为。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Checker 配置契约片段
|
||||
系统 SHALL 支持 checker 提供自身 TypeBox 配置契约片段,用于描述该 checker 的 target 领域分组和 expect 分组。checker 契约 SHALL 区分 Authoring schema 与 Normalized schema。Authoring schema SHALL 描述用户 YAML 可书写形式,包括变量引用和 expect 简写;Normalized schema SHALL 描述 `normalizeAuthoringConfig()` 输出形式,不接受变量引用、不接受 expect primitive 简写。公共配置加载模块 SHALL 通过 registry 获取已注册 checker 的契约片段,并按用途组合为运行时 Ajv 契约校验流程和外部 `probe-config.schema.json` 导出流程。
|
||||
|
||||
#### Scenario: HTTP checker 提供契约片段
|
||||
- **WHEN** HTTP checker 被注册
|
||||
- **THEN** registry SHALL 能提供 HTTP target 和 HTTP expect 的 TypeBox 契约片段
|
||||
|
||||
#### Scenario: Cmd checker 提供契约片段
|
||||
- **WHEN** Cmd checker 被注册
|
||||
- **THEN** registry SHALL 能提供 Cmd target 和 Cmd expect 的 TypeBox 契约片段
|
||||
|
||||
#### Scenario: 新 checker 只维护自身契约
|
||||
- **WHEN** 开发者新增一个 checker 类型
|
||||
- **THEN** 该 checker SHALL 提供自身 TypeBox 配置契约和语义 validator,而不需要把 checker 专属字段写入中央手工校验逻辑
|
||||
|
||||
#### Scenario: 外部 schema 通过 registry 生成
|
||||
- **WHEN** 系统生成 `probe-config.schema.json`
|
||||
- **THEN** 生成流程 SHALL 从 registry 获取已注册 checker 的 Authoring 契约片段,并将其组合进完整配置 schema
|
||||
|
||||
#### Scenario: 运行时 schema 通过 registry 生成
|
||||
- **WHEN** config-loader 执行运行时 AJV 契约校验
|
||||
- **THEN** 校验流程 SHALL 从 registry 获取已注册 checker 的 Normalized 契约片段,并将其组合进完整配置 schema
|
||||
|
||||
#### Scenario: 契约组装不依赖全局 singleton
|
||||
- **WHEN** 测试或 schema 生成流程需要组装配置契约
|
||||
- **THEN** 系统 SHALL 支持传入 fresh CheckerRegistry 实例完成契约组装,避免重复注册或全局状态污染
|
||||
|
||||
### Requirement: Checker 启动期语义校验
|
||||
系统 SHALL 支持 checker 提供启动期语义 validator,用于校验 TypeBox/Ajv 契约不适合表达或需要 checker 业务知识判断的配置规则。语义 validator MUST 在 resolver 填充最终 ResolvedTarget 之前执行,并 MUST 返回 `ConfigValidationIssue[]`。
|
||||
|
||||
#### Scenario: checker 语义校验先于 resolve
|
||||
- **WHEN** config-loader 准备解析一个 target
|
||||
- **THEN** 系统 SHALL 先完成该 target 的 checker 语义校验,再调用 checker.resolve()
|
||||
|
||||
#### Scenario: 语义校验失败阻止启动
|
||||
- **WHEN** checker 语义 validator 发现非法配置
|
||||
- **THEN** 系统 SHALL 以配置错误退出,不进入 checker 执行阶段
|
||||
|
||||
### Requirement: 结构化配置校验 issue
|
||||
系统 SHALL 使用统一 `ConfigValidationIssue` 表示配置校验问题,至少包含 `code`、`path`、`message`,并支持可选 `targetName`。契约校验和 checker 语义校验都 SHALL 产出该结构,由配置加载模块统一渲染为中文错误。
|
||||
|
||||
#### Scenario: Ajv 错误转换为 issue
|
||||
- **WHEN** Ajv 校验发现 required、type 或 additionalProperties 错误
|
||||
- **THEN** 系统 SHALL 将该错误转换为 `ConfigValidationIssue`,保留配置路径和可读 message
|
||||
|
||||
#### Scenario: checker validator 返回 issue
|
||||
- **WHEN** checker 语义 validator 发现非法 XPath 或正则表达式
|
||||
- **THEN** checker SHALL 返回 `ConfigValidationIssue`,而不是直接抛出最终用户错误字符串
|
||||
|
||||
### Requirement: Checker 接口定义
|
||||
系统 SHALL 在 `src/server/checker/runner/types.ts` 中定义面向扩展的泛型 `CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase>`,包含 `type`、`configKey`、Authoring/Normalized TypeBox 配置契约、启动期语义校验、`resolve`、`execute`、`serialize`、`buildDetail` 成员。泛型参数 SHALL 约束 `execute` 和 `serialize` 方法的 target 参数类型,使 checker 实现内部获得编译期类型安全。默认泛型参数 `= ResolvedTargetBase` 保证中间层(registry、engine、config-loader)无需指定泛型。
|
||||
|
||||
#### Scenario: Checker 接口包含必要方法
|
||||
- **WHEN** 开发者实现一个新的 Checker
|
||||
- **THEN** 该实现 MUST 提供 `type`(字符串标识)、`configKey`(配置分组名)、Authoring/Normalized TypeBox 配置契约、启动期语义校验、`resolve(target, context)`(解析配置并填充默认值)、`execute(target, ctx)`(执行探测返回 CheckResult)、`serialize(target)`(返回 target 展示文本和 config JSON)和 `buildDetail(observation)`(从 observation 构造人可读摘要)
|
||||
|
||||
#### Scenario: CheckerContext 注入 signal
|
||||
- **WHEN** 引擎调用 `checker.execute(target, ctx)`
|
||||
- **THEN** `ctx.signal` SHALL 是一个由引擎创建的 `AbortSignal`,在超时或引擎关闭时 abort
|
||||
|
||||
#### Scenario: resolve 不承担通用契约校验
|
||||
- **WHEN** config-loader 调用 checker.resolve()
|
||||
- **THEN** checker.resolve() SHALL 假定配置已经通过 Normalized TypeBox/Ajv 契约校验和启动期语义校验,只负责默认值填充、路径解析和领域配置转换
|
||||
|
||||
#### Scenario: resolve 接收 Normalized target
|
||||
- **WHEN** config-loader 调用 checker.resolve()
|
||||
- **THEN** 传入的 target SHALL 已通过 Normalized schema 和语义校验,且不包含变量引用、Authoring expect primitive 简写或 Raw content/keyed DSL
|
||||
|
||||
#### Scenario: type 与 configKey 默认一致
|
||||
- **WHEN** checker 定义 `type: "tcp"`
|
||||
- **THEN** checker 的 `configKey` SHALL 默认使用 `"tcp"`,对应 target 的 `tcp` 分组和 defaults.tcp 分组
|
||||
|
||||
#### Scenario: 接口方法使用泛型约束
|
||||
- **WHEN** 开发者查看 `CheckerDefinition<TResolved>` 接口签名
|
||||
- **THEN** `resolve` 的返回值 SHALL 为 `TResolved`;`execute` 的参数 SHALL 为 `TResolved`;`serialize` 的参数 SHALL 为 `TResolved`
|
||||
|
||||
#### Scenario: checker 实现无需手动断言
|
||||
- **WHEN** HttpChecker 实现 `CheckerDefinition<ResolvedHttpTarget>`
|
||||
- **THEN** `execute` 方法的 target 参数类型 SHALL 直接为 `ResolvedHttpTarget`,无需在方法内部使用 `as` 类型断言
|
||||
|
||||
#### Scenario: registry 使用默认泛型参数
|
||||
- **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`(使用默认泛型参数),对外提供类型擦除后的接口。
|
||||
|
||||
#### Scenario: 注册并获取 Checker
|
||||
- **WHEN** 调用 `registry.register(new HttpChecker())` 后再调用 `registry.get("http")`
|
||||
- **THEN** 返回的 SHALL 是之前注册的 HttpChecker 实例(类型为 `CheckerDefinition`)
|
||||
|
||||
#### Scenario: 获取未注册的 type
|
||||
- **WHEN** 调用 `registry.get("unknown")` 且未注册对应 type 的 checker
|
||||
- **THEN** 系统 SHALL 抛出错误,提示不支持的 probe type
|
||||
|
||||
#### Scenario: 重复注册
|
||||
- **WHEN** 同一 type 值被重复 `register()`
|
||||
- **THEN** 系统 SHALL 抛出错误,提示该 type 已注册
|
||||
|
||||
#### Scenario: 查询支持的 type 列表
|
||||
- **WHEN** 注册了 "http" 和 "cmd" 两个 checker 后查询 `registry.supportedTypes`
|
||||
- **THEN** 返回的数组 SHALL 包含 `["http", "cmd"]`(按注册顺序)
|
||||
|
||||
### Requirement: 引擎通过 registry 调度 checker
|
||||
系统 SHALL 在 `ProbeEngine.runCheck()` 中通过 `checkerRegistry.get(target.type).execute(target, ctx)` 调度检查,替代原有的 `switch/case` 分支。
|
||||
|
||||
#### Scenario: 引擎使用 registry 调度
|
||||
- **WHEN** engine 需要执行一个 type 为 "http" 的 target
|
||||
- **THEN** engine SHALL 从 `checkerRegistry` 中获取对应 checker 并调用其 `execute()` 方法,不再使用 `switch/case`
|
||||
|
||||
#### Scenario: 引擎注入超时 signal
|
||||
- **WHEN** engine 调度一次 checker 执行
|
||||
- **THEN** engine SHALL 创建 `AbortController`,设置超时定时器,将 `controller.signal` 注入 `CheckerContext`,执行完成后清理定时器
|
||||
|
||||
### Requirement: 配置解析通过 registry 委托 checker
|
||||
系统 SHALL 在 `config-loader.ts` 的配置加载流程中通过 `checkerRegistry` 发现已注册 checker,组合公共 TypeBox 契约与 checker 契约,并将 checker 专属语义校验和解析委托给对应 checker。`validateConfig()` SHALL 仅保留公共语义校验(name 非空、name 不重复、group 类型、type 已注册等)和契约调度职责,不包含 checker 专属字段校验。
|
||||
|
||||
#### Scenario: 配置契约通过 registry 组合
|
||||
- **WHEN** config-loader 校验配置文件
|
||||
- **THEN** config-loader SHALL 从 `checkerRegistry` 获取已注册 checker 的契约片段,并用于校验 defaults 与 targets 中对应 checker 的配置形状
|
||||
|
||||
#### Scenario: Authoring 契约通过 registry 组合
|
||||
- **WHEN** 系统导出用户配置 JSON Schema
|
||||
- **THEN** 配置 builder SHALL 从 `checkerRegistry` 获取已注册 checker 的 Authoring 契约片段
|
||||
|
||||
#### Scenario: Normalized 契约通过 registry 组合
|
||||
- **WHEN** config-loader 校验 normalized 配置对象
|
||||
- **THEN** config-loader SHALL 从 `checkerRegistry` 获取已注册 checker 的 Normalized 契约片段
|
||||
|
||||
#### Scenario: 配置解析委托 checker
|
||||
- **WHEN** config-loader 解析一个 type 为 "cmd" 的 target
|
||||
- **THEN** config-loader SHALL 调用 `checkerRegistry.get("cmd")` 获取对应 checker,并委托该 checker 执行语义校验和 resolve
|
||||
|
||||
#### Scenario: 通用字段校验保留在 config-loader
|
||||
- **WHEN** YAML 配置中某个 target 缺少 name 或 type 字段
|
||||
- **THEN** config-loader 的公共校验流程 SHALL 仍负责校验这些通用字段
|
||||
|
||||
#### Scenario: type 专属校验下沉到 checker
|
||||
- **WHEN** YAML 配置中 HTTP target 缺少 `http.url`
|
||||
- **THEN** HTTP checker 的契约或语义校验 SHALL 抛出校验错误,提示缺少必填字段
|
||||
|
||||
#### Scenario: HTTP method 非法校验
|
||||
- **WHEN** YAML 配置中 HTTP target 的 `http.method` 不是大写合法方法枚举值
|
||||
- **THEN** HTTP checker 契约或语义校验 SHALL 抛出校验错误,提示 method 不合法
|
||||
|
||||
#### Scenario: URL 格式校验
|
||||
- **WHEN** YAML 配置中 HTTP target 的 `http.url` 不以 `http://` 或 `https://` 开头
|
||||
- **THEN** HttpChecker 的语义校验 SHALL 抛出校验错误,提示 URL 格式不合法
|
||||
|
||||
### Requirement: 存储序列化通过 registry 获取展示格式
|
||||
系统 SHALL 在 `ProbeStore.syncTargets()` 中通过 `checkerRegistry.get(t.type).serialize(t)` 获取每个 target 的展示摘要(`target` 列)和配置 JSON(`config` 列),替代 `buildTargetDisplay()` / `buildTargetConfig()` 中的类型分支。系统 SHALL 将 `targets.expect` 持久化为 null,不依赖 Raw expect 或 Resolved expect。
|
||||
|
||||
#### Scenario: 序列化委托 checker
|
||||
- **WHEN** store 同步 targets 表
|
||||
- **THEN** store SHALL 对每个 target 调用对应 checker 的 `serialize()` 方法获取 `{ target, config }`
|
||||
|
||||
#### Scenario: expect 持久化不依赖 rawExpect
|
||||
- **WHEN** store 同步带 expect 的 target 到 targets 表
|
||||
- **THEN** store SHALL 将 `targets.expect` 写入 NULL,MUST NOT 依赖 `rawExpect` 或 Raw expect 快照
|
||||
|
||||
### Requirement: Checker resolve 只接收已去糖配置
|
||||
每个 checker 的 `resolve()` SHALL 接收已通过 Normalized schema 和语义校验的配置,不再包含变量引用、Authoring expect primitive 简写或 Raw content/keyed DSL。`config-loader` SHALL 继续通过 registry 委托 checker resolve,MUST NOT 在中间层理解 checker 专属 expect 字段。
|
||||
|
||||
#### Scenario: resolve 不再展开 Raw expect
|
||||
- **WHEN** config-loader 解析一个带 `expect.durationMs: {equals: 1000}` 的 target
|
||||
- **THEN** 对应 checker 的 resolved target SHALL 直接使用 Normalized expect 中的 `{equals: 1000}`,resolve 只负责默认值和运行期配置转换
|
||||
|
||||
#### Scenario: 中间层不感知 checker expect 字段
|
||||
- **WHEN** 新增 checker 定义自己的 Raw/Resolved expect 字段
|
||||
- **THEN** config-loader SHALL 只调用该 checker 的 `validate()` 和 `resolve()`,不新增 checker 类型分支
|
||||
|
||||
### Requirement: 共享 expect 断言函数
|
||||
系统 SHALL 在 `src/server/checker/expect/` 中提供可被多个 checker 复用的 expect 基础设施。共享基础设施 SHALL 包含 value expectation、content expectations、keyed expectations、status code 断言、headers keyed 断言、failure 构造和 ReDoS 校验。checker 专用的状态类断言 SHALL 保留在对应 checker 目录,或在多个 checker 复用时移动到共享模块。仅被单个 checker 使用且不属于通用 value/content/keyed/status/header 模型的断言模块 SHALL 位于该 checker 目录内。
|
||||
|
||||
#### Scenario: 共享 ValueExpectation 断言
|
||||
- **WHEN** 任何 checker 需要对数字、字符串、布尔或 JSON value 执行 matcher 匹配
|
||||
- **THEN** SHALL 调用共享 value expectation 工具执行 `equals`、`contains`、`regex`、`exists`、`empty`、`gt`、`gte`、`lt` 和 `lte` 语义
|
||||
|
||||
#### Scenario: 共享 ContentExpectations 断言
|
||||
- **WHEN** HTTP body、LLM output、Cmd stdout/stderr、UDP response 或 TCP banner 需要执行返回内容校验
|
||||
- **THEN** SHALL 调用共享 content expectations 工具,而不是在 checker 目录内复制 contains/regex/json/css/xpath 逻辑
|
||||
|
||||
#### Scenario: 共享 KeyedExpectations 断言
|
||||
- **WHEN** HTTP 或 LLM checker 需要校验响应 headers,或 DB checker 需要校验 rows 中的列值
|
||||
- **THEN** SHALL 调用共享 keyed expectations 工具,并按调用方规则决定 key 是否大小写敏感
|
||||
|
||||
#### Scenario: 共享 headers 断言
|
||||
- **WHEN** HTTP 或 LLM checker 需要校验响应 headers
|
||||
- **THEN** SHALL 调用共享 header expectation 包装函数,确保 header key 大小写不敏感
|
||||
|
||||
#### Scenario: 共享 regex ReDoS 校验
|
||||
- **WHEN** 任一 matcher 或 content expectation 配置 `regex`
|
||||
- **THEN** SHALL 调用共享 ReDoS 校验工具在启动期拒绝危险正则
|
||||
|
||||
#### Scenario: 共享 failure 构造
|
||||
- **WHEN** 任何 checker 需要构造 CheckFailure 对象
|
||||
- **THEN** SHALL 调用 `expect/failure.ts` 中的 `errorFailure()` 或 `mismatchFailure()`,并保留 actual 截断策略
|
||||
|
||||
#### Scenario: 共享 status 断言
|
||||
- **WHEN** HTTP 或 LLM checker 需要校验响应状态码
|
||||
- **THEN** SHALL 复用共享 status code 断言函数,支持精确状态码和 `1xx` 到 `5xx` 范围模式
|
||||
|
||||
### Requirement: 超时控制由引擎注入 signal
|
||||
Checker 实现的 `execute()` MUST 使用 `ctx.signal` 感知超时,不得自行创建 `AbortController` 或 `setTimeout` 用于超时控制。Cmd checker 和 ping checker 可在 signal abort 时 `proc.kill()` 以确保子进程被终止。
|
||||
|
||||
#### Scenario: HTTP checker 使用 signal
|
||||
- **WHEN** HttpChecker 执行 HTTP 请求
|
||||
- **THEN** SHALL 将 `ctx.signal` 传入 `fetch()` 的 `signal` 选项,不自行创建 `AbortController`
|
||||
|
||||
#### Scenario: Cmd checker 响应 signal
|
||||
- **WHEN** CommandChecker 执行命令且 signal 被 abort
|
||||
- **THEN** SHALL 调用 `proc.kill()` 终止子进程,并在 CheckResult 中记录超时错误
|
||||
|
||||
#### Scenario: Ping checker 响应 signal
|
||||
- **WHEN** IcmpChecker 执行 ping 命令且 signal 被 abort
|
||||
- **THEN** SHALL 调用 `proc.kill()` 终止 ping 子进程,并在 CheckResult 中记录超时错误
|
||||
|
||||
### Requirement: CheckFailure.phase 使用 string 类型
|
||||
`shared/api.ts` 中 `CheckFailure.phase` 的类型 SHALL 定义为 `string`,替代原有的硬编码联合类型 `"status" | "duration" | "headers" | "body" | "exitCode" | "stdout" | "stderr"`。
|
||||
|
||||
#### Scenario: phase 支持 checker 专用值
|
||||
- **WHEN** cmd checker 在执行失败(spawn error)时生成 failure
|
||||
- **THEN** `failure.phase` SHALL 可以是 `"spawn"` 等任意字符串值,类型系统 SHALL 不报错
|
||||
|
||||
#### Scenario: 前端展示 phase 不依赖硬编码类型
|
||||
- **WHEN** 前端收到任意 phase 字符串值
|
||||
- **THEN** 前端 SHALL 直接展示而不做类型判断
|
||||
@@ -1,6 +1,6 @@
|
||||
## Purpose
|
||||
|
||||
定义项目代码质量门禁、格式化检查、快速检查和完整验证命令的行为要求,确保开发者可以通过文档化命令稳定验证源码质量、基础测试和生产构建。
|
||||
定义项目代码质量门禁、格式化检查、Git hooks 自动化质量门禁、提交信息格式校验、快速检查和完整验证命令的行为要求,确保开发者可以通过文档化命令稳定验证源码质量、基础测试和生产构建。
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -55,6 +55,52 @@
|
||||
- **WHEN** 不同开发者在不同操作系统上运行格式化(通过 ESLint 或独立 Prettier)
|
||||
- **THEN** 由于所有格式化参数均显式定义,产物 SHALL 完全一致
|
||||
|
||||
### Requirement: pre-commit 自动质量检查
|
||||
|
||||
项目 SHALL 通过 husky 和 lint-staged 在 git commit 前自动对变更文件运行 ESLint(含 Prettier 格式)检查。由于 eslint-plugin-prettier 已集成格式检查,lint-staged SHALL 仅运行 `eslint --fix` 即可同时修复代码质量和格式问题。
|
||||
|
||||
#### Scenario: 变更 TypeScript 文件后提交
|
||||
- **WHEN** 开发者 stage 了 `.ts` 或 `.tsx` 文件并执行 `git commit`
|
||||
- **THEN** lint-staged SHALL 自动对变更文件运行 `eslint --fix`(含格式修复),修复后继续提交
|
||||
|
||||
#### Scenario: 变更 Markdown 或 JSON 文件后提交
|
||||
- **WHEN** 开发者 stage 了 `.md`、`.json`、`.yaml` 或 `.yml` 文件并执行 `git commit`
|
||||
- **THEN** lint-staged SHALL 自动对变更文件运行 `prettier --write`
|
||||
|
||||
#### Scenario: lint 检查失败阻止提交
|
||||
- **WHEN** 变更文件存在无法自动修复的 ESLint 错误(含格式错误)
|
||||
- **THEN** pre-commit hook MUST 以非零状态退出,阻止提交
|
||||
|
||||
#### Scenario: 无变更文件提交
|
||||
- **WHEN** 开发者执行 `git commit` 但无 stage 文件
|
||||
- **THEN** lint-staged SHALL 正常通过,不阻止提交
|
||||
|
||||
### Requirement: 提交信息格式校验
|
||||
项目 SHALL 通过 commitlint 在 git commit 时校验提交信息必须符合 "类型: 简短描述" 格式,类型限定为 feat/fix/refactor/docs/style/test/chore。
|
||||
|
||||
#### Scenario: 有效的中文提交信息
|
||||
- **WHEN** 开发者提交信息为 "feat: 新增导入排序功能"
|
||||
- **THEN** commit-msg hook SHALL 通过校验
|
||||
|
||||
#### Scenario: 缺少类型前缀的提交信息
|
||||
- **WHEN** 开发者提交信息为 "新增导入排序功能"(无 "feat:" 前缀)
|
||||
- **THEN** commit-msg hook MUST 以非零状态退出,提示正确格式
|
||||
|
||||
#### Scenario: 无效的提交类型
|
||||
- **WHEN** 开发者提交信息使用不在允许列表中的类型(如 "update: 修改配置")
|
||||
- **THEN** commit-msg hook MUST 以非零状态退出,提示可用类型
|
||||
|
||||
### Requirement: husky 初始化自动化
|
||||
项目 SHALL 通过 `prepare` 生命周期脚本在 `bun install` 时自动初始化 husky。
|
||||
|
||||
#### Scenario: 首次安装依赖
|
||||
- **WHEN** 开发者运行 `bun install`
|
||||
- **THEN** husky SHALL 自动初始化,安装 pre-commit 和 commit-msg hooks
|
||||
|
||||
#### Scenario: 已有 husky 配置时安装
|
||||
- **WHEN** 开发者运行 `bun install` 且 husky 已初始化
|
||||
- **THEN** husky 初始化 SHALL 跳过,不覆盖已有配置
|
||||
|
||||
### Requirement: TypeScript 未使用变量检测
|
||||
项目 SHALL 启用 TypeScript `noUnusedLocals` 编译选项,将未使用的局部变量检测为编译错误。
|
||||
|
||||
@@ -115,7 +161,7 @@
|
||||
- **THEN** 系统 SHALL 执行 `tests/` 目录下所有 `*.test.ts` 和 `*.test.tsx` 文件
|
||||
|
||||
### Requirement: 完整验证命令
|
||||
项目 SHALL 提供完整 `verify` 命令,用于提交前或发布前验证当前源码、测试和生产构建。原 executable smoke test 暂时移除,后续通过独立变更重新设计。
|
||||
项目 SHALL 提供完整 `verify` 命令,用于提交前或发布前验证当前源码、测试和生产构建。
|
||||
|
||||
#### Scenario: 运行完整验证
|
||||
- **WHEN** 开发者运行 `bun run verify`
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 Git hooks 自动化质量门禁行为,在 pre-commit 阶段自动运行代码检查和格式化,在 commit-msg 阶段校验提交信息格式。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: pre-commit 自动质量检查
|
||||
|
||||
项目 SHALL 通过 husky 和 lint-staged 在 git commit 前自动对变更文件运行 ESLint(含 Prettier 格式)检查。由于 eslint-plugin-prettier 已集成格式检查,lint-staged SHALL 仅运行 `eslint --fix` 即可同时修复代码质量和格式问题。
|
||||
|
||||
#### Scenario: 变更 TypeScript 文件后提交
|
||||
- **WHEN** 开发者 stage 了 `.ts` 或 `.tsx` 文件并执行 `git commit`
|
||||
- **THEN** lint-staged SHALL 自动对变更文件运行 `eslint --fix`(含格式修复),修复后继续提交
|
||||
|
||||
#### Scenario: 变更 Markdown 或 JSON 文件后提交
|
||||
- **WHEN** 开发者 stage 了 `.md`、`.json`、`.yaml` 或 `.yml` 文件并执行 `git commit`
|
||||
- **THEN** lint-staged SHALL 自动对变更文件运行 `prettier --write`
|
||||
|
||||
#### Scenario: lint 检查失败阻止提交
|
||||
- **WHEN** 变更文件存在无法自动修复的 ESLint 错误(含格式错误)
|
||||
- **THEN** pre-commit hook MUST 以非零状态退出,阻止提交
|
||||
|
||||
#### Scenario: 无变更文件提交
|
||||
- **WHEN** 开发者执行 `git commit` 但无 stage 文件
|
||||
- **THEN** lint-staged SHALL 正常通过,不阻止提交
|
||||
|
||||
### Requirement: 提交信息格式校验
|
||||
项目 SHALL 通过 commitlint 在 git commit 时校验提交信息必须符合 "类型: 简短描述" 格式,类型限定为 feat/fix/refactor/docs/style/test/chore。
|
||||
|
||||
#### Scenario: 有效的中文提交信息
|
||||
- **WHEN** 开发者提交信息为 "feat: 新增导入排序功能"
|
||||
- **THEN** commit-msg hook SHALL 通过校验
|
||||
|
||||
#### Scenario: 缺少类型前缀的提交信息
|
||||
- **WHEN** 开发者提交信息为 "新增导入排序功能"(无 "feat:" 前缀)
|
||||
- **THEN** commit-msg hook MUST 以非零状态退出,提示正确格式
|
||||
|
||||
#### Scenario: 无效的提交类型
|
||||
- **WHEN** 开发者提交信息使用不在允许列表中的类型(如 "update: 修改配置")
|
||||
- **THEN** commit-msg hook MUST 以非零状态退出,提示可用类型
|
||||
|
||||
### Requirement: husky 初始化自动化
|
||||
项目 SHALL 通过 `prepare` 生命周期脚本在 `bun install` 时自动初始化 husky。
|
||||
|
||||
#### Scenario: 首次安装依赖
|
||||
- **WHEN** 开发者运行 `bun install`
|
||||
- **THEN** husky SHALL 自动初始化,安装 pre-commit 和 commit-msg hooks
|
||||
|
||||
#### Scenario: 已有 husky 配置时安装
|
||||
- **WHEN** 开发者运行 `bun install` 且 husky 已初始化
|
||||
- **THEN** husky 初始化 SHALL 跳过,不覆盖已有配置
|
||||
@@ -1,5 +1,3 @@
|
||||
# Container Image Packaging
|
||||
|
||||
## Purpose
|
||||
|
||||
Provide Alpine-based multi-stage Docker container image packaging for DiAL, enabling containerized deployment with musl executables, minimal runtime dependencies, and documented build/run workflows.
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 styles.css 中集中管理的前端样式工具类和 CSS 自定义属性,供 TDesign 组件之外的自定义组件(StatusDot、StatusBar 等)引用。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 状态色 CSS 类
|
||||
styles.css SHALL 定义状态指示相关的 CSS 类,颜色使用 TDesign tokens。
|
||||
|
||||
#### Scenario: StatusDot 颜色类
|
||||
- **WHEN** StatusDot 组件渲染
|
||||
- **THEN** 组件 SHALL 使用 `.status-dot` 基础类 + `.status-dot--up`(background: `--td-success-color`)或 `.status-dot--down`(background: `--td-error-color`)修饰类,不使用内联 style
|
||||
|
||||
#### Scenario: StatusDot 发光阴影
|
||||
- **WHEN** StatusDot 组件渲染
|
||||
- **THEN** `.status-dot--up` SHALL 定义 `box-shadow` 使用 `--td-success-color`,`.status-dot--down` SHALL 定义 `box-shadow` 使用 `--td-error-color`
|
||||
|
||||
#### Scenario: StatusBar 色块类
|
||||
- **WHEN** StatusBar 组件渲染色块
|
||||
- **THEN** 组件 SHALL 使用 `.status-bar-block` 基础类 + `.status-bar-block--up`(background: `--td-success-color`)、`.status-bar-block--down`(background: `--td-error-color`)或 `.status-bar-block--empty`(background: `--td-bg-color-component-disabled`)修饰类,不使用内联 style
|
||||
|
||||
### Requirement: 可用率色阶 CSS 变量
|
||||
styles.css SHALL 定义 10 级可用率色阶 CSS 自定义属性,使用项目自定义色值。
|
||||
|
||||
#### Scenario: 色阶变量定义
|
||||
- **WHEN** 可用率进度条渲染
|
||||
- **THEN** 色阶 SHALL 通过 CSS 自定义属性 `--avail-0` 到 `--avail-9` 定义,值为项目自定义色值(`#d54941` 到 `#3dba60`)
|
||||
|
||||
#### Scenario: 色阶渐变方向
|
||||
- **WHEN** 色阶变量被引用
|
||||
- **THEN** 色阶 SHALL 从红色(0-30%)经橙色(30-60%)过渡到绿色(60-100%)
|
||||
|
||||
### Requirement: 辅助工具类
|
||||
styles.css SHALL 定义前端组件复用的工具类,包含页面布局相关类。
|
||||
|
||||
#### Scenario: 文本禁用色类
|
||||
- **WHEN** 延迟列无数据需要显示占位符
|
||||
- **THEN** 组件 SHALL 使用 `.text-disabled` 类(color: `--td-text-color-disabled`)
|
||||
|
||||
#### Scenario: 等宽数字类
|
||||
- **WHEN** 数值需要等宽显示
|
||||
- **THEN** 组件 SHALL 使用 `.tabular-nums` 类(font-variant-numeric: tabular-nums)
|
||||
|
||||
#### Scenario: 延迟色值类
|
||||
- **WHEN** 延迟数值渲染
|
||||
- **THEN** 组件 SHALL 使用 `.latency-ok`、`.latency-warn` 或 `.latency-error` 类
|
||||
|
||||
#### Scenario: 延迟值容器类
|
||||
- **WHEN** 延迟数值需要固定宽度对齐
|
||||
- **THEN** 组件 SHALL 使用 `.latency-value` 类(display: inline-block; min-width: 7ch; white-space: nowrap)
|
||||
|
||||
#### Scenario: 全宽布局类
|
||||
- **WHEN** 组件需要占满父容器宽度
|
||||
- **THEN** 组件 SHALL 使用 `.full-width` 类(width: 100%)
|
||||
|
||||
#### Scenario: 可点击表格类
|
||||
- **WHEN** PrimaryTable 行支持点击交互
|
||||
- **THEN** 表格 SHALL 使用 `.clickable-table` 类(cursor: pointer)
|
||||
|
||||
#### Scenario: Tab 面板内边距类
|
||||
- **WHEN** Drawer 内 Tabs 面板需要内边距
|
||||
- **THEN** TabPanel SHALL 使用 `className="tab-panel-padded"` prop 传入类名
|
||||
|
||||
#### Scenario: 内容区居中类
|
||||
- **WHEN** Dashboard 内容区需要居中且限制最大宽度
|
||||
- **THEN** 内容区 SHALL 使用 `.dashboard-content` 类(max-width: 1400px; margin: 0 auto; padding: var(--td-comp-paddingTB-xl) var(--td-comp-paddingLR-xl))
|
||||
|
||||
#### Scenario: 页面背景色
|
||||
- **WHEN** Dashboard 页面渲染
|
||||
- **THEN** `.dashboard` 类 SHALL 设置 background: var(--td-bg-color-page),min-height: 100vh,width: 100%
|
||||
|
||||
#### Scenario: 品牌标识类
|
||||
- **WHEN** HeadMenu logo 区域渲染品牌名和副标题
|
||||
- **THEN** 品牌 SHALL 使用 `.dashboard-brand` 类(display: inline-flex; align-items: baseline; gap: var(--td-comp-margin-s)),品牌名 SHALL 使用 `.dashboard-logo` 类(font-size: calc(var(--td-font-size-title-large) + 6px); font-weight: 700),副标题 SHALL 使用 `.dashboard-subtitle` 类(font-size: var(--td-font-size-body-medium); color: var(--td-text-color-secondary))
|
||||
|
||||
#### Scenario: Header 右侧操作区类
|
||||
- **WHEN** HeadMenu operations 区域渲染主题模式选择器、刷新频率选择器和倒计时/按钮
|
||||
- **THEN** 容器 SHALL 使用 `.dashboard-header-controls` 类(display: inline-flex; align-items: center; gap: var(--td-comp-margin-s); margin-right: var(--td-comp-margin-xxl))
|
||||
|
||||
#### Scenario: Header 右侧操作区单行布局
|
||||
- **WHEN** Header 右侧操作区渲染
|
||||
- **THEN** `.dashboard-header-controls` SHALL 保持桌面单行水平布局,不为该区域新增窄屏换行或收纳规则
|
||||
|
||||
#### Scenario: 倒计时文本类
|
||||
- **WHEN** 倒计时文本或刷新按钮渲染
|
||||
- **THEN** 容器 SHALL 使用 `.dashboard-countdown` 类(display: inline-flex; align-items: center; font-variant-numeric: tabular-nums; min-width: 5ch),确保数字等宽且格式切换不抖动
|
||||
|
||||
#### Scenario: SummaryCard 居中类
|
||||
- **WHEN** SummaryCards 内 Statistic 需要居中
|
||||
- **THEN** Statistic 所在的 Col SHALL 使用 `.summary-stat-col` 类(text-align: center)
|
||||
|
||||
#### Scenario: SummaryCards 行间距类
|
||||
- **WHEN** SummaryCards 容器需要与下方内容保持间距
|
||||
- **THEN** 容器 SHALL 使用 `.summary-cards-row` 类(margin-bottom: var(--td-comp-margin-xl))
|
||||
|
||||
#### Scenario: Drawer 时间控件单行类
|
||||
- **WHEN** Drawer 时间选择器需要单行布局
|
||||
- **THEN** 控件容器 SHALL 使用 `.drawer-time-controls` 类(display: flex; align-items: center; gap: var(--td-comp-margin-m); width: 100%),日期选择器 SHALL 使用 `.drawer-date-range` 类(flex: 1; min-width: 360px)
|
||||
|
||||
#### Scenario: Drawer 时间控件响应式
|
||||
- **WHEN** 视口宽度 ≤ 768px
|
||||
- **THEN** `.drawer-time-controls` SHALL 启用 flex-wrap,`.drawer-date-range` min-width 改为 100%
|
||||
|
||||
#### Scenario: 概览统计卡片类
|
||||
- **WHEN** Drawer 概览统计区渲染
|
||||
- **THEN** 统计卡片 SHALL 使用 `.overview-stat-card` 类(background: var(--td-bg-color-container-hover)),并使用 TDesign Statistic 组件自带的上下布局(title 在上、value 在下),通过 `.summary-stat-col` 类(text-align: center)实现内容居中。系统 SHALL NOT 使用已移除的 `.overview-stat-item` 和 `.overview-stat-value` 类。
|
||||
|
||||
### Requirement: NumberFlow 倒计时样式类
|
||||
styles.css SHALL 定义 NumberFlow 倒计时相关样式类,供 Header 倒计时组件使用。样式 SHALL 继承 TDesign 文本颜色或使用 TDesign CSS tokens,不得使用组件内联 `style`、硬编码色值、`!important` 或覆盖 TDesign 内部类名。
|
||||
|
||||
#### Scenario: 倒计时滚动容器类
|
||||
- **WHEN** Header 自动刷新倒计时以 NumberFlow 形式渲染
|
||||
- **THEN** 倒计时 SHALL 使用集中定义的滚动容器类,保持 inline-flex、baseline 对齐、nowrap 和 tabular-nums
|
||||
|
||||
#### Scenario: 倒计时数字类
|
||||
- **WHEN** NumberFlow 数字渲染
|
||||
- **THEN** 数字 SHALL 使用集中定义的数字类配置 line-height 和 NumberFlow mask CSS 变量,减少滚动边缘突兀感
|
||||
|
||||
#### Scenario: 倒计时单位类
|
||||
- **WHEN** 分钟或秒单位文本渲染
|
||||
- **THEN** 单位 SHALL 使用集中定义的单位类与数字保持基线对齐,并继承当前 TDesign 文本色
|
||||
|
||||
#### Scenario: 不使用内联样式
|
||||
- **WHEN** RefreshCountdown 组件渲染 NumberFlow 倒计时
|
||||
- **THEN** 组件 SHALL 通过 `className` 引用 styles.css 中的样式类,不得通过 React `style` prop 设置 NumberFlow 展示样式
|
||||
|
||||
### Requirement: 异常行背景类
|
||||
styles.css SHALL 定义 DOWN 行的背景色和左侧竖线,使用安全选择器且不使用 `!important`。
|
||||
|
||||
#### Scenario: DOWN 行背景色
|
||||
- **WHEN** 表格行标记为 DOWN 状态
|
||||
- **THEN** 行 SHALL 通过 `.t-table tr.row-down` 选择器获得浅红色背景
|
||||
|
||||
#### Scenario: DOWN 行左侧竖线
|
||||
- **WHEN** 表格行标记为 DOWN 状态
|
||||
- **THEN** 行 SHALL 通过 `.t-table tr.row-down` 选择器获得 `border-left: 3px solid var(--td-error-color)`
|
||||
|
||||
#### Scenario: DOWN 行 hover 状态
|
||||
- **WHEN** 鼠标悬停在 DOWN 行上
|
||||
- **THEN** 行背景 SHALL 通过 `.t-table--hoverable tbody tr.row-down:hover` 选择器显示 hover 状态色
|
||||
@@ -1,51 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 Dashboard 页面骨架布局:顶部导航栏(含品牌标识、主题模式选择器、刷新频率选择器和倒计时控件)、内容区域居中与最大宽度、页面背景色。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 页面骨架布局
|
||||
Dashboard SHALL 使用 TDesign Layout 组件体系构建页面骨架,包含顶部导航栏和内容区域。
|
||||
|
||||
#### Scenario: Layout 结构
|
||||
- **WHEN** Dashboard 页面渲染
|
||||
- **THEN** 页面 SHALL 使用 TDesign `Layout` 组件包裹 `Layout.Header` 和 `Layout.Content`
|
||||
|
||||
#### Scenario: 顶部导航栏
|
||||
- **WHEN** Dashboard 页面渲染
|
||||
- **THEN** `Layout.Header` SHALL 内嵌 TDesign `HeadMenu` 组件,`logo` prop 渲染品牌名 "DiAL" 和副标题 "统一拨测平台"(水平排列),`operations` prop 渲染主题模式选择器、刷新频率选择器和倒计时/刷新按钮组合控件
|
||||
|
||||
#### Scenario: Header 右侧操作区
|
||||
- **WHEN** Dashboard 页面渲染
|
||||
- **THEN** HeadMenu operations 区域 SHALL 包含主题模式 RadioGroup、刷新频率 RadioGroup 和倒计时文本(或手动刷新按钮),三者水平排列并垂直居中
|
||||
|
||||
#### Scenario: 主题选择器位置
|
||||
- **WHEN** HeadMenu operations 区域渲染
|
||||
- **THEN** 主题模式 RadioGroup SHALL 位于刷新频率 RadioGroup 前面
|
||||
|
||||
#### Scenario: Header 右侧操作区位置
|
||||
- **WHEN** HeadMenu 渲染
|
||||
- **THEN** operations 区域 SHALL 使用右侧 margin 向内收缩,避免紧贴浏览器右边缘
|
||||
|
||||
#### Scenario: 内容区域居中
|
||||
- **WHEN** Dashboard 内容区渲染
|
||||
- **THEN** `Layout.Content` 内部 SHALL 使用 CSS 类限制最大宽度(max-width: 1400px)并水平居中
|
||||
|
||||
#### Scenario: 页面背景色
|
||||
- **WHEN** Dashboard 页面渲染
|
||||
- **THEN** 页面背景色 SHALL 使用 `var(--td-bg-color-page)`,内容卡片浮于当前 TDesign 主题背景之上
|
||||
|
||||
### Requirement: Header 版本号展示
|
||||
Dashboard SHALL 在顶部导航栏品牌区域展示当前运行实例的应用版本号,版本号 SHALL 使用 `/api/meta` 返回的 `version` 字段,并以 `v` 前缀显示。
|
||||
|
||||
#### Scenario: Meta 数据已加载
|
||||
- **WHEN** Dashboard 成功获取 `/api/meta` 且返回 `version: "0.1.0"`
|
||||
- **THEN** Header 品牌区域 SHALL 展示 `v0.1.0`
|
||||
|
||||
#### Scenario: Meta 数据尚未加载或请求失败
|
||||
- **WHEN** Dashboard 尚未获取到有效 `version`
|
||||
- **THEN** Header SHALL 保持可用并省略版本号占位,不影响品牌名、主题模式选择器、刷新频率选择器和倒计时/刷新按钮渲染
|
||||
|
||||
#### Scenario: 版本号视觉层级
|
||||
- **WHEN** Header 展示版本号
|
||||
- **THEN** 版本号 SHALL 使用次级文本样式弱展示,不得使用内联 style、硬编码色值、`!important` 或覆盖 TDesign 内部类名
|
||||
327
openspec/specs/dashboard/spec.md
Normal file
327
openspec/specs/dashboard/spec.md
Normal file
@@ -0,0 +1,327 @@
|
||||
## Purpose
|
||||
|
||||
定义拨测系统前端 Dashboard 页面:页面骨架布局、刷新频率控制与倒计时、CSS 工具类基础设施、总览统计卡片、Dashboard 数据查询、加载和错误状态处理。分组表格布局见 `target-table`,目标详情 Drawer 见 `target-detail-drawer`,数据轮询和缓存见 `tanstack-query-data-layer`。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 页面骨架布局
|
||||
Dashboard SHALL 使用 TDesign Layout 组件体系构建页面骨架,包含顶部导航栏和内容区域。
|
||||
|
||||
#### Scenario: Layout 结构
|
||||
- **WHEN** Dashboard 页面渲染
|
||||
- **THEN** 页面 SHALL 使用 TDesign `Layout` 组件包裹 `Layout.Header` 和 `Layout.Content`
|
||||
|
||||
#### Scenario: 顶部导航栏
|
||||
- **WHEN** Dashboard 页面渲染
|
||||
- **THEN** `Layout.Header` SHALL 内嵌 TDesign `HeadMenu` 组件,`logo` prop 渲染品牌名 "DiAL" 和副标题 "统一拨测平台"(水平排列),`operations` prop 渲染主题模式选择器、刷新频率选择器和倒计时/刷新按钮组合控件
|
||||
|
||||
#### Scenario: Header 右侧操作区
|
||||
- **WHEN** Dashboard 页面渲染
|
||||
- **THEN** HeadMenu operations 区域 SHALL 包含主题模式 RadioGroup、刷新频率 RadioGroup 和倒计时文本(或手动刷新按钮),三者水平排列并垂直居中
|
||||
|
||||
#### Scenario: 主题选择器位置
|
||||
- **WHEN** HeadMenu operations 区域渲染
|
||||
- **THEN** 主题模式 RadioGroup SHALL 位于刷新频率 RadioGroup 前面
|
||||
|
||||
#### Scenario: Header 右侧操作区位置
|
||||
- **WHEN** HeadMenu 渲染
|
||||
- **THEN** operations 区域 SHALL 使用右侧 margin 向内收缩,避免紧贴浏览器右边缘
|
||||
|
||||
#### Scenario: 内容区域居中
|
||||
- **WHEN** Dashboard 内容区渲染
|
||||
- **THEN** `Layout.Content` 内部 SHALL 使用 CSS 类限制最大宽度(max-width: 1400px)并水平居中
|
||||
|
||||
#### Scenario: 页面背景色
|
||||
- **WHEN** Dashboard 页面渲染
|
||||
- **THEN** 页面背景色 SHALL 使用 `var(--td-bg-color-page)`,内容卡片浮于当前 TDesign 主题背景之上
|
||||
|
||||
### Requirement: Header 版本号展示
|
||||
Dashboard SHALL 在顶部导航栏品牌区域展示当前运行实例的应用版本号,版本号 SHALL 使用 `/api/meta` 返回的 `version` 字段,并以 `v` 前缀显示。
|
||||
|
||||
#### Scenario: Meta 数据已加载
|
||||
- **WHEN** Dashboard 成功获取 `/api/meta` 且返回 `version: "0.1.0"`
|
||||
- **THEN** Header 品牌区域 SHALL 展示 `v0.1.0`
|
||||
|
||||
#### Scenario: Meta 数据尚未加载或请求失败
|
||||
- **WHEN** Dashboard 尚未获取到有效 `version`
|
||||
- **THEN** Header SHALL 保持可用并省略版本号占位,不影响品牌名、主题模式选择器、刷新频率选择器和倒计时/刷新按钮渲染
|
||||
|
||||
#### Scenario: 版本号视觉层级
|
||||
- **WHEN** Header 展示版本号
|
||||
- **THEN** 版本号 SHALL 使用次级文本样式弱展示,不得使用内联 style、硬编码色值、`!important` 或覆盖 TDesign 内部类名
|
||||
|
||||
### Requirement: 刷新频率选择器
|
||||
HeadMenu operations 区域 SHALL 提供 RadioGroup 组件供用户选择刷新频率。
|
||||
|
||||
#### Scenario: RadioGroup 渲染
|
||||
- **WHEN** Dashboard 页面渲染
|
||||
- **THEN** HeadMenu operations 区域 SHALL 显示 RadioGroup(theme="button", variant="default-filled"),选项为:手动、10秒、30秒、1分钟、5分钟
|
||||
|
||||
#### Scenario: 默认选中
|
||||
- **WHEN** 页面首次加载
|
||||
- **THEN** RadioGroup SHALL 默认选中"30秒"
|
||||
|
||||
#### Scenario: 切换频率立即刷新
|
||||
- **WHEN** 用户切换刷新频率选项
|
||||
- **THEN** 系统 SHALL 立即触发一次数据刷新,然后应用新的刷新间隔
|
||||
|
||||
### Requirement: 倒计时显示
|
||||
RadioGroup 右侧 SHALL 显示距下次自动刷新的倒计时。倒计时逻辑 SHALL 封装在独立的 `RefreshCountdown` 组件中,App 组件 SHALL NOT 持有每秒更新的 `now` state。自动倒计时数字 SHALL 使用 `@number-flow/react` 提供滚动过渡,非倒计时状态 SHALL 保持普通文本或按钮语义。
|
||||
|
||||
#### Scenario: RefreshCountdown 组件封装
|
||||
- **WHEN** Dashboard 页面渲染
|
||||
- **THEN** 倒计时显示 SHALL 由独立的 `RefreshCountdown` 组件负责,该组件内部持有 `now` state 和每秒 `setInterval`,渲染边界限制在该组件内部
|
||||
|
||||
#### Scenario: RefreshCountdown props
|
||||
- **WHEN** RefreshCountdown 组件渲染
|
||||
- **THEN** 组件 SHALL 接收 `dashboardUpdatedAt: number`、`refreshInterval: number`、`isFetching: boolean`、`isManualRefresh: boolean`、`onRefresh: () => void` 作为 props
|
||||
|
||||
#### Scenario: NumberFlow 数字滚动
|
||||
- **WHEN** 自动刷新模式下已完成首次刷新且当前未处于刷新中状态
|
||||
- **THEN** 倒计时数字 SHALL 使用 `@number-flow/react` 的 `NumberFlow` 渲染,并使用向下滚动趋势表达倒计时递减
|
||||
|
||||
#### Scenario: 秒级间隔格式
|
||||
- **WHEN** 自动刷新间隔小于 60 秒
|
||||
- **THEN** 倒计时 SHALL 显示为"xx秒"格式(如"26秒")
|
||||
|
||||
#### Scenario: 分钟级稳定格式
|
||||
- **WHEN** 自动刷新间隔大于等于 60 秒
|
||||
- **THEN** 倒计时 SHALL 显示为"x分xx秒"格式,秒数 SHALL 固定为两位(如"4分30秒"、"0分09秒")
|
||||
|
||||
#### Scenario: 时间数字边界
|
||||
- **WHEN** 分钟级倒计时中的秒数在 59 到 00 边界变化
|
||||
- **THEN** 秒数十位 SHALL 按时间显示规则限制在 0 到 5 之间滚动
|
||||
|
||||
#### Scenario: 无前缀
|
||||
- **WHEN** 倒计时显示
|
||||
- **THEN** 可见倒计时文本 SHALL 不包含任何前缀(如"下一次刷新:"),直接显示时间
|
||||
|
||||
#### Scenario: 可访问文本
|
||||
- **WHEN** NumberFlow 倒计时渲染
|
||||
- **THEN** 倒计时容器 SHALL 暴露与当前倒计时等价的可访问文本,供测试和辅助技术读取
|
||||
|
||||
#### Scenario: 刷新中状态
|
||||
- **WHEN** 数据正在刷新(isFetching=true 且 isLoading=false)
|
||||
- **THEN** 倒计时文本 SHALL 显示为"刷新中..."
|
||||
|
||||
#### Scenario: 等待首次刷新状态
|
||||
- **WHEN** 自动刷新模式下尚未完成首次刷新
|
||||
- **THEN** 倒计时文本 SHALL 显示为"等待首次刷新"
|
||||
|
||||
### Requirement: App 组件渲染隔离
|
||||
App 组件 SHALL NOT 持有任何高频更新的 state(如每秒更新的时钟),确保 App 的重渲染频率与数据刷新频率一致(默认 30 秒一次)。
|
||||
|
||||
#### Scenario: App 无 now state
|
||||
- **WHEN** App 组件渲染
|
||||
- **THEN** App SHALL NOT 包含 `useState` 管理的时钟 state,也 SHALL NOT 包含每秒触发的 `setInterval`
|
||||
|
||||
#### Scenario: App 重渲染频率
|
||||
- **WHEN** Dashboard 处于自动刷新模式
|
||||
- **THEN** App 组件的重渲染 SHALL 仅由 TanStack Query 的 refetch 触发(频率等于用户选择的刷新间隔),而非每秒触发
|
||||
|
||||
### Requirement: 手动刷新按钮
|
||||
选择"手动"模式时,倒计时区域 SHALL 替换为刷新按钮。
|
||||
|
||||
#### Scenario: 手动模式显示按钮
|
||||
- **WHEN** 用户选择"手动"刷新频率
|
||||
- **THEN** 倒计时区域 SHALL 替换为刷新图标按钮
|
||||
|
||||
#### Scenario: 点击刷新
|
||||
- **WHEN** 用户点击刷新按钮
|
||||
- **THEN** 系统 SHALL 触发一次数据刷新
|
||||
|
||||
#### Scenario: 刷新中禁用
|
||||
- **WHEN** 数据正在刷新
|
||||
- **THEN** 刷新按钮 SHALL 显示 loading 状态且 disabled,防止连续点击
|
||||
|
||||
### Requirement: 布局稳定性
|
||||
倒计时/按钮容器 SHALL 保持布局稳定,避免内容变化导致的抖动。NumberFlow 倒计时 SHALL 通过分组同步和等宽数字样式降低位数、单位和动画变化带来的布局偏移。
|
||||
|
||||
#### Scenario: 数字等宽
|
||||
- **WHEN** 倒计时数字变化
|
||||
- **THEN** 容器和 NumberFlow 倒计时 SHALL 使用 tabular-nums 字体特性,确保数字等宽不抖动
|
||||
|
||||
#### Scenario: NumberFlow 分组同步
|
||||
- **WHEN** 分钟级倒计时同时渲染分钟和秒数
|
||||
- **THEN** 分钟和秒数 SHALL 使用 `NumberFlowGroup` 同步布局变化
|
||||
|
||||
#### Scenario: 格式切换不抖动
|
||||
- **WHEN** 倒计时在按钮、秒级文本和分钟级文本之间切换
|
||||
- **THEN** 容器 SHALL 使用 min-width 确保最小宽度,避免 RadioGroup 位移
|
||||
|
||||
### Requirement: 状态色 CSS 类
|
||||
styles.css SHALL 定义状态指示相关的 CSS 类,颜色使用 TDesign tokens。
|
||||
|
||||
#### Scenario: StatusDot 颜色类
|
||||
- **WHEN** StatusDot 组件渲染
|
||||
- **THEN** 组件 SHALL 使用 `.status-dot` 基础类 + `.status-dot--up`(background: `--td-success-color`)或 `.status-dot--down`(background: `--td-error-color`)修饰类,不使用内联 style
|
||||
|
||||
#### Scenario: StatusDot 发光阴影
|
||||
- **WHEN** StatusDot 组件渲染
|
||||
- **THEN** `.status-dot--up` SHALL 定义 `box-shadow` 使用 `--td-success-color`,`.status-dot--down` SHALL 定义 `box-shadow` 使用 `--td-error-color`
|
||||
|
||||
#### Scenario: StatusBar 色块类
|
||||
- **WHEN** StatusBar 组件渲染色块
|
||||
- **THEN** 组件 SHALL 使用 `.status-bar-block` 基础类 + `.status-bar-block--up`(background: `--td-success-color`)、`.status-bar-block--down`(background: `--td-error-color`)或 `.status-bar-block--empty`(background: `--td-bg-color-component-disabled`)修饰类,不使用内联 style
|
||||
|
||||
### Requirement: 可用率色阶 CSS 变量
|
||||
styles.css SHALL 定义 10 级可用率色阶 CSS 自定义属性,使用项目自定义色值。
|
||||
|
||||
#### Scenario: 色阶变量定义
|
||||
- **WHEN** 可用率进度条渲染
|
||||
- **THEN** 色阶 SHALL 通过 CSS 自定义属性 `--avail-0` 到 `--avail-9` 定义,值为项目自定义色值(`#d54941` 到 `#3dba60`)
|
||||
|
||||
#### Scenario: 色阶渐变方向
|
||||
- **WHEN** 色阶变量被引用
|
||||
- **THEN** 色阶 SHALL 从红色(0-30%)经橙色(30-60%)过渡到绿色(60-100%)
|
||||
|
||||
### Requirement: 辅助工具类
|
||||
styles.css SHALL 定义前端组件复用的工具类,包含页面布局相关类。
|
||||
|
||||
#### Scenario: 文本禁用色类
|
||||
- **WHEN** 延迟列无数据需要显示占位符
|
||||
- **THEN** 组件 SHALL 使用 `.text-disabled` 类(color: `--td-text-color-disabled`)
|
||||
|
||||
#### Scenario: 等宽数字类
|
||||
- **WHEN** 数值需要等宽显示
|
||||
- **THEN** 组件 SHALL 使用 `.tabular-nums` 类(font-variant-numeric: tabular-nums)
|
||||
|
||||
#### Scenario: 延迟色值类
|
||||
- **WHEN** 延迟数值渲染
|
||||
- **THEN** 组件 SHALL 使用 `.latency-ok`、`.latency-warn` 或 `.latency-error` 类
|
||||
|
||||
#### Scenario: 延迟值容器类
|
||||
- **WHEN** 延迟数值需要固定宽度对齐
|
||||
- **THEN** 组件 SHALL 使用 `.latency-value` 类(display: inline-block; min-width: 7ch; white-space: nowrap)
|
||||
|
||||
#### Scenario: 全宽布局类
|
||||
- **WHEN** 组件需要占满父容器宽度
|
||||
- **THEN** 组件 SHALL 使用 `.full-width` 类(width: 100%)
|
||||
|
||||
#### Scenario: 可点击表格类
|
||||
- **WHEN** PrimaryTable 行支持点击交互
|
||||
- **THEN** 表格 SHALL 使用 `.clickable-table` 类(cursor: pointer)
|
||||
|
||||
#### Scenario: Tab 面板内边距类
|
||||
- **WHEN** Drawer 内 Tabs 面板需要内边距
|
||||
- **THEN** TabPanel SHALL 使用 `className="tab-panel-padded"` prop 传入类名
|
||||
|
||||
#### Scenario: 内容区居中类
|
||||
- **WHEN** Dashboard 内容区需要居中且限制最大宽度
|
||||
- **THEN** 内容区 SHALL 使用 `.dashboard-content` 类(max-width: 1400px; margin: 0 auto; padding: var(--td-comp-paddingTB-xl) var(--td-comp-paddingLR-xl))
|
||||
|
||||
#### Scenario: 页面背景色
|
||||
- **WHEN** Dashboard 页面渲染
|
||||
- **THEN** `.dashboard` 类 SHALL 设置 background: var(--td-bg-color-page),min-height: 100vh,width: 100%
|
||||
|
||||
#### Scenario: 品牌标识类
|
||||
- **WHEN** HeadMenu logo 区域渲染品牌名和副标题
|
||||
- **THEN** 品牌 SHALL 使用 `.dashboard-brand` 类(display: inline-flex; align-items: baseline; gap: var(--td-comp-margin-s)),品牌名 SHALL 使用 `.dashboard-logo` 类(font-size: calc(var(--td-font-size-title-large) + 6px); font-weight: 700),副标题 SHALL 使用 `.dashboard-subtitle` 类(font-size: var(--td-font-size-body-medium); color: var(--td-text-color-secondary))
|
||||
|
||||
#### Scenario: Header 右侧操作区类
|
||||
- **WHEN** HeadMenu operations 区域渲染主题模式选择器、刷新频率选择器和倒计时/按钮
|
||||
- **THEN** 容器 SHALL 使用 `.dashboard-header-controls` 类(display: inline-flex; align-items: center; gap: var(--td-comp-margin-s); margin-right: var(--td-comp-margin-xxl))
|
||||
|
||||
#### Scenario: Header 右侧操作区单行布局
|
||||
- **WHEN** Header 右侧操作区渲染
|
||||
- **THEN** `.dashboard-header-controls` SHALL 保持桌面单行水平布局,不为该区域新增窄屏换行或收纳规则
|
||||
|
||||
#### Scenario: 倒计时文本类
|
||||
- **WHEN** 倒计时文本或刷新按钮渲染
|
||||
- **THEN** 容器 SHALL 使用 `.dashboard-countdown` 类(display: inline-flex; align-items: center; font-variant-numeric: tabular-nums; min-width: 5ch),确保数字等宽且格式切换不抖动
|
||||
|
||||
#### Scenario: SummaryCard 居中类
|
||||
- **WHEN** SummaryCards 内 Statistic 需要居中
|
||||
- **THEN** Statistic 所在的 Col SHALL 使用 `.summary-stat-col` 类(text-align: center)
|
||||
|
||||
#### Scenario: SummaryCards 行间距类
|
||||
- **WHEN** SummaryCards 容器需要与下方内容保持间距
|
||||
- **THEN** 容器 SHALL 使用 `.summary-cards-row` 类(margin-bottom: var(--td-comp-margin-xl))
|
||||
|
||||
#### Scenario: Drawer 时间控件单行类
|
||||
- **WHEN** Drawer 时间选择器需要单行布局
|
||||
- **THEN** 控件容器 SHALL 使用 `.drawer-time-controls` 类(display: flex; align-items: center; gap: var(--td-comp-margin-m); width: 100%),日期选择器 SHALL 使用 `.drawer-date-range` 类(flex: 1; min-width: 360px)
|
||||
|
||||
#### Scenario: Drawer 时间控件响应式
|
||||
- **WHEN** 视口宽度 ≤ 768px
|
||||
- **THEN** `.drawer-time-controls` SHALL 启用 flex-wrap,`.drawer-date-range` min-width 改为 100%
|
||||
|
||||
#### Scenario: 概览统计卡片类
|
||||
- **WHEN** Drawer 概览统计区渲染
|
||||
- **THEN** 统计卡片 SHALL 使用 `.overview-stat-card` 类(background: var(--td-bg-color-container-hover)),并使用 TDesign Statistic 组件自带的上下布局(title 在上、value 在下),通过 `.summary-stat-col` 类(text-align: center)实现内容居中。系统 SHALL NOT 使用已移除的 `.overview-stat-item` 和 `.overview-stat-value` 类。
|
||||
|
||||
### Requirement: NumberFlow 倒计时样式类
|
||||
styles.css SHALL 定义 NumberFlow 倒计时相关样式类,供 Header 倒计时组件使用。样式 SHALL 继承 TDesign 文本颜色或使用 TDesign CSS tokens,不得使用组件内联 `style`、硬编码色值、`!important` 或覆盖 TDesign 内部类名。
|
||||
|
||||
#### Scenario: 倒计时滚动容器类
|
||||
- **WHEN** Header 自动刷新倒计时以 NumberFlow 形式渲染
|
||||
- **THEN** 倒计时 SHALL 使用集中定义的滚动容器类,保持 inline-flex、baseline 对齐、nowrap 和 tabular-nums
|
||||
|
||||
#### Scenario: 倒计时数字类
|
||||
- **WHEN** NumberFlow 数字渲染
|
||||
- **THEN** 数字 SHALL 使用集中定义的数字类配置 line-height 和 NumberFlow mask CSS 变量,减少滚动边缘突兀感
|
||||
|
||||
#### Scenario: 倒计时单位类
|
||||
- **WHEN** 分钟或秒单位文本渲染
|
||||
- **THEN** 单位 SHALL 使用集中定义的单位类与数字保持基线对齐,并继承当前 TDesign 文本色
|
||||
|
||||
#### Scenario: 不使用内联样式
|
||||
- **WHEN** RefreshCountdown 组件渲染 NumberFlow 倒计时
|
||||
- **THEN** 组件 SHALL 通过 `className` 引用 styles.css 中的样式类,不得通过 React `style` prop 设置 NumberFlow 展示样式
|
||||
|
||||
### Requirement: 异常行背景类
|
||||
styles.css SHALL 定义 DOWN 行的背景色和左侧竖线,使用安全选择器且不使用 `!important`。
|
||||
|
||||
#### Scenario: DOWN 行背景色
|
||||
- **WHEN** 表格行标记为 DOWN 状态
|
||||
- **THEN** 行 SHALL 通过 `.t-table tr.row-down` 选择器获得浅红色背景
|
||||
|
||||
#### Scenario: DOWN 行左侧竖线
|
||||
- **WHEN** 表格行标记为 DOWN 状态
|
||||
- **THEN** 行 SHALL 通过 `.t-table tr.row-down` 选择器获得 `border-left: 3px solid var(--td-error-color)`
|
||||
|
||||
#### Scenario: DOWN 行 hover 状态
|
||||
- **WHEN** 鼠标悬停在 DOWN 行上
|
||||
- **THEN** 行背景 SHALL 通过 `.t-table--hoverable tbody tr.row-down:hover` 选择器显示 hover 状态色
|
||||
|
||||
### Requirement: Dashboard 数据查询
|
||||
Dashboard SHALL 通过 `GET /api/dashboard` 获取首屏总览统计和目标列表数据。
|
||||
|
||||
#### Scenario: 查询 Dashboard 数据
|
||||
- **WHEN** 页面处于打开状态
|
||||
- **THEN** 前端 SHALL 使用 TanStack Query 请求 `GET /api/dashboard?window=24h&recentLimit=30`
|
||||
|
||||
#### Scenario: 统计数据自动刷新
|
||||
- **WHEN** 页面处于打开状态
|
||||
- **THEN** Dashboard 数据 SHALL 通过 TanStack Query 的 refetchInterval=8000 自动刷新
|
||||
|
||||
#### Scenario: 元信息独立查询
|
||||
- **WHEN** 页面需要 checker 类型列表
|
||||
- **THEN** 前端 SHALL 继续通过 `GET /api/meta` 独立查询 checkerTypes
|
||||
|
||||
### Requirement: 总览统计卡片
|
||||
Dashboard SHALL 在页面顶部使用单个 TDesign Card 组件内嵌一行居中的 Statistic 展示总览统计,包含总目标数、正常数、异常数和窗口异常事件数。
|
||||
|
||||
#### Scenario: 展示统计卡片
|
||||
- **WHEN** 用户打开 Dashboard 页面
|
||||
- **THEN** 页面顶部 SHALL 使用单个 TDesign Card(无 shadow、无 bordered)内嵌 TDesign Row/Col 布局展示 4 个居中的 Statistic:全部目标数(color=blue)、正常目标数(color=green)、异常目标数(color=red)、24h 异常事件数(color=orange)
|
||||
|
||||
#### Scenario: 指标居中显示
|
||||
- **WHEN** SummaryCards 渲染
|
||||
- **THEN** 每个 Statistic 所在的 Col SHALL 使用 `.summary-stat-col` 类实现标题和数字居中对齐
|
||||
|
||||
#### Scenario: 异常事件数据来源
|
||||
- **WHEN** SummaryCards 渲染 24h 异常事件数
|
||||
- **THEN** 该数值 SHALL 使用 DashboardResponse.summary.incidents 字段,标题 SHALL 基于当前 window 展示为"24h 异常事件数"
|
||||
|
||||
### Requirement: 页面加载与错误状态
|
||||
Dashboard SHALL 使用 TDesign Skeleton 组件处理首次加载状态,使用 Alert 处理错误。
|
||||
|
||||
#### Scenario: 首次加载
|
||||
- **WHEN** 页面首次加载且数据尚未返回
|
||||
- **THEN** 页面 SHALL 使用 TDesign Skeleton 组件(animation="gradient")展示页面骨架,模拟 Summary 区域和 Table 区域的大致结构
|
||||
|
||||
#### Scenario: API 请求失败
|
||||
- **WHEN** 前端 API 请求失败
|
||||
- **THEN** 页面 SHALL 使用 TDesign Alert 组件(theme=error)显示错误提示
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义历史拨测数据的自动清理机制:可配置的保留时长和定时清理调度。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 数据保留配置
|
||||
系统 SHALL 支持通过 `server.storage.retention` 配置项指定历史数据保留时长,格式为持续时间字符串(`<数字><单位>`,单位支持 `d`/`h`/`m`)。
|
||||
|
||||
#### Scenario: 配置 7 天保留
|
||||
- **WHEN** 配置文件中 `server.storage.retention` 设置为 `"7d"`
|
||||
- **THEN** 系统 SHALL 保留最近 7 天的检查结果,清理更早的数据
|
||||
|
||||
#### Scenario: 配置小时级保留
|
||||
- **WHEN** 配置文件中 `server.storage.retention` 设置为 `"24h"`
|
||||
- **THEN** 系统 SHALL 保留最近 24 小时的检查结果
|
||||
|
||||
#### Scenario: 未配置 retention
|
||||
- **WHEN** 配置文件中未指定 `server.storage.retention`
|
||||
- **THEN** 系统 SHALL 使用默认值 `"7d"`
|
||||
|
||||
#### Scenario: 无效 retention 格式
|
||||
- **WHEN** 配置文件中 `server.storage.retention` 格式不合法(如 `"abc"`、`"7x"`)
|
||||
- **THEN** 系统 SHALL 在配置校验阶段报错,拒绝启动
|
||||
|
||||
### Requirement: 定时清理调度
|
||||
系统 SHALL 以固定间隔(1 小时)定期执行数据清理,删除超过保留时长的历史检查结果,并清理已无关联检查结果的非活跃目标行。
|
||||
|
||||
#### Scenario: 引擎启动后首次清理
|
||||
- **WHEN** ProbeEngine 启动
|
||||
- **THEN** 系统 SHALL 立即执行一次清理,然后每隔 1 小时再次执行
|
||||
|
||||
#### Scenario: 清理执行
|
||||
- **WHEN** 清理定时器触发
|
||||
- **THEN** 系统 SHALL 删除 `check_results` 表中 `timestamp` 早于 `now - retentionMs` 的所有记录
|
||||
|
||||
#### Scenario: 清理空壳非活跃目标
|
||||
- **WHEN** 清理定时器触发且 check_results 过期清理执行完毕
|
||||
- **THEN** 系统 SHALL 删除 `targets` 表中 `active = 0` 且在 `check_results` 表中不存在任何关联记录的目标行
|
||||
|
||||
#### Scenario: 引擎停止时清除定时器
|
||||
- **WHEN** ProbeEngine.stop() 被调用
|
||||
- **THEN** 系统 SHALL 清除清理定时器,不再执行后续清理
|
||||
|
||||
#### Scenario: retention 为 0 时不清理
|
||||
- **WHEN** 配置的 retention 解析为 0 毫秒
|
||||
- **THEN** 系统 SHALL 不注册清理定时器,数据永久保留
|
||||
@@ -1,6 +1,6 @@
|
||||
## Purpose
|
||||
|
||||
定义基于 SQLite 的拨测数据持久化存储:targets 同步(含分组信息)、check_results 追加写入、Dashboard 和 Metrics 数据查询支持、延迟百分位取数、时间范围和分页查询、索引与聚合查询。
|
||||
定义基于 SQLite 的拨测数据持久化存储:targets 同步(含分组信息)、check_results 追加写入、Dashboard 和 Metrics 数据查询支持、延迟百分位取数、时间范围和分页查询、索引与聚合查询、批量查询方法(getLatestChecksMap、getAllTargetStats、getAllRecentSamples)、N+1 查询优化,以及 prepared statement 使用 query() 缓存。
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -225,3 +225,89 @@ ProbeStore SHALL 提供 `prune(retentionMs: number)` 方法,删除超过保留
|
||||
#### Scenario: 活跃目标永不清理
|
||||
- **WHEN** 存在 active=1 的 target 且该 target 在 check_results 表中无关联记录
|
||||
- **THEN** 系统 SHALL NOT 删除该 target 行
|
||||
|
||||
### Requirement: 批量查询最新检查结果
|
||||
系统 SHALL 提供 `getLatestChecksMap` 方法,通过单次 SQL 查询获取所有 target 的最新一次 check 结果,返回 Map 结构供调用方按 target_id 索引。
|
||||
|
||||
#### Scenario: 获取所有目标的最新检查
|
||||
- **WHEN** 调用 `getLatestChecksMap()`
|
||||
- **THEN** 系统 SHALL 执行子查询找到每个 target_id 的 MAX(timestamp),再 JOIN 回 check_results 获取完整行,返回 `Map<number, StoredCheckResult | null>`
|
||||
|
||||
#### Scenario: 批量查询目标无历史记录
|
||||
- **WHEN** 某 target 在 check_results 表中无任何记录
|
||||
- **THEN** 该 target_id 在返回的 Map 中 SHALL 不存在对应的 key
|
||||
|
||||
### Requirement: 批量查询目标统计
|
||||
系统 SHALL 提供 `getAllTargetStats` 方法,通过单次 SQL GROUP BY 聚合查询获取所有 target 的拨测统计(totalChecks 和 availability)。availability 计算精度 SHALL 与 `getTargetStats` 一致,统一使用 `Math.round(value * 100) / 100` 保留两位小数。
|
||||
|
||||
#### Scenario: 获取所有目标的聚合统计
|
||||
- **WHEN** 调用 `getAllTargetStats()`
|
||||
- **THEN** 系统 SHALL 执行 `SELECT target_id, COUNT(*), SUM(CASE WHEN matched=1 THEN 1 ELSE 0 END) FROM check_results GROUP BY target_id`,在内存中计算 availability 并返回 `Map<number, { totalChecks, availability }>`
|
||||
|
||||
#### Scenario: 统计查询目标无历史记录
|
||||
- **WHEN** 某 target 在 check_results 表中无任何记录
|
||||
- **THEN** 该 target_id 在返回的 Map 中 SHALL 不存在对应的 key
|
||||
|
||||
#### Scenario: availability 精度
|
||||
- **WHEN** 计算 availability(upCount / totalChecks * 100)
|
||||
- **THEN** 结果 SHALL 使用 `Math.round(value * 100) / 100` 四舍五入保留两位小数,与 `getTargetStats` 方法一致
|
||||
|
||||
### Requirement: 批量查询所有目标的最近采样数据
|
||||
系统 SHALL 提供 `getAllRecentSamples(limit: number)` 方法,通过单次 SQL 查询获取所有 target 的最近 N 条采样数据,返回 `Map<number, Array<{ timestamp: string; duration_ms: number | null; matched: number }>>` 结构。
|
||||
|
||||
#### Scenario: 获取所有目标的最近采样
|
||||
- **WHEN** 调用 `getAllRecentSamples(30)`
|
||||
- **THEN** 系统 SHALL 通过单次 SQL 查询获取每个 target 最近 30 条记录,返回按 target_id 索引的 Map
|
||||
|
||||
#### Scenario: 采样查询目标无历史记录
|
||||
- **WHEN** 某 target 在 check_results 表中无任何记录
|
||||
- **THEN** 该 target_id 在返回的 Map 中 SHALL 不存在对应的 key
|
||||
|
||||
#### Scenario: 采样数据排序
|
||||
- **WHEN** 获取采样数据
|
||||
- **THEN** 每个 target 的记录 SHALL 按 timestamp 降序排列(最新在前)
|
||||
|
||||
#### Scenario: 采样目标无数据返回空数组
|
||||
- **WHEN** 某 target 在 getAllRecentSamples 返回的 Map 中不存在
|
||||
- **THEN** 该 target 的 recentSamples SHALL 为空数组
|
||||
|
||||
### Requirement: summary 查询使用批量方法
|
||||
`getSummary` 方法 SHALL 使用 `getLatestChecksMap` 一次性获取所有 target 的最新检查结果,而非对每个 target 逐条查询。
|
||||
|
||||
#### Scenario: 统计总览使用批量查询
|
||||
- **WHEN** 调用 `store.getSummary()`
|
||||
- **THEN** 系统 SHALL 调用 `getLatestChecksMap()` 一次获取所有最新结果,在内存中遍历统计 up/down 数量,而非循环 N 次调用 `getLatestCheck()`
|
||||
|
||||
### Requirement: targets 列表使用批量方法
|
||||
`handleTargets`(routes/targets.ts 中生成 TargetStatus[] 的逻辑)SHALL 使用 `getLatestChecksMap`、`getAllTargetStats` 和 `getAllRecentSamples` 替代逐目标查询,消除 N+1 查询。
|
||||
|
||||
#### Scenario: 目标列表使用批量查询
|
||||
- **WHEN** 处理 `GET /api/targets` 请求
|
||||
- **THEN** 系统 SHALL 分别调用 `getLatestChecksMap()`、`getAllTargetStats()`、`getAllRecentSamples(30)` 批量获取数据,在内存中组装 TargetStatus 数组,而非对每个 target 逐条查询数据库
|
||||
|
||||
### Requirement: prepared statement 使用 query() 缓存
|
||||
ProbeStore 中不涉及事务内复用的单次读/写操作 SHALL 使用 `this.db.query()` 而非 `this.db.prepare()`,利用 bun:sqlite 内置的 statement 缓存机制。
|
||||
|
||||
#### Scenario: insertCheckResult 使用 query
|
||||
- **WHEN** 写入一条检查结果
|
||||
- **THEN** `insertCheckResult` SHALL 使用 `this.db.query("INSERT INTO ...").run(...)` 而非 `this.db.prepare("INSERT INTO ...").run(...)`
|
||||
|
||||
#### Scenario: getHistory 查询使用 query
|
||||
- **WHEN** 查询历史记录(包括 COUNT 和分页查询)
|
||||
- **THEN** `getHistory` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
|
||||
|
||||
#### Scenario: getTargetStats 查询使用 query
|
||||
- **WHEN** 查询单目标统计
|
||||
- **THEN** `getTargetStats` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
|
||||
|
||||
#### Scenario: getTrend 查询使用 query
|
||||
- **WHEN** 查询趋势数据
|
||||
- **THEN** `getTrend` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
|
||||
|
||||
#### Scenario: getRecentSamples 查询使用 query
|
||||
- **WHEN** 查询采样数据
|
||||
- **THEN** `getRecentSamples` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
|
||||
|
||||
#### Scenario: syncTargets 事务保持 prepare(例外)
|
||||
- **WHEN** 同步 targets 配置(事务内多次复用 insertStmt/updateStmt/deleteStmt)
|
||||
- **THEN** `syncTargets` 方法 SHALL 保持使用 `this.db.prepare()`,因需要在事务闭包内持有引用
|
||||
@@ -1,25 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 ESLint 与 Prettier 集成方案,通过 eslint-plugin-prettier 将 Prettier 格式检查统一纳入 ESLint 工作流,减少独立工具调用,提升开发体验。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: ESLint 集成 Prettier 格式检查
|
||||
|
||||
项目 SHALL 通过 `eslint-plugin-prettier` 将 Prettier 格式规则集成为 ESLint 规则,使单次 `eslint .` 运行同时报告代码质量问题和格式问题。ESLint 配置 SHALL 注册 `plugin:prettier/recommended` 规则集,该规则集自动加载 Prettier 配置并禁用与 Prettier 冲突的 ESLint 规则。
|
||||
|
||||
#### Scenario: lint 运行同时检查格式
|
||||
- **WHEN** 开发者运行 `bun run lint`
|
||||
- **THEN** ESLint SHALL 同时报告代码质量违规和 Prettier 格式违规
|
||||
|
||||
#### Scenario: lint --fix 自动格式化
|
||||
- **WHEN** 开发者运行 `eslint --fix` 或 lint-staged 自动触发 `eslint --fix`
|
||||
- **THEN** ESLint SHALL 对可自动修复的代码质量问题和格式问题一并修正,包括调用 Prettier 重写文件格式
|
||||
|
||||
#### Scenario: Prettier 配置被正确读取
|
||||
- **WHEN** ESLint 通过 eslint-plugin-prettier 检查代码格式
|
||||
- **THEN** 检查结果 SHALL 与独立运行 `prettier --check` 的结果完全一致
|
||||
|
||||
#### Scenario: 不因集成降低 lint 性能
|
||||
- **WHEN** 开发者运行 `bun run lint`
|
||||
- **THEN** 运行时间 SHOULD 不超过原 `eslint .` + `prettier --check` 总耗时的 120%
|
||||
@@ -1,209 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 HTTP 拨测中响应体校验方法集(contains/regex/json/css/xpath)、操作符系统和响应头校验的行为规范。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 响应体多种校验方法
|
||||
系统 SHALL 支持通过共享 `ContentExpectations` 对 HTTP 响应体进行有序内容校验。`expect.body` MUST 为 expectation 数组。每个 expectation SHALL 为直接 `ValueMatcher`,或 `json`、`css`、`xpath` extractor expectation 之一。直接 matcher SHALL 作用于完整响应体文本。`json` SHALL 解析响应体为 JSON 并用 JSONPath 子集提取值。`css` SHALL 使用 CSS selector 从 HTML 中提取元素文本或属性。`xpath` SHALL 使用 XPath 从 XML/HTML 中提取节点值。Extractor 未配置 matcher 时,resolve 阶段 SHALL 将其 matcher 物化为 `{exists: true}`。
|
||||
|
||||
#### Scenario: contains 子串匹配
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{contains: "healthy"}]`,且响应体包含 `"healthy"`
|
||||
- **THEN** 系统 SHALL 判定该 body expectation 通过
|
||||
|
||||
#### Scenario: regex 正则匹配
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{regex: '"status"\\s*:\\s*"ok"'}]`,且响应体匹配该正则
|
||||
- **THEN** 系统 SHALL 判定该 body expectation 通过
|
||||
|
||||
#### Scenario: json JSONPath 等值匹配
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{json: {path: "$.status", equals: "ok"}}]`,且响应 JSON 中 `$.status` 值为 `"ok"`
|
||||
- **THEN** 系统 SHALL 判定该 body expectation 通过
|
||||
|
||||
#### Scenario: json JSONPath 存在性匹配
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{json: {path: "$.status"}}]`,且响应 JSON 中存在 `$.status`
|
||||
- **THEN** resolve 阶段 SHALL 将该 expectation 按 `exists: true` 语义物化并在运行期判定通过
|
||||
|
||||
#### Scenario: css 选择器匹配
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{css: {selector: "div#health", equals: "OK"}}]`,且 HTML 中存在 `div#health` 元素文本为 `"OK"`
|
||||
- **THEN** 系统 SHALL 判定该 body expectation 通过
|
||||
|
||||
#### Scenario: css 选择器匹配属性值
|
||||
- **WHEN** HTTP target 配置 css expectation 带 `attr: "content"` 用于提取属性,且属性值匹配期望
|
||||
- **THEN** 系统 SHALL 判定该 body expectation 通过
|
||||
|
||||
#### Scenario: xpath 表达式匹配
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{xpath: {path: "/root/status/text()", equals: "ok"}}]`,且 XML 中 `/root/status` 节点文本为 `"ok"`
|
||||
- **THEN** 系统 SHALL 判定该 body expectation 通过
|
||||
|
||||
#### Scenario: 提取器无匹配目标失败
|
||||
- **WHEN** HTTP target 配置了 json、css 或 xpath expectation 且对应路径、元素或节点不存在,并且 expectation 未配置 `exists: false`
|
||||
- **THEN** 系统 SHALL 判定 matched 为 false
|
||||
|
||||
### Requirement: 多种 body 校验方法 AND 组合
|
||||
系统 SHALL 支持在 `expect.body` 数组中同时配置多条内容 expectation,所有 expectation 均通过时 matched 方为 true。系统 SHALL 按数组顺序执行 expectation,任一 expectation 失败后 MUST NOT 继续执行后续 expectation。
|
||||
|
||||
#### Scenario: 多种方法全部通过
|
||||
- **WHEN** HTTP target 的 `expect.body` 数组依次配置 contains、json、regex,且全部通过
|
||||
- **THEN** 系统 SHALL 判定 matched 为 true
|
||||
|
||||
#### Scenario: 多种方法任一失败
|
||||
- **WHEN** HTTP target 的 `expect.body` 数组第一条 contains 不通过,后续还有 json expectation
|
||||
- **THEN** 系统 SHALL 判定 matched 为 false,且不再检查后续 json expectation
|
||||
|
||||
#### Scenario: 直接 matcher 多字段组合
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{contains: "ok", regex: "status"}]`,且响应体同时满足 contains 和 regex
|
||||
- **THEN** 系统 SHALL 判定该 expectation 通过
|
||||
|
||||
### Requirement: 操作符系统
|
||||
系统 SHALL 支持通过共享 `ValueMatcher` 对提取值和文本值进行比较:`equals`(深度等值)、`contains`(子串包含)、`regex`(正则匹配)、`empty`(空值判断)、`exists`(存在性判断)、`gte`/`lte`/`gt`/`lt`(数值比较)。系统 MUST NOT 支持旧 `match` 字段。
|
||||
|
||||
#### Scenario: equals 匹配 JSON value
|
||||
- **WHEN** 配置 `{equals: {status: "ok"}}`,且实际值为相同 JSON object
|
||||
- **THEN** 系统 SHALL 使用深度相等判定通过
|
||||
|
||||
#### Scenario: 显式 contains matcher
|
||||
- **WHEN** 配置 `{contains: "success"}`,且实际值字符串化后包含 `"success"`
|
||||
- **THEN** 系统 SHALL 判定该规则通过
|
||||
|
||||
#### Scenario: 显式 regex matcher
|
||||
- **WHEN** 配置 `{regex: '\\d+\\.\\d+\\.\\d+'}`,且实际值字符串化后匹配该正则
|
||||
- **THEN** 系统 SHALL 判定该规则通过
|
||||
|
||||
#### Scenario: empty matcher 判断为空
|
||||
- **WHEN** 配置 `{empty: true}`,且实际值为空数组 `[]`
|
||||
- **THEN** 系统 SHALL 判定该规则通过
|
||||
|
||||
#### Scenario: exists matcher 判断不存在
|
||||
- **WHEN** 配置 `{exists: false}`,且实际值为 `undefined`
|
||||
- **THEN** 系统 SHALL 判定该规则通过
|
||||
|
||||
#### Scenario: 数值比较 matcher
|
||||
- **WHEN** 配置 `{gt: 0, lt: 1000}`,且实际值为 `500`
|
||||
- **THEN** 系统 SHALL 对同一字段进行多 matcher 复合比较,全部通过则该规则通过
|
||||
|
||||
### Requirement: 响应头校验
|
||||
系统 SHALL 支持通过共享 `KeyedExpectations` 配置 `expect.headers` 对 HTTP 响应头进行键值断言,header 名称匹配 MUST 不区分大小写。header 期望值 MUST 为 `RawValueExpectation`,primitive 字面量 SHALL 在 resolve 阶段等价为 `{equals: <value>}`。
|
||||
|
||||
#### Scenario: 响应头字面量匹配
|
||||
- **WHEN** HTTP target 配置 `expect.headers: {"Content-Type": "application/json"}`,且响应包含该 header 且值精确匹配
|
||||
- **THEN** 系统 SHALL 判定 headers 阶段通过
|
||||
|
||||
#### Scenario: 响应头 matcher 匹配
|
||||
- **WHEN** HTTP target 配置 `expect.headers: {"Content-Type": {contains: "application/json"}}`,且响应 header 值包含该文本
|
||||
- **THEN** 系统 SHALL 判定 headers 阶段通过
|
||||
|
||||
#### Scenario: 响应头缺失
|
||||
- **WHEN** HTTP target 配置了某个 header 但响应中不存在该 header,且未配置 `exists: false`
|
||||
- **THEN** 系统 SHALL 判定 matched 为 false
|
||||
|
||||
### Requirement: 结构化 expect 失败信息
|
||||
系统 SHALL 在任一 expect 规则失败时生成结构化 failure,用于标识失败阶段、规则路径、期望值、实际值和可读错误信息。actual 值 SHALL 在构造时截断至不超过 200 字符,超出部分以省略标记和总字符数替代。expected 值不截断。
|
||||
|
||||
#### Scenario: body 规则失败信息
|
||||
- **WHEN** HTTP target 的 `expect.body[1].json` 规则失败
|
||||
- **THEN** failure SHALL 包含 kind=`mismatch`、phase=`body`、path 指向 `expect.body[1]`,并包含 message
|
||||
|
||||
#### Scenario: actual 值截断
|
||||
- **WHEN** 失败规则的实际值为字符串且长度超过 200 字符
|
||||
- **THEN** failure.actual SHALL 为前 200 字符加 `…(共 N 字符)` 后缀,其中 N 为原始总字符数
|
||||
|
||||
#### Scenario: actual 值未超限
|
||||
- **WHEN** 失败规则的实际值为字符串且长度不超过 200 字符
|
||||
- **THEN** failure.actual SHALL 保留完整原始值,不做截断
|
||||
|
||||
#### Scenario: actual 值为对象或数组
|
||||
- **WHEN** 失败规则的实际值为对象或数组,且 JSON 序列化后长度超过 200 字符
|
||||
- **THEN** failure.actual SHALL 为序列化后前 200 字符加 `…(共 N 字符)` 后缀
|
||||
|
||||
#### Scenario: actual 值为标量
|
||||
- **WHEN** 失败规则的实际值为 number、boolean、null 或 undefined
|
||||
- **THEN** failure.actual SHALL 保留原始值,不做截断
|
||||
|
||||
### Requirement: 状态码范围匹配
|
||||
系统 SHALL 支持在 `expect.status` 数组中使用范围模式字符串(`"1xx"`、`"2xx"`、`"3xx"`、`"4xx"`、`"5xx"`),与精确数字混合使用。范围模式 SHALL 匹配对应百位段内的所有状态码。其他范围模式 SHALL 在启动期配置校验失败。
|
||||
|
||||
#### Scenario: 1xx 范围匹配
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["1xx"]`,且响应状态码为 101
|
||||
- **THEN** 系统 SHALL 判定状态码匹配
|
||||
|
||||
#### Scenario: 2xx 范围匹配
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]`,且响应状态码为 200
|
||||
- **THEN** 系统 SHALL 判定状态码匹配
|
||||
|
||||
#### Scenario: 2xx 范围匹配 204
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]`,且响应状态码为 204
|
||||
- **THEN** 系统 SHALL 判定状态码匹配
|
||||
|
||||
#### Scenario: 2xx 范围不匹配 301
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]`,且响应状态码为 301
|
||||
- **THEN** 系统 SHALL 判定状态码不匹配
|
||||
|
||||
#### Scenario: 混合精确值与范围模式
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["2xx", 301]`,且响应状态码为 301
|
||||
- **THEN** 系统 SHALL 判定状态码匹配(精确值 301 匹配)
|
||||
|
||||
#### Scenario: 混合精确值与范围模式范围命中
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["2xx", 301]`,且响应状态码为 204
|
||||
- **THEN** 系统 SHALL 判定状态码匹配(2xx 范围命中)
|
||||
|
||||
#### Scenario: 5xx 范围匹配
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["5xx"]`,且响应状态码为 503
|
||||
- **THEN** 系统 SHALL 判定状态码匹配
|
||||
|
||||
#### Scenario: 非 HTTP 范围模式启动失败
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["6xx"]`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败
|
||||
|
||||
### Requirement: HTTP expect 规则启动期校验
|
||||
系统 SHALL 在启动期校验 HTTP expect 中已支持字段的类型、格式、未知字段和可编译表达式。HTTP expect SHALL 只允许 `status`、`headers`、`body` 和 `durationMs` 字段。`expect.body` MUST 为 `RawContentExpectations` 数组。直接 `ValueMatcher` 对象 MUST 至少包含一个合法 matcher。Extractor expectation MUST 只包含 `json`、`css`、`xpath` 中的一种 extractor。Extractor 内部可以不配置 matcher,并 SHALL 在 resolve 阶段以存在性 matcher 作为通过语义。`equals` matcher SHALL 支持任意 JSON value,包括数组和对象。系统 SHALL 在启动期对所有 `regex` 执行静态 ReDoS 检测。语义校验 MUST NOT 修改 Raw HTTP expect 输入。
|
||||
|
||||
#### Scenario: body rule 使用 regex 字段
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{regex: "ok|healthy"}]` 且 regex 可编译且无 ReDoS 风险
|
||||
- **THEN** 系统 SHALL 接受该配置,并在运行期按 regex expectation 匹配响应体
|
||||
|
||||
#### Scenario: body rule 不支持 match 字段
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{match: "ok"}]`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示 `match` 是未知字段或不支持字段
|
||||
|
||||
#### Scenario: body rule 多 extractor 非法
|
||||
- **WHEN** HTTP target 的同一条 body expectation 同时配置 `json` 和 `css`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败
|
||||
|
||||
#### Scenario: matcher regex 正则非法
|
||||
- **WHEN** HTTP target 的 expect.headers、body 直接 matcher 或 extractor 内部 matcher 配置了不可编译的 regex
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败
|
||||
|
||||
#### Scenario: matcher 数值比较类型非法
|
||||
- **WHEN** HTTP target 的 matcher 配置 gt、gte、lt 或 lte,且对应值不是有限数字
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败
|
||||
|
||||
#### Scenario: matcher 布尔类型非法
|
||||
- **WHEN** HTTP target 的 matcher 配置 empty 或 exists,且对应值不是布尔值
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败
|
||||
|
||||
#### Scenario: JSONPath 子集非法
|
||||
- **WHEN** HTTP target 的 json body expectation path 不符合系统支持的 JSONPath 子集
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败
|
||||
|
||||
#### Scenario: matcher 未知字段非法
|
||||
- **WHEN** HTTP target 的 matcher 配置了 `foo: "bar"` 等未知字段
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败
|
||||
|
||||
#### Scenario: durationMs matcher 非法
|
||||
- **WHEN** HTTP target 配置 `expect.durationMs` 不是合法 `RawValueExpectation` 或其中数值 matcher 不是有限数字
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败
|
||||
|
||||
### Requirement: HTTP body 运行期失败结构化
|
||||
系统 SHALL 将 HTTP body 运行期失败记录为结构化 CheckFailure,并保留与具体 expectation 相关的 phase 和 path。响应内容不符合配置 SHALL 记录为 mismatch;响应内容无法按配置解析或解码 SHALL 记录为 error。
|
||||
|
||||
#### Scenario: JSON 响应不是合法 JSON
|
||||
- **WHEN** HTTP target 配置 json body expectation,但响应体不是合法 JSON
|
||||
- **THEN** 系统 SHALL 记录 `failure.kind="error"`、`failure.phase="body"`,且 failure.path SHALL 指向对应 json expectation
|
||||
|
||||
#### Scenario: CSS selector 无匹配元素
|
||||
- **WHEN** HTTP target 配置 css body expectation,但响应 HTML 中无匹配元素
|
||||
- **THEN** 系统 SHALL 记录 `failure.kind="mismatch"`、`failure.phase="body"`,且 failure.path SHALL 指向对应 css expectation
|
||||
|
||||
#### Scenario: XPath 无匹配节点
|
||||
- **WHEN** HTTP target 配置 xpath body expectation,但响应 XML/HTML 中无匹配节点
|
||||
- **THEN** 系统 SHALL 记录 `failure.kind="mismatch"`、`failure.phase="body"`,且 failure.path SHALL 指向对应 xpath expectation
|
||||
@@ -1,6 +1,6 @@
|
||||
## Purpose
|
||||
|
||||
定义共享 expect 断言规则系统的核心概念和基础设施:ValueMatcher 统一匹配器、ContentExpectations 内容断言数组、KeyedExpectations 键控断言数组、以及相关的启动期校验和失败路径规范。
|
||||
定义共享 expect 断言规则系统的核心概念和基础设施:ValueMatcher 统一匹配器、ContentExpectations 内容断言数组、KeyedExpectations 键控断言数组、HTTP 场景特有规则(状态码范围匹配、body 运行期失败结构化)、以及相关的启动期校验和失败路径规范。
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -249,3 +249,53 @@
|
||||
#### Scenario: actual 截断
|
||||
- **WHEN** matcher 失败时 actual 字符串长度超过 200 字符
|
||||
- **THEN** 系统 SHALL 使用现有截断策略保存 failure.actual,避免历史记录写入过长内容
|
||||
|
||||
### Requirement: 状态码范围匹配
|
||||
系统 SHALL 支持在 `expect.status` 数组中使用范围模式字符串(`"1xx"`、`"2xx"`、`"3xx"`、`"4xx"`、`"5xx"`),与精确数字混合使用。范围模式 SHALL 匹配对应百位段内的所有状态码。其他范围模式 SHALL 在启动期配置校验失败。
|
||||
|
||||
#### Scenario: 1xx 范围匹配
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["1xx"]`,且响应状态码为 101
|
||||
- **THEN** 系统 SHALL 判定状态码匹配
|
||||
|
||||
#### Scenario: 2xx 范围匹配
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]`,且响应状态码为 200
|
||||
- **THEN** 系统 SHALL 判定状态码匹配
|
||||
|
||||
#### Scenario: 2xx 范围匹配 204
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]`,且响应状态码为 204
|
||||
- **THEN** 系统 SHALL 判定状态码匹配
|
||||
|
||||
#### Scenario: 2xx 范围不匹配 301
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]`,且响应状态码为 301
|
||||
- **THEN** 系统 SHALL 判定状态码不匹配
|
||||
|
||||
#### Scenario: 混合精确值与范围模式
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["2xx", 301]`,且响应状态码为 301
|
||||
- **THEN** 系统 SHALL 判定状态码匹配(精确值 301 匹配)
|
||||
|
||||
#### Scenario: 混合精确值与范围模式范围命中
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["2xx", 301]`,且响应状态码为 204
|
||||
- **THEN** 系统 SHALL 判定状态码匹配(2xx 范围命中)
|
||||
|
||||
#### Scenario: 5xx 范围匹配
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["5xx"]`,且响应状态码为 503
|
||||
- **THEN** 系统 SHALL 判定状态码匹配
|
||||
|
||||
#### Scenario: 非 HTTP 范围模式启动失败
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["6xx"]`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败
|
||||
|
||||
### Requirement: HTTP body 运行期失败结构化
|
||||
系统 SHALL 将 HTTP body 运行期失败记录为结构化 CheckFailure,并保留与具体 expectation 相关的 phase 和 path。响应内容不符合配置 SHALL 记录为 mismatch;响应内容无法按配置解析或解码 SHALL 记录为 error。
|
||||
|
||||
#### Scenario: JSON 响应不是合法 JSON
|
||||
- **WHEN** HTTP target 配置 json body expectation,但响应体不是合法 JSON
|
||||
- **THEN** 系统 SHALL 记录 `failure.kind="error"`、`failure.phase="body"`,且 failure.path SHALL 指向对应 json expectation
|
||||
|
||||
#### Scenario: CSS selector 无匹配元素
|
||||
- **WHEN** HTTP target 配置 css body expectation,但响应 HTML 中无匹配元素
|
||||
- **THEN** 系统 SHALL 记录 `failure.kind="mismatch"`、`failure.phase="body"`,且 failure.path SHALL 指向对应 css expectation
|
||||
|
||||
#### Scenario: XPath 无匹配节点
|
||||
- **WHEN** HTTP target 配置 xpath body expectation,但响应 XML/HTML 中无匹配节点
|
||||
- **THEN** 系统 SHALL 记录 `failure.kind="mismatch"`、`failure.phase="body"`,且 failure.path SHALL 指向对应 xpath expectation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## Purpose
|
||||
|
||||
定义基于 Vite dev server + Bun API server 的前端开发工作流、开发期 API 访问和共享契约的行为要求。
|
||||
定义基于 Vite dev server + Bun API server 的前端开发工作流、生产前端构建与 code splitting 策略、开发期 API 访问和共享契约的行为要求。
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -15,6 +15,66 @@
|
||||
- **WHEN** 开发者运行前端生产构建命令
|
||||
- **THEN** 系统 SHALL 通过 Vite build(Rolldown)产出优化的前端静态资源到 `dist/web/`
|
||||
|
||||
### Requirement: Vite 前端构建配置
|
||||
系统 SHALL 使用 Vite 作为前端构建工具,配置文件位于项目根目录 `vite.config.ts`,以 `src/web` 为 root,产出到 `dist/web/`。
|
||||
|
||||
#### Scenario: 运行 Vite 生产构建
|
||||
- **WHEN** 构建脚本执行 `bunx --bun vite build`
|
||||
- **THEN** Vite SHALL 将 `src/web/index.html` 及其引用的所有模块构建到 `dist/web/` 目录,包含 `index.html` 和 `assets/` 子目录
|
||||
|
||||
#### Scenario: 产出文件名包含 content hash
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** `assets/` 目录下的 JS 和 CSS 文件名 SHALL 包含 content hash(如 `index-a1b2c3.js`)
|
||||
|
||||
### Requirement: Code Splitting 策略
|
||||
系统 SHALL 配置 Vite 的 Rolldown code splitting,将 vendor 库分离为独立 chunks,并通过 `React.lazy()` 动态导入实现按需加载。
|
||||
|
||||
#### Scenario: React 相关库分离
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** `react`、`react-dom`、`scheduler` SHALL 被打包到名为 `vendor-react` 的独立 chunk
|
||||
|
||||
#### Scenario: TDesign 相关库分离
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** `tdesign-react`、`tdesign-icons-react` 相关模块 SHALL 被打包到名为 `vendor-tdesign` 的独立 chunk
|
||||
|
||||
#### Scenario: 图表库分离
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** `recharts` 和 `d3-*` 相关模块 SHALL 被打包到名为 `vendor-chart` 的独立 chunk
|
||||
|
||||
#### Scenario: TargetDetailDrawer 延迟加载
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** `TargetDetailDrawer` 及其依赖(recharts、D3、DateRangePicker 等)SHALL 通过 `React.lazy()` 动态导入,被 Rolldown 自动拆分为异步 chunk,不包含在初始加载的 JS 中
|
||||
|
||||
#### Scenario: Drawer 首次渲染无闪烁
|
||||
- **WHEN** 用户首次点击目标触发 Drawer 渲染
|
||||
- **THEN** Drawer SHALL 通过 `<Suspense fallback={null}>` 包裹,利用其默认 visible=false 状态避免加载期间的视觉闪烁
|
||||
|
||||
### Requirement: CSS 处理
|
||||
系统 SHALL 通过 Vite 处理 CSS 导入,产出独立的 CSS 文件。TDesign 组件样式 SHALL 保持全量导入方式。
|
||||
|
||||
#### Scenario: CSS 文件产出
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** 所有 CSS 导入 SHALL 被提取为独立的 `.css` 文件到 `assets/` 目录
|
||||
|
||||
#### Scenario: CSS 压缩
|
||||
- **WHEN** Vite 执行生产构建
|
||||
- **THEN** 产出的 CSS 文件 SHALL 经过压缩处理
|
||||
|
||||
#### Scenario: TDesign CSS 全量导入
|
||||
- **WHEN** 前端入口文件初始化样式
|
||||
- **THEN** 系统 SHALL 通过 `tdesign-react/dist/reset.css` 和 `tdesign-react/dist/tdesign.min.css` 全量导入 TDesign 组件样式
|
||||
|
||||
### Requirement: 前端构建产物拆分
|
||||
前端生产构建 SHALL 将 vendor 依赖拆分为独立 chunk,利用浏览器并行加载和长期缓存。
|
||||
|
||||
#### Scenario: vendor chunk 拆分
|
||||
- **WHEN** 执行前端生产构建
|
||||
- **THEN** 构建产物 SHALL 包含独立的 vendor chunk(react、tdesign、recharts 各自独立),而非单个 bundle
|
||||
|
||||
#### Scenario: 业务代码变更不影响 vendor 缓存
|
||||
- **WHEN** 仅修改业务代码(src/web/ 下非 node_modules 文件)并重新构建
|
||||
- **THEN** vendor chunk 的文件名(含 hash)SHALL 保持不变,浏览器缓存 SHALL 继续有效
|
||||
|
||||
### Requirement: 前端开发期 API 代理
|
||||
前端开发服务器 SHALL 通过 Vite proxy 配置将 API 请求转发到后端 server。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## Purpose
|
||||
|
||||
定义基于 Vite + Bun 的全栈应用运行时,包括 HTTP server、API 命名空间、健康检查、生产静态资源服务和 SPA fallback 行为。
|
||||
定义基于 Vite + Bun 的全栈应用运行时,包括统一服务 bootstrap、声明式 Bun routes、API 路由组织、HTTP server、API 命名空间、健康检查、生产静态资源服务和 SPA fallback 行为。
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -113,3 +113,108 @@
|
||||
#### Scenario: SIGINT/SIGTERM 处理
|
||||
- **WHEN** 开发模式进程收到 SIGINT 或 SIGTERM 信号
|
||||
- **THEN** 系统 SHALL 调用 engine.stop() 停止调度、调用 store.close() 关闭数据库连接后退出进程
|
||||
|
||||
### Requirement: 路径参数支持
|
||||
系统 SHALL 使用 routes 对象的 `:param` 语法声明路径参数,替代手动 regex 匹配。
|
||||
|
||||
#### Scenario: 带路径参数的 API 路由
|
||||
- **WHEN** 客户端请求 `/api/targets/123/history`
|
||||
- **THEN** 系统 SHALL 通过 `routes` 中注册的 `/api/targets/:id/history` 匹配,并通过 `req.params.id` 获取参数值 `"123"`
|
||||
|
||||
#### Scenario: 路径参数类型
|
||||
- **WHEN** route handler 接收到路径参数
|
||||
- **THEN** 参数值 SHALL 为字符串类型,handler 负责进行类型转换和校验
|
||||
|
||||
### Requirement: HTTP Method 声明
|
||||
系统 SHALL 在 routes 对象中为每个 API 端点以 per-method handler 形式声明支持的 HTTP method;未匹配 method 的 API 请求 SHALL 落入 `/api/*` 通配符并返回 JSON 404。
|
||||
|
||||
#### Scenario: 单 method 端点
|
||||
- **WHEN** API 端点只支持 GET 方法
|
||||
- **THEN** 该端点 SHALL 以 `{ GET(req) { ... } }` 形式注册
|
||||
|
||||
#### Scenario: 不支持的 method 请求
|
||||
- **WHEN** 客户端使用未声明的 method 请求 API 端点
|
||||
- **THEN** `/api/*` 通配符 SHALL 返回 JSON 格式的 404 错误响应
|
||||
|
||||
### Requirement: 路由按职责拆分
|
||||
系统 SHALL 将 HTTP 路由处理逻辑按 API 端点拆分为独立模块,每个模块导出 route handler 函数供 routes 对象统一注册。
|
||||
|
||||
#### Scenario: health 端点独立路由
|
||||
- **WHEN** 客户端请求 `GET /health`
|
||||
- **THEN** `routes/health.ts` 导出的 handler 负责处理,返回 HealthResponse JSON
|
||||
|
||||
#### Scenario: summary 端点独立路由
|
||||
- **WHEN** 客户端请求 `GET /api/summary`
|
||||
- **THEN** `routes/summary.ts` 导出的 handler 负责处理,委托 store 查询并返回 SummaryResponse JSON
|
||||
|
||||
#### Scenario: targets 端点独立路由
|
||||
- **WHEN** 客户端请求 `GET /api/targets`
|
||||
- **THEN** `routes/targets.ts` 导出的 handler 负责处理,委托 store 查询并返回 TargetStatus[] JSON
|
||||
|
||||
#### Scenario: history 端点独立路由
|
||||
- **WHEN** 客户端请求 `GET /api/targets/:id/history?from=ISO&to=ISO`
|
||||
- **THEN** `routes/history.ts` 导出的 handler 负责处理,通过 `req.params.id` 获取路径参数,包含参数校验、store 查询和 HistoryResponse 返回
|
||||
|
||||
#### Scenario: trend 端点独立路由
|
||||
- **WHEN** 客户端请求 `GET /api/targets/:id/trend?from=ISO&to=ISO`
|
||||
- **THEN** `routes/trend.ts` 导出的 handler 负责处理,通过 `req.params.id` 获取路径参数,包含参数校验、store 查询和 TrendPoint[] 返回
|
||||
|
||||
### Requirement: 共享辅助函数集中管理
|
||||
系统 SHALL 将跨路由共享的响应格式化函数抽取到 helpers.ts 模块,单一职责、集中管理。
|
||||
|
||||
#### Scenario: createApiError 集中定义
|
||||
- **WHEN** 任意路由需要返回 API 错误响应
|
||||
- **THEN** 从 `helpers.ts` 导入 `createApiError` 函数,提供错误信息和状态码
|
||||
|
||||
#### Scenario: jsonResponse 集中定义
|
||||
- **WHEN** 任意路由需要返回 JSON 响应
|
||||
- **THEN** 从 `helpers.ts` 导入 `jsonResponse` 函数,处理 HEAD 方法、Content-Type 和安全头
|
||||
|
||||
#### Scenario: mapCheckResult 集中定义
|
||||
- **WHEN** 需要将 StoredCheckResult 映射为 API CheckResult
|
||||
- **THEN** 从 `helpers.ts` 导入 `mapCheckResult` 函数,处理 failure JSON 解析和格式转换
|
||||
|
||||
### Requirement: 统一启动引导函数
|
||||
系统 SHALL 提供 `src/server/bootstrap.ts` 导出 `bootstrap(options: BootstrapOptions)` 函数,封装完整的服务启动序列:加载配置、初始化正式 logger、创建 store、同步 targets、创建并启动 engine、启动 HTTP server、注册 shutdown handler。配置加载失败发生在正式 logger 初始化之前,系统 SHALL 使用 console fallback 输出启动失败信息。配置加载成功后的启动失败 SHALL 使用正式 logger 输出 `fatal` 后退出。
|
||||
|
||||
#### Scenario: 开发模式启动
|
||||
- **WHEN** `dev.ts` 调用 `bootstrap({ configPath, mode: "development" })`
|
||||
- **THEN** 系统 SHALL 完成完整启动序列,不传入 staticAssets,并初始化运行时 logger
|
||||
|
||||
#### Scenario: 生产模式启动(带静态资源)
|
||||
- **WHEN** code-generated entry 调用 `bootstrap({ configPath, mode: "production", staticAssets })`
|
||||
- **THEN** 系统 SHALL 完成完整启动序列,将 staticAssets 传递给 startServer,并初始化运行时 logger
|
||||
|
||||
#### Scenario: 配置加载失败处理
|
||||
- **WHEN** 配置文件读取、YAML 解析或配置校验失败
|
||||
- **THEN** 系统 SHALL 通过 console fallback 输出错误信息并以非零退出码退出进程
|
||||
|
||||
#### Scenario: 配置加载后的启动失败处理
|
||||
- **WHEN** logger、store、engine 或 HTTP server 初始化失败
|
||||
- **THEN** 系统 SHALL 通过正式 logger 输出 `fatal` 日志并以非零退出码退出进程
|
||||
|
||||
#### Scenario: 优雅关机
|
||||
- **WHEN** 进程收到 SIGINT 或 SIGTERM 信号
|
||||
- **THEN** bootstrap 注册的 shutdown handler SHALL 调用 engine.stop()、store.close() 和 logger.flush() 后退出
|
||||
|
||||
### Requirement: BootstrapOptions 接口
|
||||
`bootstrap` 函数 SHALL 接受 `BootstrapOptions` 参数,包含 `configPath: string`、`mode: RuntimeMode` 和可选的 `staticAssets?: StaticAssets`。
|
||||
|
||||
#### Scenario: 最小配置(开发模式)
|
||||
- **WHEN** 仅传入 configPath 和 mode
|
||||
- **THEN** 系统 SHALL 正常启动,startServer 不接收 staticAssets 参数
|
||||
|
||||
#### Scenario: 生产模式配置
|
||||
- **WHEN** 传入 configPath、mode 和 staticAssets
|
||||
- **THEN** 系统 SHALL 将 staticAssets 传递给 startServer
|
||||
|
||||
### Requirement: dev.ts 和生产入口使用 bootstrap
|
||||
`dev.ts` 和 `src/server/main.ts` SHALL 调用 `bootstrap()` 而非各自维护启动序列。
|
||||
|
||||
#### Scenario: dev.ts 调用 bootstrap
|
||||
- **WHEN** 开发者运行 `bun run dev`
|
||||
- **THEN** `dev.ts` SHALL 调用 `bootstrap` 完成启动
|
||||
|
||||
#### Scenario: main.ts 调用 bootstrap
|
||||
- **WHEN** 生产可执行文件启动
|
||||
- **THEN** `main.ts` SHALL 调用 `bootstrap` 完成启动
|
||||
|
||||
@@ -25,8 +25,172 @@
|
||||
|
||||
#### Scenario: HTTP target 覆盖 headers
|
||||
- **WHEN** YAML 中某个 HTTP target 显式配置 `http.headers`
|
||||
- **THEN** 该 target SHALL 使用自身 headers 字段的值,不再与规则文件级 defaults 合并
|
||||
- **THEN** 该 target SHALL 使用自身 headers 字段的值
|
||||
|
||||
#### Scenario: HTTP target 配置 ignoreSSL
|
||||
- **WHEN** YAML 中 HTTP target 设置 `http.ignoreSSL: true`
|
||||
- **THEN** 系统 SHALL 解析该字段并在执行时跳过 SSL 证书校验
|
||||
|
||||
#### Scenario: HTTP target 配置 maxRedirects
|
||||
- **WHEN** YAML 中 HTTP target 设置 `http.maxRedirects: 5`
|
||||
- **THEN** 系统 SHALL 解析该字段并在执行时允许最多跟随 5 次重定向
|
||||
|
||||
#### Scenario: HTTP 序列化展示摘要
|
||||
- **WHEN** 系统同步 HTTP target 到 targets 表
|
||||
- **THEN** `target` 展示摘要 SHALL 为 HTTP URL,`config` JSON SHALL 包含 resolved 后的 url、method、headers、body、ignoreSSL、maxBodyBytes 和 maxRedirects
|
||||
|
||||
### Requirement: HTTP checker 执行
|
||||
系统 SHALL 按 HTTP target 配置发起 HTTP 请求,使用引擎注入的 `ctx.signal` 响应超时。系统 SHALL 支持手动重定向跟随(`redirect: "manual"`),在 `maxRedirects` 限制内跟随 301/302/303/307/308 重定向。系统 SHALL 记录完整执行耗时和 HTTP observation,并在网络错误、超时、响应体超限或字符编码不支持时产生结构化失败信息。
|
||||
|
||||
#### Scenario: HTTP 请求成功
|
||||
- **WHEN** HTTP target 指向可正常响应的 HTTP 服务,且未配置 expect 或 `expect.status` 为 `[200]`
|
||||
- **THEN** 系统 SHALL 记录 `matched=true`、`durationMs` 和包含 statusCode、headers、bodyPreview、contentType、contentLength 的 observation
|
||||
|
||||
#### Scenario: 使用 ctx.signal 响应超时
|
||||
- **WHEN** 引擎注入的 `ctx.signal` 在 HTTP 请求过程中 abort
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,observation SHALL 为 null,failure 的 kind 为 `error`,phase 为 `request`,message 包含超时信息
|
||||
|
||||
#### Scenario: 网络错误
|
||||
- **WHEN** HTTP 请求因网络错误(DNS 解析失败、连接被拒绝等)失败
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,observation SHALL 为 null,failure 的 kind 为 `error`,phase 为 `request`
|
||||
|
||||
#### Scenario: 重定向跟随
|
||||
- **WHEN** HTTP target 配置 `http.maxRedirects: 3` 且服务端返回 301/302/303/307/308 重定向
|
||||
- **THEN** 系统 SHALL 在 maxRedirects 限制内自动跟随重定向,并返回最终响应供 expect 校验
|
||||
|
||||
#### Scenario: 重定向次数耗尽
|
||||
- **WHEN** HTTP target 配置 `http.maxRedirects: 1` 且服务端返回超过 1 次重定向
|
||||
- **THEN** 系统 SHALL 将最后一次重定向响应作为最终结果返回,不继续跟随
|
||||
|
||||
#### Scenario: POST 重定向为 GET
|
||||
- **WHEN** HTTP target 使用 POST 方法且收到 301 或 302 重定向,或收到 303 重定向
|
||||
- **THEN** 系统 SHALL 将后续请求方法改为 GET,移除 Content-Type 和 Content-Length 请求头,并移除 body
|
||||
|
||||
#### Scenario: 跨域重定向移除敏感头
|
||||
- **WHEN** 重定向到不同 origin 的目标
|
||||
- **THEN** 系统 SHALL 移除 Authorization 和 Cookie 请求头
|
||||
|
||||
#### Scenario: 响应体读取与大小限制
|
||||
- **WHEN** HTTP target 配置了 body expect 且响应体未超过 `maxBodyBytes`
|
||||
- **THEN** 系统 SHALL 读取完整响应体,按 Content-Type 字符编码解码,并用于 body expect 校验
|
||||
|
||||
#### Scenario: 响应体超过大小限制
|
||||
- **WHEN** HTTP target 配置了 body expect 且响应体超过 `maxBodyBytes`
|
||||
- **THEN** 系统 SHALL 停止读取,记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `body`,message 包含响应体超过大小限制的信息
|
||||
|
||||
#### Scenario: 不支持的字符编码
|
||||
- **WHEN** HTTP 响应的 Content-Type 指定了不支持的 charset
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `body`,message 包含不支持的字符编码信息
|
||||
|
||||
#### Scenario: 无 body expect 时不读取响应体
|
||||
- **WHEN** HTTP target 未配置 `expect.body` 或 body expect 为空数组
|
||||
- **THEN** 系统 SHALL NOT 读取响应体内容,bodyPreview SHALL 为 null
|
||||
|
||||
#### Scenario: GET/HEAD 请求不发送 body
|
||||
- **WHEN** HTTP target 配置 `http.method: GET` 或 `http.method: HEAD`
|
||||
- **THEN** 系统 SHALL NOT 发送请求体,即使配置了 `http.body`
|
||||
|
||||
### Requirement: HTTP expect 校验
|
||||
系统 SHALL 支持 HTTP 专属 expect,包括 `status`、`headers`、`body` 和 `durationMs`,并按 status、headers、early-timeout(仅当配置了 body expect 时)、body、durationMs 的阶段顺序快速失败。`status` SHALL 保持状态码数组语义,支持精确数字(100-599)和范围模式(`1xx` 到 `5xx`),未配置时在 Resolved expect 中默认 `[200]`。`headers` SHALL 使用共享 `RawKeyedExpectations` 输入并在运行期使用 `KeyedExpectations`,header key 大小写不敏感。`body` MUST 使用共享 `RawContentExpectations` 数组输入并在运行期使用 `ContentExpectations`,支持直接 ValueMatcher 以及 json/css/xpath 提取器。`durationMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation` 校验完整执行耗时。
|
||||
|
||||
#### Scenario: 默认 status 成功语义
|
||||
- **WHEN** HTTP target 未显式配置 `expect.status`
|
||||
- **THEN** 系统 SHALL 在 Resolved HTTP expect 中使用默认 `status: [200]` 进行校验
|
||||
|
||||
#### Scenario: status 精确值匹配
|
||||
- **WHEN** HTTP target 配置 `expect.status: [200, 201]` 且响应状态码为 201
|
||||
- **THEN** 系统 SHALL 判定 status 阶段通过,继续后续 expect 阶段
|
||||
|
||||
#### Scenario: status 范围模式匹配
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]` 且响应状态码为 204
|
||||
- **THEN** 系统 SHALL 判定 status 阶段通过
|
||||
|
||||
#### Scenario: status 不匹配快速失败
|
||||
- **WHEN** HTTP target 配置 `expect.status: [200]` 且响应状态码为 500
|
||||
- **THEN** 系统 SHALL 立即返回 `matched=false`,failure 的 phase 为 `status`
|
||||
|
||||
#### Scenario: headers 校验通过
|
||||
- **WHEN** HTTP target 配置 `expect.headers: {Content-Type: {contains: "application/json"}}` 且响应包含对应 header
|
||||
- **THEN** 系统 SHALL 判定 headers 阶段通过,header key 匹配大小写不敏感
|
||||
|
||||
#### Scenario: headers 校验失败
|
||||
- **WHEN** HTTP target 配置 `expect.headers: {X-Custom: {exists: true}}` 且响应不包含该 header
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 phase 为 `headers`
|
||||
|
||||
#### Scenario: body ContentExpectations 校验通过
|
||||
- **WHEN** HTTP target 配置 `expect.body: [{contains: "ok"}, {json: {path: "$.status", equals: "success"}}]` 且响应体满足全部条件
|
||||
- **THEN** 系统 SHALL 判定 body 阶段通过
|
||||
|
||||
#### Scenario: body ContentExpectations 快速失败
|
||||
- **WHEN** HTTP target 配置两条 body expectation 且第一条失败
|
||||
- **THEN** 系统 SHALL 返回第一条失败 expectation 的 failure,phase 为 `body`,并 MUST NOT 执行第二条 expectation
|
||||
|
||||
#### Scenario: body 非 JSON 响应触发 JSONPath 失败
|
||||
- **WHEN** HTTP target 配置 json body expectation 但响应体不是合法 JSON
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `body`
|
||||
|
||||
#### Scenario: early-timeout 快速失败
|
||||
- **WHEN** HTTP target 配置了 body expect 和 `expect.durationMs: {lte: 500}`,且请求状态码和 headers 通过后已耗时超过 500ms
|
||||
- **THEN** 系统 SHALL 在读取响应体之前判定 durationMs 超限,不读取响应体,直接返回 `matched=false`,failure 的 phase 为 `duration`
|
||||
|
||||
#### Scenario: durationMs 校验
|
||||
- **WHEN** HTTP target 配置 `expect.durationMs: {lte: 1000}` 且完整执行耗时超过 1000ms
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 phase 为 `duration`
|
||||
|
||||
### Requirement: HTTP checker 启动期配置校验
|
||||
系统 SHALL 在启动期对 HTTP checker 的配置契约和语义执行严格校验。HTTP target 的 `http` 分组 SHALL 只允许 `url`、`method`、`headers`、`body`、`ignoreSSL`、`maxBodyBytes`、`maxRedirects` 字段。HTTP expect SHALL 只允许 `status`、`headers`、`body`、`durationMs` 字段。未知字段、非法类型、非法 URL 协议、非法 status 值、非法 maxBodyBytes、非法 ContentExpectations 和不可编译正则 MUST 导致启动期配置错误。语义校验 MUST NOT 修改 Raw HTTP expect 输入。
|
||||
|
||||
#### Scenario: http.url 为空字符串
|
||||
- **WHEN** YAML 中 HTTP target 配置 `http.url: ""`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示缺少 http.url 字段
|
||||
|
||||
#### Scenario: http.url 协议非法
|
||||
- **WHEN** YAML 中 HTTP target 的 `http.url` 不以 `http://` 或 `https://` 开头
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 URL 格式不合法
|
||||
|
||||
#### Scenario: http.url 格式非法
|
||||
- **WHEN** YAML 中 HTTP target 的 `http.url` 不是合法 URL
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 URL 格式不合法
|
||||
|
||||
#### Scenario: http.maxBodyBytes 格式非法
|
||||
- **WHEN** YAML 中 HTTP target 的 `http.maxBodyBytes` 不是合法 size 值
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 maxBodyBytes 格式错误
|
||||
|
||||
#### Scenario: http 分组未知字段失败
|
||||
- **WHEN** YAML 中 HTTP target 的 `http` 分组包含未知字段
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 http 分组包含未知字段
|
||||
|
||||
#### Scenario: expect.status 数字非法
|
||||
- **WHEN** YAML 中 HTTP target 的 `expect.status` 包含非整数或不在 100-599 范围内的数字
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 status 数字不合法
|
||||
|
||||
#### Scenario: expect.status 模式非法
|
||||
- **WHEN** YAML 中 HTTP target 的 `expect.status` 包含不符合 `1xx` 到 `5xx` 格式的字符串
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 status 模式不合法
|
||||
|
||||
#### Scenario: expect.body 必须为数组
|
||||
- **WHEN** YAML 中 HTTP target 的 `expect.body` 已配置但不是数组
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.body 必须为数组
|
||||
|
||||
#### Scenario: HTTP expect 未知字段失败
|
||||
- **WHEN** YAML 中 HTTP target 的 expect 包含 `connected`、`exitCode`、`maxDurationMs` 或其他非 HTTP expect 字段
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
|
||||
|
||||
### Requirement: HTTP observation 与 detail
|
||||
HTTP checker SHALL 在 observation 中记录 statusCode(number)、headers(Record<string, string>,截断至前 20 个)、bodyPreview(string | null,截断至前 1024 字符)、contentType(string | null)、contentLength(number | null)。API detail SHALL 由 `buildDetail` 从 observation 动态构造,格式为 `HTTP {statusCode}`。网络错误或超时导致无法收集 observation 时,observation SHALL 为 null,detail SHALL 为 null。
|
||||
|
||||
#### Scenario: 正常响应 observation
|
||||
- **WHEN** HTTP 请求成功返回
|
||||
- **THEN** observation SHALL 包含 statusCode、截断后的 headers、截断后的 bodyPreview、contentType 和 contentLength
|
||||
|
||||
#### Scenario: 未读取响应体时 bodyPreview 为 null
|
||||
- **WHEN** HTTP target 未配置 body expect
|
||||
- **THEN** observation.bodyPreview SHALL 为 null
|
||||
|
||||
#### Scenario: 请求失败 observation 为 null
|
||||
- **WHEN** HTTP 请求因网络错误或超时失败
|
||||
- **THEN** observation SHALL 为 null,detail SHALL 为 null
|
||||
|
||||
#### Scenario: detail 格式
|
||||
- **WHEN** API 序列化 HTTP CheckResult
|
||||
- **THEN** detail SHALL 为 `HTTP {statusCode}` 格式(如 `HTTP 200`)
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
## Requirements
|
||||
|
||||
### Requirement: LLM Checker 注册与模块结构
|
||||
系统 SHALL 提供 `type: llm` checker,用于大模型服务的应用层拨测。LLM checker MUST 位于 `src/server/checker/runner/llm/` 自包含目录,并通过 `src/server/checker/runner/index.ts` 注册到 `CheckerRegistry`。LLM checker SHALL 复用现有 checker 抽象、配置 schema 组装、启动期语义校验、引擎调度、存储序列化和共享 expect 基础设施。
|
||||
系统 SHALL 提供 `type: llm` checker,用于大模型服务的应用层拨测。LLM checker MUST 位于自包含目录,并通过 checker 注册入口注册到 `CheckerRegistry`。LLM checker SHALL 复用现有 checker 抽象、配置 schema 组装、启动期语义校验、引擎调度、存储序列化和共享 expect 基础设施。
|
||||
|
||||
#### Scenario: 注册 LLM checker
|
||||
- **WHEN** 系统初始化默认 checker registry
|
||||
- **THEN** registry SHALL 包含 `llm` 类型,且 `/api/meta` 返回的 `checkerTypes` SHALL 包含 `llm`
|
||||
|
||||
#### Scenario: LLM checker 目录自包含
|
||||
- **WHEN** 开发者查看 `src/server/checker/runner/llm/` 目录
|
||||
- **WHEN** 开发者查看 LLM checker 目录
|
||||
- **THEN** 该目录 SHALL 包含 LLM checker 的类型、schema、语义校验、provider 创建、observation 构建、expect 断言、执行逻辑和模块入口
|
||||
|
||||
#### Scenario: 不扩展存储和 API 结构
|
||||
@@ -53,18 +53,6 @@ LLM checker SHALL 解析 `llm.provider`、`llm.url`、`llm.model`、`llm.prompt`
|
||||
- **WHEN** 系统读取只包含 `type: llm` 以及 `llm.provider`、`llm.url`、`llm.model`、`llm.prompt` 的 target
|
||||
- **THEN** 系统 SHALL 解析为 LLM target,并填充 `mode=http`、`key=""`、`ignoreSSL=false`、`options.maxOutputTokens=16`、`options.temperature=0`
|
||||
|
||||
#### Scenario: headers 默认值合并
|
||||
- **WHEN** `defaults.llm.headers` 和 target `llm.headers` 同时配置同名 header
|
||||
- **THEN** LLM checker SHALL 按原始 header key 浅合并 headers,并由 target `llm.headers` 覆盖 defaults 中同名 key
|
||||
|
||||
#### Scenario: options 默认值合并
|
||||
- **WHEN** `defaults.llm.options` 和 target `llm.options` 同时配置同名 option
|
||||
- **THEN** LLM checker SHALL 浅合并 options,并由 target `llm.options` 覆盖 defaults 中同名字段
|
||||
|
||||
#### Scenario: providerOptions 默认值合并
|
||||
- **WHEN** `defaults.llm.providerOptions` 和 target `llm.providerOptions` 同时配置同名 provider namespace
|
||||
- **THEN** LLM checker SHALL 按 provider namespace 浅合并 providerOptions,并由 target namespace 覆盖 defaults 中同名 namespace
|
||||
|
||||
#### Scenario: Anthropic Bearer token
|
||||
- **WHEN** target 配置 `llm.provider: anthropic` 和非空 `llm.authToken`
|
||||
- **THEN** LLM checker SHALL 将 `authToken` 映射到 Anthropic SDK 的 Bearer token 认证字段
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义系统运行时元数据 API:checker 类型列表、应用版本号等元信息的对外暴露方式。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Meta 信息 API
|
||||
系统 SHALL 提供 `GET /api/meta` 端点,返回系统运行时元数据,包括应用版本号和 checker 类型列表。未匹配 method SHALL 按 API 通配符处理为 JSON 404,不再保留自定义 HEAD/405 语义。
|
||||
|
||||
#### Scenario: 获取 checker 类型列表和版本号
|
||||
- **WHEN** 客户端请求 `GET /api/meta`
|
||||
- **THEN** 系统 SHALL 返回 JSON `{ checkerTypes: string[], version: string }`,其中 `checkerTypes` 包含所有已注册的 checker 类型标识符(如 `["http", "cmd"]`),`version` 为当前运行实例的 `MAJOR.MINOR.PATCH` 应用版本
|
||||
|
||||
#### Scenario: 类型列表来源
|
||||
- **WHEN** 系统启动并注册了 checker
|
||||
- **THEN** `/api/meta` 返回的 `checkerTypes` SHALL 与 `CheckerRegistry.supportedTypes` 完全一致
|
||||
|
||||
#### Scenario: 版本号来源
|
||||
- **WHEN** 系统启动并确定应用版本
|
||||
- **THEN** `/api/meta` 返回的 `version` SHALL 与启动时注入的应用版本完全一致
|
||||
|
||||
#### Scenario: 不支持的 method 请求
|
||||
- **WHEN** 客户端使用 POST/PUT/DELETE/HEAD 等未声明 method 请求 `/api/meta`
|
||||
- **THEN** `/api/*` 通配符 SHALL 返回 JSON 404 响应
|
||||
|
||||
### Requirement: MetaResponse 共享类型
|
||||
系统 SHALL 在 `src/shared/api.ts` 中定义 `MetaResponse` 类型。
|
||||
|
||||
#### Scenario: MetaResponse 类型定义
|
||||
- **WHEN** 前后端引用 `MetaResponse` 类型
|
||||
- **THEN** 该类型 SHALL 包含 `checkerTypes: string[]` 和 `version: string` 字段
|
||||
@@ -1,6 +1,6 @@
|
||||
## Purpose
|
||||
|
||||
定义拨测系统的 REST API 端点:Dashboard 聚合 API、单目标指标 API、带时间范围和分页的历史记录、共享类型定义和 API 错误处理。
|
||||
定义拨测系统的 REST API 端点:Dashboard 聚合 API、单目标指标 API、历史记录 API、Meta 信息 API、共享类型定义和 API 错误处理。
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -75,11 +75,11 @@ Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态
|
||||
- **THEN** 每个 recentSamples 元素 SHALL 包含 timestamp、durationMs、up 字段,其中 up 为 boolean 且等于 matched
|
||||
|
||||
### Requirement: 单目标指标 API
|
||||
系统 SHALL 提供 `GET /api/targets/:id/metrics` 端点,返回 Drawer 概览所需的单目标统计和趋势数据。仅活跃目标的指标 SHALL 可查询。端点的详细计算规则(P95/P99、MTTR、故障分析、趋势分桶等)定义在 `target-metrics-api` 能力中。
|
||||
系统 SHALL 提供 `GET /api/targets/:id/metrics` 端点,返回单个目标在指定时间窗口内的概览统计和趋势数据。仅活跃目标的指标 SHALL 可查询。
|
||||
|
||||
#### Scenario: 指定时间范围查询指标
|
||||
#### Scenario: 获取目标指标
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=ISO&to=ISO&bucket=1h` 且该目标为活跃目标
|
||||
- **THEN** 系统 SHALL 返回 targetId、window、stats、trend 字段
|
||||
- **THEN** 系统 SHALL 返回 JSON 对象包含 targetId、window、stats、trend 字段
|
||||
|
||||
#### Scenario: from 或 to 参数缺失
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/metrics` 未提供 from 或 to 参数
|
||||
@@ -101,6 +101,134 @@ Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=ISO&to=ISO&bucket=5m`
|
||||
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
|
||||
|
||||
### Requirement: 指标统计字段
|
||||
单目标指标 API SHALL 返回基于时间窗口计算的完整统计字段。
|
||||
|
||||
#### Scenario: stats 字段
|
||||
- **WHEN** metrics 响应包含 stats
|
||||
- **THEN** stats SHALL 包含 totalChecks、upChecks、downChecks、availability、avgDurationMs、p95DurationMs、p99DurationMs、mttr、longestOutage、incidentCount、currentStreak 字段
|
||||
|
||||
#### Scenario: trend 字段
|
||||
- **WHEN** metrics 响应包含 trend
|
||||
- **THEN** trend SHALL 为数组,每个元素包含 bucketStart、avgDurationMs、minDurationMs、maxDurationMs、availability、totalChecks、upChecks、downChecks 字段
|
||||
|
||||
### Requirement: P95/P99 延迟计算
|
||||
系统 SHALL 在后端应用层计算 P95 和 P99 延迟百分位数。
|
||||
|
||||
#### Scenario: 正常计算 P95
|
||||
- **WHEN** 时间窗口内存在成功检查记录(matched=1 且 duration_ms 不为 null)
|
||||
- **THEN** 系统 SHALL 取出所有成功检查的 duration_ms,在后端应用层排序后取第 95 百分位值返回为 p95DurationMs
|
||||
|
||||
#### Scenario: 正常计算 P99
|
||||
- **WHEN** 时间窗口内存在成功检查记录
|
||||
- **THEN** 系统 SHALL 取第 99 百分位值返回为 p99DurationMs
|
||||
|
||||
#### Scenario: 无成功检查记录
|
||||
- **WHEN** 时间窗口内无 matched=1 且 duration_ms 不为 null 的记录
|
||||
- **THEN** p95DurationMs 和 p99DurationMs SHALL 返回 null
|
||||
|
||||
#### Scenario: 百分位计算方法
|
||||
- **WHEN** 计算第 N 百分位
|
||||
- **THEN** 系统 SHALL 将 duration_ms 升序排列,取 index = ceil(count * N / 100) - 1 位置的值
|
||||
|
||||
### Requirement: MTTR 计算
|
||||
系统 SHALL 在后端应用层计算平均恢复时间(Mean Time To Recovery)。
|
||||
|
||||
#### Scenario: 存在已恢复的故障段
|
||||
- **WHEN** 时间窗口内存在至少一个已恢复的故障段(连续 matched=0 后跟 matched=1)
|
||||
- **THEN** 系统 SHALL 计算所有已恢复故障段的平均持续时间(从首个 matched=0 的 timestamp 到恢复后首个 matched=1 的 timestamp 之差),返回为 mttr(毫秒)
|
||||
|
||||
#### Scenario: 无已恢复的故障段
|
||||
- **WHEN** 时间窗口内无已恢复的故障段(全部正常,或当前仍在故障中且无历史恢复)
|
||||
- **THEN** mttr SHALL 返回 null
|
||||
|
||||
#### Scenario: 当前正在故障中
|
||||
- **WHEN** 时间窗口内最后一段故障尚未恢复
|
||||
- **THEN** 该未恢复的故障段 SHALL 不计入 MTTR 平均值
|
||||
|
||||
#### Scenario: 窗口起始即为故障且后续恢复
|
||||
- **WHEN** 时间窗口内第一条记录即为 matched=0(故障跨越了 from 边界),且该故障段在窗口内恢复
|
||||
- **THEN** 该故障段 SHALL 不计入 MTTR 平均值(因无法确定真实故障开始时间),但 SHALL 计入 incidentCount
|
||||
|
||||
### Requirement: 最长故障时长
|
||||
系统 SHALL 在后端应用层计算时间窗口内最长的单次故障持续时间。
|
||||
|
||||
#### Scenario: 存在故障段
|
||||
- **WHEN** 时间窗口内存在故障段
|
||||
- **THEN** 系统 SHALL 返回最长故障段的持续时间为 longestOutage(毫秒)
|
||||
|
||||
#### Scenario: 无故障
|
||||
- **WHEN** 时间窗口内无 matched=0 的记录
|
||||
- **THEN** longestOutage SHALL 返回 null
|
||||
|
||||
#### Scenario: 窗口起始即为故障
|
||||
- **WHEN** 时间窗口内第一条记录即为 matched=0
|
||||
- **THEN** 该故障段的持续时间 SHALL 从 from 参数开始计算
|
||||
|
||||
#### Scenario: 当前正在故障中
|
||||
- **WHEN** 最后一段故障尚未恢复
|
||||
- **THEN** 该故障段的持续时间 SHALL 计算为从故障开始到时间窗口 to 参数的时间差
|
||||
|
||||
### Requirement: 故障事件计数
|
||||
系统 SHALL 在后端应用层计算时间窗口内的故障事件次数。
|
||||
|
||||
#### Scenario: 计算故障事件数
|
||||
- **WHEN** 时间窗口内存在状态翻转(matched 从 1 变为 0)
|
||||
- **THEN** 系统 SHALL 返回翻转次数为 incidentCount
|
||||
|
||||
#### Scenario: 无故障事件
|
||||
- **WHEN** 时间窗口内所有检查均为 matched=1
|
||||
- **THEN** incidentCount SHALL 返回 0
|
||||
|
||||
#### Scenario: 窗口起始即为故障
|
||||
- **WHEN** 时间窗口内第一条记录即为 matched=0 且无前序记录可判断翻转
|
||||
- **THEN** 该故障 SHALL 计为 1 次事件
|
||||
|
||||
#### Scenario: 连续异常只计一次
|
||||
- **WHEN** 某目标连续 10 次 matched=0
|
||||
- **THEN** 该连续异常段 SHALL 仅计为 1 次事件
|
||||
|
||||
### Requirement: 当前连续状态
|
||||
系统 SHALL 返回目标当前的连续状态信息。
|
||||
|
||||
#### Scenario: 当前连续正常
|
||||
- **WHEN** 目标最近的检查记录连续为 matched=1
|
||||
- **THEN** currentStreak SHALL 返回 `{ up: true, count: N }`,N 为连续正常的检查次数
|
||||
|
||||
#### Scenario: 当前连续异常
|
||||
- **WHEN** 目标最近的检查记录连续为 matched=0
|
||||
- **THEN** currentStreak SHALL 返回 `{ up: false, count: N }`,N 为连续异常的检查次数
|
||||
|
||||
#### Scenario: 连续状态达到取数上限
|
||||
- **WHEN** 连续状态次数达到后端取数或计算上限
|
||||
- **THEN** currentStreak SHALL 返回 `{ up: boolean, count: N, capped: true }`,前端据此展示上限标记
|
||||
|
||||
#### Scenario: 无检查记录
|
||||
- **WHEN** 目标没有任何检查记录
|
||||
- **THEN** currentStreak SHALL 返回 null
|
||||
|
||||
### Requirement: 趋势数据应用层分桶
|
||||
系统 SHALL 在后端应用层按 UTC 小时分桶生成趋势数据。
|
||||
|
||||
#### Scenario: 按小时生成趋势
|
||||
- **WHEN** metrics 请求 bucket=`1h`
|
||||
- **THEN** 系统 SHALL 按 UTC 小时生成 trend 数组,每个点包含该小时内的 totalChecks、upChecks、downChecks、availability、avgDurationMs、minDurationMs、maxDurationMs
|
||||
|
||||
#### Scenario: 小时内无成功检查
|
||||
- **WHEN** 某小时内存在检查记录但无成功检查记录
|
||||
- **THEN** avgDurationMs、minDurationMs、maxDurationMs SHALL 返回 null,availability SHALL 基于 upChecks/totalChecks 返回 0
|
||||
|
||||
#### Scenario: 小时内无检查记录
|
||||
- **WHEN** 某小时内没有任何检查记录
|
||||
- **THEN** 系统 MAY 不返回该小时对应的 trend 点
|
||||
|
||||
### Requirement: 无数据口径
|
||||
系统 SHALL 对无数据窗口返回稳定的空指标口径。
|
||||
|
||||
#### Scenario: 窗口内无检查记录
|
||||
- **WHEN** 指定时间窗口内没有任何检查记录
|
||||
- **THEN** stats SHALL 返回 totalChecks=0、upChecks=0、downChecks=0、availability=0、avgDurationMs=null、p95DurationMs=null、p99DurationMs=null、mttr=null、longestOutage=null、incidentCount=0、currentStreak=null,trend SHALL 返回空数组
|
||||
|
||||
### Requirement: 历史记录 API
|
||||
系统 SHALL 保留 `GET /api/targets/:id/history` 端点,支持时间范围筛选和分页返回指定目标的拨测记录。仅活跃目标的历史记录 SHALL 可查询。
|
||||
|
||||
@@ -221,16 +349,20 @@ Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态
|
||||
- **THEN** API SHALL 返回 failure 为 null
|
||||
|
||||
### Requirement: Meta 信息 API
|
||||
系统 SHALL 提供 `GET /api/meta` 端点,返回系统运行时元数据。未匹配 method SHALL 按 API 通配符处理为 JSON 404,不再保留自定义 HEAD/405 语义。
|
||||
系统 SHALL 提供 `GET /api/meta` 端点,返回系统运行时元数据,包括应用版本号和 checker 类型列表。未匹配 method SHALL 按 API 通配符处理为 JSON 404。
|
||||
|
||||
#### Scenario: 获取 checker 类型列表
|
||||
#### Scenario: 获取 checker 类型列表和版本号
|
||||
- **WHEN** 客户端请求 `GET /api/meta`
|
||||
- **THEN** 系统 SHALL 返回 JSON `{ checkerTypes: string[] }`,包含所有已注册的 checker 类型标识符
|
||||
- **THEN** 系统 SHALL 返回 JSON `{ checkerTypes: string[], version: string }`,其中 `checkerTypes` 包含所有已注册的 checker 类型标识符,`version` 为当前运行实例的 `MAJOR.MINOR.PATCH` 应用版本
|
||||
|
||||
#### Scenario: 类型列表来源
|
||||
- **WHEN** 系统启动并注册了 checker
|
||||
- **THEN** `/api/meta` 返回的 `checkerTypes` SHALL 与 `CheckerRegistry.supportedTypes` 完全一致
|
||||
|
||||
#### Scenario: 版本号来源
|
||||
- **WHEN** 系统启动并确定应用版本
|
||||
- **THEN** `/api/meta` 返回的 `version` SHALL 与启动时注入的应用版本完全一致
|
||||
|
||||
#### Scenario: 不支持的 method 请求
|
||||
- **WHEN** 客户端使用 POST/PUT/DELETE/HEAD 等未声明 method 请求 `/api/meta`
|
||||
- **THEN** `/api/*` 通配符 SHALL 返回 JSON 404 响应
|
||||
@@ -240,4 +372,4 @@ Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态
|
||||
|
||||
#### Scenario: MetaResponse 类型定义
|
||||
- **WHEN** 前后端引用 `MetaResponse` 类型
|
||||
- **THEN** 该类型 SHALL 包含 `checkerTypes: string[]` 字段
|
||||
- **THEN** 该类型 SHALL 包含 `checkerTypes: string[]` 和 `version: string` 字段
|
||||
|
||||
@@ -447,12 +447,40 @@ JSON Schema 契约 SHALL 覆盖业务无关的结构规则,包括字段类型
|
||||
- **THEN** 系统 SHALL 将默认值 `./data` 基于 configDir 解析为绝对路径
|
||||
|
||||
### Requirement: target 通用元信息字段约束
|
||||
系统 SHALL 在 YAML target 通用字段中支持 `description` 字段,并对 `id`、`name` 和 `description` 执行契约校验。`id` MUST 为 1 到 30 个字符。`name` MUST 为 null 或 1 到 30 个字符的字符串,且语义校验 SHALL 拒绝仅包含空白字符的 name。`description` MUST 为 null 或不超过 500 个字符的字符串,且 MAY 为空字符串。
|
||||
系统 SHALL 在 YAML target 通用字段中对 `id`、`name` 和 `description` 执行契约校验。`id` MUST 为 1 到 30 个字符,符合 `[a-zA-Z0-9][a-zA-Z0-9_-]*` 命名规则,MUST 在所有 targets 中全局唯一,MUST NOT 参与变量替换。`name` MUST 为 null 或 1 到 30 个字符的字符串,支持变量替换,MUST NOT 要求全局唯一,MUST NOT 参与 target 唯一性判定,语义校验 SHALL 拒绝仅包含空白字符的 name。`description` MUST 为 null 或不超过 500 个字符的字符串,支持变量替换,且 MAY 为空字符串,MUST NOT 参与 target 唯一性判定。`name` 为 null 时前端展示 SHALL 使用 `name ?? id` 作为目标名称文案,但该 fallback MUST NOT 改变 target 本身的 name 值。
|
||||
|
||||
#### Scenario: description 字段解析
|
||||
- **WHEN** 系统读取包含 `description: "检查生产 API 健康状态"` 的 target
|
||||
- **THEN** 系统 SHALL 将该字段解析为 target 的目标说明
|
||||
|
||||
#### Scenario: id 包含下划线和连字符
|
||||
- **WHEN** target 配置 `id: "db_check-01"`
|
||||
- **THEN** 系统 SHALL 使用该 id 作为 target 的唯一标识符
|
||||
|
||||
#### Scenario: id 不合法报错
|
||||
- **WHEN** target 配置 `id: "_invalid"` 或 `id: "-start"` 或 `id: "has space"`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 id 不符合命名规则
|
||||
|
||||
#### Scenario: id 重复报错
|
||||
- **WHEN** 两个 target 配置相同的 `id: "api-health"`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 id 重复
|
||||
|
||||
#### Scenario: id 不参与变量替换
|
||||
- **WHEN** target 配置 `id: "${VAR}"` 形式的变量引用
|
||||
- **THEN** 系统 SHALL NOT 对 id 执行变量替换
|
||||
|
||||
#### Scenario: name 使用变量
|
||||
- **WHEN** target 配置 `name: "${env} API 健康检查"` 且 variables 中 `env: "生产"`
|
||||
- **THEN** 系统 SHALL 将 name 解析为 "生产 API 健康检查"
|
||||
|
||||
#### Scenario: name 显式 null
|
||||
- **WHEN** target 配置 `id: "api-health"` 和 `name: null`
|
||||
- **THEN** 系统 SHALL 接受该配置,并在解析、存储和 API 响应中保留 name 为 null
|
||||
|
||||
#### Scenario: name 为 null 时前端展示 fallback
|
||||
- **WHEN** 前端展示 name 为 null 的 target
|
||||
- **THEN** 前端 SHALL 显示该 target 的 id 作为目标名称文案
|
||||
|
||||
#### Scenario: name 为 null 通过校验
|
||||
- **WHEN** 系统读取包含 `name: null` 或省略 `name` 的 target
|
||||
- **THEN** 系统 SHALL 接受该配置
|
||||
@@ -489,6 +517,33 @@ JSON Schema 契约 SHALL 覆盖业务无关的结构规则,包括字段类型
|
||||
- **WHEN** target 的 `description` 通过变量替换后超过 500 个字符
|
||||
- **THEN** 系统 SHALL 在契约校验阶段以错误退出,提示 description 字段长度错误
|
||||
|
||||
### Requirement: target 分组
|
||||
系统 SHALL 支持在每个 target 上配置可选的 `group` 字段用于将目标归类到不同分组。未指定 `group` 的 target SHALL 归入 `"default"` 分组。系统 SHALL 保证 "default" 分组始终排在最前面,其余分组按配置文件中首次出现的顺序排列。系统 SHALL 在 API 响应中返回每个 target 的分组信息。系统 SHALL 在数据库 targets 表中持久化每个 target 的分组信息。
|
||||
|
||||
#### Scenario: 配置分组名称
|
||||
- **WHEN** YAML 配置中某个 target 指定 `group: "搜索引擎"`
|
||||
- **THEN** 系统 SHALL 将该 target 归类到 "搜索引擎" 分组
|
||||
|
||||
#### Scenario: 不配置分组
|
||||
- **WHEN** YAML 配置中某个 target 未指定 `group` 字段
|
||||
- **THEN** 系统 SHALL 将该 target 归类到 "default" 分组
|
||||
|
||||
#### Scenario: default 分组排最前
|
||||
- **WHEN** 配置中存在多个分组(包括 "default" 和自定义分组)
|
||||
- **THEN** API 返回的目标列表中 "default" 分组的目标 SHALL 排在其他分组之前
|
||||
|
||||
#### Scenario: 自定义分组按出现顺序
|
||||
- **WHEN** 配置中 "搜索引擎" 分组在 "后端服务" 分组之前首次出现
|
||||
- **THEN** API 返回中 "搜索引擎" 分组 SHALL 排在 "后端服务" 分组之前
|
||||
|
||||
#### Scenario: targets 列表包含分组
|
||||
- **WHEN** 客户端请求 `GET /api/targets`
|
||||
- **THEN** 响应中每个目标 SHALL 包含 `group` 字段,值为该目标所属的分组名称
|
||||
|
||||
#### Scenario: 持久化分组信息
|
||||
- **WHEN** 系统同步 targets 到数据库
|
||||
- **THEN** 每个 target 的 `grp` 列 SHALL 存储其分组名称,未配置分组的存储 `"default"`
|
||||
|
||||
### Requirement: 配置 schema 导出包含 target 元信息约束
|
||||
系统 SHALL 在导出的 Authoring `probe-config.schema.json` 中包含 target `id`、`name` 和 `description` 的长度约束和可空类型,用于编辑器提示和外部校验。导出 schema SHALL 面向用户可书写规则文件,因此还 SHALL 接受支持变量替换字段中的完整变量引用字符串和 expect 简写。
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义拨测系统前端 Dashboard 页面:总览统计卡片、Dashboard 数据查询、加载和错误状态处理。页面骨架布局见 `dashboard-layout`,分组表格布局见 `target-table`,目标详情 Drawer 见 `target-detail-drawer`,数据轮询和缓存见 `tanstack-query-data-layer`。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Dashboard 数据查询
|
||||
Dashboard SHALL 通过 `GET /api/dashboard` 获取首屏总览统计和目标列表数据。
|
||||
|
||||
#### Scenario: 查询 Dashboard 数据
|
||||
- **WHEN** 页面处于打开状态
|
||||
- **THEN** 前端 SHALL 使用 TanStack Query 请求 `GET /api/dashboard?window=24h&recentLimit=30`
|
||||
|
||||
#### Scenario: 统计数据自动刷新
|
||||
- **WHEN** 页面处于打开状态
|
||||
- **THEN** Dashboard 数据 SHALL 通过 TanStack Query 的 refetchInterval=8000 自动刷新
|
||||
|
||||
#### Scenario: 元信息独立查询
|
||||
- **WHEN** 页面需要 checker 类型列表
|
||||
- **THEN** 前端 SHALL 继续通过 `GET /api/meta` 独立查询 checkerTypes
|
||||
|
||||
### Requirement: 总览统计卡片
|
||||
Dashboard SHALL 在页面顶部使用单个 TDesign Card 组件内嵌一行居中的 Statistic 展示总览统计,包含总目标数、正常数、异常数和窗口异常事件数。
|
||||
|
||||
#### Scenario: 展示统计卡片
|
||||
- **WHEN** 用户打开 Dashboard 页面
|
||||
- **THEN** 页面顶部 SHALL 使用单个 TDesign Card(无 shadow、无 bordered)内嵌 TDesign Row/Col 布局展示 4 个居中的 Statistic:全部目标数(color=blue)、正常目标数(color=green)、异常目标数(color=red)、24h 异常事件数(color=orange)
|
||||
|
||||
#### Scenario: 指标居中显示
|
||||
- **WHEN** SummaryCards 渲染
|
||||
- **THEN** 每个 Statistic 所在的 Col SHALL 使用 `.summary-stat-col` 类实现标题和数字居中对齐
|
||||
|
||||
#### Scenario: 异常事件数据来源
|
||||
- **WHEN** SummaryCards 渲染 24h 异常事件数
|
||||
- **THEN** 该数值 SHALL 使用 DashboardResponse.summary.incidents 字段,标题 SHALL 基于当前 window 展示为"24h 异常事件数"
|
||||
|
||||
### Requirement: 页面加载与错误状态
|
||||
Dashboard SHALL 使用 TDesign Skeleton 组件处理首次加载状态,使用 Alert 处理错误。
|
||||
|
||||
#### Scenario: 首次加载
|
||||
- **WHEN** 页面首次加载且数据尚未返回
|
||||
- **THEN** 页面 SHALL 使用 TDesign Skeleton 组件(animation="gradient")展示页面骨架,模拟 Summary 区域和 Table 区域的大致结构
|
||||
|
||||
#### Scenario: API 请求失败
|
||||
- **WHEN** 前端 API 请求失败
|
||||
- **THEN** 页面 SHALL 使用 TDesign Alert 组件(theme=error)显示错误提示
|
||||
|
||||
### Requirement: 前端构建产物拆分
|
||||
前端生产构建 SHALL 将 vendor 依赖拆分为独立 chunk,利用浏览器并行加载和长期缓存。
|
||||
|
||||
#### Scenario: vendor chunk 拆分
|
||||
- **WHEN** 执行前端生产构建
|
||||
- **THEN** 构建产物 SHALL 包含独立的 vendor chunk(react、tdesign、recharts 各自独立),而非单个 bundle
|
||||
|
||||
#### Scenario: 业务代码变更不影响 vendor 缓存
|
||||
- **WHEN** 仅修改业务代码(src/web/ 下非 node_modules 文件)并重新构建
|
||||
- **THEN** vendor chunk 的文件名(含 hash)SHALL 保持不变,浏览器缓存 SHALL 继续有效
|
||||
@@ -1,6 +1,6 @@
|
||||
## Purpose
|
||||
|
||||
定义拨测调度引擎的行为:按 interval 分组定时、组内并发拨测、expect 结果校验和结果持久化。
|
||||
定义拨测调度引擎的行为:按 interval 分组定时、组内并发拨测、通用 expect 校验、结果持久化和定期数据清理。各 checker 类型的执行语义和专属 expect 校验规则定义在各自 checker 的规范中。
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -38,181 +38,24 @@
|
||||
- **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-Type` header 包含 `charset=gbk` 或 `charset="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=false`、`failure.kind="error"`、`failure.phase="body"` 的解码失败结果
|
||||
|
||||
### Requirement: 请求超时控制
|
||||
系统 SHALL 对每次 checker 执行实施超时控制,超时时间使用目标配置的 timeout 值。
|
||||
系统 SHALL 对每次 checker 执行实施超时控制,超时时间使用目标配置的 timeout 值。引擎 SHALL 通过 AbortController 向 checker 注入超时 signal。
|
||||
|
||||
#### Scenario: HTTP 请求超时
|
||||
- **WHEN** HTTP 请求在 timeout 时间内未收到响应
|
||||
- **THEN** 系统 SHALL 中止该请求,记录为失败并标注超时错误
|
||||
|
||||
#### Scenario: cmd 执行超时
|
||||
- **WHEN** cmd 进程在 timeout 时间内未退出
|
||||
- **THEN** 系统 MUST 终止该子进程,记录为失败并标注超时错误
|
||||
|
||||
#### Scenario: 请求在超时前完成
|
||||
#### Scenario: checker 在超时前完成
|
||||
- **WHEN** checker 在超时前完成执行
|
||||
- **THEN** 系统 SHALL 正常记录执行结果并进入 expect 校验
|
||||
|
||||
#### Scenario: checker 执行超时
|
||||
- **WHEN** checker 在 timeout 时间内未完成执行
|
||||
- **THEN** 系统 SHALL 中止该检查,记录为失败并标注超时错误
|
||||
|
||||
### 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
|
||||
系统 SHALL 在 checker 执行完成后根据目标类型的 Resolved expect 执行计划校验观测结果,校验结果和首个失败原因记入 check result。各 checker 类型 SHALL 定义各自的 expect 执行顺序、默认状态语义和快速失败策略。`durationMs` SHALL 表示完整 checker 执行耗时。
|
||||
|
||||
#### 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=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 字段。
|
||||
|
||||
@@ -229,15 +72,11 @@ HTTP checker SHALL 将运行期失败归属到实际失败阶段。请求、网
|
||||
- **THEN** 系统 SHALL 记录 matched=false、observation、failure.kind="mismatch" 和具体不匹配信息
|
||||
|
||||
### Requirement: runner 选择
|
||||
系统 SHALL 根据 target.type 选择对应 runner 执行检查。
|
||||
系统 SHALL 根据 target.type 通过 CheckerRegistry 选择对应 checker 执行检查。
|
||||
|
||||
#### Scenario: 选择 HTTP runner
|
||||
- **WHEN** target.type 为 `http`
|
||||
- **THEN** 系统 SHALL 使用 HTTP runner 执行该目标
|
||||
|
||||
#### Scenario: 选择 cmd runner
|
||||
- **WHEN** target.type 为 `cmd`
|
||||
- **THEN** 系统 SHALL 使用 cmd runner 执行该目标
|
||||
#### Scenario: 根据 type 选择 checker
|
||||
- **WHEN** target.type 为已注册的 checker 类型
|
||||
- **THEN** 系统 SHALL 通过 `checkerRegistry.get(type)` 获取对应 checker 并执行
|
||||
|
||||
### Requirement: 定期数据清理
|
||||
ProbeEngine SHALL 在启动时注册数据清理定时器,定期调用 ProbeStore.prune() 清理过期数据和空壳非活跃目标。
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 Header 刷新频率选择器组件的交互行为:频率切换、倒计时显示、手动刷新按钮、布局稳定性。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 刷新频率选择器
|
||||
HeadMenu operations 区域 SHALL 提供 RadioGroup 组件供用户选择刷新频率。
|
||||
|
||||
#### Scenario: RadioGroup 渲染
|
||||
- **WHEN** Dashboard 页面渲染
|
||||
- **THEN** HeadMenu operations 区域 SHALL 显示 RadioGroup(theme="button", variant="default-filled"),选项为:手动、10秒、30秒、1分钟、5分钟
|
||||
|
||||
#### Scenario: 默认选中
|
||||
- **WHEN** 页面首次加载
|
||||
- **THEN** RadioGroup SHALL 默认选中"30秒"
|
||||
|
||||
#### Scenario: 切换频率立即刷新
|
||||
- **WHEN** 用户切换刷新频率选项
|
||||
- **THEN** 系统 SHALL 立即触发一次数据刷新,然后应用新的刷新间隔
|
||||
|
||||
### Requirement: 倒计时显示
|
||||
RadioGroup 右侧 SHALL 显示距下次自动刷新的倒计时。倒计时逻辑 SHALL 封装在独立的 `RefreshCountdown` 组件中,App 组件 SHALL NOT 持有每秒更新的 `now` state。自动倒计时数字 SHALL 使用 `@number-flow/react` 提供滚动过渡,非倒计时状态 SHALL 保持普通文本或按钮语义。
|
||||
|
||||
#### Scenario: RefreshCountdown 组件封装
|
||||
- **WHEN** Dashboard 页面渲染
|
||||
- **THEN** 倒计时显示 SHALL 由独立的 `RefreshCountdown` 组件负责,该组件内部持有 `now` state 和每秒 `setInterval`,渲染边界限制在该组件内部
|
||||
|
||||
#### Scenario: RefreshCountdown props
|
||||
- **WHEN** RefreshCountdown 组件渲染
|
||||
- **THEN** 组件 SHALL 接收 `dashboardUpdatedAt: number`、`refreshInterval: number`、`isFetching: boolean`、`isManualRefresh: boolean`、`onRefresh: () => void` 作为 props
|
||||
|
||||
#### Scenario: NumberFlow 数字滚动
|
||||
- **WHEN** 自动刷新模式下已完成首次刷新且当前未处于刷新中状态
|
||||
- **THEN** 倒计时数字 SHALL 使用 `@number-flow/react` 的 `NumberFlow` 渲染,并使用向下滚动趋势表达倒计时递减
|
||||
|
||||
#### Scenario: 秒级间隔格式
|
||||
- **WHEN** 自动刷新间隔小于 60 秒
|
||||
- **THEN** 倒计时 SHALL 显示为"xx秒"格式(如"26秒")
|
||||
|
||||
#### Scenario: 分钟级稳定格式
|
||||
- **WHEN** 自动刷新间隔大于等于 60 秒
|
||||
- **THEN** 倒计时 SHALL 显示为"x分xx秒"格式,秒数 SHALL 固定为两位(如"4分30秒"、"0分09秒")
|
||||
|
||||
#### Scenario: 时间数字边界
|
||||
- **WHEN** 分钟级倒计时中的秒数在 59 到 00 边界变化
|
||||
- **THEN** 秒数十位 SHALL 按时间显示规则限制在 0 到 5 之间滚动
|
||||
|
||||
#### Scenario: 无前缀
|
||||
- **WHEN** 倒计时显示
|
||||
- **THEN** 可见倒计时文本 SHALL 不包含任何前缀(如"下一次刷新:"),直接显示时间
|
||||
|
||||
#### Scenario: 可访问文本
|
||||
- **WHEN** NumberFlow 倒计时渲染
|
||||
- **THEN** 倒计时容器 SHALL 暴露与当前倒计时等价的可访问文本,供测试和辅助技术读取
|
||||
|
||||
#### Scenario: 刷新中状态
|
||||
- **WHEN** 数据正在刷新(isFetching=true 且 isLoading=false)
|
||||
- **THEN** 倒计时文本 SHALL 显示为"刷新中..."
|
||||
|
||||
#### Scenario: 等待首次刷新状态
|
||||
- **WHEN** 自动刷新模式下尚未完成首次刷新
|
||||
- **THEN** 倒计时文本 SHALL 显示为"等待首次刷新"
|
||||
|
||||
### Requirement: App 组件渲染隔离
|
||||
App 组件 SHALL NOT 持有任何高频更新的 state(如每秒更新的时钟),确保 App 的重渲染频率与数据刷新频率一致(默认 30 秒一次)。
|
||||
|
||||
#### Scenario: App 无 now state
|
||||
- **WHEN** App 组件渲染
|
||||
- **THEN** App SHALL NOT 包含 `useState` 管理的时钟 state,也 SHALL NOT 包含每秒触发的 `setInterval`
|
||||
|
||||
#### Scenario: App 重渲染频率
|
||||
- **WHEN** Dashboard 处于自动刷新模式
|
||||
- **THEN** App 组件的重渲染 SHALL 仅由 TanStack Query 的 refetch 触发(频率等于用户选择的刷新间隔),而非每秒触发
|
||||
|
||||
### Requirement: 手动刷新按钮
|
||||
选择"手动"模式时,倒计时区域 SHALL 替换为刷新按钮。
|
||||
|
||||
#### Scenario: 手动模式显示按钮
|
||||
- **WHEN** 用户选择"手动"刷新频率
|
||||
- **THEN** 倒计时区域 SHALL 替换为刷新图标按钮
|
||||
|
||||
#### Scenario: 点击刷新
|
||||
- **WHEN** 用户点击刷新按钮
|
||||
- **THEN** 系统 SHALL 触发一次数据刷新
|
||||
|
||||
#### Scenario: 刷新中禁用
|
||||
- **WHEN** 数据正在刷新
|
||||
- **THEN** 刷新按钮 SHALL 显示 loading 状态且 disabled,防止连续点击
|
||||
|
||||
### Requirement: 布局稳定性
|
||||
倒计时/按钮容器 SHALL 保持布局稳定,避免内容变化导致的抖动。NumberFlow 倒计时 SHALL 通过分组同步和等宽数字样式降低位数、单位和动画变化带来的布局偏移。
|
||||
|
||||
#### Scenario: 数字等宽
|
||||
- **WHEN** 倒计时数字变化
|
||||
- **THEN** 容器和 NumberFlow 倒计时 SHALL 使用 tabular-nums 字体特性,确保数字等宽不抖动
|
||||
|
||||
#### Scenario: NumberFlow 分组同步
|
||||
- **WHEN** 分钟级倒计时同时渲染分钟和秒数
|
||||
- **THEN** 分钟和秒数 SHALL 使用 `NumberFlowGroup` 同步布局变化
|
||||
|
||||
#### Scenario: 格式切换不抖动
|
||||
- **WHEN** 倒计时在按钮、秒级文本和分钟级文本之间切换
|
||||
- **THEN** 容器 SHALL 使用 min-width 确保最小宽度,避免 RadioGroup 位移
|
||||
@@ -1,50 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
TBD - 统一服务启动引导函数,封装开发和生产模式的完整启动序列。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 统一启动引导函数
|
||||
系统 SHALL 提供 `src/server/bootstrap.ts` 导出 `bootstrap(options: BootstrapOptions)` 函数,封装完整的服务启动序列:加载配置、初始化正式 logger、创建 store、同步 targets、创建并启动 engine、启动 HTTP server、注册 shutdown handler。配置加载失败发生在正式 logger 初始化之前,系统 SHALL 使用 console fallback 输出启动失败信息。配置加载成功后的启动失败 SHALL 使用正式 logger 输出 `fatal` 后退出。
|
||||
|
||||
#### Scenario: 开发模式启动
|
||||
- **WHEN** `dev.ts` 调用 `bootstrap({ configPath, mode: "development" })`
|
||||
- **THEN** 系统 SHALL 完成完整启动序列,不传入 staticAssets,并初始化运行时 logger
|
||||
|
||||
#### Scenario: 生产模式启动(带静态资源)
|
||||
- **WHEN** code-generated entry 调用 `bootstrap({ configPath, mode: "production", staticAssets })`
|
||||
- **THEN** 系统 SHALL 完成完整启动序列,将 staticAssets 传递给 startServer,并初始化运行时 logger
|
||||
|
||||
#### Scenario: 配置加载失败处理
|
||||
- **WHEN** 配置文件读取、YAML 解析或配置校验失败
|
||||
- **THEN** 系统 SHALL 通过 console fallback 输出错误信息并以非零退出码退出进程
|
||||
|
||||
#### Scenario: 配置加载后的启动失败处理
|
||||
- **WHEN** logger、store、engine 或 HTTP server 初始化失败
|
||||
- **THEN** 系统 SHALL 通过正式 logger 输出 `fatal` 日志并以非零退出码退出进程
|
||||
|
||||
#### Scenario: 优雅关机
|
||||
- **WHEN** 进程收到 SIGINT 或 SIGTERM 信号
|
||||
- **THEN** bootstrap 注册的 shutdown handler SHALL 调用 engine.stop()、store.close() 和 logger.flush() 后退出
|
||||
|
||||
### Requirement: BootstrapOptions 接口
|
||||
`bootstrap` 函数 SHALL 接受 `BootstrapOptions` 参数,包含 `configPath: string`、`mode: RuntimeMode` 和可选的 `staticAssets?: StaticAssets`。
|
||||
|
||||
#### Scenario: 最小配置(开发模式)
|
||||
- **WHEN** 仅传入 configPath 和 mode
|
||||
- **THEN** 系统 SHALL 正常启动,startServer 不接收 staticAssets 参数
|
||||
|
||||
#### Scenario: 生产模式配置
|
||||
- **WHEN** 传入 configPath、mode 和 staticAssets
|
||||
- **THEN** 系统 SHALL 将 staticAssets 传递给 startServer
|
||||
|
||||
### Requirement: dev.ts 和生产入口使用 bootstrap
|
||||
`dev.ts` 和 `src/server/main.ts` SHALL 调用 `bootstrap()` 而非各自维护启动序列。
|
||||
|
||||
#### Scenario: dev.ts 调用 bootstrap
|
||||
- **WHEN** 开发者运行 `bun run dev`
|
||||
- **THEN** `dev.ts` SHALL 调用 `bootstrap` 完成启动
|
||||
|
||||
#### Scenario: main.ts 调用 bootstrap
|
||||
- **WHEN** 生产可执行文件启动
|
||||
- **THEN** `main.ts` SHALL 调用 `bootstrap` 完成启动
|
||||
@@ -1,6 +1,6 @@
|
||||
## Purpose
|
||||
|
||||
定义将 Vite 构建的前端资源通过 code generation 嵌入 Bun 后端,打包为单个 standalone executable 的生产构建、运行配置和验证要求。
|
||||
定义将 Vite 构建的前端资源通过 code generation 嵌入 Bun 后端、静态资源服务与 Content-Type 处理、打包为单个 standalone executable 的生产构建、运行配置和验证要求。
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -89,3 +89,63 @@ executable MUST 将环境相关运行时配置保留在嵌入的前端和 server
|
||||
#### Scenario: 升迁后重新构建
|
||||
- **WHEN** 开发者先升迁 `package.json.version` 再运行生产构建命令
|
||||
- **THEN** 新生成的 standalone executable SHALL 返回升迁后的版本号
|
||||
|
||||
### Requirement: 构建时资源扫描与 Code Generation
|
||||
构建脚本 SHALL 在 Vite build 完成后扫描 `dist/web/` 目录,自动生成 TypeScript 文件,为每个静态资源创建 `import ... with { type: "file" }` 声明。
|
||||
|
||||
#### Scenario: 生成资源导入文件
|
||||
- **WHEN** 构建脚本扫描 `dist/web/` 目录
|
||||
- **THEN** 系统 SHALL 在 `.build/static-assets.ts` 中为每个文件生成 `import fN from "<path>" with { type: "file" }` 语句,并导出 `StaticAssets` 对象
|
||||
|
||||
#### Scenario: StaticAssets 对象结构
|
||||
- **WHEN** `static-assets.ts` 被生成
|
||||
- **THEN** 导出的对象 SHALL 包含 `indexHtml: Blob` 和 `files: Record<string, Blob>` 两个字段,其中 files 的 key 为 URL 路径(如 `/assets/index-a1b2c3.js`)
|
||||
|
||||
#### Scenario: 生成 production server entry
|
||||
- **WHEN** 构建脚本生成资源导入文件后
|
||||
- **THEN** 系统 SHALL 在 `.build/server-entry.ts` 中生成 production 入口,import bootstrap、config 和 staticAssets 并调用 bootstrap
|
||||
|
||||
### Requirement: 运行时静态资源服务
|
||||
系统 SHALL 提供 `serveStaticAsset` 函数,根据请求路径从 StaticAssets 中查找并返回对应资源。
|
||||
|
||||
#### Scenario: 请求根路径
|
||||
- **WHEN** 请求路径为 `/`
|
||||
- **THEN** 系统 SHALL 返回 `indexHtml`,Content-Type 为 `text/html; charset=utf-8`,Cache-Control 为 `no-cache`
|
||||
|
||||
#### Scenario: 请求已知静态资源
|
||||
- **WHEN** 请求路径匹配 `files` 中的某个 key
|
||||
- **THEN** 系统 SHALL 返回对应 Blob,Content-Type 根据文件扩展名推断,Cache-Control 为 `public, max-age=31536000, immutable`
|
||||
|
||||
#### Scenario: 请求未知带扩展名路径
|
||||
- **WHEN** 请求路径包含文件扩展名但未匹配任何已知资源
|
||||
- **THEN** 系统 SHALL 返回 404 响应
|
||||
|
||||
#### Scenario: SPA Fallback
|
||||
- **WHEN** 请求路径不包含文件扩展名且不以 `/api/` 开头
|
||||
- **THEN** 系统 SHALL 返回 `indexHtml`(SPA fallback)
|
||||
|
||||
### Requirement: Content-Type 推断
|
||||
系统 SHALL 根据文件扩展名推断正确的 Content-Type header。
|
||||
|
||||
#### Scenario: JavaScript 文件
|
||||
- **WHEN** 请求路径以 `.js` 或 `.mjs` 结尾
|
||||
- **THEN** Content-Type SHALL 为 `text/javascript; charset=utf-8`
|
||||
|
||||
#### Scenario: CSS 文件
|
||||
- **WHEN** 请求路径以 `.css` 结尾
|
||||
- **THEN** Content-Type SHALL 为 `text/css; charset=utf-8`
|
||||
|
||||
#### Scenario: SVG 文件
|
||||
- **WHEN** 请求路径以 `.svg` 结尾
|
||||
- **THEN** Content-Type SHALL 为 `image/svg+xml`
|
||||
|
||||
### Requirement: 静态资源 import specifier SHALL 使用平台无关分隔符
|
||||
构建时静态资源 code generation SHALL 将文件系统相对路径转换为 ESM import specifier,并确保生成的 import 路径在 Windows、macOS、Linux 开发环境下都使用 `/` 作为分隔符。
|
||||
|
||||
#### Scenario: Windows 相对路径转换为 import specifier
|
||||
- **WHEN** code generation 将 Windows 文件系统相对路径 `..\\dist\\web\\assets\\app.js` 转换为静态资源 import specifier
|
||||
- **THEN** 生成的 import specifier SHALL 为 `../dist/web/assets/app.js`,且 MUST NOT 包含 `\\`
|
||||
|
||||
#### Scenario: POSIX 相对路径保持 import specifier 形式
|
||||
- **WHEN** code generation 将 POSIX 文件系统相对路径 `../dist/web/assets/app.js` 转换为静态资源 import specifier
|
||||
- **THEN** 生成的 import specifier SHALL 保持为 `../dist/web/assets/app.js`
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
# Static Asset Embedding
|
||||
|
||||
定义构建时将 Vite 产出的前端静态资源嵌入 Bun 可执行文件的 code generation 流程和运行时静态资源服务逻辑。
|
||||
|
||||
## Purpose
|
||||
|
||||
支持将 Vite 构建的前端资源通过 `import with { type: "file" }` 嵌入 Bun 可执行文件,实现单文件交付的同时保持正确的缓存策略和 Content-Type 处理。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 构建时资源扫描与 Code Generation
|
||||
构建脚本 SHALL 在 Vite build 完成后扫描 `dist/web/` 目录,自动生成 TypeScript 文件,为每个静态资源创建 `import ... with { type: "file" }` 声明。
|
||||
|
||||
#### Scenario: 生成资源导入文件
|
||||
- **WHEN** 构建脚本扫描 `dist/web/` 目录
|
||||
- **THEN** 系统 SHALL 在 `.build/static-assets.ts` 中为每个文件生成 `import fN from "<path>" with { type: "file" }` 语句,并导出 `StaticAssets` 对象
|
||||
|
||||
#### Scenario: StaticAssets 对象结构
|
||||
- **WHEN** `static-assets.ts` 被生成
|
||||
- **THEN** 导出的对象 SHALL 包含 `indexHtml: Blob` 和 `files: Record<string, Blob>` 两个字段,其中 files 的 key 为 URL 路径(如 `/assets/index-a1b2c3.js`)
|
||||
|
||||
#### Scenario: 生成 production server entry
|
||||
- **WHEN** 构建脚本生成资源导入文件后
|
||||
- **THEN** 系统 SHALL 在 `.build/server-entry.ts` 中生成 production 入口,import bootstrap、config 和 staticAssets 并调用 bootstrap
|
||||
|
||||
### Requirement: 运行时静态资源服务
|
||||
系统 SHALL 提供 `serveStaticAsset` 函数,根据请求路径从 StaticAssets 中查找并返回对应资源。
|
||||
|
||||
#### Scenario: 请求根路径
|
||||
- **WHEN** 请求路径为 `/`
|
||||
- **THEN** 系统 SHALL 返回 `indexHtml`,Content-Type 为 `text/html; charset=utf-8`,Cache-Control 为 `no-cache`
|
||||
|
||||
#### Scenario: 请求已知静态资源
|
||||
- **WHEN** 请求路径匹配 `files` 中的某个 key
|
||||
- **THEN** 系统 SHALL 返回对应 Blob,Content-Type 根据文件扩展名推断,Cache-Control 为 `public, max-age=31536000, immutable`
|
||||
|
||||
#### Scenario: 请求未知带扩展名路径
|
||||
- **WHEN** 请求路径包含文件扩展名但未匹配任何已知资源
|
||||
- **THEN** 系统 SHALL 返回 404 响应
|
||||
|
||||
#### Scenario: SPA Fallback
|
||||
- **WHEN** 请求路径不包含文件扩展名且不以 `/api/` 开头
|
||||
- **THEN** 系统 SHALL 返回 `indexHtml`(SPA fallback)
|
||||
|
||||
### Requirement: Content-Type 推断
|
||||
系统 SHALL 根据文件扩展名推断正确的 Content-Type header。
|
||||
|
||||
#### Scenario: JavaScript 文件
|
||||
- **WHEN** 请求路径以 `.js` 或 `.mjs` 结尾
|
||||
- **THEN** Content-Type SHALL 为 `text/javascript; charset=utf-8`
|
||||
|
||||
#### Scenario: CSS 文件
|
||||
- **WHEN** 请求路径以 `.css` 结尾
|
||||
- **THEN** Content-Type SHALL 为 `text/css; charset=utf-8`
|
||||
|
||||
#### Scenario: SVG 文件
|
||||
- **WHEN** 请求路径以 `.svg` 结尾
|
||||
- **THEN** Content-Type SHALL 为 `image/svg+xml`
|
||||
|
||||
### Requirement: 静态资源 import specifier SHALL 使用平台无关分隔符
|
||||
构建时静态资源 code generation SHALL 将文件系统相对路径转换为 ESM import specifier,并确保生成的 import 路径在 Windows、macOS、Linux 开发环境下都使用 `/` 作为分隔符。
|
||||
|
||||
#### Scenario: Windows 相对路径转换为 import specifier
|
||||
- **WHEN** code generation 将 Windows 文件系统相对路径 `..\\dist\\web\\assets\\app.js` 转换为静态资源 import specifier
|
||||
- **THEN** 生成的 import specifier SHALL 为 `../dist/web/assets/app.js`,且 MUST NOT 包含 `\\`
|
||||
|
||||
#### Scenario: POSIX 相对路径保持 import specifier 形式
|
||||
- **WHEN** code generation 将 POSIX 文件系统相对路径 `../dist/web/assets/app.js` 转换为静态资源 import specifier
|
||||
- **THEN** 生成的 import specifier SHALL 保持为 `../dist/web/assets/app.js`
|
||||
@@ -1,45 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 target 分组能力:YAML 配置中的 group 字段、后端存储、API 传递和前端分组排序。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: target 分组配置
|
||||
系统 SHALL 支持在每个 target 上配置可选的 `group` 字段,用于将目标归类到不同分组。未指定 `group` 的 target SHALL 归入 `"default"` 分组。
|
||||
|
||||
#### Scenario: 配置分组名称
|
||||
- **WHEN** YAML 配置中某个 target 指定 `group: "搜索引擎"`
|
||||
- **THEN** 系统 SHALL 将该 target 归类到 "搜索引擎" 分组
|
||||
|
||||
#### Scenario: 不配置分组
|
||||
- **WHEN** YAML 配置中某个 target 未指定 `group` 字段
|
||||
- **THEN** 系统 SHALL 将该 target 归类到 "default" 分组
|
||||
|
||||
#### Scenario: group 字段类型校验
|
||||
- **WHEN** YAML 配置中某个 target 的 `group` 字段不是字符串
|
||||
- **THEN** 系统 SHALL 以错误退出并提示 group 字段类型错误
|
||||
|
||||
### Requirement: 分组排序
|
||||
系统 SHALL 保证 "default" 分组始终排在最前面,其余分组按配置文件中首次出现的顺序排列。
|
||||
|
||||
#### Scenario: default 分组排最前
|
||||
- **WHEN** 配置中存在多个分组(包括 "default" 和自定义分组)
|
||||
- **THEN** API 返回的目标列表中 "default" 分组的目标 SHALL 排在其他分组之前
|
||||
|
||||
#### Scenario: 自定义分组按出现顺序
|
||||
- **WHEN** 配置中 "搜索引擎" 分组在 "后端服务" 分组之前首次出现
|
||||
- **THEN** API 返回中 "搜索引擎" 分组 SHALL 排在 "后端服务" 分组之前
|
||||
|
||||
### Requirement: 分组信息 API 传递
|
||||
系统 SHALL 在 API 响应中返回每个 target 的分组信息。
|
||||
|
||||
#### Scenario: targets 列表包含分组
|
||||
- **WHEN** 客户端请求 `GET /api/targets`
|
||||
- **THEN** 响应中每个目标 SHALL 包含 `group` 字段,值为该目标所属的分组名称
|
||||
|
||||
### Requirement: 分组存储
|
||||
系统 SHALL 在数据库 targets 表中持久化每个 target 的分组信息。
|
||||
|
||||
#### Scenario: 持久化分组信息
|
||||
- **WHEN** 系统同步 targets 到数据库
|
||||
- **THEN** 每个 target 的 `grp` 列 SHALL 存储其分组名称,未配置分组的存储 `"default"`
|
||||
@@ -1,110 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 target 的 id/name 双字段标识体系:id 作为唯一标识符,name 作为可选展示名称。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: target id 字段
|
||||
每个 target SHALL 包含必填的 `id` 字段作为唯一标识符。`id` SHALL 符合 `[a-zA-Z0-9][a-zA-Z0-9_-]*` 命名规则。`id` 长度 MUST 为 1 到 30 个字符。`id` MUST 在所有 targets 中全局唯一。`id` MUST NOT 参与变量替换。
|
||||
|
||||
#### Scenario: 合法 id
|
||||
- **WHEN** target 配置 `id: "api-health"`
|
||||
- **THEN** 系统 SHALL 使用该 id 作为 target 的唯一标识符
|
||||
|
||||
#### Scenario: id 包含下划线和连字符
|
||||
- **WHEN** target 配置 `id: "db_check-01"`
|
||||
- **THEN** 系统 SHALL 使用该 id 作为 target 的唯一标识符
|
||||
|
||||
#### Scenario: id 缺失报错
|
||||
- **WHEN** target 未配置 `id` 字段
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示该 target 缺少 id 字段
|
||||
|
||||
#### Scenario: id 为空字符串报错
|
||||
- **WHEN** target 配置 `id: ""`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 id 不能为空
|
||||
|
||||
#### Scenario: id 超过最大长度报错
|
||||
- **WHEN** target 配置超过 30 个字符的 `id`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 id 长度不合法
|
||||
|
||||
#### Scenario: id 不合法报错
|
||||
- **WHEN** target 配置 `id: "_invalid"` 或 `id: "-start"` 或 `id: "has space"`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 id 不符合命名规则
|
||||
|
||||
#### Scenario: id 重复报错
|
||||
- **WHEN** 两个 target 配置相同的 `id: "api-health"`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 id 重复
|
||||
|
||||
### Requirement: target name 字段
|
||||
每个 target SHALL 支持可选的 `name` 字段作为展示名称元信息。`name` 缺省或显式配置为 `null` 时 SHALL 在配置解析、运行时模型、存储和 API 中保留为 null。前端展示目标名称时 SHALL 使用 `name ?? id`,但该 fallback MUST NOT 改变 target 本身的 name 值。显式配置的 `name` MUST 为长度 1 到 30 个字符的字符串,且去除首尾空白后 MUST 不为空。`name` SHALL 支持变量替换。`name` MUST NOT 要求全局唯一,MUST NOT 参与 target 唯一性判定。
|
||||
|
||||
#### Scenario: 配置 name
|
||||
- **WHEN** target 配置 `id: "api-health"` 和 `name: "API 健康检查"`
|
||||
- **THEN** 系统 SHALL 在解析后保留 name 为 "API 健康检查"
|
||||
|
||||
#### Scenario: name 使用变量
|
||||
- **WHEN** target 配置 `name: "${env} API 健康检查"` 且 variables 中 `env: "生产"`
|
||||
- **THEN** 系统 SHALL 将 name 解析为 "生产 API 健康检查"
|
||||
|
||||
#### Scenario: name 缺省保留为 null
|
||||
- **WHEN** target 配置 `id: "api-health"` 但未配置 `name`
|
||||
- **THEN** 系统 SHALL 在解析、存储和 API 响应中保留 name 为 null
|
||||
|
||||
#### Scenario: name 显式 null
|
||||
- **WHEN** target 配置 `id: "api-health"` 和 `name: null`
|
||||
- **THEN** 系统 SHALL 接受该配置,并在解析、存储和 API 响应中保留 name 为 null
|
||||
|
||||
#### Scenario: name 空 YAML 值
|
||||
- **WHEN** target 配置 `id: "api-health"` 且 `name:` 后不提供值
|
||||
- **THEN** 系统 SHALL 将该 name 按 null 处理,并接受该配置
|
||||
|
||||
#### Scenario: name 为 null 时展示 fallback
|
||||
- **WHEN** 前端展示 name 为 null 的 target
|
||||
- **THEN** 前端 SHALL 显示该 target 的 id 作为目标名称文案
|
||||
|
||||
#### Scenario: name 为空字符串报错
|
||||
- **WHEN** target 配置 `name: ""`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 name 不能为空
|
||||
|
||||
#### Scenario: name 仅包含空白字符报错
|
||||
- **WHEN** target 配置 `name: " "`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 name 不能为空
|
||||
|
||||
#### Scenario: name 超过最大长度报错
|
||||
- **WHEN** target 配置超过 30 个字符的 `name`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 name 长度不合法
|
||||
|
||||
#### Scenario: 多个 target 使用相同 name
|
||||
- **WHEN** 两个 target 配置不同 id 但相同 `name: "健康检查"`
|
||||
- **THEN** 系统 SHALL 接受该配置,不报错(name 不要求全局唯一)
|
||||
|
||||
### Requirement: target description 字段
|
||||
每个 target SHALL 支持可选的 `description` 字段作为目标说明。`description` 缺省或显式配置为 `null` 时 SHALL 在配置解析、运行时模型、存储和 API 中保留为 null。`description` SHALL 支持变量替换。`description` 字符串长度 MUST 不超过 500 个字符,且允许为空字符串。`description` MUST NOT 参与 target 唯一性判定。
|
||||
|
||||
#### Scenario: 配置 description
|
||||
- **WHEN** target 配置 `description: "检查生产 API 健康状态"`
|
||||
- **THEN** 系统 SHALL 使用该值作为目标说明
|
||||
|
||||
#### Scenario: description 使用变量
|
||||
- **WHEN** target 配置 `description: "${env} 环境健康检查"` 且 variables 中 `env: "生产"`
|
||||
- **THEN** 系统 SHALL 将目标说明解析为 "生产 环境健康检查"
|
||||
|
||||
#### Scenario: description 缺省
|
||||
- **WHEN** target 未配置 `description`
|
||||
- **THEN** 系统 SHALL 接受该配置,且目标说明为 null
|
||||
|
||||
#### Scenario: description 显式 null
|
||||
- **WHEN** target 配置 `description: null`
|
||||
- **THEN** 系统 SHALL 接受该配置,且目标说明为 null
|
||||
|
||||
#### Scenario: description 空 YAML 值
|
||||
- **WHEN** target 配置 `description:` 后不提供值
|
||||
- **THEN** 系统 SHALL 将该 description 按 null 处理,并接受该配置
|
||||
|
||||
#### Scenario: description 为空字符串
|
||||
- **WHEN** target 配置 `description: ""`
|
||||
- **THEN** 系统 SHALL 接受该配置,且目标说明为空字符串
|
||||
|
||||
#### Scenario: description 超过最大长度报错
|
||||
- **WHEN** target 配置超过 500 个字符的 `description`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 description 长度不合法
|
||||
@@ -1,164 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义单目标指标 API 的端点、响应类型、延迟百分位计算、MTTR 计算、最长故障时长计算、故障事件计数、当前连续状态、趋势数据应用层分桶和无数据口径。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 单目标指标 API
|
||||
系统 SHALL 提供 `GET /api/targets/:id/metrics` 端点,返回单个目标在指定时间窗口内的概览统计和趋势数据。
|
||||
|
||||
#### Scenario: 获取目标指标
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=ISO&to=ISO&bucket=1h`
|
||||
- **THEN** 系统 SHALL 返回 JSON 对象包含 targetId、window、stats、trend 字段
|
||||
|
||||
#### Scenario: from 或 to 参数缺失
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/metrics` 未提供 from 或 to 参数
|
||||
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
|
||||
|
||||
#### Scenario: 目标不存在
|
||||
- **WHEN** 客户端请求 `GET /api/targets/999/metrics?from=ISO&to=ISO`
|
||||
- **THEN** 系统 SHALL 返回 404 状态码和错误信息
|
||||
|
||||
#### Scenario: 无效的目标 ID
|
||||
- **WHEN** 客户端请求 `GET /api/targets/abc/metrics?from=ISO&to=ISO`
|
||||
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
|
||||
|
||||
#### Scenario: bucket 参数缺失
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=ISO&to=ISO` 未提供 bucket 参数
|
||||
- **THEN** 系统 SHALL 默认使用 bucket=`1h`
|
||||
|
||||
#### Scenario: 不支持的 bucket 参数
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=ISO&to=ISO&bucket=5m`
|
||||
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
|
||||
|
||||
### Requirement: TargetMetricsResponse 共享类型
|
||||
系统 SHALL 在 `src/shared/api.ts` 中定义 `TargetMetricsResponse` 类型。
|
||||
|
||||
#### Scenario: 类型定义
|
||||
- **WHEN** 前后端引用 `TargetMetricsResponse` 类型
|
||||
- **THEN** 该类型 SHALL 包含 targetId(number)、window(from/to/bucket)、stats 和 trend 字段
|
||||
|
||||
#### Scenario: stats 字段
|
||||
- **WHEN** metrics 响应包含 stats
|
||||
- **THEN** stats SHALL 包含 totalChecks、upChecks、downChecks、availability、avgDurationMs、p95DurationMs、p99DurationMs、mttr、longestOutage、incidentCount、currentStreak 字段
|
||||
|
||||
#### Scenario: trend 字段
|
||||
- **WHEN** metrics 响应包含 trend
|
||||
- **THEN** trend SHALL 为数组,每个元素包含 bucketStart、avgDurationMs、minDurationMs、maxDurationMs、availability、totalChecks、upChecks、downChecks 字段
|
||||
|
||||
### Requirement: P95/P99 延迟计算
|
||||
系统 SHALL 在后端应用层计算 P95 和 P99 延迟百分位数。
|
||||
|
||||
#### Scenario: 正常计算 P95
|
||||
- **WHEN** 时间窗口内存在成功检查记录(matched=1 且 duration_ms 不为 null)
|
||||
- **THEN** 系统 SHALL 取出所有成功检查的 duration_ms,在后端应用层排序后取第 95 百分位值返回为 p95DurationMs
|
||||
|
||||
#### Scenario: 正常计算 P99
|
||||
- **WHEN** 时间窗口内存在成功检查记录
|
||||
- **THEN** 系统 SHALL 取第 99 百分位值返回为 p99DurationMs
|
||||
|
||||
#### Scenario: 无成功检查记录
|
||||
- **WHEN** 时间窗口内无 matched=1 且 duration_ms 不为 null 的记录
|
||||
- **THEN** p95DurationMs 和 p99DurationMs SHALL 返回 null
|
||||
|
||||
#### Scenario: 百分位计算方法
|
||||
- **WHEN** 计算第 N 百分位
|
||||
- **THEN** 系统 SHALL 将 duration_ms 升序排列,取 index = ceil(count * N / 100) - 1 位置的值
|
||||
|
||||
### Requirement: MTTR 计算
|
||||
系统 SHALL 在后端应用层计算平均恢复时间(Mean Time To Recovery)。
|
||||
|
||||
#### Scenario: 存在已恢复的故障段
|
||||
- **WHEN** 时间窗口内存在至少一个已恢复的故障段(连续 matched=0 后跟 matched=1)
|
||||
- **THEN** 系统 SHALL 计算所有已恢复故障段的平均持续时间(从首个 matched=0 的 timestamp 到恢复后首个 matched=1 的 timestamp 之差),返回为 mttr(毫秒)
|
||||
|
||||
#### Scenario: 无已恢复的故障段
|
||||
- **WHEN** 时间窗口内无已恢复的故障段(全部正常,或当前仍在故障中且无历史恢复)
|
||||
- **THEN** mttr SHALL 返回 null
|
||||
|
||||
#### Scenario: 当前正在故障中
|
||||
- **WHEN** 时间窗口内最后一段故障尚未恢复
|
||||
- **THEN** 该未恢复的故障段 SHALL 不计入 MTTR 平均值
|
||||
|
||||
#### Scenario: 窗口起始即为故障且后续恢复
|
||||
- **WHEN** 时间窗口内第一条记录即为 matched=0(故障跨越了 from 边界),且该故障段在窗口内恢复
|
||||
- **THEN** 该故障段 SHALL 不计入 MTTR 平均值(因无法确定真实故障开始时间),但 SHALL 计入 incidentCount
|
||||
|
||||
### Requirement: 最长故障时长
|
||||
系统 SHALL 在后端应用层计算时间窗口内最长的单次故障持续时间。
|
||||
|
||||
#### Scenario: 存在故障段
|
||||
- **WHEN** 时间窗口内存在故障段
|
||||
- **THEN** 系统 SHALL 返回最长故障段的持续时间为 longestOutage(毫秒)
|
||||
|
||||
#### Scenario: 无故障
|
||||
- **WHEN** 时间窗口内无 matched=0 的记录
|
||||
- **THEN** longestOutage SHALL 返回 null
|
||||
|
||||
#### Scenario: 窗口起始即为故障
|
||||
- **WHEN** 时间窗口内第一条记录即为 matched=0
|
||||
- **THEN** 该故障段的持续时间 SHALL 从 from 参数开始计算
|
||||
|
||||
#### Scenario: 当前正在故障中
|
||||
- **WHEN** 最后一段故障尚未恢复
|
||||
- **THEN** 该故障段的持续时间 SHALL 计算为从故障开始到时间窗口 to 参数的时间差
|
||||
|
||||
### Requirement: 故障事件计数
|
||||
系统 SHALL 在后端应用层计算时间窗口内的故障事件次数。
|
||||
|
||||
#### Scenario: 计算故障事件数
|
||||
- **WHEN** 时间窗口内存在状态翻转(matched 从 1 变为 0)
|
||||
- **THEN** 系统 SHALL 返回翻转次数为 incidentCount
|
||||
|
||||
#### Scenario: 无故障事件
|
||||
- **WHEN** 时间窗口内所有检查均为 matched=1
|
||||
- **THEN** incidentCount SHALL 返回 0
|
||||
|
||||
#### Scenario: 窗口起始即为故障
|
||||
- **WHEN** 时间窗口内第一条记录即为 matched=0 且无前序记录可判断翻转
|
||||
- **THEN** 该故障 SHALL 计为 1 次事件
|
||||
|
||||
#### Scenario: 连续异常只计一次
|
||||
- **WHEN** 某目标连续 10 次 matched=0
|
||||
- **THEN** 该连续异常段 SHALL 仅计为 1 次事件
|
||||
|
||||
### Requirement: 当前连续状态
|
||||
系统 SHALL 返回目标当前的连续状态信息。
|
||||
|
||||
#### Scenario: 当前连续正常
|
||||
- **WHEN** 目标最近的检查记录连续为 matched=1
|
||||
- **THEN** currentStreak SHALL 返回 `{ up: true, count: N }`,N 为连续正常的检查次数
|
||||
|
||||
#### Scenario: 当前连续异常
|
||||
- **WHEN** 目标最近的检查记录连续为 matched=0
|
||||
- **THEN** currentStreak SHALL 返回 `{ up: false, count: N }`,N 为连续异常的检查次数
|
||||
|
||||
#### Scenario: 连续状态达到取数上限
|
||||
- **WHEN** 连续状态次数达到后端取数或计算上限
|
||||
- **THEN** currentStreak SHALL 返回 `{ up: boolean, count: N, capped: true }`,前端据此展示上限标记
|
||||
|
||||
#### Scenario: 无检查记录
|
||||
- **WHEN** 目标没有任何检查记录
|
||||
- **THEN** currentStreak SHALL 返回 null
|
||||
|
||||
### Requirement: 趋势数据应用层分桶
|
||||
系统 SHALL 在后端应用层按 UTC 小时分桶生成趋势数据。
|
||||
|
||||
#### Scenario: 按小时生成趋势
|
||||
- **WHEN** metrics 请求 bucket=`1h`
|
||||
- **THEN** 系统 SHALL 按 UTC 小时生成 trend 数组,每个点包含该小时内的 totalChecks、upChecks、downChecks、availability、avgDurationMs、minDurationMs、maxDurationMs
|
||||
|
||||
#### Scenario: 小时内无成功检查
|
||||
- **WHEN** 某小时内存在检查记录但无成功检查记录
|
||||
- **THEN** avgDurationMs、minDurationMs、maxDurationMs SHALL 返回 null,availability SHALL 基于 upChecks/totalChecks 返回 0
|
||||
|
||||
#### Scenario: 小时内无检查记录
|
||||
- **WHEN** 某小时内没有任何检查记录
|
||||
- **THEN** 系统 MAY 不返回该小时对应的 trend 点
|
||||
|
||||
### Requirement: 无数据口径
|
||||
系统 SHALL 对无数据窗口返回稳定的空指标口径。
|
||||
|
||||
#### Scenario: 窗口内无检查记录
|
||||
- **WHEN** 指定时间窗口内没有任何检查记录
|
||||
- **THEN** stats SHALL 返回 totalChecks=0、upChecks=0、downChecks=0、availability=0、avgDurationMs=null、p95DurationMs=null、p99DurationMs=null、mttr=null、longestOutage=null、incidentCount=0、currentStreak=null,trend SHALL 返回空数组
|
||||
@@ -23,14 +23,6 @@
|
||||
- **WHEN** YAML 中 tcp target 的 `tcp.port` 不是 1 到 65535 之间的整数
|
||||
- **THEN** 系统 SHALL 以配置错误退出,并提示 tcp.port 必须为合法 TCP 端口
|
||||
|
||||
#### Scenario: tcp defaults 覆盖 banner 参数
|
||||
- **WHEN** YAML 中配置 `defaults.tcp.bannerReadTimeout: 1000` 和 `defaults.tcp.maxBannerBytes: "8KB"`
|
||||
- **THEN** 未显式配置对应字段的 tcp target SHALL 使用 defaults.tcp 中的值
|
||||
|
||||
#### Scenario: per-target banner 参数覆盖 defaults
|
||||
- **WHEN** defaults.tcp 配置了 banner 参数,且某个 tcp target 显式配置 `tcp.bannerReadTimeout` 或 `tcp.maxBannerBytes`
|
||||
- **THEN** 该 target SHALL 使用自身 tcp 分组中的值
|
||||
|
||||
#### Scenario: tcp 序列化展示摘要
|
||||
- **WHEN** 系统同步 tcp target 到 targets 表
|
||||
- **THEN** `target` 展示摘要 SHALL 为 `<host>:<port>`,`config` JSON SHALL 包含 resolved 后的 host、port、readBanner、bannerReadTimeout 和 maxBannerBytes
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# Test Output Cleanliness
|
||||
|
||||
## Purpose
|
||||
|
||||
确保测试运行时输出干净、无噪音,便于开发者快速定位问题。
|
||||
|
||||
@@ -27,14 +27,6 @@
|
||||
- **WHEN** YAML 中 udp target 未配置 `udp.payload`
|
||||
- **THEN** 系统 SHALL 使用空字符串作为 payload,并在执行时发送零长度 UDP datagram
|
||||
|
||||
#### Scenario: udp defaults 覆盖通用 UDP 参数
|
||||
- **WHEN** YAML 中配置 `defaults.udp.encoding: "hex"`、`defaults.udp.responseEncoding: "hex"` 和 `defaults.udp.maxResponseBytes: "8KB"`
|
||||
- **THEN** 未显式配置对应字段的 udp target SHALL 使用 defaults.udp 中的值
|
||||
|
||||
#### Scenario: per-target UDP 参数覆盖 defaults
|
||||
- **WHEN** defaults.udp 配置了 encoding、responseEncoding 或 maxResponseBytes,且某个 udp target 显式配置对应字段
|
||||
- **THEN** 该 target SHALL 使用自身 udp 分组中的值
|
||||
|
||||
#### Scenario: udp 分组未知字段失败
|
||||
- **WHEN** YAML 中 udp target 的 `udp` 分组包含 `dnsQuery`、`expectResponse` 或其他未知字段
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 udp 分组包含未知字段
|
||||
@@ -129,7 +121,7 @@
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `error`,phase 为 `response`,message 包含响应被截断的信息
|
||||
|
||||
#### Scenario: maxResponseBytes 格式非法
|
||||
- **WHEN** YAML 中 udp target 或 defaults.udp 的 `maxResponseBytes` 不是非负整数或合法 size string
|
||||
- **WHEN** YAML 中 udp target 的 `maxResponseBytes` 不是非负整数或合法 size string
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 maxResponseBytes 格式错误
|
||||
|
||||
### Requirement: udp expect 校验
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# Vite Frontend Bundling
|
||||
|
||||
定义 Vite 作为前端构建工具的配置、产出结构和优化策略。
|
||||
|
||||
## Purpose
|
||||
|
||||
使用 Vite 的 Rolldown 引擎完成前端打包,实现 code splitting、vendor chunk 分离和 CSS 优化,解决 Bun bundler 前端性能问题。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Vite 前端构建配置
|
||||
系统 SHALL 使用 Vite 作为前端构建工具,配置文件位于项目根目录 `vite.config.ts`,以 `src/web` 为 root,产出到 `dist/web/`。
|
||||
|
||||
#### Scenario: 运行 Vite 生产构建
|
||||
- **WHEN** 构建脚本执行 `bunx --bun vite build`
|
||||
- **THEN** Vite SHALL 将 `src/web/index.html` 及其引用的所有模块构建到 `dist/web/` 目录,包含 `index.html` 和 `assets/` 子目录
|
||||
|
||||
#### Scenario: 产出文件名包含 content hash
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** `assets/` 目录下的 JS 和 CSS 文件名 SHALL 包含 content hash(如 `index-a1b2c3.js`)
|
||||
|
||||
### Requirement: Code Splitting 策略
|
||||
系统 SHALL 配置 Vite 的 Rolldown code splitting,将 vendor 库分离为独立 chunks,并通过 `React.lazy()` 动态导入实现按需加载。
|
||||
|
||||
#### Scenario: React 相关库分离
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** `react`、`react-dom`、`scheduler` SHALL 被打包到名为 `vendor-react` 的独立 chunk
|
||||
|
||||
#### Scenario: TDesign 相关库分离
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** `tdesign-react`、`tdesign-icons-react` 相关模块 SHALL 被打包到名为 `vendor-tdesign` 的独立 chunk
|
||||
|
||||
#### Scenario: 图表库分离
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** `recharts` 和 `d3-*` 相关模块 SHALL 被打包到名为 `vendor-chart` 的独立 chunk
|
||||
|
||||
#### Scenario: TargetDetailDrawer 延迟加载
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** `TargetDetailDrawer` 及其依赖(recharts、D3、DateRangePicker 等)SHALL 通过 `React.lazy()` 动态导入,被 Rolldown 自动拆分为异步 chunk,不包含在初始加载的 JS 中
|
||||
|
||||
#### Scenario: Drawer 首次渲染无闪烁
|
||||
- **WHEN** 用户首次点击目标触发 Drawer 渲染
|
||||
- **THEN** Drawer SHALL 通过 `<Suspense fallback={null}>` 包裹,利用其默认 visible=false 状态避免加载期间的视觉闪烁
|
||||
|
||||
### Requirement: CSS 处理
|
||||
系统 SHALL 通过 Vite 处理 CSS 导入,产出独立的 CSS 文件。TDesign 组件样式 SHALL 保持全量导入方式。
|
||||
|
||||
#### Scenario: CSS 文件产出
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** 所有 CSS 导入 SHALL 被提取为独立的 `.css` 文件到 `assets/` 目录
|
||||
|
||||
#### Scenario: CSS 压缩
|
||||
- **WHEN** Vite 执行生产构建
|
||||
- **THEN** 产出的 CSS 文件 SHALL 经过压缩处理
|
||||
|
||||
#### Scenario: TDesign CSS 全量导入
|
||||
- **WHEN** 前端入口文件初始化样式
|
||||
- **THEN** 系统 SHALL 通过 `tdesign-react/dist/reset.css` 和 `tdesign-react/dist/tdesign.min.css` 全量导入 TDesign 组件样式
|
||||
@@ -1,5 +1,3 @@
|
||||
# Capability: windows-test-compat
|
||||
|
||||
## Purpose
|
||||
|
||||
确保测试在 Windows、macOS、Linux 平台上的兼容性,包括文件句柄释放后的目录清理重试机制和跨平台命令测试约定。
|
||||
|
||||
Reference in New Issue
Block a user