chore: 清理过时 specs,新增 schemas 目录,更新审查提示词
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
审查 OpenSpec apply 完成后以及后续手动修补后的实际实现,判断代码、测试、变更文档是否一致,识别偏离、漏记和可优化点,并将确认后的实际变更同步回变更文档,按以下流程执行。
|
||||
审查 OpenSpec apply 完成后以及后续手动修补后的实际变更,判断实际产物、验证结果和变更文档是否与 `design.md` source of truth 一致,识别偏离、漏记和可优化点,并将确认后的实际变更同步回变更文档,按以下流程执行。
|
||||
|
||||
## 约束
|
||||
|
||||
- 先审查再修复;未经用户确认,不修改代码或变更文档
|
||||
- 默认按 `spec-driven` workflow 审查;识别 change 后先确认 `schemaName`;若实际 schema 不同,说明差异,仅对实际存在的 artifacts 做审查
|
||||
- 禁止同步或修改 `openspec/specs/` 下的主规范——主规范同步属于 archive 阶段,不在此提示词范围内;本次 change 目录下的 `specs/*.md`、代码、测试、README 等均可修改
|
||||
- 优先使用当前会话中的实现说明、测试结论、手动修补记录和已生成的变更文档;仅在无法明确 change、`schemaName`、改动范围或修补来源时,再用提问工具或 OpenSpec 命令补充定位
|
||||
- 不要因为代码已经存在就自动以代码为准;先判断差异属于"文档要求未实现"、"测试后新增修补"还是"意外偏离/回归"
|
||||
- 每批代码或文档修改执行前用提问工具获得用户确认
|
||||
- 先审查再修复;未经用户确认,不修改实际产物或变更文档
|
||||
- 默认按 `fast-drive` workflow 审查;识别 change 后先确认 `schemaName`;若实际 schema 不同,说明差异,仅对实际存在的 artifacts 做审查
|
||||
- 在 `fast-drive` workflow 下,核心 artifacts 是 `design.md` 和 `tasks.md`;不要要求存在 `proposal.md` 或 `specs/*.md`
|
||||
- 在 `fast-drive` workflow 下,`design.md` 是 scope、requirements、decisions、guardrails、execution direction 和 verification expectations 的 source of truth,`tasks.md` 是 apply 进度和验证闭环的 tracking 文件
|
||||
- 禁止同步或修改 `openspec/specs/` 下的主规范;若实际 schema 包含 `specs/*.md`,也只允许修改本次 change 目录下实际存在的 spec artifacts,主规范同步属于 archive 阶段,不在此提示词范围内
|
||||
- 优先使用当前会话中的执行说明、验证结论、手动修补记录和已生成的变更文档;仅在无法明确 change、`schemaName`、改动范围或修补来源时,再用提问工具或 OpenSpec 命令补充定位
|
||||
- 不要因为实际产物已经存在就自动以实际产物为准;先判断差异属于“design 要求未完成”、“验证后新增修补”、“合理落地细化”还是“意外偏离/回归”
|
||||
- 每批实际产物或文档修改执行前用提问工具获得用户确认
|
||||
- 删除/重写前用提问工具获得用户确认,并先备份原文件为 `{file}.bak.{timestamp}`
|
||||
- 若修改代码涉及新逻辑、模块结构、API、实体或用户可见行为,同步更新测试、相关变更文档和 README
|
||||
- 若修改实际产物涉及新行为、流程、接口、内容、数据、配置、责任边界或用户可见结果,同步更新验证材料、相关变更文档和必要的文档/沟通材料
|
||||
|
||||
## 1. 收集
|
||||
|
||||
@@ -22,18 +24,34 @@
|
||||
|
||||
a) 先并行读取核心入口和配置,确定范围:
|
||||
|
||||
- 本次 change 的 `proposal.md`
|
||||
- `openspec/config.yaml`
|
||||
- 本次 change 的 `design.md`
|
||||
- 本次 change 的 `tasks.md`
|
||||
- workflow context/configuration,例如存在时读取 `openspec/config.yaml`
|
||||
- 若可定位到 schema,读取对应 schema;`fast-drive` 下优先读取 `openspec/schemas/fast-drive/schema.yaml`
|
||||
|
||||
b) 从 `proposal.md` 提取 `Capabilities` / `Modified Capabilities`,确定 proposal 已声明的 spec 列表
|
||||
b) 从 `design.md` 提取审查基准:
|
||||
|
||||
c) 并行获取改动范围:`git diff --name-only`、`git diff --name-only --cached`;若工作区已干净,再结合文档和代码模块反推
|
||||
- `Context`
|
||||
- `Discussion Notes`
|
||||
- `Requirements`
|
||||
- `Goals / Non-Goals`
|
||||
- `Execution Guardrails`
|
||||
- `Affected Areas`
|
||||
- `Decisions`
|
||||
- `Execution Plan`
|
||||
- `Verification Plan`
|
||||
- `Risks / Trade-offs`
|
||||
- `Open Questions`
|
||||
|
||||
d) 对比 git diff 涉及的模块与 proposal 已声明的 spec,识别 apply 阶段新增改动触及但 proposal 未覆盖的现有 spec,合并为完整 spec 读取列表;相关性来源还包括:手动修补涉及的受影响能力、`design.md` Impact 中提到的模块、相关代码对应的现有能力
|
||||
c) 从 `tasks.md` 提取任务状态、已完成项、未完成项、验证任务和文档/沟通任务;重点记录所有已标记完成的 `- [x]` 或等价完成状态。
|
||||
|
||||
e) 并行读取完整 spec 列表和其余 artifacts(`design.md`、`tasks.md`、相关源码、测试文件、README、架构文档)
|
||||
d) 获取实际改动范围:若在版本控制工作区中,优先使用 `git diff --name-only`、`git diff --name-only --cached`;若工作区已干净或不适用版本控制,再结合 `design.md`、`tasks.md`、验证记录和执行记录反推。
|
||||
|
||||
f) 收集当前会话中与本次变更相关的实现说明、apply 过程中的偏离、测试失败、手动修补原因、待确认事项
|
||||
e) 并行读取实际改动范围、`Affected Areas`、`Execution Plan`、`Verification Plan` 涉及的实际产物、参考材料、验证材料、流程说明、配置、文档或沟通材料。
|
||||
|
||||
f) 收集当前会话中与本次变更相关的执行说明、apply 过程中的偏离、验证失败、手动修补原因、验证命令或检查结果、待确认事项。
|
||||
|
||||
g) 若实际 schema 不是 `fast-drive`,只读取实际存在的 artifacts;若存在 `proposal.md`、`specs/*.md`,再按该 schema 的要求补充读取和审查。
|
||||
|
||||
若当前上下文无法明确 change 或文档路径:
|
||||
|
||||
@@ -42,66 +60,75 @@ f) 收集当前会话中与本次变更相关的实现说明、apply 过程中
|
||||
|
||||
若已明确 change,但尚未确认 `schemaName`,先读取 change 元数据或执行 `openspec status --change "{name}" --json` 确认。
|
||||
|
||||
若缺少测试结果或手动修补记录,明确说明本次无法可靠判断部分差异的来源,仅能基于代码与文档现状审查。
|
||||
若缺少验证结果或手动修补记录,明确说明本次无法可靠判断部分差异的来源,仅能基于实际产物与文档现状审查。
|
||||
|
||||
## 2. 分析
|
||||
|
||||
按以下优先级检查:
|
||||
|
||||
| 优先级 | 维度 | 检查点 |
|
||||
| ------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| P0 | 实际实现与测试结论 | 当前代码的真实行为是什么;apply 后是否有手动改动或测试后修补;测试是否证明这些实现有效;若缺少测试结果,标记相关结论为"未验证";检查是否存在回归、未覆盖场景或被掩盖的问题 |
|
||||
| P1 | 文档同步性 | 对本次 change 目录下实际存在的 artifacts 检查:已落地的实现、测试后新增修补、边界处理、异常路径、验证结论是否已同步回变更文档;若影响模块结构、API、实体或用户可见行为,再检查 README 是否同步 |
|
||||
| P2 | Spec 覆盖完整性 | 对比实际代码改动范围与 proposal 中定义的 `Capabilities` / `Modified Capabilities`,识别 apply 阶段新增的功能是否触及了更多现有 spec;若触及,列入补充清单,在本次 change 的 specs 中新增对应的 spec 文件,并更新 `proposal.md` 的 `Modified Capabilities` |
|
||||
| P3 | 文档要求覆盖 | 对实际存在的 artifacts 检查:文档中承诺的目标、方案、Requirement、Scenario 是否都已实现;在 `spec-driven` 下重点检查 `proposal.md`、`design.md`、`specs/*.md`、`tasks.md` |
|
||||
| P4 | 实现质量 | 代码结构、复用、命名、复杂度、错误处理、测试质量、与项目现有模式的一致性是否存在明显问题或可优化点 |
|
||||
| 优先级 | 维度 | 检查点 |
|
||||
| ------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| P0 | 实际变更与验证结论 | 当前实际产物的真实状态是什么;apply 后是否有手动改动或验证后修补;验证是否证明这些变更有效;若缺少验证结果,标记相关结论为“未验证”;检查是否存在回归、未覆盖场景或被掩盖的问题 |
|
||||
| P1 | `design.md` 一致性 | 实际变更是否符合 `Requirements`、`Goals / Non-Goals`、`Execution Guardrails`、`Decisions`、`Execution Plan` 和 `Verification Plan`;`Open Questions` 是否已明确区分 blocking / non-blocking 或写出 `None`;是否违反被明确否决的方案、用户偏好或约束 |
|
||||
| P2 | `tasks.md` 真实性 | 已完成任务是否真的完成并完成必要验证;未完成任务是否仍然必要;apply 或手动修补是否引入了需要补充的新任务、验证任务或文档/沟通任务 |
|
||||
| P3 | 文档回写完整性 | 已落地的实际变更、验证后新增修补、边界处理、异常路径、验证结论、实际触达产物是否已同步回 `design.md` 和 `tasks.md`;若影响行为、流程、接口、内容、数据、配置、责任边界或用户可见结果,再检查必要的文档/沟通材料是否同步 |
|
||||
| P4 | 质量与可维护性 | 实际产物的结构、清晰度、一致性、可维护性、风险处理、移交质量、验证质量、与现有模式的一致性是否存在明显问题或可优化点 |
|
||||
| P5 | Schema 兼容性 | 对实际存在的 artifacts 检查是否符合其 schema;若不是 `fast-drive`,仅按实际 artifacts 检查,不凭空要求 `fast-drive` 专属结构;最终 artifacts 是否仍保留模板注释、空表格行或占位任务文本 |
|
||||
|
||||
分析时区分三类差异:
|
||||
分析时区分四类差异:
|
||||
|
||||
- 文档要求已明确,但代码未实现或实现不完整 → 需补充代码或测试
|
||||
- 代码因测试暴露问题、手动修补或合理落地细化而新增/变更 → 需回写文档
|
||||
- 代码与文档不一致,且无法判断应以哪边为准 → 列入待确认清单
|
||||
- `design.md` 要求已明确,但实际变更未完成或完成不充分 → 需补充实际工作或验证
|
||||
- 实际变更因验证暴露问题、手动修补或合理落地细化而新增/变更 → 需回写 `design.md` 和/或 `tasks.md`
|
||||
- 实际变更与 `design.md` 不一致,且无法判断应以哪边为准 → 列入待确认清单
|
||||
- `tasks.md` 状态与实际完成情况或验证结果不一致 → 修正任务状态或补充任务
|
||||
|
||||
不要把以下情况直接视为合理修补:
|
||||
|
||||
- 通过 `skip`、`only`、弱化断言、绕过错误处理来让测试通过
|
||||
- 为了贴合现有代码而降低已确认的 Requirement 或行为约束
|
||||
- 未经过讨论和验证就扩大功能范围
|
||||
- 通过跳过、弱化或绕过验证来声称变更完成
|
||||
- 为了贴合当前实际产物而降低已确认的 requirement、acceptance criteria 或 guardrail
|
||||
- 未经过讨论和验证就扩大功能、流程、内容或责任范围
|
||||
- 违反 `Execution Guardrails`、被拒绝方案或 `Open Questions` 中尚未解决的 blocker
|
||||
|
||||
重点识别:
|
||||
|
||||
- 文档要求但未落地的功能、场景、异常处理或验证步骤
|
||||
- apply 完成后新增的代码修补、边界处理、接口调整、行为变化未同步到文档
|
||||
- `tasks.md` 标记完成,但代码、测试或文档未闭环
|
||||
- `Modified Capabilities` 在本次 change 的 specs 中是否已更新(注意:仅更新 change 目录下的 specs,不动 `openspec/specs/`);apply 阶段新增功能触及的未覆盖 spec 是否已补充到本次 change 的 `specs/` 目录
|
||||
- 代码存在明显的重复、复杂度过高、命名不清、错误处理薄弱、测试质量不足等问题
|
||||
- `design.md` 要求但未落地的结果、流程、内容、场景、异常处理、文档/沟通更新或验证步骤
|
||||
- 实际变更偏离 `Goals / Non-Goals`、`Execution Guardrails`、`Decisions` 或 `Execution Plan` 的地方
|
||||
- apply 完成后新增的修补、边界处理、接口调整、行为变化、流程变化或内容变化未同步到 `design.md`
|
||||
- `Affected Areas` 与实际改动范围不一致,导致新会话无法复盘真实影响面
|
||||
- `Verification Plan` 中要求的验证、质量检查、审阅、批准、沟通检查或 manual checks 未执行或未记录
|
||||
- `tasks.md` 标记完成,但实际产物、验证、文档或沟通未闭环
|
||||
- `design.md` 或 `tasks.md` 仍保留 `<!-- ... -->` 模板注释、空表格行、`Replace with...`、`TBD`、`TODO` 等未解决占位内容
|
||||
- 必要的文档/沟通材料未同步影响行为、流程、接口、内容、数据、配置、责任边界或用户可见结果的变更
|
||||
- 实际产物存在明显的重复、复杂度过高、表达不清、责任不明、风险处理薄弱、验证质量不足等问题
|
||||
- `fast-drive` change 中仍错误依赖 `proposal.md`、`specs/*.md`、`Capabilities` 或 `Modified Capabilities` 的内容
|
||||
|
||||
输出审查结果:
|
||||
|
||||
1. **问题总览表**:问题类型 × 涉及文件数
|
||||
2. **实际改动与修补清单**:本次实现中已落地的主要功能、后续修补和验证结论;若缺少测试结果,对未验证部分单独标记
|
||||
3. **未覆盖清单**:文档要求但未在代码中实现或未充分验证的内容
|
||||
4. **需回写文档清单**:代码和测试中已确认、但文档未体现的实现、修补或约束变化
|
||||
5. **方向待确认清单**:代码与文档不一致,且无法判断应以哪边为准的事项
|
||||
6. **Spec 补充清单**:apply 阶段新增功能触及但 proposal 未覆盖的现有 spec,列出需新增的 spec 文件名、对应的主 spec 路径、需描述的变更内容
|
||||
7. **任务状态问题清单**:未真正完成、状态错误或需补充的新任务
|
||||
8. **测试问题清单**:缺失覆盖、掩盖错误、验证不足或修补后未回归验证的测试问题
|
||||
9. **代码质量/优化清单**:可优化的实现问题和建议
|
||||
2. **实际变更与修补清单**:本次已落地的主要变更、后续修补和验证结论;若缺少验证结果,对未验证部分单独标记
|
||||
3. **Design 偏离清单**:实际变更未完成、完成不充分或偏离 `design.md` 的内容
|
||||
4. **需回写文档清单**:实际产物和验证中已确认、但 `design.md`、`tasks.md` 或相关文档/沟通材料未体现的变更、修补或约束变化
|
||||
5. **方向待确认清单**:实际变更与 `design.md` 不一致,且无法判断应以哪边为准的事项
|
||||
6. **任务状态问题清单**:未真正完成、状态错误或需补充的新任务
|
||||
7. **验证问题清单**:缺失覆盖、掩盖错误、验证不足或修补后未回归验证的问题
|
||||
8. **质量/优化清单**:可优化的实际产物问题和建议
|
||||
9. **Schema 差异清单**:实际 schema 与默认 `fast-drive` 不同时,列出因此跳过或改按实际 artifacts 审查的内容
|
||||
10. **逐项分析**:每个问题说明位置、问题、影响、建议和建议修复方向
|
||||
|
||||
若所有清单均为空,输出"审查通过,未发现问题",跳至步骤 5。
|
||||
若所有清单均为空,输出“审查通过,未发现问题”,跳至步骤 5。
|
||||
|
||||
## 3. 计划(用户确认)
|
||||
|
||||
先针对"方向待确认清单"用提问工具逐项向用户确认。
|
||||
先针对“方向待确认清单”用提问工具逐项向用户确认。
|
||||
|
||||
再整理完整修复方案,按类别列出:
|
||||
|
||||
- 代码或测试补充:补实现、补异常处理、补回归测试、修复掩盖错误的测试
|
||||
- 文档回写:同步本次 change 目录下的 `proposal.md`、`design.md`、`tasks.md`、`specs/*.md`、README 中遗漏或过时的内容(禁止同步到 `openspec/specs/`)
|
||||
- Spec 补充:将 apply 阶段新增功能触及的现有 spec 复制到本次 change 的 `specs/` 目录并更新,同步更新 `proposal.md` 的 `Modified Capabilities`
|
||||
- 实际工作或验证补充:补完成、补异常处理、补回归验证、修复被弱化或绕过的验证
|
||||
- Design 回写:同步 `design.md` 中遗漏或过时的 requirements、guardrails、affected areas、decisions、execution plan、verification plan、risks 或 open questions
|
||||
- 任务状态修正:修正已完成/未完成状态,补充 apply 后新增但已完成的修补任务或验证任务
|
||||
- 代码质量优化:在不改变目标行为的前提下优化结构、复用、命名或可维护性
|
||||
- 文档/沟通同步:同步行为、流程、接口、内容、数据、配置、责任边界或用户可见结果变化
|
||||
- 质量优化:在不改变目标结果的前提下优化结构、表达、一致性、可维护性或移交质量
|
||||
- Schema 兼容处理:若实际 schema 不是 `fast-drive`,按实际存在的 artifacts 说明额外文档同步项
|
||||
|
||||
对每个拟修改的文件说明:
|
||||
|
||||
@@ -115,36 +142,38 @@ f) 收集当前会话中与本次变更相关的实现说明、apply 过程中
|
||||
|
||||
## 4. 执行
|
||||
|
||||
逐项执行已确认的代码、测试和文档修复。
|
||||
逐项执行已确认的实际产物、验证和文档修复。
|
||||
|
||||
若涉及删除或重写:
|
||||
|
||||
- 先创建备份文件 `{file}.bak.{timestamp}`
|
||||
- 再执行修改
|
||||
|
||||
若修改了代码或测试:
|
||||
若修改了实际产物或验证材料:
|
||||
|
||||
- 同步更新相关变更文档;若影响模块结构、API、实体或用户可见行为,再同步 README
|
||||
- 运行相关测试;若修补影响范围较大,再补充执行受影响的回归测试
|
||||
- 同步更新相关变更文档;若影响行为、流程、接口、内容、数据、配置、责任边界或用户可见结果,再同步必要的文档/沟通材料
|
||||
- 运行或执行相关验证;若修补影响范围较大,再补充执行受影响的回归验证
|
||||
|
||||
若修改了文档:
|
||||
|
||||
- 确认本次 change 目录下的变更文档之间保持一致;重点检查 `proposal.md`、`design.md`、`tasks.md`、`specs/*.md`
|
||||
- 若 apply 后新增修补改变了能力边界或行为约束,同步更新本次 change 的 `Capabilities` / `Modified Capabilities`
|
||||
- 若"Spec 补充清单"中有需新增的 spec:先从 `openspec/specs/` 复制对应的原 spec 到本次 change 的 `specs/` 目录,再基于实际改动更新其内容;禁止修改 `openspec/specs/` 下的原文件
|
||||
- 禁止将本次 change 的 specs 同步到 `openspec/specs/`,该操作属于 archive 阶段
|
||||
- 在 `fast-drive` workflow 下,确认 `design.md` 仍是 source of truth,`tasks.md` 仍从 `design.md` 派生
|
||||
- 确认 `design.md` 的 requirements、guardrails、affected areas、decisions、execution plan、verification plan、risks 和 open questions 与实际变更一致
|
||||
- 确认 `tasks.md` 每个完成任务都有对应实际产物和必要验证,新增修补已补充任务或记录在合适任务中
|
||||
- 禁止将本次 change 内容同步到 `openspec/specs/`,该操作属于 archive 阶段
|
||||
- 在 `fast-drive` workflow 下不创建 `proposal.md` 或 `specs/*.md`;若实际 schema 不是 `fast-drive`,则按实际 schema 的 required artifacts 创建或更新本次 change 目录下的 artifacts
|
||||
|
||||
执行后重新读取所有被修改的代码、测试和文档,并复核:
|
||||
执行后重新读取所有被修改的实际产物、验证材料和文档,并复核:
|
||||
|
||||
- "未覆盖清单" 是否已清空或已标注保留原因
|
||||
- "需回写文档清单" 是否已清空
|
||||
- "Spec 补充清单" 是否已清空或已标注保留原因
|
||||
- "方向待确认清单" 是否已清空或已记录用户决策
|
||||
- "任务状态问题清单" 和 "测试问题清单" 是否已清空或已标注残留原因
|
||||
- "代码质量/优化清单" 中哪些已处理,哪些有意延期
|
||||
- “Design 偏离清单” 是否已清空或已标注保留原因
|
||||
- “需回写文档清单” 是否已清空
|
||||
- “方向待确认清单” 是否已清空或已记录用户决策
|
||||
- “任务状态问题清单” 和 “验证问题清单” 是否已清空或已标注残留原因
|
||||
- “质量/优化清单” 中哪些已处理,哪些有意延期
|
||||
- 必要的文档/沟通材料是否已按影响范围同步
|
||||
- 所有模板注释、空表格行和占位文本是否已清空或替换为有效内容
|
||||
|
||||
## 5. 收尾
|
||||
|
||||
列出所有修改的文件、备份文件、测试命令与结果、文档同步摘要和剩余风险。
|
||||
列出所有修改的文件、备份文件、验证命令或检查结果、文档同步摘要和剩余风险。
|
||||
|
||||
若本次因缺少测试结果、修补记录或上下文而降级执行,或有问题因信息不足暂未处理,单独说明。
|
||||
若本次因缺少验证结果、修补记录或上下文而降级执行,或有问题因信息不足暂未处理,单独说明。
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
审查本次 OpenSpec 变更文档是否与前序讨论、当前代码现状和 OpenSpec 文档规范一致,识别遗漏、冲突和不合理假设,并给出可执行的补充建议,按以下流程执行。
|
||||
审查本次 OpenSpec 变更文档是否与前序讨论、当前实际状态和实际 OpenSpec workflow 一致,重点检查 `fast-drive` workflow 下的 `design.md` 是否足以在上下文压缩或新会话中指导后续 `apply`,并识别遗漏、冲突和不合理假设,给出可执行的补充建议,按以下流程执行。
|
||||
|
||||
## 约束
|
||||
|
||||
- 仅修改本次变更文档,不修改源码
|
||||
- 默认按 `spec-driven` workflow 审查;识别 change 后先确认 `schemaName`;若实际 schema 不同,说明差异,仅对实际存在的 artifacts 做审查
|
||||
- 优先使用当前会话中的讨论和已生成的变更文档;仅在无法明确 change、`schemaName` 或文档范围时,再用提问工具或 OpenSpec 命令补充定位
|
||||
- 仅修改本次变更文档,不修改实际产物
|
||||
- 默认按 `fast-drive` workflow 审查;识别 change 后先确认 `schemaName`;若实际 schema 不同,说明差异,仅对实际存在的 artifacts 做审查
|
||||
- 在 `fast-drive` workflow 下,核心 artifacts 是 `design.md` 和 `tasks.md`;不要要求存在 `proposal.md` 或 `specs/*.md`
|
||||
- 在 `fast-drive` workflow 下,`design.md` 是 scope、requirements、decisions、guardrails、execution direction 和 verification expectations 的 source of truth,`tasks.md` 必须从 `design.md` 派生
|
||||
- 优先使用当前会话中的讨论、explore/propose 阶段结论和已生成的变更文档;仅在无法明确 change、`schemaName` 或文档范围时,再用提问工具或 OpenSpec 命令补充定位
|
||||
- 每批文档修改建议执行前用提问工具获得用户确认
|
||||
- 删除/重写前用提问工具获得用户确认,并先备份原文件为 `{file}.bak.{timestamp}`
|
||||
|
||||
@@ -19,12 +21,28 @@
|
||||
|
||||
a) 先并行读取核心入口和配置,确定范围:
|
||||
|
||||
- 本次 change 的 `proposal.md`
|
||||
- `openspec/config.yaml`
|
||||
- 本次 change 的 `design.md`
|
||||
- 本次 change 的 `tasks.md`
|
||||
- workflow context/configuration,例如存在时读取 `openspec/config.yaml`
|
||||
- 若可定位到 schema,读取对应 schema;`fast-drive` 下优先读取 `openspec/schemas/fast-drive/schema.yaml`
|
||||
|
||||
b) 从 `proposal.md` 提取 `Capabilities` / `Modified Capabilities`,确定需要读取的 spec 列表;相关性来源还包括:讨论中提到的受影响能力、`design.md` Impact 中提到的模块、相关代码对应的现有能力
|
||||
b) 从 `design.md` 提取审查基准:
|
||||
|
||||
c) 并行读取已确定的 spec 和其余 artifacts(`design.md`、`tasks.md`、相关源码、测试、README、架构文档)
|
||||
- `Context`
|
||||
- `Discussion Notes`
|
||||
- `Requirements`
|
||||
- `Goals / Non-Goals`
|
||||
- `Execution Guardrails`
|
||||
- `Affected Areas`
|
||||
- `Decisions`
|
||||
- `Execution Plan`
|
||||
- `Verification Plan`
|
||||
- `Risks / Trade-offs`
|
||||
- `Open Questions`
|
||||
|
||||
c) 基于 `Affected Areas`、`Execution Plan`、`Verification Plan`、讨论中提到的受影响范围,并行读取相关实际产物、参考材料、验证材料、流程说明、配置、文档或沟通材料,确认文档是否贴合当前实际状态。
|
||||
|
||||
d) 若实际 schema 不是 `fast-drive`,只读取实际存在的 artifacts;若存在 `proposal.md`、`specs/*.md`,再按该 schema 的要求补充读取和审查。
|
||||
|
||||
若当前上下文无法明确 change 或文档路径:
|
||||
|
||||
@@ -33,47 +51,55 @@ c) 并行读取已确定的 spec 和其余 artifacts(`design.md`、`tasks.md`
|
||||
|
||||
若已明确 change,但尚未确认 `schemaName`,先读取 change 元数据或执行 `openspec status --change "{name}" --json` 确认。
|
||||
|
||||
若缺少讨论记录,明确说明本次降级为"文档 + 代码现状审查",不做讨论一致性结论。
|
||||
若缺少讨论记录,明确说明本次降级为“文档 + 当前实际状态审查”,不做讨论一致性结论。
|
||||
|
||||
## 2. 分析
|
||||
|
||||
按以下优先级检查:
|
||||
|
||||
| 优先级 | 维度 | 检查点 |
|
||||
| ------ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| P0 | 讨论一致性 | 仅在存在讨论记录时检查:文档是否完整覆盖已确认的目标、范围、非目标、约束、边界条件、风险、决策点、待办事项;若无讨论记录,标记为"跳过" |
|
||||
| P1 | 代码现实性 | 文档对当前模块、接口、数据结构、命名、依赖、目录结构、复用路径的描述是否准确;是否把"计划变更"误写成"当前现状";是否遗漏真实受影响的现有能力 |
|
||||
| P2 | 文档内部一致性 | 对实际存在的 artifacts 检查是否互相支撑;在 `spec-driven` 下重点检查 `proposal.md`、`design.md`、`tasks.md`、`specs/*.md`;`Capabilities` / `Modified Capabilities` 是否完整;每个 capability 是否有对应 spec;`tasks.md` 是否覆盖 `design.md` 和 `specs/*.md` |
|
||||
| P3 | OpenSpec 合规性 | 对实际存在的 artifacts 检查是否遵循 OpenSpec 格式和术语;`specs/*.md` 是否只描述行为与约束、不混入实现细节;`tasks.md` 是否一行一个任务;是否混入 git 操作任务 |
|
||||
| 优先级 | 维度 | 检查点 |
|
||||
| ------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| P0 | 讨论承接性 | 仅在存在讨论记录时检查:`design.md` 是否完整记录已确认的目标、非目标、用户偏好、约束、边界条件、风险、关键决策、被否决方案和待澄清事项;若无讨论记录,标记为“跳过” |
|
||||
| P1 | `design.md` 自包含性 | `design.md` 是否足以让看不到前序对话的执行者继续工作;是否包含完整 required sections;`Open Questions` 是否明确区分 blocking / non-blocking 或写出 `None`;是否存在依赖未记录聊天上下文的隐含要求 |
|
||||
| P2 | 当前状态真实性 | `design.md` 对当前实际产物、流程、接口、内容、数据、配置、依赖、责任边界、参考材料和验证入口的描述是否准确;是否把“计划变更”误写成“当前现状”;`Affected Areas` 是否遗漏真实受影响区域 |
|
||||
| P3 | `tasks.md` 派生性 | `tasks.md` 是否从 `design.md` 派生;是否覆盖 requirements、guardrails、decisions、execution plan 和 verification plan;是否依赖 `proposal.md` 或 `specs/*.md` 中未写入 `design.md` 的内容 |
|
||||
| P4 | OpenSpec 合规性 | 对实际存在的 artifacts 检查是否遵循对应 schema 和 OpenSpec 术语;`tasks.md` 是否一行一个 `- [ ]` checkbox 任务、按 `##` numbered headings 分组、无无关的仓库/版本控制/发布操作任务;`design.md` 是否避免 task checkbox;最终 artifacts 是否仍保留模板注释、空表格行或占位任务文本 |
|
||||
|
||||
分析时区分两类情况:
|
||||
|
||||
- 文档对当前代码现状的描述错误
|
||||
- 文档描述的是预期变更,本来就应当与当前代码不同
|
||||
- 文档对当前实际状态的描述错误
|
||||
- 文档描述的是预期变更,本来就应当与当前状态不同
|
||||
|
||||
重点识别:
|
||||
|
||||
- 讨论中已确定但文档未记录的内容
|
||||
- 文档基于错误现状做出的设计或任务拆分
|
||||
- 文档之间相互冲突的目标、方案、约束、任务
|
||||
- `Modified Capabilities` 在本次 change 的 specs 中是否已更新(注意:仅更新 change 目录下的 specs,不动 `openspec/specs/`)
|
||||
- 讨论中已确定但 `design.md` 未记录的内容
|
||||
- `design.md` 中缺失或含糊的 requirements、acceptance criteria、guardrails、decisions、verification expectations
|
||||
- `Open Questions` 未明确区分 blocking / non-blocking、与 `tasks.md` 冲突,或包含 apply 前必须解决的 blocker
|
||||
- `tasks.md` 未覆盖 `design.md` 的要求、约束、执行计划、验证计划或文档/沟通更新要求
|
||||
- `tasks.md` 标记了无法验证、跨行、过大、顺序错误或包含无关仓库/版本控制/发布操作的任务
|
||||
- 文档仍保留 `<!-- ... -->` 模板注释、空表格行、`Replace with...`、`TBD`、`TODO` 等未解决占位内容
|
||||
- 文档基于错误当前状态做出的设计或任务拆分
|
||||
- 文档之间相互冲突的目标、方案、约束、任务和验证要求
|
||||
- `fast-drive` change 中仍错误依赖 `proposal.md`、`specs/*.md`、`Capabilities` 或 `Modified Capabilities` 的内容
|
||||
|
||||
输出审查结果:
|
||||
|
||||
1. **问题总览表**:问题类型 × 涉及文档数
|
||||
2. **讨论遗漏清单**:讨论已确定但文档未体现的内容;若缺少讨论记录,标记为"未审查"
|
||||
3. **现实性问题清单**:与当前代码现状不符的描述、假设或影响分析
|
||||
4. **文档冲突清单**:proposal、design、tasks、specs 之间的不一致
|
||||
5. **OpenSpec 规范问题清单**:格式、术语、结构问题
|
||||
6. **待澄清清单**:仅靠讨论和代码仍无法判断的事项
|
||||
7. **逐项分析**:每个问题说明位置、问题、影响、建议
|
||||
8. **补充建议方案**:按文件列出建议补充/修正的内容、原因和可选方案
|
||||
2. **讨论遗漏清单**:讨论已确定但 `design.md` 未体现的内容;若缺少讨论记录,标记为“未审查”
|
||||
3. **Design 自包含性问题清单**:缺失、含糊或无法指导新会话 apply 的内容
|
||||
4. **当前状态问题清单**:与当前实际状态不符的描述、假设或影响分析
|
||||
5. **Tasks 派生与覆盖问题清单**:`tasks.md` 未从 `design.md` 正确派生或覆盖不足的内容
|
||||
6. **文档冲突清单**:`design.md`、`tasks.md` 和实际存在的其他 artifacts 之间的不一致
|
||||
7. **OpenSpec 规范问题清单**:格式、术语、结构问题
|
||||
8. **待澄清清单**:仅靠讨论和当前状态仍无法判断的事项
|
||||
9. **逐项分析**:每个问题说明位置、问题、影响、建议
|
||||
10. **补充建议方案**:按文件列出建议补充/修正的内容、原因和可选方案
|
||||
|
||||
若所有清单均为空,输出"审查通过,未发现问题",跳至步骤 5。
|
||||
若所有清单均为空,输出“审查通过,未发现问题”,跳至步骤 5。
|
||||
|
||||
## 3. 计划(用户确认)
|
||||
|
||||
先针对"待澄清清单"用提问工具逐项向用户确认。
|
||||
先针对“待澄清清单”用提问工具逐项向用户确认。
|
||||
|
||||
再整理完整修复方案,按文件列出:
|
||||
|
||||
@@ -86,7 +112,9 @@ c) 并行读取已确定的 spec 和其余 artifacts(`design.md`、`tasks.md`
|
||||
|
||||
## 4. 执行
|
||||
|
||||
逐项修改已确认的变更文档,不修改源码。
|
||||
逐项修改已确认的变更文档,不修改实际产物。
|
||||
|
||||
在 `fast-drive` workflow 下,通常只修改本次 change 的 `design.md` 和 `tasks.md`;若实际 schema 存在其他 artifacts,仅在确有必要且用户确认后修改实际存在的 artifacts。
|
||||
|
||||
若涉及删除或重写:
|
||||
|
||||
@@ -95,11 +123,14 @@ c) 并行读取已确定的 spec 和其余 artifacts(`design.md`、`tasks.md`
|
||||
|
||||
执行后重新读取所有被修改的文档,并复核:
|
||||
|
||||
- "讨论遗漏清单" 是否已清空或已标注保留原因
|
||||
- "现实性问题清单" 是否已清空或已标注为预期变更
|
||||
- "文档冲突清单" 是否已清空
|
||||
- "OpenSpec 规范问题清单" 是否已清空
|
||||
- "待澄清清单" 是否已清空或已记录用户决策
|
||||
- “讨论遗漏清单” 是否已清空或已标注保留原因
|
||||
- “Design 自包含性问题清单” 是否已清空
|
||||
- “当前状态问题清单” 是否已清空或已标注为预期变更
|
||||
- “Tasks 派生与覆盖问题清单” 是否已清空
|
||||
- “文档冲突清单” 是否已清空
|
||||
- “OpenSpec 规范问题清单” 是否已清空
|
||||
- “待澄清清单” 是否已清空或已记录用户决策
|
||||
- 所有模板注释、空表格行和占位文本是否已清空或替换为有效内容
|
||||
|
||||
## 5. 收尾
|
||||
|
||||
|
||||
181
openspec/schemas/fast-drive/schema.yaml
Normal file
181
openspec/schemas/fast-drive/schema.yaml
Normal file
@@ -0,0 +1,181 @@
|
||||
name: fast-drive
|
||||
version: 1
|
||||
description: Fast OpenSpec workflow - design -> tasks -> apply
|
||||
artifacts:
|
||||
- id: design
|
||||
generates: design.md
|
||||
description: Self-contained solution brief and execution plan
|
||||
template: design.md
|
||||
instruction: |
|
||||
Create design.md as the self-contained source of truth for what will
|
||||
change, why it is changing, and how the work will be executed.
|
||||
|
||||
This workflow does not use proposal or specs artifacts. design.md MUST
|
||||
preserve the important outcomes from prior exploration and user
|
||||
discussion so a later apply phase can proceed correctly even after
|
||||
context compression or in a new session.
|
||||
|
||||
Write for someone who cannot see the earlier conversation. Keep simple
|
||||
changes concise, but include enough detail to make execution
|
||||
unambiguous. Add more detail when any apply:
|
||||
|
||||
- Cross-cutting change across multiple systems, teams, workstreams, or
|
||||
artifacts
|
||||
|
||||
- New dependency, integration, vendor, tool, policy, or external input
|
||||
|
||||
- Significant information model, process model, data model, or ownership
|
||||
changes
|
||||
|
||||
- Security, privacy, compliance, performance, operational, or migration
|
||||
complexity
|
||||
|
||||
- Ambiguity that benefits from decisions before execution
|
||||
|
||||
- Prior discussion settled non-obvious requirements, constraints, or
|
||||
rejected alternatives
|
||||
|
||||
Required sections:
|
||||
|
||||
- **Context**: Problem, current state, relevant references, and the user
|
||||
request that triggered this change
|
||||
|
||||
- **Discussion Notes**: Key points from exploration or prior discussion
|
||||
that must not be lost. Include agreed conclusions, user preferences,
|
||||
constraints, and important rejected ideas.
|
||||
|
||||
- **Requirements**: Expected outcomes, behavior/process/interface/content
|
||||
changes, continuity expectations, and acceptance criteria.
|
||||
|
||||
- **Goals / Non-Goals**: What this change will achieve and what is
|
||||
explicitly out of scope.
|
||||
|
||||
- **Execution Guardrails**: Must-follow constraints, forbidden approaches,
|
||||
preserved behavior/processes, dependency limits, and project- or
|
||||
workflow-specific boundaries.
|
||||
|
||||
- **Affected Areas**: Concrete artifacts, references, stakeholders,
|
||||
systems, workstreams, documents, configurations, assets, or handoffs that
|
||||
are relevant to the change.
|
||||
|
||||
- **Decisions**: Key choices with rationale (why X over Y?). For each
|
||||
important decision, include alternatives considered and why they were not
|
||||
chosen.
|
||||
|
||||
- **Execution Plan**: Main workstreams or artifacts to change, integration
|
||||
or handoff points, sequencing, and any rollout notes.
|
||||
|
||||
- **Verification Plan**: Validation checks, reviews, approvals,
|
||||
acceptance checks, documentation checks, communication checks, and manual
|
||||
checks needed to prove the change is complete.
|
||||
|
||||
- **Risks / Trade-offs**: Known limitations and things that could go
|
||||
wrong.
|
||||
Format: [Risk] -> Mitigation
|
||||
|
||||
- **Open Questions**: Outstanding decisions, assumptions, or unknowns to
|
||||
resolve before execution. Separate blocking questions that must pause
|
||||
apply from non-blocking follow-ups. Use "None" if there are no open
|
||||
questions.
|
||||
|
||||
Optional sections when relevant:
|
||||
|
||||
- **Migration / Rollout Plan**: Rollout steps, communication, ownership,
|
||||
rollback, or continuity strategy.
|
||||
|
||||
Focus on preserving requirements, rationale, constraints, and approach.
|
||||
Avoid line-by-line or step-by-step details unless a detail is a deliberate
|
||||
decision from the discussion.
|
||||
|
||||
Prefer durable summaries over chat transcripts. Use concrete artifact
|
||||
names, data/information shapes, examples, stakeholders, ownership, and
|
||||
edge cases when they affect execution.
|
||||
|
||||
Do not use task checkboxes in design.md; checkboxes belong only in
|
||||
tasks.md.
|
||||
|
||||
Final design.md must not contain unresolved template comments, empty
|
||||
table rows, or placeholder text.
|
||||
|
||||
If information is missing, state assumptions and open questions instead
|
||||
of inventing hidden requirements. Do not rely on unstated chat context.
|
||||
requires: []
|
||||
- id: tasks
|
||||
generates: tasks.md
|
||||
description: Trackable execution checklist derived from design.md
|
||||
template: tasks.md
|
||||
instruction: |
|
||||
Create tasks.md by breaking design.md into executable work.
|
||||
|
||||
**IMPORTANT: Follow the template below exactly.** The apply phase parses
|
||||
checkbox format to track progress. Tasks not using `- [ ]` will not be
|
||||
tracked.
|
||||
|
||||
Guidelines:
|
||||
|
||||
- Derive tasks from design.md. Do not depend on proposal.md or specs
|
||||
artifacts; any relevant prior discussion must already be captured in
|
||||
design.md.
|
||||
|
||||
- Group related tasks under `##` numbered headings
|
||||
|
||||
- Each task MUST be a single-line checkbox: `- [ ] X.Y Task description`
|
||||
|
||||
- Tasks should be small enough to complete in one session
|
||||
|
||||
- Order tasks by dependency (what must be done first?)
|
||||
|
||||
- Start with context review tasks when execution depends on guardrails,
|
||||
affected areas, or open questions
|
||||
|
||||
- Include validation tasks for checks, reviews, approvals, acceptance,
|
||||
documentation, communication, and manual checks when required
|
||||
|
||||
- Do not include repository, version-control, or release operation tasks
|
||||
unless they are explicitly part of the change scope
|
||||
|
||||
- Final tasks.md must not contain unresolved template comments, empty
|
||||
table rows, or placeholder task text
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
## 1. Context Review
|
||||
|
||||
- [ ] 1.1 Read design.md and identify scope, requirements, decisions, guardrails, and open questions
|
||||
- [ ] 1.2 Review relevant artifacts and references listed in Affected Areas
|
||||
|
||||
## 2. Execution
|
||||
|
||||
- [ ] 2.1 Execute first concrete work item from design.md
|
||||
- [ ] 2.2 Execute next concrete work item from design.md
|
||||
|
||||
## 3. Validation
|
||||
|
||||
- [ ] 3.1 Run required validation from Verification Plan
|
||||
- [ ] 3.2 Perform quality checks required by the project or workflow
|
||||
- [ ] 3.3 Perform required manual review or acceptance checks from Verification Plan
|
||||
|
||||
## 4. Documentation / Communication
|
||||
|
||||
- [ ] 4.1 Update relevant documentation, runbooks, communication materials, or project references if behavior, process, interface, configuration, or usage changed
|
||||
```
|
||||
|
||||
Reference design.md for scope, requirements, decisions, execution
|
||||
direction, and verification expectations.
|
||||
|
||||
Each task should be verifiable: it must be clear when the task is done.
|
||||
requires:
|
||||
- design
|
||||
apply:
|
||||
requires:
|
||||
- design
|
||||
- tasks
|
||||
tracks: tasks.md
|
||||
instruction: |
|
||||
Read design.md first, then tasks.md.
|
||||
Also follow workflow context/configuration, such as openspec/config.yaml when available, and any relevant project or workflow documentation referenced by design.md.
|
||||
Treat design.md as the source of truth for scope, requirements, decisions, guardrails, execution direction, and verification expectations.
|
||||
Work through pending tasks in dependency order and mark complete as you go.
|
||||
Mark a task complete only after its execution and required verification are done.
|
||||
Pause if tasks conflict with design.md, if design.md has blocking open questions, or if clarification is needed.
|
||||
77
openspec/schemas/fast-drive/templates/design.md
Normal file
77
openspec/schemas/fast-drive/templates/design.md
Normal file
@@ -0,0 +1,77 @@
|
||||
## Context
|
||||
|
||||
<!-- Problem, current state, relevant references, and triggering user request -->
|
||||
|
||||
## Discussion Notes
|
||||
|
||||
<!-- Key conclusions from exploration or prior discussion that apply must preserve -->
|
||||
|
||||
- Agreed conclusions:
|
||||
- User preferences:
|
||||
- Constraints:
|
||||
- Rejected ideas:
|
||||
|
||||
## Requirements
|
||||
|
||||
<!-- Expected outcomes, behavior/process/interface/content changes, continuity expectations, and acceptance criteria -->
|
||||
|
||||
| Requirement | Acceptance Criteria |
|
||||
| ----------- | ------------------- |
|
||||
| | |
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
<!-- What this design aims to achieve -->
|
||||
|
||||
**Non-Goals:**
|
||||
<!-- What is explicitly out of scope -->
|
||||
|
||||
## Execution Guardrails
|
||||
|
||||
<!-- Must-follow constraints, forbidden approaches, preserved behavior/processes, dependency limits, and project- or workflow-specific boundaries -->
|
||||
|
||||
- Dependencies:
|
||||
- Constraints:
|
||||
- Quality Bar:
|
||||
- Stakeholders:
|
||||
- Documentation / Communication:
|
||||
- Compatibility / Continuity:
|
||||
|
||||
## Affected Areas
|
||||
|
||||
<!-- Concrete artifacts, references, stakeholders, systems, workstreams, documents, configurations, assets, or handoffs relevant to this change -->
|
||||
|
||||
| Area | Artifacts / References | Expected Change | Notes |
|
||||
| ---- | ---------------------- | --------------- | ----- |
|
||||
| <!-- Area --> | <!-- Artifacts / References --> | <!-- Expected Change --> | <!-- Notes --> |
|
||||
|
||||
## Decisions
|
||||
|
||||
<!-- Key decisions, rationale, and alternatives considered -->
|
||||
|
||||
| Decision | Rationale | Alternatives Rejected |
|
||||
| -------- | --------- | --------------------- |
|
||||
| | | |
|
||||
|
||||
## Execution Plan
|
||||
|
||||
<!-- Main workstreams or artifacts to change, integration or handoff points, sequencing, and rollout notes -->
|
||||
|
||||
## Verification Plan
|
||||
|
||||
<!-- Validation checks, reviews, approvals, acceptance checks, documentation checks, communication checks, and manual checks needed -->
|
||||
|
||||
| Requirement / Risk | Verification |
|
||||
| ------------------ | ------------ |
|
||||
| | |
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
<!-- Format: [Risk] -> Mitigation -->
|
||||
|
||||
## Open Questions
|
||||
|
||||
| Status | Question | Decision Needed |
|
||||
| ------ | -------- | --------------- |
|
||||
| None | No open questions. | None |
|
||||
19
openspec/schemas/fast-drive/templates/tasks.md
Normal file
19
openspec/schemas/fast-drive/templates/tasks.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## 1. Context Review
|
||||
|
||||
- [ ] 1.1 Read design.md and identify scope, requirements, decisions, guardrails, and open questions
|
||||
- [ ] 1.2 Review relevant artifacts and references listed in Affected Areas
|
||||
|
||||
## 2. Execution
|
||||
|
||||
- [ ] 2.1 Execute first concrete work item from design.md
|
||||
- [ ] 2.2 Execute next concrete work item from design.md
|
||||
|
||||
## 3. Validation
|
||||
|
||||
- [ ] 3.1 Run required validation from Verification Plan
|
||||
- [ ] 3.2 Perform quality checks required by the project or workflow
|
||||
- [ ] 3.3 Perform required manual review or acceptance checks from Verification Plan
|
||||
|
||||
## 4. Documentation / Communication
|
||||
|
||||
- [ ] 4.1 Update relevant documentation, runbooks, communication materials, or project references if behavior, process, interface, configuration, or usage changed
|
||||
@@ -1,284 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 checker 模块的内聚化组织结构,确保每个 checker 以独立目录形式存在,包含其全部类型定义、schema 声明、语义校验、执行逻辑和断言逻辑。同时定义严格的依赖方向约束、Checker 接口定义、CheckerRegistry 注册中心、配置契约片段、配置校验 issue、引擎调度和服务注册委托。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Checker 目录内聚结构
|
||||
每个 checker SHALL 以独立目录形式存在于 checker runner 目录下,目录内 SHALL 包含该 checker 的全部类型定义、schema 声明、语义校验、执行逻辑和断言逻辑。
|
||||
|
||||
#### Scenario: checker 目录完整性
|
||||
- **WHEN** 开发者查看某个 checker 目录
|
||||
- **THEN** 该目录 SHALL 包含该 checker 的全部类型定义、schema 声明、语义校验、执行逻辑和断言逻辑
|
||||
|
||||
#### Scenario: 新增 checker 最小改动
|
||||
- **WHEN** 开发者新增一个 checker 类型(如 dns)
|
||||
- **THEN** 开发者 SHALL 只需创建 checker 目录及其内部文件,并在注册列表中添加一行 import 和一行数组项
|
||||
|
||||
### Requirement: 断言基础设施
|
||||
系统 SHALL 提供所有 checker 共享的断言基础设施,使用 Raw/Resolved expectation 术语和 value/content/keyed/status/headers 模块边界。
|
||||
|
||||
### Requirement: Schema 体系
|
||||
系统 SHALL 通过 schema 体系组织配置校验、契约片段和 issue 报告,支持从 registry 动态构建整体配置 schema、共享 schema 片段引用、Ajv 校验入口和 ConfigValidationIssue 构造。
|
||||
|
||||
### Requirement: 依赖方向约束
|
||||
checker 系统内的模块依赖 SHALL 遵循严格的分层方向。
|
||||
|
||||
#### Scenario: checker 之间无横向依赖
|
||||
- **WHEN** 开发者查看任何 checker 目录的 import 语句
|
||||
- **THEN** 该 checker SHALL NOT 导入其他 checker 目录的任何模块
|
||||
|
||||
#### Scenario: expect/ 不依赖 runner/
|
||||
- **WHEN** 开发者查看 `expect/` 目录的 import 语句
|
||||
- **THEN** `expect/` 中的文件 SHALL NOT 导入 `runner/` 目录的任何模块
|
||||
|
||||
#### Scenario: schema/ 不依赖 runner/ 的具体 checker
|
||||
- **WHEN** 开发者查看 `schema/` 目录的 import 语句
|
||||
- **THEN** `schema/` 中的文件 SHALL 仅通过 `CheckerDefinition` 接口与 checker 交互,SHALL NOT 直接导入具体 checker 目录
|
||||
|
||||
### Requirement: 显式注册列表
|
||||
系统 SHALL 在 checker 注册入口文件中使用显式 import 列表注册所有 checker。
|
||||
|
||||
#### Scenario: 注册入口结构
|
||||
- **WHEN** 开发者查看 checker 注册入口文件
|
||||
- **THEN** 该文件 SHALL 包含所有 checker 的静态 import 和一个 checker 实例数组,通过循环调用 `registry.register()` 完成注册
|
||||
|
||||
#### Scenario: 新增 checker 注册
|
||||
- **WHEN** 开发者新增一个 checker
|
||||
- **THEN** 开发者 SHALL 在注册入口文件中添加一行 import 和一行数组项,无需修改其他文件
|
||||
|
||||
### 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 定义面向扩展的泛型 `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 直接展示而不做类型判断
|
||||
@@ -1,158 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 CheckResult 的 observation 数据模型、各 checker 类型 observation 结构、截断策略、序列化规则和 detail 动态构造机制。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Observation 数据模型
|
||||
CheckResult SHALL 包含 `observation: Record<string, unknown> | null` 字段,用于承载 checker 执行过程中收集的结构化观测数据。observation 为 null 表示执行过程中无法形成有意义的领域观测数据(如进程 spawn 失败、内部异常、请求在拿到响应前失败且无可记录元数据等场景)。各 checker SHALL 自行定义 observation 的内部结构,不做跨 checker 类型的统一约束。
|
||||
|
||||
#### Scenario: 正常执行返回 observation
|
||||
- **WHEN** checker 执行成功、expect 断言失败或产生可收集上下文的负向结果
|
||||
- **THEN** CheckResult.observation SHALL 包含该 checker 类型定义的完整观测数据
|
||||
|
||||
#### Scenario: 异常执行返回 null observation
|
||||
- **WHEN** checker 执行过程中发生无法收集领域观测数据的异常
|
||||
- **THEN** CheckResult.observation SHALL 为 null
|
||||
|
||||
### Requirement: HTTP Checker Observation
|
||||
HTTP checker 的 observation SHALL 包含 statusCode(number)、headers(Record<string, string>,截断)、bodyPreview(string | null,截断)、contentType(string | null)、contentLength(number | null)。
|
||||
|
||||
#### Scenario: 正常 HTTP 响应
|
||||
- **WHEN** HTTP 请求成功返回
|
||||
- **THEN** observation SHALL 包含响应状态码、截断后的响应 headers、响应体预览、Content-Type 和 Content-Length,即使未配置 body expect 也 SHALL 采集 bodyPreview
|
||||
|
||||
#### Scenario: HTTP 请求失败
|
||||
- **WHEN** HTTP 请求因网络错误或超时失败
|
||||
- **THEN** observation SHALL 为 null
|
||||
|
||||
### Requirement: TCP Checker Observation
|
||||
TCP checker 的 observation SHALL 包含 connected(boolean)、connectTimeMs(number | null)、banner(string | null,截断)、error(string | null)。
|
||||
|
||||
#### Scenario: TCP 连接成功且读取 banner
|
||||
- **WHEN** TCP 连接成功并读取到 banner
|
||||
- **THEN** observation SHALL 包含 connected=true、连接耗时、截断后的 banner 内容
|
||||
|
||||
#### Scenario: TCP 连接失败
|
||||
- **WHEN** TCP 连接失败
|
||||
- **THEN** observation SHALL 包含 connected=false 和错误信息
|
||||
|
||||
### Requirement: UDP Checker Observation
|
||||
UDP checker 的 observation SHALL 包含 responded(boolean)、durationMs(number)、responseSize(number | null)、responsePreview(string | null,截断)、sourceAddress(string | null)、sourcePort(number | null)、error(string | null)。durationMs 用于 API 序列化层生成包含耗时的 UDP detail 摘要。
|
||||
|
||||
#### Scenario: UDP 收到响应
|
||||
- **WHEN** UDP 发送数据后收到响应
|
||||
- **THEN** observation SHALL 包含 responded=true、响应大小、截断后的响应预览、来源地址和端口
|
||||
|
||||
#### Scenario: UDP 未收到响应
|
||||
- **WHEN** UDP 发送数据后超时未收到响应
|
||||
- **THEN** observation SHALL 包含 responded=false
|
||||
|
||||
### Requirement: 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: ICMP 正常返回统计
|
||||
- **WHEN** ping 命令成功执行并解析出统计数据
|
||||
- **THEN** observation SHALL 包含完整的 ICMPStats 字段
|
||||
|
||||
#### Scenario: ICMP 命令失败
|
||||
- **WHEN** ping 命令未找到或超时
|
||||
- **THEN** observation SHALL 为 null 或包含 error 字段
|
||||
|
||||
### Requirement: DB Checker Observation
|
||||
DB checker 的 observation SHALL 包含 connected(boolean)、rowCount(number | null)、rowsPreview(unknown[] | null,截断前 N 行)、error(string | null)。
|
||||
|
||||
#### Scenario: 数据库查询成功
|
||||
- **WHEN** 数据库连接和查询成功
|
||||
- **THEN** observation SHALL 包含 connected=true、行数、截断后的行预览
|
||||
|
||||
#### Scenario: 仅探活无查询
|
||||
- **WHEN** 数据库配置仅探活连接(无 query)
|
||||
- **THEN** observation SHALL 包含 connected=true,rowCount 和 rowsPreview 为 null
|
||||
|
||||
### Requirement: CMD Checker Observation
|
||||
CMD checker 的 observation SHALL 包含 exitCode(number | null)、stdoutPreview(string | null,截断)、stderrPreview(string | null,截断)、error(string | null)。
|
||||
|
||||
#### Scenario: 命令正常执行
|
||||
- **WHEN** 命令执行完成
|
||||
- **THEN** observation SHALL 包含退出码、截断后的 stdout 和 stderr 预览
|
||||
|
||||
#### Scenario: 命令 spawn 失败
|
||||
- **WHEN** 命令进程无法启动
|
||||
- **THEN** observation SHALL 为 null
|
||||
|
||||
### Requirement: LLM Checker Observation
|
||||
LLM checker SHALL 保留执行期 `LlmCheckObservation.outputText` 完整文本用于 expect 校验,并从执行期 observation 派生持久化 observation。持久化 observation SHALL 包含 provider(string)、mode(string)、model(string)、http({ status, statusText, headers } | null,headers 截断)、finishReason(string | null)、rawFinishReason(string | null)、outputPreview(string | null,截断)、outputLength(number | null)、usage({ inputTokens, outputTokens, totalTokens } | null)、stream({ completed, firstTokenMs } | null)、warnings(string[])。
|
||||
|
||||
#### Scenario: LLM HTTP 模式成功
|
||||
- **WHEN** LLM 以 HTTP 模式成功执行
|
||||
- **THEN** observation SHALL 包含 provider、mode、model、HTTP 元数据、finish 原因、截断后的输出预览、完整输出长度和 token 用量
|
||||
|
||||
#### Scenario: LLM Stream 模式成功
|
||||
- **WHEN** LLM 以 stream 模式成功执行
|
||||
- **THEN** observation SHALL 额外包含 stream 观测数据(completed、firstTokenMs)
|
||||
|
||||
### Requirement: Observation 截断策略
|
||||
各 checker SHALL 对 observation 中的大文本和集合字段执行截断,防止存储膨胀。
|
||||
|
||||
#### Scenario: HTTP body 截断
|
||||
- **WHEN** HTTP 响应体超过 1024 字符
|
||||
- **THEN** bodyPreview SHALL 截断为前 1024 字符
|
||||
|
||||
#### Scenario: HTTP headers 截断
|
||||
- **WHEN** HTTP 响应 headers 超过 20 个
|
||||
- **THEN** headers SHALL 仅保留前 20 个
|
||||
|
||||
#### Scenario: TCP banner 截断
|
||||
- **WHEN** TCP banner 内容超过 256 字符
|
||||
- **THEN** banner SHALL 截断为前 256 字符
|
||||
|
||||
#### Scenario: UDP response 截断
|
||||
- **WHEN** UDP 响应预览超过 512 字符
|
||||
- **THEN** responsePreview SHALL 截断为前 512 字符
|
||||
|
||||
#### Scenario: DB rows 截断
|
||||
- **WHEN** 查询返回超过 5 行
|
||||
- **THEN** rowsPreview SHALL 仅保留前 5 行
|
||||
|
||||
#### Scenario: CMD stdout/stderr 截断
|
||||
- **WHEN** stdout 或 stderr 超过 1024 字符
|
||||
- **THEN** 对应 Preview 字段 SHALL 截断为前 1024 字符
|
||||
|
||||
#### Scenario: LLM output 截断
|
||||
- **WHEN** LLM 输出文本超过 512 字符
|
||||
- **THEN** outputPreview SHALL 截断为前 512 字符
|
||||
|
||||
#### Scenario: LLM headers 截断
|
||||
- **WHEN** LLM 响应 headers 超过 20 个
|
||||
- **THEN** headers SHALL 仅保留前 20 个
|
||||
|
||||
### Requirement: Observation 序列化规则
|
||||
observation SHALL 使用 JSON.stringify 序列化为 TEXT 格式存入 SQLite,使用 JSON.parse 反序列化读出。不引入额外的序列化依赖。
|
||||
|
||||
#### Scenario: 写入 observation
|
||||
- **WHEN** 存储 CheckResult 到数据库
|
||||
- **THEN** observation 字段 SHALL 使用 JSON.stringify 序列化后存入 TEXT 列;observation 为 null 时 SHALL 存入 SQL NULL
|
||||
|
||||
#### Scenario: 读取 observation
|
||||
- **WHEN** 从数据库读取 CheckResult
|
||||
- **THEN** observation 字段 SHALL 使用 JSON.parse 反序列化;SQL NULL 值 SHALL 映射为 null
|
||||
|
||||
#### Scenario: 使用 SQLite CLI 直接查看
|
||||
- **WHEN** 开发者使用 sqlite3 CLI 工具查看 check_results 表
|
||||
- **THEN** observation 列 SHALL 为人可读的 JSON 文本
|
||||
|
||||
### Requirement: Detail 动态构造
|
||||
CheckResult SHALL 包含 `detail: string | null` 字段(替代原 statusDetail),该字段 SHALL 不持久化到数据库,而是在 API 序列化层根据 target type 从 observation 动态构造。每个 checker SHALL 提供 `buildDetail(observation)` 方法,定义该 checker 类型的人可读摘要格式。
|
||||
|
||||
#### Scenario: API 返回 detail 字段
|
||||
- **WHEN** API 序列化 CheckResult 返回给前端
|
||||
- **THEN** 系统 SHALL 根据 target type 调用对应 checker 的 buildDetail 方法,从 observation 动态生成 detail 字段
|
||||
|
||||
#### Scenario: observation 为 null 时
|
||||
- **WHEN** observation 为 null
|
||||
- **THEN** detail SHALL 为 null
|
||||
|
||||
#### Scenario: 各 checker 的 detail 格式
|
||||
- **WHEN** 各 checker 的 buildDetail 被调用
|
||||
- **THEN** HTTP SHALL 返回 `"HTTP {statusCode}"` 格式;TCP SHALL 返回连接状态和 banner 摘要;UDP SHALL 返回响应状态和大小摘要;Ping SHALL 返回存活状态、平均延迟和丢包率摘要;ICMP SHALL 返回存活状态、平均延迟和丢包率摘要;DB SHALL 返回连接状态或行数摘要;CMD SHALL 返回 `"exitCode={N}"` 格式;LLM SHALL 返回 provider、mode、状态码、finish 原因、输出长度和 token 用量摘要
|
||||
@@ -1,133 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 Cmd 类型拨测目标:通过 `type: cmd` 配置执行本地命令(如进程检查、脚本健康检测),捕获 exit code、stdout、stderr,按 expect 规则校验并生成 matched 判定。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: cmd target 配置
|
||||
系统 SHALL 支持 `type: cmd` 的 target 配置,通过 `cmd.exec` 和 `cmd.args` 描述本地命令,并使用 cmd 专用字段配置工作目录、环境变量和输出限制。
|
||||
|
||||
#### Scenario: 解析 cmd target
|
||||
- **WHEN** YAML 中 target 配置 `type: cmd`、`cmd.exec: "pgrep"` 和 `cmd.args: ["nginx"]`
|
||||
- **THEN** 系统 SHALL 将其解析为 cmd checker,并保留 exec、args、cwd、env、maxOutputBytes、interval、timeout 和 expect 配置
|
||||
|
||||
#### Scenario: cmd target 缺少 exec
|
||||
- **WHEN** YAML 中 target 配置 `type: cmd` 但缺少 `cmd.exec`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 cmd.exec 字段
|
||||
|
||||
#### Scenario: cwd 相对配置文件目录解析
|
||||
- **WHEN** cmd target 配置 `cmd.cwd: "scripts"` 且配置文件位于 `/opt/checker/probes.yaml`
|
||||
- **THEN** 系统 SHALL 将 cwd 解析为 `/opt/checker/scripts`
|
||||
|
||||
#### Scenario: cmd 不使用 shell
|
||||
- **WHEN** cmd target 配置 `exec` 和 `args`
|
||||
- **THEN** 系统 MUST 直接执行该程序和参数,不通过 shell 解释整段命令字符串
|
||||
|
||||
#### Scenario: env 默认继承并允许覆盖
|
||||
- **WHEN** cmd target 配置 `cmd.env: {LANG: "C"}` 且当前进程环境包含 `PATH`
|
||||
- **THEN** 系统 SHALL 继承当前进程的全部环境变量,并将 `LANG` 覆盖为 `"C"`
|
||||
|
||||
#### Scenario: 不支持 stdin
|
||||
- **WHEN** cmd target 配置并执行命令
|
||||
- **THEN** 系统 MUST NOT 向子进程 stdin 写入数据,避免命令因等待输入而阻塞
|
||||
|
||||
### Requirement: cmd checker 执行
|
||||
系统 SHALL 按 cmd target 配置执行本地命令,记录执行耗时、退出码、stdout 和 stderr observation,并在执行失败时产生结构化错误信息。
|
||||
|
||||
#### Scenario: 命令正常退出
|
||||
- **WHEN** cmd target 执行的进程正常退出且 exit code 为 0
|
||||
- **THEN** 系统 SHALL 记录 `durationMs` 和包含 exitCode、stdoutPreview、stderrPreview 的 observation,并进入 expect 校验;API detail SHALL 为 `exitCode=0`
|
||||
|
||||
#### Scenario: 命令非零退出
|
||||
- **WHEN** cmd target 执行的进程正常退出但 exit code 为 1
|
||||
- **THEN** 系统 SHALL 记录包含 exitCode、stdoutPreview、stderrPreview 的 observation,并由 expect.exitCode 决定 matched 结果;API detail SHALL 为 `exitCode=1`
|
||||
|
||||
#### Scenario: 命令启动失败
|
||||
- **WHEN** cmd target 的 exec 不存在或无法启动
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,observation SHALL 为 null,并在 failure 中写入 kind=`error` 和可读错误信息
|
||||
|
||||
#### Scenario: 命令超时
|
||||
- **WHEN** cmd target 在 timeout 时间内未结束
|
||||
- **THEN** 系统 MUST 终止该子进程,记录 `matched=false`,并在 failure 中写入命令超时信息;如已收集输出片段,observation SHALL 包含 stdoutPreview、stderrPreview 和 error
|
||||
|
||||
#### Scenario: 命令输出超限
|
||||
- **WHEN** cmd target 的 stdout 和 stderr 合计输出超过 `maxOutputBytes`
|
||||
- **THEN** 系统 MUST 停止收集输出并终止该检查,记录 `matched=false`,并在 failure 中写入输出超限信息;observation SHALL 包含已截断输出预览和 error
|
||||
|
||||
### Requirement: cmd expect 校验
|
||||
系统 SHALL 支持 cmd 专用 expect,包括 `exitCode`、`durationMs`、`stdout` 和 `stderr`,并按 exitCode、durationMs、stdout、stderr 的阶段顺序快速失败。`exitCode` SHALL 保持有限整数数组语义,未配置时在 Resolved expect 中默认 `[0]`。`durationMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation` 校验完整命令执行耗时。`stdout` 和 `stderr` MUST 使用共享 `RawContentExpectations` 数组输入并在运行期使用 `ContentExpectations`,直接 matcher 作用于对应输出文本,`json` extractor SHALL 支持对 JSON CLI 输出执行 JSONPath 断言。
|
||||
|
||||
#### Scenario: 默认 exitCode 成功语义
|
||||
- **WHEN** cmd target 未显式配置 `expect.exitCode`
|
||||
- **THEN** 系统 SHALL 在 Resolved cmd expect 中使用默认 `exitCode: [0]` 进行校验
|
||||
|
||||
#### Scenario: 显式 exitCode 校验
|
||||
- **WHEN** cmd target 配置 `expect.exitCode: [0, 2]` 且实际 exit code 为 2
|
||||
- **THEN** 系统 SHALL 判定 exitCode 阶段通过,并继续后续 expect 阶段
|
||||
|
||||
#### Scenario: exitCode 不匹配快速失败
|
||||
- **WHEN** cmd target 配置 `expect.exitCode: [0]` 且实际 exit code 为 1
|
||||
- **THEN** 系统 SHALL 立即返回 `matched=false`,并在 failure 中写入 phase=`exitCode`、path=`exitCode`、expected 和 actual
|
||||
|
||||
#### Scenario: durationMs 校验
|
||||
- **WHEN** cmd target 配置 `expect.durationMs: {lte: 1000}` 且实际执行耗时为 1500ms
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 phase 为 `duration`
|
||||
|
||||
#### Scenario: stdout 按配置顺序校验
|
||||
- **WHEN** cmd target 配置 `expect.stdout` 为两个 ContentExpectations,第一条通过且第二条失败
|
||||
- **THEN** 系统 SHALL 先执行第一条 stdout expectation,再执行第二条,并将 failure.path 指向失败的 `stdout[1]`
|
||||
|
||||
#### Scenario: stderr 校验为空
|
||||
- **WHEN** cmd target 配置 `expect.stderr: [{empty: true}]` 且实际 stderr 为空字符串
|
||||
- **THEN** 系统 SHALL 判定 stderr 阶段通过
|
||||
|
||||
#### Scenario: stdout JSON 输出校验
|
||||
- **WHEN** cmd target 输出 stdout 为 `{"status":"ok"}` 且配置 `expect.stdout: [{json: {path: "$.status", equals: "ok"}}]`
|
||||
- **THEN** 系统 SHALL 判定 stdout 阶段通过
|
||||
|
||||
#### Scenario: stdout 失败后不检查 stderr
|
||||
- **WHEN** cmd target 同时配置 stdout 和 stderr expectation,且 stdout expectation 失败
|
||||
- **THEN** 系统 SHALL 快速失败并 MUST NOT 继续执行 stderr expectation
|
||||
|
||||
### Requirement: cmd checker 启动期配置校验
|
||||
系统 SHALL 在启动期对 cmd checker 的配置契约和语义执行严格校验。Cmd target 的 `cmd` 分组 SHALL 只允许 `exec`、`args`、`cwd`、`env`、`maxOutputBytes` 字段。Cmd expect SHALL 只允许 `exitCode`、`durationMs`、`stdout`、`stderr` 字段。未知字段、非法类型、不可编译正则和 ReDoS 风险正则 MUST 导致启动期配置错误。`expect.exitCode` SHALL 保留原有有限整数数组语义,不限制到特定平台范围。语义校验 MUST NOT 修改 Raw cmd expect 输入。
|
||||
|
||||
#### Scenario: cmd args 类型非法
|
||||
- **WHEN** YAML 中 cmd target 配置 `cmd.args` 不是字符串数组
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 cmd.args 格式错误
|
||||
|
||||
#### Scenario: cmd env 值类型非法
|
||||
- **WHEN** YAML 中 cmd target 配置 `cmd.env`,且任一环境变量值不是字符串
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 cmd.env 对应变量值必须为字符串
|
||||
|
||||
#### Scenario: cmd expect exitCode 类型非法
|
||||
- **WHEN** YAML 中 cmd target 配置 `expect.exitCode` 不是整数数组
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.exitCode 必须为整数数组
|
||||
|
||||
#### Scenario: cmd expect durationMs 非法
|
||||
- **WHEN** YAML 中 cmd target 配置 `expect.durationMs` 不是合法 `RawValueExpectation`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.durationMs 格式错误
|
||||
|
||||
#### Scenario: stdout 必须为 ContentExpectations 数组
|
||||
- **WHEN** YAML 中 cmd target 配置 `expect.stdout` 但其值不是数组
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.stdout 必须为数组
|
||||
|
||||
#### Scenario: stderr 必须为 ContentExpectations 数组
|
||||
- **WHEN** YAML 中 cmd target 配置 `expect.stderr` 但其值不是数组
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.stderr 必须为数组
|
||||
|
||||
#### Scenario: stdout text expectation 空对象非法
|
||||
- **WHEN** YAML 中 cmd target 配置 `expect.stdout: [{}]`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 stdout expectation 必须包含至少一个合法 matcher 或 extractor
|
||||
|
||||
#### Scenario: stderr text expectation 未知字段非法
|
||||
- **WHEN** YAML 中 cmd target 配置 `expect.stderr: [{foo: "bar"}]`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 stderr expectation 包含未知 matcher 或未知 extractor
|
||||
|
||||
#### Scenario: stdout regex 正则非法
|
||||
- **WHEN** YAML 中 cmd target 配置 `expect.stdout: [{regex: "[invalid"}]`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,而不是延迟到运行期抛错
|
||||
|
||||
#### Scenario: cmd expect 未知字段失败
|
||||
- **WHEN** YAML 中 cmd target 的 expect 包含 `status: [200]`、`maxDurationMs: 1000` 或其他非 cmd expect 字段
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
|
||||
@@ -1,195 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义项目代码质量门禁、格式化检查、Git hooks 自动化质量门禁、提交信息格式校验、快速检查和完整验证命令的行为要求,确保开发者可以通过文档化命令稳定验证源码质量、基础测试和生产构建。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: ESLint 代码质量门禁
|
||||
项目 SHALL 提供 ESLint 代码质量门禁,用于审查 TypeScript、React 前端、脚本和测试代码中的质量问题。ESLint 配置 SHALL 包括 `@eslint/js` recommended 规则、`typescript-eslint` recommended-type-checked 和 stylistic-type-checked 规则、`eslint-plugin-perfectionist` 导入排序规则、`eslint-plugin-import` 导入验证规则,以及精选的单项类型安全和风格规则。
|
||||
|
||||
#### Scenario: 运行 lint 检查
|
||||
- **WHEN** 开发者运行文档化的 lint 命令
|
||||
- **THEN** 系统 SHALL 使用 ESLint 检查项目源码、脚本和测试代码,并在发现违规时以非零状态退出
|
||||
|
||||
#### Scenario: 检查 React Hooks 规则
|
||||
- **WHEN** 前端 React 代码违反 Hooks 调用规则
|
||||
- **THEN** lint 命令 MUST 失败并报告对应违规
|
||||
|
||||
#### Scenario: 保护前后端边界
|
||||
- **WHEN** `src/web` 前端代码导入 `src/server` 后端运行时实现
|
||||
- **THEN** lint 命令 MUST 失败并报告前后端边界违规
|
||||
|
||||
#### Scenario: 检测类型安全违规
|
||||
- **WHEN** 代码中存在浮动的 Promise 未 await、any 类型泄漏到明确类型位置、模板字符串中包含非字符串化对象等类型安全隐患
|
||||
- **THEN** lint 命令 MUST 失败并报告对应 `@typescript-eslint` 规则违规
|
||||
|
||||
#### Scenario: 检测导入路径错误
|
||||
- **WHEN** 代码中导入路径指向不存在的文件或已废弃的路径
|
||||
- **THEN** lint 命令 MUST 失败并报告 `import/no-unresolved` 或 `import/no-deprecated` 错误
|
||||
|
||||
#### Scenario: 排除第三方模板目录
|
||||
- **WHEN** ESLint 运行检查
|
||||
- **THEN** 系统 MUST 排除 `.agents/` 等第三方模板目录,不检查其中的代码
|
||||
|
||||
#### Scenario: 排除生成产物和锁文件
|
||||
- **WHEN** ESLint 运行检查
|
||||
- **THEN** 系统 MUST 排除 `dist/`、`.build/`、`node_modules/`、`openspec/`、`.opencode/`、`.claude/`、`.codex/`、`*.bun-build`、`bun.lock`、`data/` 等非源码目录
|
||||
|
||||
### Requirement: Prettier 代码格式门禁
|
||||
|
||||
项目 SHALL 通过 `eslint-plugin-prettier` 将 Prettier 格式检查集成为 ESLint 规则,使 `lint` 命令同时覆盖代码质量和格式检查(原独立的 `format:check` 命令不再存在,格式检查统一通过 `lint` 完成)。项目仍保留独立的 `format` 命令(`prettier --write`)用于快速格式化。Prettier 配置 SHALL 显式声明 `printWidth`、`semi`、`singleQuote`、`trailingComma`、`bracketSpacing`、`arrowParens`、`endOfLine`、`tabWidth`、`useTabs` 全部格式化参数。
|
||||
|
||||
#### Scenario: 检查代码格式
|
||||
- **WHEN** 开发者运行 `bun run lint`
|
||||
- **THEN** ESLint SHALL 通过 eslint-plugin-prettier 检查受管理文件格式,并在发现未格式化文件时以非零状态退出
|
||||
|
||||
#### Scenario: 自动格式化代码
|
||||
- **WHEN** 开发者运行 `bun run format` 或 `eslint --fix`
|
||||
- **THEN** 系统 SHALL 使用 Prettier 重写受管理文件的格式
|
||||
|
||||
#### Scenario: 排除 OpenSpec 文档和生成产物
|
||||
- **WHEN** Prettier 格式化或格式检查运行(通过 ESLint 或独立 Prettier 命令)
|
||||
- **THEN** 系统 MUST 排除 `openspec/`、`dist/`、`.build/`、`node_modules/`、`bun.lock`、`skills-lock.json`、`.agents/`、`data/`、`*.bun-build`、`.opencode/`、`.claude/`、`.codex/` 和临时构建产物
|
||||
|
||||
#### Scenario: 格式化配置一致性
|
||||
- **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` 编译选项,将未使用的局部变量检测为编译错误。
|
||||
|
||||
#### Scenario: 存在未使用的局部变量
|
||||
- **WHEN** TypeScript 代码中存在声明但未被引用的局部变量
|
||||
- **THEN** `tsc --noEmit` MUST 以非零状态退出并报告未使用变量
|
||||
|
||||
### Requirement: TypeScript 索引签名属性访问检测
|
||||
项目 SHALL 启用 TypeScript `noPropertyAccessFromIndexSignature` 编译选项,禁止通过点号访问未显式声明的属性。
|
||||
|
||||
#### Scenario: 通过点号访问 Record 动态属性
|
||||
- **WHEN** 代码通过 `.property` 点号语法访问 `Record<string, T>` 类型或索引签名类型的属性
|
||||
- **THEN** `tsc --noEmit` MUST 以非零状态退出,强制使用 `["property"]` 括号语法显式访问
|
||||
|
||||
### Requirement: ESLint 导入自动排序
|
||||
项目 SHALL 通过 `eslint-plugin-perfectionist` 对导入语句进行自动排序,确保导入顺序一致性。
|
||||
|
||||
#### Scenario: 导入语句无序排列
|
||||
- **WHEN** 文件中导入语句未按要求排序
|
||||
- **THEN** `eslint --fix` SHALL 自动重排 import 声明和 named imports 内部顺序
|
||||
|
||||
#### Scenario: type import 与 value import 混合
|
||||
- **WHEN** 文件中同时存在 `import type` 和 `import` 语句
|
||||
- **THEN** perfectionist SHALL 正确识别并分别排序,不将 type 和 value 导入混淆
|
||||
|
||||
### Requirement: ESLint 导入路径验证
|
||||
项目 SHALL 通过 `eslint-plugin-import` 验证导入路径的有效性和一致性。
|
||||
|
||||
#### Scenario: 导入不存在的模块路径
|
||||
- **WHEN** 代码中导入了不存在或路径错误的模块
|
||||
- **THEN** lint 命令 MUST 失败并报告 `import/no-unresolved` 错误
|
||||
|
||||
#### Scenario: 存在重复导入
|
||||
- **WHEN** 同一个模块在同一文件中被多次导入
|
||||
- **THEN** `eslint --fix` SHALL 自动合并重复导入为目标模块的单条导入
|
||||
|
||||
#### Scenario: 存在循环依赖
|
||||
- **WHEN** 模块 A 导入模块 B,同时模块 B 导入模块 A
|
||||
- **THEN** lint 命令 MUST 报告 `import/no-cycle` 警告
|
||||
|
||||
### Requirement: 快速检查命令
|
||||
|
||||
项目 SHALL 提供快速 `check` 命令,用于日常开发期间验证代码质量和基础行为。
|
||||
|
||||
#### Scenario: 运行快速检查
|
||||
- **WHEN** 开发者运行 `bun run check`
|
||||
- **THEN** 系统 SHALL 依次执行 schema 检查、类型检查、lint(含格式)和单元/组件测试(`bun test`)
|
||||
|
||||
#### Scenario: 快速检查失败
|
||||
- **WHEN** `check` 中任一子检查失败
|
||||
- **THEN** `check` MUST 以非零状态退出且不静默忽略失败
|
||||
|
||||
### Requirement: 分层测试运行命令
|
||||
项目 SHALL 提供分层的测试运行命令,支持按需执行不同层级的测试。
|
||||
|
||||
#### Scenario: 运行全部单元和组件测试
|
||||
- **WHEN** 开发者运行 `bun test`
|
||||
- **THEN** 系统 SHALL 执行 `tests/` 目录下所有 `*.test.ts` 和 `*.test.tsx` 文件
|
||||
|
||||
### Requirement: 完整验证命令
|
||||
项目 SHALL 提供完整 `verify` 命令,用于提交前或发布前验证当前源码、测试和生产构建。
|
||||
|
||||
#### Scenario: 运行完整验证
|
||||
- **WHEN** 开发者运行 `bun run verify`
|
||||
- **THEN** 系统 SHALL 先运行 `check`,再运行生产构建
|
||||
|
||||
#### Scenario: 完整验证失败
|
||||
- **WHEN** `verify` 中任一阶段失败
|
||||
- **THEN** `verify` MUST 以非零状态退出且不能继续声明验证成功
|
||||
|
||||
### Requirement: 测试代码 ESLint 禁用最小化
|
||||
项目测试代码 SHALL 优先通过类型化 helper、类型化 mock、显式 no-op 和受控断言模式满足已启用的 ESLint 类型感知规则。受本变更审计的项目自有测试文件 MUST NOT 保留用于压制可通过代码结构解决的 `eslint-disable` 指令。
|
||||
|
||||
#### Scenario: 消除组件测试文件级禁用
|
||||
- **WHEN** ESLint 检查 `tests/web/components/App.test.tsx`
|
||||
- **THEN** 该文件 MUST 不使用文件级 `eslint-disable` 关闭 `@typescript-eslint/no-require-imports` 或 `@typescript-eslint/no-unsafe-*` 规则,并且测试中的 hook mock SHALL 使用类型化引用或等价方式访问 mock API
|
||||
|
||||
#### Scenario: 消除配置加载测试重复 await 禁用
|
||||
- **WHEN** `tests/server/checker/config-loader.test.ts` 断言 `loadConfig()` 异步失败
|
||||
- **THEN** 测试 SHALL 使用 helper 或显式 try/catch 断言错误实例与消息,MUST 不通过逐行 `eslint-disable-next-line @typescript-eslint/await-thenable` 压制 Bun `expect(...).rejects` 类型不匹配
|
||||
|
||||
#### Scenario: 测试环境 no-op polyfill 保持可解释
|
||||
- **WHEN** `tests/setup.ts` 为 jsdom 测试环境定义浏览器 API polyfill
|
||||
- **THEN** intentional no-op SHALL 使用显式可解释写法表达,MUST 不通过文件级 `eslint-disable @typescript-eslint/no-empty-function` 关闭空函数检查
|
||||
|
||||
#### Scenario: release 测试拦截 process.exit 保持窄作用域
|
||||
- **WHEN** `tests/scripts/release.test.ts` 验证无效 release target 会触发 `process.exit(1)`
|
||||
- **THEN** 测试 SHALL 使用受控 mock 或等价窄作用域替换并在断言后恢复,MUST 不通过 `eslint-disable-next-line @typescript-eslint/unbound-method` 保存未绑定方法
|
||||
|
||||
#### Scenario: 质量门禁验证禁用清理
|
||||
- **WHEN** 开发者运行 `bun run lint`
|
||||
- **THEN** ESLint MUST 检查项目自有测试代码并在无上述 `eslint-disable` 指令的情况下通过
|
||||
@@ -1,72 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义前端组件测试基础设施和覆盖要求,确保所有 React 组件的渲染、交互和状态流转行为经过验证。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: jsdom 测试环境配置
|
||||
项目 SHALL 通过 `tests/setup.ts` preload 脚本为组件测试提供 jsdom DOM 环境,包含 TDesign 组件所需的浏览器 API polyfill。
|
||||
|
||||
#### Scenario: 组件测试可以渲染 React 组件
|
||||
- **WHEN** 组件测试文件使用 `@testing-library/react` 的 `render` 函数渲染组件
|
||||
- **THEN** 组件 SHALL 在 jsdom 环境中正常渲染,可通过 `screen` 查询 DOM 元素
|
||||
|
||||
#### Scenario: TDesign 组件依赖的浏览器 API 可用
|
||||
- **WHEN** 组件测试渲染使用了 TDesign 组件(Table、Drawer、Skeleton 等)的业务组件
|
||||
- **THEN** jsdom 环境 SHALL 提供 `ResizeObserver`、`IntersectionObserver`、`window.matchMedia` 等 polyfill,不因缺失 API 而抛错
|
||||
|
||||
#### Scenario: recharts 图表组件被 mock
|
||||
- **WHEN** 组件测试渲染包含 recharts 图表的组件
|
||||
- **THEN** recharts 模块 SHALL 被 mock 为简单占位元素,不依赖 SVG 渲染能力
|
||||
|
||||
### Requirement: 组件测试使用 @testing-library/react
|
||||
项目 SHALL 使用 `@testing-library/react` 作为组件测试工具,遵循"测试用户行为而非实现细节"的原则。
|
||||
|
||||
#### Scenario: 通过用户可见内容查询元素
|
||||
- **WHEN** 测试需要查找页面元素
|
||||
- **THEN** 测试 SHALL 优先使用 `getByText`、`getByRole`、`getByLabelText` 等语义化查询,而非 CSS 选择器或 testId
|
||||
|
||||
#### Scenario: 通过用户交互触发行为
|
||||
- **WHEN** 测试需要模拟用户操作
|
||||
- **THEN** 测试 SHALL 使用 `fireEvent` 或 `userEvent` 模拟点击、输入等操作,而非直接调用组件内部方法
|
||||
|
||||
### Requirement: 所有前端组件 SHALL 有组件测试覆盖
|
||||
项目 SHALL 为 `src/web/components/` 下的每个组件和 `src/web/app.tsx` 提供对应的组件测试文件。
|
||||
|
||||
#### Scenario: 纯展示组件测试
|
||||
- **WHEN** 组件为纯展示组件(如 StatusDot、SummaryCards)
|
||||
- **THEN** 测试 SHALL 验证给定 props 时渲染正确的文本和结构,以及 null/空数据时的条件渲染
|
||||
|
||||
#### Scenario: 交互组件测试
|
||||
- **WHEN** 组件包含用户交互(如 TargetDetailDrawer 的 Tab 切换、RefreshCountdown 的按钮点击)
|
||||
- **THEN** 测试 SHALL 验证交互触发正确的回调函数调用和参数传递
|
||||
|
||||
#### Scenario: 条件渲染测试
|
||||
- **WHEN** 组件根据 loading/error/empty 状态展示不同内容(如 OverviewTab)
|
||||
- **THEN** 测试 SHALL 覆盖所有条件分支:loading skeleton、正常数据渲染、空数据占位
|
||||
|
||||
#### Scenario: 数据驱动组件测试
|
||||
- **WHEN** 组件接收列表数据渲染(如 TargetBoard 的分组、HistoryTab 的表格)
|
||||
- **THEN** 测试 SHALL 验证数据正确映射到 UI 元素,包括空列表和多项数据的情况
|
||||
|
||||
### Requirement: 组件测试的 Mock 边界
|
||||
组件测试 SHALL 只 mock 系统边界(网络请求),不 mock 内部实现。
|
||||
|
||||
#### Scenario: Mock fetch 而非 React Query
|
||||
- **WHEN** 组件通过 `@tanstack/react-query` 发起数据请求
|
||||
- **THEN** 测试 SHALL mock `globalThis.fetch` 返回预设响应,使用真实的 `QueryClientProvider` 包裹组件
|
||||
|
||||
#### Scenario: 不 mock TDesign 组件
|
||||
- **WHEN** 业务组件使用 TDesign 组件
|
||||
- **THEN** 测试 SHALL 真实渲染 TDesign 组件,验证 props 传递和集成行为的正确性
|
||||
|
||||
### Requirement: 组件测试文件组织
|
||||
组件测试文件 SHALL 位于 `tests/web/components/` 目录下,文件名与组件名对应。
|
||||
|
||||
#### Scenario: 测试文件命名
|
||||
- **WHEN** 为 `src/web/components/TargetBoard.tsx` 编写组件测试
|
||||
- **THEN** 测试文件 SHALL 位于 `tests/web/components/TargetBoard.test.tsx`
|
||||
|
||||
#### Scenario: App 组件测试位置
|
||||
- **WHEN** 为 `src/web/app.tsx` 编写组件测试
|
||||
- **THEN** 测试文件 SHALL 位于 `tests/web/components/App.test.tsx`
|
||||
@@ -1,226 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义配置文件的变量定义、引用、解析和替换机制,支持集中管理共享值和环境变量注入。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: variables 段定义
|
||||
配置文件 SHALL 支持可选的顶层 `variables` 段,用于定义变量键值对。variables 的 key SHALL 符合 `[a-zA-Z_][a-zA-Z0-9_]*` 命名规则。variables 的 value SHALL 仅支持 string、number、boolean 三种类型,MUST NOT 支持 null、array、object。variables 段自身 MUST NOT 支持引用其他变量或环境变量(值为纯字面量)。
|
||||
|
||||
#### Scenario: 定义字符串变量
|
||||
- **WHEN** 配置文件包含 `variables: { base_url: "https://api.example.com" }`
|
||||
- **THEN** 系统 SHALL 解析 base_url 为字符串类型变量,值为 "https://api.example.com"
|
||||
|
||||
#### Scenario: 定义数字变量
|
||||
- **WHEN** 配置文件包含 `variables: { port: 5432 }`
|
||||
- **THEN** 系统 SHALL 解析 port 为 number 类型变量,值为 5432
|
||||
|
||||
#### Scenario: 定义布尔变量
|
||||
- **WHEN** 配置文件包含 `variables: { ssl_enabled: true }`
|
||||
- **THEN** 系统 SHALL 解析 ssl_enabled 为 boolean 类型变量,值为 true
|
||||
|
||||
#### Scenario: 变量值为 null 报错
|
||||
- **WHEN** 配置文件包含 `variables: { empty: null }`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 variables 的值不允许为 null
|
||||
|
||||
#### Scenario: 变量值为数组报错
|
||||
- **WHEN** 配置文件包含 `variables: { list: [1, 2, 3] }`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 variables 的值不允许为 array
|
||||
|
||||
#### Scenario: 变量值为对象报错
|
||||
- **WHEN** 配置文件包含 `variables: { obj: { a: 1 } }`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 variables 的值不允许为 object
|
||||
|
||||
#### Scenario: 变量 key 不合法报错
|
||||
- **WHEN** 配置文件包含 `variables: { "123start": "value" }`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示变量名不符合命名规则
|
||||
|
||||
#### Scenario: 不定义 variables 段
|
||||
- **WHEN** 配置文件不包含 variables 段
|
||||
- **THEN** 系统 SHALL 正常启动,支持变量替换的配置字段中的 `${...}` 引用仅从环境变量和表达式默认值查找
|
||||
|
||||
### Requirement: 变量引用语法
|
||||
支持变量替换的字符串值 SHALL 支持 `${key}` 语法引用变量。系统 SHALL 支持 `${key|default}` 语法设置默认值,其中第一个 `|` 为分隔符,后续 `|` 属于默认值内容。系统 SHALL 支持 `${key|}` 显式设置空字符串默认值。系统 SHALL 支持 `$${...}` 转义语法输出字面量 `${...}`。
|
||||
|
||||
#### Scenario: 简单变量引用
|
||||
- **WHEN** target 字段值为 `"${base_url}/health"` 且 variables 中定义 `base_url: "https://api.example.com"`
|
||||
- **THEN** 系统 SHALL 将该字段替换为 `"https://api.example.com/health"`
|
||||
|
||||
#### Scenario: 带默认值的变量引用
|
||||
- **WHEN** target 字段值为 `"${DB_PORT|5432}"` 且 variables 和环境变量中均不存在 DB_PORT
|
||||
- **THEN** 系统 SHALL 将该字段替换为使用默认值(类型推断后为 number 5432)
|
||||
|
||||
#### Scenario: 默认值包含管道符
|
||||
- **WHEN** target 字段值为 `"${PATTERN|foo|bar}"` 且变量不存在
|
||||
- **THEN** 系统 SHALL 使用 `"foo|bar"` 作为默认值(第一个 `|` 为分隔符)
|
||||
|
||||
#### Scenario: 空默认值
|
||||
- **WHEN** 支持变量替换的字段值为 `"${OPTIONAL_VALUE|}"` 且 variables 和环境变量中均不存在 OPTIONAL_VALUE
|
||||
- **THEN** 系统 SHALL 使用空字符串 `""` 作为默认值且不报 unresolved-variable 错误
|
||||
|
||||
#### Scenario: 转义语法
|
||||
- **WHEN** target 字段值为 `"Hello $${name}"`
|
||||
- **THEN** 系统 SHALL 输出 `"Hello ${name}"`,不进行变量替换
|
||||
|
||||
#### Scenario: 多个变量引用
|
||||
- **WHEN** target 字段值为 `"${protocol}://${host}:${port}/api"`
|
||||
- **THEN** 系统 SHALL 逐个解析并替换所有变量引用,结果为拼接后的字符串
|
||||
|
||||
#### Scenario: 无变量引用的字符串
|
||||
- **WHEN** target 字段值为 `"https://example.com"` 且不含 `${...}` 模式
|
||||
- **THEN** 系统 SHALL 保持原样,不做任何处理
|
||||
|
||||
### Requirement: 变量解析优先级
|
||||
系统 SHALL 按以下优先级解析变量引用:variables 定义 → 环境变量 → 默认值。如果三者均不存在,系统 SHALL 以配置错误退出。
|
||||
|
||||
#### Scenario: variables 优先于环境变量
|
||||
- **WHEN** variables 中定义 `port: 5432` 且环境变量 `port=3000` 也存在
|
||||
- **THEN** 系统 SHALL 使用 variables 中的值 5432
|
||||
|
||||
#### Scenario: 环境变量作为 fallback
|
||||
- **WHEN** variables 中未定义 `DB_HOST` 但环境变量 `DB_HOST=localhost` 存在
|
||||
- **THEN** 系统 SHALL 使用环境变量的值 "localhost"
|
||||
|
||||
#### Scenario: 默认值作为最终 fallback
|
||||
- **WHEN** variables 和环境变量中均不存在 `CACHE_TTL`,且引用为 `${CACHE_TTL|60}`
|
||||
- **THEN** 系统 SHALL 使用默认值(类型推断后为 number 60)
|
||||
|
||||
#### Scenario: 变量未定义且无默认值报错
|
||||
- **WHEN** 支持变量替换的字段引用 `${MISSING_VAR}` 且 variables、环境变量中均不存在,也未设置默认值
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示该变量未定义
|
||||
|
||||
### Requirement: 完整引用类型保留
|
||||
当字段值仅包含单个变量引用(完整引用)时,系统 SHALL 保留变量的原始类型。完整引用的判定为:字段值去掉首尾空白后严格匹配单个 `${key}` 或 `${key|default}` 模式且无其他字符。环境变量和表达式默认值的完整引用 SHALL 做 number/boolean 类型推断,但空字符串 MUST 保持为 string `""`。
|
||||
|
||||
#### Scenario: 完整引用 number 变量
|
||||
- **WHEN** target 字段值为 `"${port}"` 且 variables 中 `port: 5432`
|
||||
- **THEN** 系统 SHALL 将该字段替换为 number 类型的 5432
|
||||
|
||||
#### Scenario: 完整引用 boolean 变量
|
||||
- **WHEN** target 字段值为 `"${ssl}"` 且 variables 中 `ssl: true`
|
||||
- **THEN** 系统 SHALL 将该字段替换为 boolean 类型的 true
|
||||
|
||||
#### Scenario: 完整引用 string 变量
|
||||
- **WHEN** target 字段值为 `"${host}"` 且 variables 中 `host: "example.com"`
|
||||
- **THEN** 系统 SHALL 将该字段替换为 string 类型的 "example.com"
|
||||
|
||||
#### Scenario: 部分引用强制为字符串
|
||||
- **WHEN** target 字段值为 `"port: ${port}"` 且 variables 中 `port: 5432`
|
||||
- **THEN** 系统 SHALL 将该字段替换为 string 类型的 "port: 5432"
|
||||
|
||||
#### Scenario: 环境变量完整引用类型推断
|
||||
- **WHEN** target 字段值为 `"${MAX_REDIRECTS}"` 且环境变量 `MAX_REDIRECTS=5`
|
||||
- **THEN** 系统 SHALL 将该字段替换为 number 类型的 5(类型推断)
|
||||
|
||||
#### Scenario: 默认值完整引用类型推断
|
||||
- **WHEN** target 字段值为 `"${TIMEOUT|30}"` 且变量不存在
|
||||
- **THEN** 系统 SHALL 将该字段替换为 number 类型的 30(类型推断)
|
||||
|
||||
#### Scenario: 默认值推断为 boolean
|
||||
- **WHEN** target 字段值为 `"${IGNORE_SSL|false}"` 且变量不存在
|
||||
- **THEN** 系统 SHALL 将该字段替换为 boolean 类型的 false
|
||||
|
||||
#### Scenario: 默认值无法推断保持字符串
|
||||
- **WHEN** target 字段值为 `"${HOST|localhost}"` 且变量不存在
|
||||
- **THEN** 系统 SHALL 将该字段替换为 string 类型的 "localhost"
|
||||
|
||||
#### Scenario: 空默认值保持字符串
|
||||
- **WHEN** 支持变量替换的字段值为 `"${OPTIONAL_VALUE|}"` 且变量不存在
|
||||
- **THEN** 系统 SHALL 将该字段替换为 string 类型的空字符串 `""`,MUST NOT 推断为 number 0
|
||||
|
||||
#### Scenario: 空环境变量保持字符串
|
||||
- **WHEN** 支持变量替换的字段值为 `"${EMPTY_ENV}"` 且环境变量 `EMPTY_ENV` 存在但值为空字符串
|
||||
- **THEN** 系统 SHALL 将该字段替换为 string 类型的空字符串 `""`,MUST NOT 推断为 number 0
|
||||
|
||||
### Requirement: 替换范围限制
|
||||
变量替换 SHALL 作用于 Authoring Config 的 `server`、`probes` 和 `targets` 段中的字符串值。`variables` 段自身 MUST NOT 参与变量替换。系统 SHALL 递归遍历支持范围内对象树中所有字符串 value 进行替换,包括嵌套对象和数组元素中的字符串。系统 MUST NOT 替换对象 key。`targets[].id` 和 `targets[].type` 字段 MUST NOT 参与变量替换;target 内部其他路径上名为 `id` 或 `type` 的字段 SHALL 正常参与变量替换。顶层 `defaults` 不再是合法配置段,因此不属于变量替换范围。变量替换完成后,Normalized Config MUST NOT 保留顶层 `variables` 段。
|
||||
|
||||
#### Scenario: target 嵌套对象中的变量替换
|
||||
- **WHEN** target 配置 `http.headers.Authorization: "${token}"` 且 variables 中定义 `token: "Bearer abc"`
|
||||
- **THEN** 系统 SHALL 将该 header 值替换为 "Bearer abc"
|
||||
|
||||
#### Scenario: target 数组元素中的变量替换
|
||||
- **WHEN** target 配置 `cmd.args: ["--host", "${host}"]` 且 variables 中定义 `host: "localhost"`
|
||||
- **THEN** 系统 SHALL 将数组第二个元素替换为 "localhost"
|
||||
|
||||
#### Scenario: server 段变量替换
|
||||
- **WHEN** server 配置 `listen.host: "${server_host}"` 且 variables 中定义 `server_host: "0.0.0.0"`
|
||||
- **THEN** 系统 SHALL 将 server.listen.host 替换为 "0.0.0.0"
|
||||
|
||||
#### Scenario: probes 段变量替换
|
||||
- **WHEN** probes 配置 `execution.maxConcurrentChecks: "${max_checks}"` 且 variables 中定义 `max_checks: 5`
|
||||
- **THEN** 系统 SHALL 将 probes.execution.maxConcurrentChecks 替换为 number 5
|
||||
|
||||
#### Scenario: variables 段不替换
|
||||
- **WHEN** variables 配置 `host: "${HOST}"` 且环境变量 HOST 存在
|
||||
- **THEN** 系统 SHALL 保持 variables.host 为字面量 `"${HOST}"`,不进行替换
|
||||
|
||||
#### Scenario: target id 字段不替换
|
||||
- **WHEN** target 配置 `id: "${my_id}"` 且 variables 中定义 `my_id: "test"`
|
||||
- **THEN** 系统 SHALL 保持 id 字段值为字面量 `"${my_id}"`,不进行替换
|
||||
|
||||
#### Scenario: target type 字段不替换
|
||||
- **WHEN** target 配置 `type: "${checker_type}"` 且 variables 中定义 `checker_type: "http"`
|
||||
- **THEN** 系统 SHALL 保持 type 字段值为字面量 `"${checker_type}"`,不进行替换
|
||||
|
||||
#### Scenario: target 嵌套 id 字段参与替换
|
||||
- **WHEN** target 内部非顶层身份字段配置 `expect.rows[0].id.equals: "${expected_id}"` 且 variables 中定义 `expected_id: 1`
|
||||
- **THEN** 系统 SHALL 将该嵌套 id 字段替换为 number 1
|
||||
|
||||
#### Scenario: target 嵌套 type 字段参与替换
|
||||
- **WHEN** target 内部非顶层身份字段配置 `expect.body[0].json.path: "$.${field_type}"` 且 variables 中定义 `field_type: "type"`
|
||||
- **THEN** 系统 SHALL 对该嵌套字段执行变量替换
|
||||
|
||||
#### Scenario: 对象 key 不替换
|
||||
- **WHEN** target 配置 `http.headers."${HEADER_NAME}": "demo"` 且 variables 中定义 `HEADER_NAME: "X-Demo"`
|
||||
- **THEN** 系统 SHALL 保持 header key 为字面量 `"${HEADER_NAME}"`,不进行替换
|
||||
|
||||
#### Scenario: defaults 段被拒绝
|
||||
- **WHEN** 配置文件声明顶层 `defaults`
|
||||
- **THEN** 系统 SHALL 在契约校验阶段拒绝该未知字段,而不是尝试变量替换
|
||||
|
||||
#### Scenario: Normalized Config 移除 variables 段
|
||||
- **WHEN** Authoring Config 包含顶层 `variables` 段且变量替换成功
|
||||
- **THEN** Normalized Config SHALL 不包含顶层 `variables` 字段
|
||||
|
||||
### Requirement: 变量替换错误报告
|
||||
变量替换阶段的错误 SHALL 作为 `ConfigValidationIssue` 输出,code 为 `unresolved-variable`。错误信息 SHALL 包含字段路径和变量名。对于 `targets[i]` 内的错误,错误信息还 SHALL 包含 target 索引、target id 和 target 展示名上下文。
|
||||
|
||||
#### Scenario: 单个 target 变量缺失报错
|
||||
- **WHEN** targets[0] (id: "api-health") 的 http.url 引用 `${base_url}` 但变量不存在
|
||||
- **THEN** 系统 SHALL 输出包含 target 索引 0、id "api-health"、路径 "http.url"、变量名 "base_url" 的错误信息
|
||||
|
||||
#### Scenario: server 变量缺失报错
|
||||
- **WHEN** server.listen.host 引用 `${server_host}` 但变量不存在
|
||||
- **THEN** 系统 SHALL 输出包含路径 "server.listen.host" 和变量名 "server_host" 的错误信息
|
||||
|
||||
#### Scenario: probes 变量缺失报错
|
||||
- **WHEN** probes.execution.maxConcurrentChecks 引用 `${max_checks}` 但变量不存在
|
||||
- **THEN** 系统 SHALL 输出包含路径 "probes.execution.maxConcurrentChecks" 和变量名 "max_checks" 的错误信息
|
||||
|
||||
#### Scenario: 多个变量缺失批量报错
|
||||
- **WHEN** 多个配置字段引用了不存在的变量
|
||||
- **THEN** 系统 SHALL 收集所有缺失变量错误后统一输出,而非遇到第一个就退出
|
||||
|
||||
### Requirement: 变量替换执行时机
|
||||
变量替换 SHALL 在 YAML 解析之后、Normalized schema 契约校验(AJV)之前执行。变量替换 SHALL 是 `normalizeAuthoringConfig()` 的一部分,替换完成后的配置对象 SHALL 继续执行 expect 简写展开并形成 Normalized Config。Normalized Config SHALL 传入后续契约校验和语义校验流程。
|
||||
|
||||
#### Scenario: target 替换后通过 schema 校验
|
||||
- **WHEN** target 配置 `http.maxRedirects: "${MAX_REDIRECTS}"` 且环境变量 `MAX_REDIRECTS=5`
|
||||
- **THEN** 系统 SHALL 先将该字段替换为 number 5,再进入 AJV 校验(期望 integer),校验通过
|
||||
|
||||
#### Scenario: target 替换后未通过 schema 校验
|
||||
- **WHEN** target 配置 `http.maxRedirects: "${MAX_REDIRECTS}"` 且环境变量 `MAX_REDIRECTS=abc`
|
||||
- **THEN** 系统 SHALL 先将该字段替换为 string "abc",再进入 AJV 校验(期望 integer),校验失败并报错
|
||||
|
||||
#### Scenario: server 替换后通过 schema 校验
|
||||
- **WHEN** server 配置 `listen.port: "${SERVER_PORT}"` 且 variables 中定义 `SERVER_PORT: 3000`
|
||||
- **THEN** 系统 SHALL 先将该字段替换为 number 3000,再进入 AJV 校验(期望 integer),校验通过
|
||||
|
||||
#### Scenario: probes 替换后通过 schema 校验
|
||||
- **WHEN** probes 配置 `execution.maxConcurrentChecks: "${MAX_CHECKS}"` 且 variables 中定义 `MAX_CHECKS: 20`
|
||||
- **THEN** 系统 SHALL 先将 probes.execution.maxConcurrentChecks 替换为 number 20,再进入 Normalized schema 校验(期望 integer),校验通过
|
||||
|
||||
#### Scenario: 变量替换后继续展开 expect 简写
|
||||
- **WHEN** Authoring Config 配置 `expect.durationMs: "${MAX_MS}"` 且 variables 中定义 `MAX_MS: 1000`
|
||||
- **THEN** Normalized Config SHALL 包含 `expect.durationMs: { equals: 1000 }`
|
||||
@@ -1,137 +0,0 @@
|
||||
## 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.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Alpine 多阶段容器镜像构建
|
||||
|
||||
系统 SHALL 提供基于 Alpine 的多阶段 Dockerfile,用于从源码构建 DiAL 容器镜像。构建阶段 SHALL 使用 Bun 官方 Alpine 镜像安装依赖并执行现有生产构建命令;运行阶段 SHALL 使用 Alpine 基础镜像并只复制生产运行所需产物。
|
||||
|
||||
#### Scenario: 构建容器镜像
|
||||
|
||||
- **WHEN** 开发者在项目根目录执行文档化的 Docker 构建命令
|
||||
- **THEN** 镜像构建 SHALL 安装锁定依赖、执行 `bun run build`,并将生成的 `dist/dial-server` 复制到最终运行镜像
|
||||
|
||||
#### Scenario: 运行镜像不包含开发依赖
|
||||
|
||||
- **WHEN** Dockerfile 生成最终运行阶段
|
||||
- **THEN** 最终运行阶段 SHALL NOT 复制源码目录、`node_modules`、`.build` 或 `dist/web` 到镜像中
|
||||
|
||||
### Requirement: Alpine musl executable 目标
|
||||
|
||||
系统 MUST 为 Alpine 运行阶段生成 musl Linux executable。Docker 构建 SHALL 根据 Docker 构建架构选择 `bun-linux-x64-musl` 或 `bun-linux-arm64-musl`,并将该目标传递给现有 Bun compile 流程。
|
||||
|
||||
#### Scenario: amd64 镜像构建
|
||||
|
||||
- **WHEN** Docker 构建目标架构为 `amd64`
|
||||
- **THEN** 构建阶段 MUST 使用 `bun-linux-x64-musl` 作为 Bun compile target
|
||||
|
||||
#### Scenario: arm64 镜像构建
|
||||
|
||||
- **WHEN** Docker 构建目标架构为 `arm64`
|
||||
- **THEN** 构建阶段 MUST 使用 `bun-linux-arm64-musl` 作为 Bun compile target
|
||||
|
||||
#### Scenario: 不支持的架构
|
||||
|
||||
- **WHEN** Docker 构建目标架构不是 `amd64` 或 `arm64`
|
||||
- **THEN** 构建 MUST 失败并输出不支持该架构的错误
|
||||
|
||||
### Requirement: 容器运行环境边界
|
||||
|
||||
运行镜像 SHALL 提供 DiAL 运行必需的系统能力,包括 HTTPS CA 证书、ICMP checker 所需 `ping` 命令、Bun musl executable 所需运行库、时区数据、非 root 运行用户和可写数据目录。运行镜像 SHALL NOT 默认提供 CMD checker 可能依赖的业务命令环境。
|
||||
|
||||
#### Scenario: 基础系统包
|
||||
|
||||
- **WHEN** 最终运行镜像构建完成
|
||||
- **THEN** 镜像 SHALL 包含 `ca-certificates`、`iputils-ping`、`libgcc`、`libstdc++` 和 `tzdata`
|
||||
|
||||
#### Scenario: 非 root 运行
|
||||
|
||||
- **WHEN** 容器启动 DiAL 进程
|
||||
- **THEN** DiAL 进程 SHALL 以非 root 用户运行,并且该用户 SHALL 能写入 `/data/dial`
|
||||
|
||||
#### Scenario: CMD checker 额外命令不内置
|
||||
|
||||
- **WHEN** 用户需要通过 CMD checker 执行 `curl`、`dig`、`psql`、`mysql`、`redis-cli` 或自定义脚本依赖
|
||||
- **THEN** 官方镜像 SHALL 要求用户通过派生镜像或运行环境自行提供这些命令
|
||||
|
||||
### Requirement: 容器默认启动约定
|
||||
|
||||
系统 SHALL 为容器镜像提供稳定的默认启动约定。镜像 SHALL 使用 `dial-server` executable 作为入口,默认配置文件路径 SHALL 为 `/etc/dial/probes.yaml`,默认服务端口 SHALL 暴露 `3000`。
|
||||
|
||||
#### Scenario: 默认配置路径
|
||||
|
||||
- **WHEN** 用户不覆盖容器启动命令
|
||||
- **THEN** 容器 SHALL 尝试使用 `/etc/dial/probes.yaml` 作为 DiAL YAML 配置文件路径启动
|
||||
|
||||
#### Scenario: 默认端口暴露
|
||||
|
||||
- **WHEN** 用户查看镜像元数据或使用文档化运行命令
|
||||
- **THEN** 镜像 SHALL 暴露并映射 DiAL 默认 HTTP 端口 `3000`
|
||||
|
||||
### Requirement: 容器专用最小配置示例
|
||||
|
||||
系统 SHALL 提供容器专用最小配置示例,用于直接挂载到默认配置路径运行。该配置 SHALL 默认监听容器网络地址 `0.0.0.0`,默认端口为 `3000`,并将存储目录设置为 `/data/dial`。
|
||||
|
||||
#### Scenario: 容器配置监听所有接口
|
||||
|
||||
- **WHEN** 用户使用容器专用配置示例启动 DiAL
|
||||
- **THEN** DiAL SHALL 监听 `0.0.0.0:3000`,使宿主机端口映射可访问服务
|
||||
|
||||
#### Scenario: 容器配置持久化目录
|
||||
|
||||
- **WHEN** 用户使用容器专用配置示例启动 DiAL
|
||||
- **THEN** SQLite 数据和默认日志 SHALL 写入 `/data/dial` 下的路径
|
||||
|
||||
#### Scenario: 容器配置避免额外命令依赖
|
||||
|
||||
- **WHEN** 用户使用容器专用配置示例启动 DiAL
|
||||
- **THEN** 示例配置 SHALL NOT 包含依赖 Bun、Node.js 或额外系统命令的 CMD checker target
|
||||
|
||||
### Requirement: 容器健康检查
|
||||
|
||||
镜像 SHALL 提供 Docker HEALTHCHECK,通过 Alpine 自带 `wget` 访问 DiAL `/health` 端点判断进程健康。健康检查 SHALL NOT 为此额外安装 `curl`。
|
||||
|
||||
#### Scenario: 健康检查成功
|
||||
|
||||
- **WHEN** DiAL 在容器内启动并响应 `/health`
|
||||
- **THEN** Docker HEALTHCHECK SHALL 通过 HTTP 请求 `/health` 成功返回健康状态
|
||||
|
||||
#### Scenario: 不额外安装 curl
|
||||
|
||||
- **WHEN** 最终运行镜像构建完成
|
||||
- **THEN** 镜像 SHALL NOT 为健康检查目的安装 `curl`
|
||||
|
||||
### Requirement: Docker 构建上下文控制
|
||||
|
||||
系统 SHALL 提供 Docker 构建上下文忽略规则,避免将开发依赖、构建产物、本地数据、环境文件和工具状态复制进构建上下文。
|
||||
|
||||
#### Scenario: 忽略开发和构建产物
|
||||
|
||||
- **WHEN** Docker 构建上下文被发送给 Docker daemon
|
||||
- **THEN** `node_modules`、`dist`、`.build`、`data`、本地环境文件和工具状态目录 SHALL 被忽略
|
||||
|
||||
### Requirement: Docker 使用文档
|
||||
|
||||
系统 SHALL 在项目文档中说明 Docker 镜像的构建、运行和扩展方式。文档 MUST 覆盖单架构构建、多架构 buildx 构建、配置挂载、数据卷、ICMP capability 和 CMD checker 派生镜像策略。
|
||||
|
||||
#### Scenario: 单架构 Docker 构建文档
|
||||
|
||||
- **WHEN** 用户阅读 Docker 部署说明
|
||||
- **THEN** 文档 SHALL 提供在本机架构构建 Alpine 镜像的命令示例
|
||||
|
||||
#### Scenario: 多架构 buildx 文档
|
||||
|
||||
- **WHEN** 用户阅读 Docker 部署说明
|
||||
- **THEN** 文档 SHALL 提供 `linux/amd64` 和 `linux/arm64` 的 buildx 构建命令示例
|
||||
|
||||
#### Scenario: ICMP capability 文档
|
||||
|
||||
- **WHEN** 用户需要在容器中运行 ICMP checker
|
||||
- **THEN** 文档 MUST 明确说明运行容器时需要授予 `NET_RAW` capability
|
||||
|
||||
#### Scenario: CMD checker 派生镜像文档
|
||||
|
||||
- **WHEN** 用户需要 CMD checker 执行官方镜像未内置的命令
|
||||
- **THEN** 文档 SHALL 提供通过派生镜像安装额外命令的示例
|
||||
@@ -1,132 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义多平台交叉编译发布流程,包括目标平台矩阵、可执行文件命名规范、压缩包打包、SHA256 校验和生成、CLI 接口。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Release 目标平台矩阵
|
||||
Release 流程 SHALL 支持以下 7 个编译目标,覆盖所有指定平台:
|
||||
|
||||
| Bun CompileTarget | OS | Arch |
|
||||
|---|---|---|
|
||||
| bun-linux-x64 | linux | x64 |
|
||||
| bun-linux-arm64 | linux | arm64 |
|
||||
| bun-linux-x64-musl | linux | x64-musl |
|
||||
| bun-linux-arm64-musl | linux | arm64-musl |
|
||||
| bun-windows-x64 | windows | x64 |
|
||||
| bun-darwin-x64 | darwin | x64 |
|
||||
| bun-darwin-arm64 | darwin | arm64 |
|
||||
|
||||
#### Scenario: 默认全平台编译
|
||||
- **WHEN** 开发者运行 `bun run release` 不带额外参数
|
||||
- **THEN** 系统 SHALL 依次编译上述 7 个目标
|
||||
|
||||
#### Scenario: 指定单一目标编译
|
||||
- **WHEN** 开发者运行 `bun run release --target linux-x64`
|
||||
- **THEN** 系统 SHALL 只编译 `bun-linux-x64` 目标
|
||||
|
||||
#### Scenario: 指定多个目标编译
|
||||
- **WHEN** 开发者运行 `bun run release --target linux-x64,darwin-arm64,windows-x64`
|
||||
- **THEN** 系统 SHALL 编译指定的 3 个目标
|
||||
|
||||
#### Scenario: 无效 target 参数
|
||||
- **WHEN** 开发者传入不存在的 `--target` 值
|
||||
- **THEN** 系统 MUST 报错退出,并列出所有可用的 target 值
|
||||
|
||||
### Requirement: Release 构建流水线
|
||||
Release 流程 SHALL 执行四步流水线:Vite 前端构建 → code generation → 多目标交叉编译 → 打包与校验和。
|
||||
|
||||
#### Scenario: Release 构建顺序
|
||||
- **WHEN** 开发者运行 `bun run release`
|
||||
- **THEN** 系统 MUST 依次执行 Vite build、code generation、多目标 Bun compile、tar.gz 打包和 SHA256 校验和生成
|
||||
|
||||
#### Scenario: Vite 构建失败
|
||||
- **WHEN** Vite build 步骤失败
|
||||
- **THEN** 系统 MUST 停止后续步骤,不执行 code generation 或编译
|
||||
|
||||
#### Scenario: 单目标编译失败
|
||||
- **WHEN** 某个目标的 Bun compile 失败
|
||||
- **THEN** 系统 MUST 停止后续打包,报告失败的目标
|
||||
|
||||
#### Scenario: 前端构建只执行一次
|
||||
- **WHEN** release 流程编译多个目标
|
||||
- **THEN** Vite build 和 code generation MUST 只执行一次,所有目标共用同一份前端产出
|
||||
|
||||
#### Scenario: 构建完成后清理
|
||||
- **WHEN** release 流程完成(无论成功或失败)
|
||||
- **THEN** 系统 SHALL 清理 `.build/` 临时目录
|
||||
|
||||
### Requirement: 可执行文件命名规范
|
||||
Release 产出的裸二进制 SHALL 使用 `dial-server-{version}-{os}-{arch}` 命名,Windows 平台附加 `.exe` 后缀。
|
||||
|
||||
#### Scenario: Linux x64 可执行文件命名
|
||||
- **WHEN** 编译目标为 bun-linux-x64 且版本为 0.1.0
|
||||
- **THEN** 裸二进制文件名 SHALL 为 `dial-server-0.1.0-linux-x64`
|
||||
|
||||
#### Scenario: Windows 可执行文件命名
|
||||
- **WHEN** 编译目标为 bun-windows-x64 且版本为 0.1.0
|
||||
- **THEN** 裸二进制文件名 SHALL 为 `dial-server-0.1.0-windows-x64.exe`
|
||||
|
||||
#### Scenario: musl 变体命名
|
||||
- **WHEN** 编译目标为 bun-linux-x64-musl 且版本为 0.1.0
|
||||
- **THEN** 裸二进制文件名 SHALL 为 `dial-server-0.1.0-linux-x64-musl`
|
||||
|
||||
### Requirement: 压缩包打包
|
||||
每个目标 SHALL 生成一个 tar.gz 压缩包,内含可执行文件、示例配置和许可证。
|
||||
|
||||
#### Scenario: 压缩包命名
|
||||
- **WHEN** 编译 linux-x64 目标且版本为 0.1.0
|
||||
- **THEN** 压缩包文件名 SHALL 为 `dial-server_0.1.0_linux_x64.tar.gz`
|
||||
|
||||
#### Scenario: 压缩包内部目录结构
|
||||
- **WHEN** 解压 `dial-server_0.1.0_linux_x64.tar.gz`
|
||||
- **THEN** 产出目录 SHALL 为 `dial-server_0.1.0_linux_x64/`,内含 `dial-server`(可执行文件)、`probes.example.yaml`(示例配置)、`LICENSE`(许可证)
|
||||
|
||||
#### Scenario: Windows 目标压缩包
|
||||
- **WHEN** 编译 windows-x64 目标且版本为 0.1.0
|
||||
- **THEN** 压缩包 SHALL 使用 `.tar.gz` 格式,内部可执行文件名 SHALL 为 `dial-server.exe`
|
||||
|
||||
#### Scenario: 压缩包内可执行文件权限
|
||||
- **WHEN** 在 Linux/macOS 上解压非 Windows 目标的压缩包
|
||||
- **THEN** 可执行文件 SHALL 具有 0o755 权限(可执行),配置文件和许可证 SHALL 具有 0o644 权限
|
||||
|
||||
#### Scenario: 版本号来源
|
||||
- **WHEN** release 流程执行
|
||||
- **THEN** 版本号 SHALL 从 `package.json.version` 读取,与 build 命令使用同一来源
|
||||
|
||||
### Requirement: SHA256 校验和
|
||||
每个压缩包 SHALL 附带一个 SHA256 校验和文件,格式兼容 `sha256sum -c`。
|
||||
|
||||
#### Scenario: 校验和文件命名
|
||||
- **WHEN** 压缩包为 `dial-server_0.1.0_linux_x64.tar.gz`
|
||||
- **THEN** 校验和文件名 SHALL 为 `dial-server_0.1.0_linux_x64.tar.gz.sha256`
|
||||
|
||||
#### Scenario: 校验和文件内容格式
|
||||
- **WHEN** 读取 `.sha256` 文件
|
||||
- **THEN** 内容 SHALL 为 `<hash> <filename>\n` 格式,其中 `<hash>` 为 64 位小写十六进制字符串,`<filename>` 为对应压缩包文件名,中间以两个空格分隔
|
||||
|
||||
#### Scenario: 校验和正确性
|
||||
- **WHEN** 使用 `sha256sum -c` 命令验证 `.sha256` 文件
|
||||
- **THEN** 校验 MUST 通过
|
||||
|
||||
### Requirement: 产出物目录结构
|
||||
Release 产出物 SHALL 按类型分目录存放在 `dist/release/` 下。
|
||||
|
||||
#### Scenario: 产出物目录布局
|
||||
- **WHEN** release 流程成功完成
|
||||
- **THEN** `dist/release/binaries/` SHALL 包含所有裸二进制文件,`dist/release/packages/` SHALL 包含所有压缩包和校验和文件
|
||||
|
||||
#### Scenario: 清理命令覆盖
|
||||
- **WHEN** 开发者运行 `bun run clean`
|
||||
- **THEN** 系统 SHALL 清理 `dist/release/` 目录
|
||||
|
||||
### Requirement: Release 报告
|
||||
Release 流程完成时 SHALL 输出构建报告,包含各产出物的文件大小。
|
||||
|
||||
#### Scenario: 成功构建报告
|
||||
- **WHEN** release 流程成功完成
|
||||
- **THEN** 系统 SHALL 输出每个裸二进制和压缩包的文件路径和大小
|
||||
|
||||
#### Scenario: 失败构建报告
|
||||
- **WHEN** release 流程失败
|
||||
- **THEN** 系统 SHALL 输出失败的具体步骤和目标平台
|
||||
@@ -1,327 +0,0 @@
|
||||
## 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,313 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义基于 SQLite 的拨测数据持久化存储:targets 同步(含分组信息)、check_results 追加写入、Dashboard 和 Metrics 数据查询支持、延迟百分位取数、时间范围和分页查询、索引与聚合查询、批量查询方法(getLatestChecksMap、getAllTargetStats、getAllRecentSamples)、N+1 查询优化,以及 prepared statement 使用 query() 缓存。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: SQLite 数据库初始化
|
||||
系统 SHALL 使用 Bun 内置 `bun:sqlite` 模块在配置的数据目录下创建 SQLite 数据库文件,并以 WAL 模式运行。数据库 schema MUST 支持 typed checker target 和结构化检查结果,targets 表 MUST 包含 `grp` 列存储分组信息、`active` 列标记活跃状态,且 targets 表的 `name` 和 `description` 列 MUST 允许 NULL。check_results 表的外键约束 SHALL 使用 `ON DELETE RESTRICT`。
|
||||
|
||||
#### Scenario: 首次启动创建数据库
|
||||
- **WHEN** 指定的数据目录下不存在数据库文件
|
||||
- **THEN** 系统 SHALL 创建数据库文件并初始化 targets 表(含 active INTEGER NOT NULL DEFAULT 1 列)和 check_results 表(外键约束为 ON DELETE RESTRICT),check_results 表包含 id(INTEGER PRIMARY KEY AUTOINCREMENT)、target_id(TEXT NOT NULL)、timestamp(TEXT NOT NULL)、matched(INTEGER NOT NULL)、duration_ms(REAL)、observation(TEXT)、failure(TEXT),不包含 status_detail 列,不包含 success 列
|
||||
|
||||
#### Scenario: targets name 列允许 NULL
|
||||
- **WHEN** 系统首次创建 targets 表
|
||||
- **THEN** targets.name 列 SHALL 允许存储 NULL
|
||||
|
||||
#### Scenario: 数据目录不存在
|
||||
- **WHEN** 配置的数据目录路径不存在
|
||||
- **THEN** 系统 SHALL 自动创建该目录
|
||||
|
||||
#### Scenario: 数据库已存在时启动
|
||||
- **WHEN** 数据库文件已存在
|
||||
- **THEN** 系统 SHALL 直接打开数据库,不重新建表
|
||||
|
||||
#### Scenario: 外键约束
|
||||
- **THEN** 系统 SHALL 启用 `PRAGMA foreign_keys = ON`
|
||||
|
||||
#### Scenario: 级联删除改为限制删除
|
||||
- **THEN** check_results 表的外键约束 SHALL 使用 `ON DELETE RESTRICT`,确保删除 target 时数据库层面阻止操作而非级联删除关联记录
|
||||
|
||||
### Requirement: targets 表同步
|
||||
系统 SHALL 在启动时将 YAML 配置中的目标列表同步到 SQLite targets 表,并持久化 target 类型、展示名称元信息、展示摘要、领域配置、调度配置、分组信息和目标说明。`targets.expect` 列当前实现写入 NULL,不持久化 expect 快照。配置中不存在的 target SHALL 被标记为非活跃而非删除。系统不需要保存原始用户输入的 Authoring expect 写法;`targets.expect` MUST NOT 被用作恢复用户 YAML 的数据源。
|
||||
|
||||
#### Scenario: 首次同步目标
|
||||
- **WHEN** 数据库为空且 YAML 中定义了 N 个 typed target
|
||||
- **THEN** 系统 SHALL 将所有目标插入 targets 表,包含 name、description、type、target、config、interval_ms、timeout_ms、expect、grp 和 active=1,其中 name 和 description 均可为 NULL,expect 列写入 NULL
|
||||
|
||||
#### Scenario: 配置变更后重新同步
|
||||
- **WHEN** YAML 配置发生变更(新增、删除或修改目标)后重启
|
||||
- **THEN** 系统 SHALL 根据 id 字段匹配:新增的插入(active=1)、删除的设置 active=0(不删除行)、修改的更新(含 name、description 和 grp 字段),已存在的目标 SHALL 设置 active=1
|
||||
|
||||
#### Scenario: 配置中移除目标
|
||||
- **WHEN** YAML 配置中移除了某个 target,该 target 在数据库中存在且 active=1
|
||||
- **THEN** 系统 SHALL 将该 target 的 active 设置为 0,保留该行及所有关联的 check_results
|
||||
|
||||
#### Scenario: 配置中恢复已移除目标
|
||||
- **WHEN** YAML 配置中重新添加了之前移除的 target(数据库中 active=0)
|
||||
- **THEN** 系统 SHALL 将该 target 的 active 设置为 1,并更新其他字段,历史 check_results 保留不变
|
||||
|
||||
#### Scenario: 未配置 name
|
||||
- **WHEN** YAML target 未配置 `name`
|
||||
- **THEN** targets 表 SHALL 将该目标的 name 存储为 NULL
|
||||
|
||||
#### Scenario: name 显式 null
|
||||
- **WHEN** YAML target 配置 `name: null`
|
||||
- **THEN** targets 表 SHALL 将该目标的 name 存储为 NULL
|
||||
|
||||
#### Scenario: 未配置 description
|
||||
- **WHEN** YAML target 未配置 `description`
|
||||
- **THEN** targets 表 SHALL 将该目标的 description 存储为 NULL
|
||||
|
||||
#### Scenario: description 显式 null
|
||||
- **WHEN** YAML target 配置 `description: null`
|
||||
- **THEN** targets 表 SHALL 将该目标的 description 存储为 NULL
|
||||
|
||||
#### Scenario: expect 列不保存原始 Authoring 写法
|
||||
- **WHEN** target 配置 `expect.body: [{json: {path: "$.status"}}]` 和 `expect.durationMs: 1000`
|
||||
- **THEN** targets 表的 expect 列 MUST NOT 保存原始 Authoring JSON 中的 `durationMs: 1000` 简写,当前实现写入 NULL
|
||||
|
||||
#### Scenario: 未配置 expect 写入 NULL
|
||||
- **WHEN** target 未配置任何 expect
|
||||
- **THEN** targets 表的 expect 列 SHALL 写入 NULL,即使 Resolved expect 中存在 checker 默认状态语义
|
||||
|
||||
### Requirement: check_results 表追加写入
|
||||
系统 SHALL 将每次检查结果追加写入 check_results 表,不更新或删除已有记录。
|
||||
|
||||
#### Scenario: 写入检查结果
|
||||
- **WHEN** 一次 checker 执行完成
|
||||
- **THEN** 系统 SHALL 插入一条包含 target_id、timestamp、matched、duration_ms、observation、failure 的记录,其中 observation 使用 JSON.stringify 序列化为 TEXT
|
||||
|
||||
#### Scenario: 查询检查结果
|
||||
- **WHEN** 系统查询 latest check 或历史 check_results
|
||||
- **THEN** 存储层 SHALL 返回 observation 字段而非 status_detail 字段,供 API 序列化层反序列化并构造 detail
|
||||
|
||||
#### Scenario: 写入结构化失败信息
|
||||
- **WHEN** checker 执行失败或 expect 不匹配
|
||||
- **THEN** 系统 SHALL 将首个失败原因序列化写入 failure 字段
|
||||
|
||||
### Requirement: 时间范围查询索引
|
||||
系统 SHALL 在 check_results 表上创建 (target_id, timestamp) 复合索引,加速按目标和时间范围的查询。
|
||||
|
||||
#### Scenario: 查询某目标的历史记录
|
||||
- **WHEN** 查询指定 target_id 的最近 N 条记录
|
||||
- **THEN** 系统 SHALL 使用索引快速定位,无需全表扫描
|
||||
|
||||
### Requirement: 目标列表按分组排序
|
||||
系统 SHALL 保证 targets 查询结果按分组排序返回,且仅返回活跃目标。
|
||||
|
||||
#### Scenario: 分组排序查询
|
||||
- **WHEN** 查询所有 targets
|
||||
- **THEN** 结果 SHALL 仅返回 active=1 的目标,将 "default" 分组目标排在首位,其余分组按 YAML 配置中首次出现的顺序(即 id 自增顺序)排列
|
||||
|
||||
### Requirement: 聚合查询支持
|
||||
数据存储 SHALL 支持按时间段获取指标计算所需数据,用于后端应用层计算可用率、平均耗时、延迟范围、趋势分桶和可靠性指标。
|
||||
|
||||
#### Scenario: 轻数据库计算边界
|
||||
- **WHEN** 实现指标相关数据查询
|
||||
- **THEN** 数据库 SHALL 主要负责存储、过滤、排序、分页、LIMIT 和标准 SQL 基础聚合,业务指标语义 SHALL 在后端应用层计算
|
||||
|
||||
#### Scenario: 可使用的基础 SQL 聚合
|
||||
- **WHEN** 查询需要减少返回数据量
|
||||
- **THEN** 系统 MAY 使用标准 SQL 的 COUNT、SUM(CASE)、AVG、MIN、MAX、GROUP BY 等基础能力
|
||||
|
||||
#### Scenario: 避免数据库承载业务语义
|
||||
- **WHEN** 实现状态翻转、故障段、MTTR、最长故障、连续状态、百分位或趋势分桶
|
||||
- **THEN** 系统 SHALL 在后端应用层实现这些规则,不依赖 SQLite 专有函数或复杂窗口函数承载业务语义
|
||||
|
||||
#### Scenario: UP/DOWN 判定
|
||||
- **WHEN** 系统需要判定目标当前状态
|
||||
- **THEN** 系统 SHALL 基于 latestCheck.matched 判定目标 UP 或 DOWN:matched=true 为 UP,matched=false 为 DOWN
|
||||
|
||||
### Requirement: Dashboard 数据查询支持
|
||||
ProbeStore SHALL 提供 Dashboard 聚合响应所需的批量取数能力,且所有查询 SHALL 仅涉及活跃 target。
|
||||
|
||||
#### Scenario: 批量获取最新检查
|
||||
- **WHEN** Dashboard API 需要计算当前 up/down 和 lastCheckTime
|
||||
- **THEN** Store SHALL 支持批量获取每个活跃 target 的最新检查记录,避免 N+1 查询
|
||||
|
||||
#### Scenario: 批量获取窗口统计基础数据
|
||||
- **WHEN** Dashboard API 需要计算各 target 在指定 window 内的 totalChecks、upChecks、downChecks 和 availability
|
||||
- **THEN** Store SHALL 支持按 target_id 批量返回指定时间窗口内的基础计数数据,仅包含活跃 target
|
||||
|
||||
#### Scenario: 批量获取最近样本
|
||||
- **WHEN** Dashboard API 需要展示 recentSamples 和计算 capped currentStreak
|
||||
- **THEN** Store SHALL 支持批量获取每个活跃 target 最近 recentLimit 条检查记录,按 target_id 分组且每组按 timestamp 降序排列
|
||||
|
||||
#### Scenario: 获取 Dashboard 异常事件序列
|
||||
- **WHEN** Dashboard API 需要计算 incidents
|
||||
- **THEN** Store SHALL 支持获取指定时间窗口内所有活跃 target 的 `{ target_id, timestamp, matched }` 序列,按 target_id 和 timestamp 升序排列,供后端应用层计算状态翻转
|
||||
|
||||
### Requirement: 单目标指标取数支持
|
||||
ProbeStore SHALL 提供单目标 metrics 响应所需的取数能力。仅活跃 target 的指标 SHALL 可查询。
|
||||
|
||||
#### Scenario: 获取目标检查点序列
|
||||
- **WHEN** Metrics API 需要计算趋势分桶、故障段、MTTR、最长故障、故障次数和连续状态
|
||||
- **THEN** Store SHALL 支持获取指定活跃 target 在 from 到 to 时间范围内的 `{ timestamp, matched, duration_ms }` 数组,按 timestamp 升序排列
|
||||
|
||||
#### Scenario: 目标不活跃
|
||||
- **WHEN** 查询 inactive target 的指标
|
||||
- **THEN** Store SHALL 返回 null(getTargetById 不匹配 active=1 的条件)
|
||||
|
||||
#### Scenario: 无检查记录
|
||||
- **WHEN** 时间窗口内无检查记录
|
||||
- **THEN** Store SHALL 返回空数组
|
||||
|
||||
### Requirement: 目标延迟百分位取数
|
||||
ProbeStore SHALL 提供 `getTargetDurations(targetId, from, to)` 方法,返回时间窗口内所有成功检查的 duration_ms 数组。
|
||||
|
||||
#### Scenario: 获取延迟数据
|
||||
- **WHEN** 调用 `getTargetDurations(targetId, from, to)`
|
||||
- **THEN** 系统 SHALL 返回该目标在时间范围内所有 matched=1 且 duration_ms 不为 null 的 duration_ms 值数组
|
||||
|
||||
#### Scenario: 延迟数据排序
|
||||
- **WHEN** 获取延迟数据
|
||||
- **THEN** 返回数组 SHALL 按 duration_ms 升序排列,供后端应用层计算 P95/P99
|
||||
|
||||
#### Scenario: 无成功检查
|
||||
- **WHEN** 时间窗口内无 matched=1 且 duration_ms 不为 null 的记录
|
||||
- **THEN** 系统 SHALL 返回空数组
|
||||
|
||||
### Requirement: 历史记录时间范围和分页查询
|
||||
系统 SHALL 继续支持按时间范围筛选并分页查询历史记录。
|
||||
|
||||
#### Scenario: 按时间范围筛选历史记录
|
||||
- **WHEN** 查询指定 target 在 from 到 to 时间范围内的历史记录
|
||||
- **THEN** 系统 SHALL 返回该时间范围内的记录,按 timestamp 降序排列
|
||||
|
||||
#### Scenario: 分页查询历史记录
|
||||
- **WHEN** 查询指定 page 和 pageSize 的历史记录
|
||||
- **THEN** 系统 SHALL 返回对应页的数据和总记录数
|
||||
|
||||
### Requirement: 目标展示摘要持久化
|
||||
数据存储 SHALL 为每个 target 持久化一个领域无关的展示摘要字段 `target`。
|
||||
|
||||
#### Scenario: HTTP target 展示摘要
|
||||
- **WHEN** 同步 HTTP target
|
||||
- **THEN** targets.target SHALL 存储该 target 的 URL
|
||||
|
||||
#### Scenario: cmd target 展示摘要
|
||||
- **WHEN** 同步 cmd target
|
||||
- **THEN** targets.target SHALL 存储由 exec 和 args 组成的命令摘要
|
||||
|
||||
#### Scenario: HTTP target config 序列化
|
||||
- **WHEN** 同步 HTTP target
|
||||
- **THEN** targets.config SHALL 存储 JSON,包含 url、method、headers、body、maxBodyBytes、ignoreSSL、maxRedirects
|
||||
|
||||
#### Scenario: cmd target config 序列化
|
||||
- **WHEN** 同步 cmd target
|
||||
- **THEN** targets.config SHALL 存储 JSON,包含 exec、args、cwd、env、maxOutputBytes
|
||||
|
||||
### Requirement: 数据清理方法
|
||||
ProbeStore SHALL 提供 `prune(retentionMs: number)` 方法,删除超过保留时长的历史检查结果并返回删除行数,同时清理已无关联检查结果的非活跃目标行。
|
||||
|
||||
#### Scenario: 清理过期数据
|
||||
- **WHEN** 调用 `prune(604800000)`(7 天毫秒数)
|
||||
- **THEN** 系统 SHALL 删除 `check_results` 表中 `timestamp` 早于当前时间减去 604800000 毫秒的所有记录,并返回实际删除的行数
|
||||
|
||||
#### Scenario: 无过期数据
|
||||
- **WHEN** 调用 `prune()` 但所有记录都在保留期内
|
||||
- **THEN** 系统 SHALL 返回 0,不删除任何记录
|
||||
|
||||
#### Scenario: 清理不影响保留期内数据
|
||||
- **WHEN** 调用 `prune()` 且存在保留期内和保留期外的记录
|
||||
- **THEN** 系统 SHALL 仅删除保留期外的记录,保留期内的记录 SHALL 不受影响
|
||||
|
||||
#### Scenario: 清理空壳非活跃目标
|
||||
- **WHEN** prune 执行完毕后,存在 active=0 的 target 且该 target 在 check_results 表中无任何关联记录
|
||||
- **THEN** 系统 SHALL 删除该空壳 target 行
|
||||
|
||||
#### Scenario: 非活跃目标仍有历史数据时不清理
|
||||
- **WHEN** 存在 active=0 的 target 但该 target 在 check_results 表中仍有关联记录
|
||||
- **THEN** 系统 SHALL NOT 删除该 target 行
|
||||
|
||||
#### 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,157 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 db 类型拨测目标的配置格式、执行逻辑、expect 断言规则和启动期校验。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: db target 配置
|
||||
系统 SHALL 支持 `type: db` 的 target 配置,通过 `db.url` 描述数据库连接字符串(遵循 Bun SQL 支持的格式),通过可选的 `db.query` 描述待执行的 SQL 语句。
|
||||
|
||||
#### Scenario: 解析仅连接的 db target
|
||||
- **WHEN** YAML 中 target 配置 `type: db` 和 `db.url: "postgres://user:pass@localhost:5432/mydb"`,未配置 `db.query`
|
||||
- **THEN** 系统 SHALL 将其解析为 db checker,仅执行连通性检测
|
||||
|
||||
#### Scenario: 解析带查询的 db target
|
||||
- **WHEN** YAML 中 target 配置 `type: db`、`db.url: "mysql://user:pass@host:3306/app"` 和 `db.query: "SELECT count(*) as cnt FROM users"`
|
||||
- **THEN** 系统 SHALL 将其解析为 db checker,执行连通性检测后执行指定 SQL 并进入 expect 校验
|
||||
|
||||
#### Scenario: db target 缺少 url
|
||||
- **WHEN** YAML 中 target 配置 `type: db` 但缺少 `db.url`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 db.url 字段
|
||||
|
||||
#### Scenario: db.url 为空字符串
|
||||
- **WHEN** YAML 中 target 配置 `db.url: ""`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,并提示 db.url 不能为空
|
||||
|
||||
#### Scenario: db.query 为空字符串
|
||||
- **WHEN** YAML 中 target 配置 `db.query: ""`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,并提示 db.query 不能为空字符串(如不需要查询则不配置该字段)
|
||||
|
||||
#### Scenario: db 分组未知字段失败
|
||||
- **WHEN** YAML 中 db target 的 `db` 分组包含 `timeout: 5` 等未知字段
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 db 分组包含未知字段
|
||||
|
||||
#### Scenario: SQLite 连接字符串
|
||||
- **WHEN** YAML 中 target 配置 `db.url: "sqlite:///data/app.db"`
|
||||
- **THEN** 系统 SHALL 将其解析为 db checker,使用 SQLite 文件数据库
|
||||
|
||||
#### Scenario: url 格式由 Bun 运行时校验
|
||||
- **WHEN** YAML 中 target 配置 `db.url` 为 Bun 不支持的格式
|
||||
- **THEN** 系统 SHALL 在执行阶段捕获连接错误并作为 phase="connect" 的 failure 返回,而非在启动期校验 URL 格式
|
||||
|
||||
### Requirement: db checker 执行
|
||||
系统 SHALL 按 db target 配置连接数据库并执行查询,每次执行都新建连接并在完成后关闭。连接能力本身作为监控指标。
|
||||
|
||||
#### Scenario: 仅连接测试成功
|
||||
- **WHEN** db target 未配置 `db.query` 且数据库连接成功
|
||||
- **THEN** 系统 SHALL 内部执行 `SELECT 1` 验证连通性,记录 `matched=true` 和 `durationMs`
|
||||
|
||||
#### Scenario: 连接失败
|
||||
- **WHEN** db target 的数据库连接失败(网络不通、认证错误、数据库不存在等)
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,failure 的 phase 为 `"connect"`,message 包含可读错误信息
|
||||
|
||||
#### Scenario: 查询执行成功
|
||||
- **WHEN** db target 配置了 `db.query` 且 SQL 执行成功返回结果集
|
||||
- **THEN** 系统 SHALL 记录 `durationMs`(从连接开始到查询完成),并进入 expect 校验
|
||||
|
||||
#### Scenario: 查询执行失败
|
||||
- **WHEN** db target 配置了 `db.query` 且 SQL 执行报错(语法错误、权限不足、表不存在等)
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,failure 的 phase 为 `"query"`,message 包含数据库返回的错误信息
|
||||
|
||||
#### Scenario: 执行超时
|
||||
- **WHEN** db target 在 timeout 时间内未完成(连接或查询)
|
||||
- **THEN** 系统 SHALL 关闭连接,记录 `matched=false`,failure 的 phase 为 `"connect"` 或 `"query"`(取决于超时发生的阶段),message 包含超时信息
|
||||
|
||||
#### Scenario: 每次执行新建连接
|
||||
- **WHEN** db target 被引擎调度执行
|
||||
- **THEN** 系统 SHALL 创建新的 SQL 连接实例(max: 1),执行完成后立即关闭连接(close timeout: 0)
|
||||
|
||||
#### Scenario: 使用 unsafe 执行用户 SQL
|
||||
- **WHEN** db target 配置了 `db.query`
|
||||
- **THEN** 系统 SHALL 使用 `sql.unsafe(query)` 执行用户配置的 SQL 文本,不限制 SQL 类型
|
||||
|
||||
#### Scenario: 响应 abort signal
|
||||
- **WHEN** 引擎注入的 `ctx.signal` 被 abort
|
||||
- **THEN** 系统 SHALL 立即关闭数据库连接
|
||||
|
||||
### Requirement: db expect 校验
|
||||
系统 SHALL 支持 db 专用 expect,包括 `durationMs`、`rowCount`、`rows` 和 `result`,按 durationMs、rowCount、rows、result 的阶段顺序快速失败。`durationMs` 和 `rowCount` SHALL 使用共享 `RawValueExpectation` 输入,并在 resolve 阶段转换为运行期 `ValueExpectation`。`rows` SHALL 保留按行索引匹配列值的语义,Raw 类型为 `Array<RawKeyedExpectations>`(外层数组按行索引,内层每个元素表达该行的列值断言),Resolved 类型为 `Array<KeyedExpectations>`。每个行规则中列值 primitive 字面量等价于 `{equals: <literal>}`。`result` MUST 使用共享 `RawContentExpectations` 数组输入,并在运行期以 `ContentExpectations` 对查询结果对象 `{ rows, rowCount }` 执行断言。
|
||||
|
||||
#### Scenario: durationMs 校验
|
||||
- **WHEN** db target 配置 `expect.durationMs: {lte: 3000}` 且实际执行耗时 4000ms
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 phase 为 `duration`
|
||||
|
||||
#### Scenario: rowCount 校验通过
|
||||
- **WHEN** db target 配置 `expect.rowCount: { gte: 1 }` 且查询返回 5 行
|
||||
- **THEN** 系统 SHALL 判定 rowCount 阶段通过,继续后续 expect 阶段
|
||||
|
||||
#### Scenario: rowCount 校验失败
|
||||
- **WHEN** db target 配置 `expect.rowCount: { gte: 1 }` 且查询返回 0 行
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 phase 为 `rowCount`,path 为 `rowCount`,expected 为 `{ gte: 1 }`,actual 为 0
|
||||
|
||||
#### Scenario: rows 按索引匹配列值 matcher 形式
|
||||
- **WHEN** db target 配置 `expect.rows: [{ cnt: { gte: 100 } }]` 且查询首行 cnt 列值为 50
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 phase 为 `row`,path 为 `rows[0].cnt`
|
||||
|
||||
#### Scenario: rows 按索引匹配列值字面量形式
|
||||
- **WHEN** db target 配置 `expect.rows: [{ status: "active" }]` 且查询首行 status 列值为 `"active"`
|
||||
- **THEN** 系统 SHALL 在 resolve 阶段将该列值解析为 `{equals: "active"}` 并判定该行该列通过
|
||||
|
||||
#### Scenario: rows 只检查声明的列
|
||||
- **WHEN** db target 配置 `expect.rows: [{ cnt: { gte: 1 } }]` 且查询首行包含 cnt、name、age 三列
|
||||
- **THEN** 系统 SHALL 仅检查 cnt 列,忽略 name 和 age 列
|
||||
|
||||
#### Scenario: rows 结果行数不足
|
||||
- **WHEN** db target 配置 `expect.rows` 包含 3 个元素但查询仅返回 2 行
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 phase 为 `row`,message 说明结果行数不足
|
||||
|
||||
#### Scenario: result JSONPath 校验
|
||||
- **WHEN** db target 查询返回首行 `{status: "active"}` 且配置 `expect.result: [{json: {path: "$.rows[0].status", equals: "active"}}]`
|
||||
- **THEN** 系统 SHALL 基于 `{rows, rowCount}` 结果对象执行 JSONPath,并判定 result 阶段通过
|
||||
|
||||
#### Scenario: result rowCount 校验
|
||||
- **WHEN** db target 查询返回 2 行且配置 `expect.result: [{json: {path: "$.rowCount", equals: 2}}]`
|
||||
- **THEN** 系统 SHALL 判定 result 阶段通过
|
||||
|
||||
#### Scenario: 无 query 时结果类 expect 被忽略
|
||||
- **WHEN** db target 未配置 `db.query` 但配置了 `expect.rowCount`、`expect.rows` 或 `expect.result`
|
||||
- **THEN** 系统 SHALL 忽略这些查询结果断言(仅 `durationMs` 生效)
|
||||
|
||||
#### Scenario: 快速失败顺序
|
||||
- **WHEN** db target 同时配置 durationMs、rowCount、rows 和 result
|
||||
- **THEN** 系统 SHALL 按 durationMs → rowCount → rows → result 顺序校验,任一阶段失败立即返回
|
||||
|
||||
### Requirement: db checker 启动期配置校验
|
||||
系统 SHALL 在启动期对 db checker 的配置契约和语义执行严格校验。Db target 的 `db` 分组 SHALL 只允许 `url` 和 `query` 字段。Db expect SHALL 只允许 `durationMs`、`rowCount`、`rows` 和 `result` 字段。未知字段、非法 ValueExpectation、非法 ContentExpectations、非法 regex 和 ReDoS 风险正则 MUST 导致启动期配置错误。语义校验 MUST NOT 修改 Raw db expect 输入。
|
||||
|
||||
#### Scenario: db expect durationMs 非法
|
||||
- **WHEN** YAML 中 db target 配置 `expect.durationMs` 不是合法 `RawValueExpectation`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.durationMs 格式错误
|
||||
|
||||
#### Scenario: db expect rowCount 非法
|
||||
- **WHEN** YAML 中 db target 配置 `expect.rowCount` 不是合法 `RawValueExpectation`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.rowCount 格式错误
|
||||
|
||||
#### Scenario: db expect rows 非法
|
||||
- **WHEN** YAML 中 db target 配置 `expect.rows` 不是对象数组
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.rows 必须为对象数组
|
||||
|
||||
#### Scenario: db expect rows 元素列值非法
|
||||
- **WHEN** YAML 中 db target 配置 `expect.rows: [{ cnt: { foo: 1 } }]`,其中 foo 不是合法 matcher
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 rows 中包含未知 matcher
|
||||
|
||||
#### Scenario: db expect rows 对象值必须显式 equals
|
||||
- **WHEN** YAML 中 db target 配置 `expect.rows: [{ payload: { status: "ok" } }]` 且 payload 值不是合法 matcher 对象
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示对象值必须显式写成 `{equals: {status: "ok"}}`
|
||||
|
||||
#### Scenario: db expect result 非法
|
||||
- **WHEN** YAML 中 db target 配置 `expect.result` 不是合法 ContentExpectations 数组
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.result 格式错误
|
||||
|
||||
#### Scenario: db expect 未知字段失败
|
||||
- **WHEN** YAML 中 db target 的 expect 包含 `status: [200]`、`maxDurationMs: 1000` 或其他非 db expect 字段
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
|
||||
|
||||
#### Scenario: db expect rows 中 regex 正则非法
|
||||
- **WHEN** YAML 中 db target 配置 `expect.rows: [{ name: { regex: "[invalid" } }]`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,而不是延迟到运行期抛错
|
||||
@@ -1,301 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义共享 expect 断言规则系统的核心概念和基础设施:ValueMatcher 统一匹配器、ContentExpectations 内容断言数组、KeyedExpectations 键控断言数组、HTTP 场景特有规则(状态码范围匹配、body 运行期失败结构化)、以及相关的启动期校验和失败路径规范。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: ValueMatcher 统一匹配器
|
||||
系统 SHALL 提供共享 `ValueMatcher` 作为所有非状态类 value expectation 的基础匹配结构。`ValueMatcher` SHALL 支持 `equals`、`contains`、`regex`、`exists`、`empty`、`gt`、`gte`、`lt` 和 `lte` 字段。`equals` MUST 支持任意 JSON value,并使用深度相等比较。`contains` 和 `regex` SHALL 将实际值转换为字符串后匹配。`gt`、`gte`、`lt` 和 `lte` SHALL 将实际值转换为有限数字后比较,无法转换为有限数字时 SHALL 判定不匹配。一个 `ValueMatcher` 对象包含多个 matcher 字段时,系统 SHALL 要求全部 matcher 均通过。
|
||||
|
||||
所有类型为 Authoring `RawValueExpectation` 的 expect 字段 SHALL 同时接受 primitive 原始值(string / number / boolean / null)作为简写形式。原始值简写 SHALL 等价于 `{ equals: value }`。系统 SHALL 在 Normalized 阶段将 primitive 原始值归一化为 `{ equals: value }` 对象形式。Normalized Config、checker 语义校验、checker.resolve() 和运行期逻辑 SHALL 仅处理 `ValueMatcher` 对象形式。数组和对象 MUST NOT 作为原始值简写;需要对数组或对象执行 equals 匹配时,配置 MUST 显式写成 `{ equals: value }`。
|
||||
|
||||
#### Scenario: equals 匹配对象
|
||||
- **WHEN** 实际值为 `{status: "ok", count: 1}` 且 matcher 为 `{equals: {status: "ok", count: 1}}`
|
||||
- **THEN** 系统 SHALL 使用深度相等判定该 matcher 通过
|
||||
|
||||
#### Scenario: contains 字符串化匹配
|
||||
- **WHEN** 实际值为 `"service ready"` 且 matcher 为 `{contains: "ready"}`
|
||||
- **THEN** 系统 SHALL 判定该 matcher 通过
|
||||
|
||||
#### Scenario: 数字范围组合匹配
|
||||
- **WHEN** 实际值为 `50` 且 matcher 为 `{gte: 0, lte: 100}`
|
||||
- **THEN** 系统 SHALL 判定该 matcher 通过
|
||||
|
||||
#### Scenario: 多 matcher 快速失败
|
||||
- **WHEN** 实际值为 `"healthy"` 且 matcher 为 `{contains: "health", regex: "^ready$"}`
|
||||
- **THEN** 系统 SHALL 在任一 matcher 不满足时判定该 matcher 对象不通过
|
||||
|
||||
#### Scenario: 字符串原始值简写在 Normalized 阶段等价 equals
|
||||
- **WHEN** Authoring Config 中 expect 字段配置为 `finishReason: "stop"`
|
||||
- **THEN** Normalized Config SHALL 将 `"stop"` 归一化为 `{equals: "stop"}`,运行期实际值为 `"stop"` 时判定通过
|
||||
|
||||
#### Scenario: 数字原始值简写在 Normalized 阶段等价 equals
|
||||
- **WHEN** Authoring Config 中 expect 字段配置为 `rowCount: 1`
|
||||
- **THEN** Normalized Config SHALL 将 `1` 归一化为 `{equals: 1}`,运行期实际值为 `1` 时判定通过
|
||||
|
||||
#### Scenario: 布尔原始值简写在 Normalized 阶段等价 equals
|
||||
- **WHEN** Authoring Config 中 RawValueExpectation 类型字段值为 `true`
|
||||
- **THEN** Normalized Config SHALL 将 `true` 归一化为 `{equals: true}`,运行期实际值为 `true` 时判定通过
|
||||
|
||||
#### Scenario: null 原始值简写在 Normalized 阶段等价 equals
|
||||
- **WHEN** Authoring Config 中 RawValueExpectation 类型字段值为 `null`
|
||||
- **THEN** Normalized Config SHALL 将 `null` 归一化为 `{equals: null}`,运行期实际值为 `null` 时判定通过
|
||||
|
||||
#### Scenario: 原始值简写不匹配
|
||||
- **WHEN** expect 字段配置为 `finishReason: "stop"` 且实际值为 `"error"`
|
||||
- **THEN** 系统 SHALL 判定不通过并生成 mismatch failure
|
||||
|
||||
### Requirement: ValueMatcher 启动期校验
|
||||
系统 SHALL 在启动期对所有 normalized `ValueMatcher` 字段执行严格的类型和语义校验。Authoring primitive 简写 MUST 在语义校验前由 Normalized 阶段转换为 `{equals: value}`,因此语义 validator SHALL 校验 `ValueMatcher` 对象而不是 Raw primitive 输入。当输入为 `ValueMatcher` 对象时,`contains` MUST 为 string。`equals` MAY 为任意 JSON value。`exists` 和 `empty` MUST 为 boolean。`gt`、`gte`、`lt` 和 `lte` MUST 为有限数字(`Number.isFinite`)。`regex` MUST 为可编译的 string pattern,并通过 ReDoS 风险校验。`ValueMatcher` 对象 MUST 至少包含一个合法 matcher 字段,空对象 `{}` SHALL 导致启动期配置错误。`ValueMatcher` 对象 MUST NOT 包含未知字段,任何不属于 `equals`、`contains`、`regex`、`exists`、`empty`、`gt`、`gte`、`lt`、`lte` 的字段 SHALL 导致启动期配置错误。
|
||||
|
||||
#### Scenario: 空 matcher 对象被拒绝
|
||||
- **WHEN** YAML 配置中任一 matcher 对象为空 `{}`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示 matcher 必须包含至少一个合法字段
|
||||
|
||||
#### Scenario: 未知 matcher 字段被拒绝
|
||||
- **WHEN** YAML 配置中任一 matcher 对象包含 `foo: "bar"` 等未知字段
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该字段未知
|
||||
|
||||
#### Scenario: 数值 matcher 非有限数字被拒绝
|
||||
- **WHEN** YAML 配置中任一 matcher 的 `gt`、`gte`、`lt` 或 `lte` 值为 `NaN`、`Infinity` 或非数字类型
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示数值 matcher 必须为有限数字
|
||||
|
||||
#### Scenario: 布尔 matcher 非布尔值被拒绝
|
||||
- **WHEN** YAML 配置中任一 matcher 的 `exists` 或 `empty` 值不是布尔类型
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该字段必须为布尔值
|
||||
|
||||
#### Scenario: 字符串原始值在 Normalized schema 中被拒绝
|
||||
- **WHEN** Normalized schema 校验 RawValueExpectation 字段值为字符串 `"stop"`
|
||||
- **THEN** schema 校验 SHALL 失败,因为 Normalized Config 只接受 `ValueMatcher` 对象
|
||||
|
||||
#### Scenario: 数字原始值在 Normalized schema 中被拒绝
|
||||
- **WHEN** Normalized schema 校验 RawValueExpectation 字段值为数字 `5000`
|
||||
- **THEN** schema 校验 SHALL 失败,因为 Normalized Config 只接受 `ValueMatcher` 对象
|
||||
|
||||
#### Scenario: null 原始值在 Normalized schema 中被拒绝
|
||||
- **WHEN** Normalized schema 校验 RawValueExpectation 字段值为 `null`
|
||||
- **THEN** schema 校验 SHALL 失败,因为 Normalized Config 只接受 `ValueMatcher` 对象
|
||||
|
||||
#### Scenario: 数组原始值被拒绝
|
||||
- **WHEN** YAML 配置中 RawValueExpectation 字段值为数组 `[1, 2]`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示必须为 primitive 原始值或 matcher 对象;如需数组 equals 匹配应写成 `{equals: [1, 2]}`
|
||||
|
||||
#### Scenario: 对象原始值必须显式 equals
|
||||
- **WHEN** YAML 配置中 RawValueExpectation 字段值为对象 `{foo: "bar"}`,且 `foo` 不是合法 matcher 字段
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示 `foo` 是未知 matcher;如需对象 equals 匹配应写成 `{equals: {foo: "bar"}}`
|
||||
|
||||
### Requirement: empty matcher 语义
|
||||
`empty: true` SHALL 在以下情况判定通过:实际值为 `null`、`undefined`、空字符串 `""`、空数组 `[]` 或空对象 `{}`。`empty: false` SHALL 在以上条件均不满足时判定通过。数字 `0` 和布尔 `false` SHALL NOT 被视为 empty。
|
||||
|
||||
#### Scenario: null 视为 empty
|
||||
- **WHEN** 实际值为 `null` 且 matcher 为 `{empty: true}`
|
||||
- **THEN** 系统 SHALL 判定该 matcher 通过
|
||||
|
||||
#### Scenario: 空字符串视为 empty
|
||||
- **WHEN** 实际值为 `""` 且 matcher 为 `{empty: true}`
|
||||
- **THEN** 系统 SHALL 判定该 matcher 通过
|
||||
|
||||
#### Scenario: 空数组视为 empty
|
||||
- **WHEN** 实际值为 `[]` 且 matcher 为 `{empty: true}`
|
||||
- **THEN** 系统 SHALL 判定该 matcher 通过
|
||||
|
||||
#### Scenario: 空对象视为 empty
|
||||
- **WHEN** 实际值为 `{}` 且 matcher 为 `{empty: true}`
|
||||
- **THEN** 系统 SHALL 判定该 matcher 通过
|
||||
|
||||
#### Scenario: 数字 0 不视为 empty
|
||||
- **WHEN** 实际值为 `0` 且 matcher 为 `{empty: true}`
|
||||
- **THEN** 系统 SHALL 判定该 matcher 不通过
|
||||
|
||||
#### Scenario: 布尔 false 不视为 empty
|
||||
- **WHEN** 实际值为 `false` 且 matcher 为 `{empty: true}`
|
||||
- **THEN** 系统 SHALL 判定该 matcher 不通过
|
||||
|
||||
### Requirement: exists 与其他 matcher 的组合语义
|
||||
当 `ValueMatcher` 同时包含 `exists: false` 和其他非存在性 matcher(如 `contains`、`regex`、`equals` 等)时,系统 SHALL 在启动期配置校验失败,提示 `exists: false` 不能与其他 matcher 组合使用。`exists: true` MAY 与其他 matcher 组合,语义为先确认存在再执行其他 matcher。
|
||||
|
||||
#### Scenario: exists false 与 contains 组合被拒绝
|
||||
- **WHEN** YAML 配置中 matcher 为 `{exists: false, contains: "foo"}`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示 `exists: false` 不能与其他 matcher 组合
|
||||
|
||||
#### Scenario: exists true 与 contains 组合允许
|
||||
- **WHEN** 实际值为 `"hello foo"` 且 matcher 为 `{exists: true, contains: "foo"}`
|
||||
- **THEN** 系统 SHALL 判定该 matcher 通过
|
||||
|
||||
### Requirement: regex 字段语义
|
||||
系统 SHALL 使用 `regex` 作为唯一正则 matcher 字段。`regex` 值 MUST 为可编译的字符串 pattern。运行期 SHALL 固定使用无 flags 的 `new RegExp(pattern).test(String(actual))` 执行匹配。系统 MUST NOT 支持旧 `match` 字段。系统 SHALL 在启动期对所有 `regex` pattern 执行可编译校验和 ReDoS 风险校验。
|
||||
|
||||
#### Scenario: regex 任意位置匹配
|
||||
- **WHEN** 实际值为 `"api status ok"` 且 matcher 为 `{regex: "status"}`
|
||||
- **THEN** 系统 SHALL 判定该 matcher 通过,因为无 flags 的 JavaScript 正则仍会搜索整个字符串中的第一次匹配
|
||||
|
||||
#### Scenario: regex 完整匹配由用户声明锚点
|
||||
- **WHEN** 实际值为 `"OK\n"` 且 matcher 为 `{regex: "^OK$"}`
|
||||
- **THEN** 系统 SHALL 判定该 matcher 不通过,因为系统 MUST NOT 默认启用 multiline flags
|
||||
|
||||
#### Scenario: match 字段启动失败
|
||||
- **WHEN** YAML 配置中任一 matcher 对象包含 `match: "ok"`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示 `match` 是未知字段或不支持字段
|
||||
|
||||
#### Scenario: regex ReDoS 风险启动失败
|
||||
- **WHEN** YAML 配置中任一 `regex` 为 `"(a+)+$"`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示正则存在 ReDoS 风险
|
||||
|
||||
### Requirement: ContentExpectations 内容断言数组
|
||||
系统 SHALL 提供共享 `ContentExpectations` 表达返回内容断言。Authoring `ContentExpectations` MUST 为有序数组,数组项 SHALL 为直接 `ValueMatcher`,或 `json`、`css`、`xpath` 三类 extractor expectation 之一。系统 SHALL 在 Normalized 阶段将 Authoring content DSL 解析为带 `kind` 字段的 Normalized `ContentExpectation` 执行计划,并按数组顺序执行全部 expectation,任一 expectation 失败时 SHALL 立即停止并返回该 expectation 的 failure。系统 MUST NOT 支持内容字段的非数组对象快捷写法。
|
||||
|
||||
#### Scenario: 直接 matcher 内容 expectation
|
||||
- **WHEN** 内容字段配置 `[{contains: "ready"}, {regex: "listening on \\d+"}]` 且原始内容同时满足两条 expectation
|
||||
- **THEN** 系统 SHALL 判定该内容字段通过
|
||||
|
||||
#### Scenario: 内容 expectation 数组快速失败
|
||||
- **WHEN** 内容字段配置三条 expectation 且第二条 expectation 失败
|
||||
- **THEN** 系统 SHALL 返回第二条 expectation 的 failure,并 MUST NOT 执行第三条 expectation
|
||||
|
||||
#### Scenario: 内容字段必须为数组
|
||||
- **WHEN** YAML 中内容字段配置为 `{contains: "ok"}` 而不是数组
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该内容字段必须为 expectation 数组
|
||||
|
||||
#### Scenario: Normalized content expectation 使用 kind
|
||||
- **WHEN** Authoring 内容字段包含直接 matcher、json extractor、css extractor 和 xpath extractor
|
||||
- **THEN** Normalized 阶段 SHALL 分别生成 `kind="value"`、`kind="json"`、`kind="css"` 和 `kind="xpath"` 的 `ContentExpectation`
|
||||
|
||||
### Requirement: ContentExpectation 互斥性约束
|
||||
一条 Raw `ContentExpectation` MUST 为直接 `ValueMatcher` 或恰好一个 extractor(`json`、`css`、`xpath` 之一)。系统 MUST NOT 允许同一条 Raw expectation 同时包含多个 extractor。直接 `ValueMatcher` expectation MUST NOT 包含 `json`、`css`、`xpath` 字段。系统 SHALL 在启动期对违反互斥性的 Raw expectation 报错。
|
||||
|
||||
#### Scenario: 多 extractor 被拒绝
|
||||
- **WHEN** YAML 中内容 expectation 为 `{json: {path: "$.a"}, css: {selector: "div"}}`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示一条 expectation 不能同时包含多个 extractor
|
||||
|
||||
#### Scenario: 直接 matcher 混入 extractor 被拒绝
|
||||
- **WHEN** YAML 中内容 expectation 为 `{contains: "ok", json: {path: "$.a"}}`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示直接 matcher 不能与 extractor 混用
|
||||
|
||||
### Requirement: 空 ContentExpectations 数组语义
|
||||
`ContentExpectations` 空数组 `[]` SHALL 被系统接受为合法配置。运行期空数组 SHALL 等价于无内容 expectation,即该内容字段的断言直接通过。
|
||||
|
||||
#### Scenario: 空 body 数组通过
|
||||
- **WHEN** HTTP target 配置 `expect.body: []` 且响应体为任意内容
|
||||
- **THEN** 系统 SHALL 判定 body 阶段通过
|
||||
|
||||
### Requirement: ContentExpectations 非字符串值序列化
|
||||
当 `ContentExpectations` 的观测源为非字符串值(如对象或数组)时,直接 `ValueMatcher` 的 `contains` 和 `regex` SHALL 先将值 JSON 序列化为字符串后匹配。`equals` SHALL 直接在原始结构化值上使用深度相等比较,不进行序列化。
|
||||
|
||||
#### Scenario: 对象序列化后 contains 匹配
|
||||
- **WHEN** ContentExpectations 观测源为 `{status: "ok"}` 且 expectation 为 `{contains: "ok"}`
|
||||
- **THEN** 系统 SHALL 将对象 JSON 序列化后执行 contains 匹配
|
||||
|
||||
#### Scenario: 对象 equals 不序列化
|
||||
- **WHEN** ContentExpectations 观测源为 `{status: "ok"}` 且 expectation 为 `{equals: {status: "ok"}}`
|
||||
- **THEN** 系统 SHALL 直接在结构化值上使用深度相等比较
|
||||
|
||||
### Requirement: ContentExpectations 提取器
|
||||
系统 SHALL 支持在 Authoring `ContentExpectations` 中使用 `json`、`css` 和 `xpath` extractor。`json.path` MUST 使用现有 JSONPath 子集。`css.selector` MUST 为非空字符串,并 MAY 配置 `attr` 提取属性值。`xpath.path` MUST 为非空字符串,并 SHALL 在启动期进行可编译校验。Extractor 内部 MAY 包含任意 `ValueMatcher` 字段。Extractor expectation 未配置任何 matcher 时,Normalized 阶段 SHALL 将其 matcher 物化为 `{ exists: true }`。
|
||||
|
||||
#### Scenario: json extractor 数字比较
|
||||
- **WHEN** 原始内容为 JSON 字符串 `{"count": 2}` 且 expectation 为 `{json: {path: "$.count", gte: 1}}`
|
||||
- **THEN** 系统 SHALL 解析 JSON、提取 `$.count` 并判定该 expectation 通过
|
||||
|
||||
#### Scenario: json extractor 存在性默认语义
|
||||
- **WHEN** 原始内容为 JSON 字符串 `{"user": {"id": null}}` 且 expectation 为 `{json: {path: "$.user.id"}}`
|
||||
- **THEN** Normalized 阶段 SHALL 将该 expectation 的 matcher 物化为 `{exists: true}` 并在运行期判定通过
|
||||
|
||||
#### Scenario: css attr 存在性默认语义
|
||||
- **WHEN** 原始内容包含 `<meta name="status" content="ok">` 且 expectation 为 `{css: {selector: "meta[name=status]", attr: "content"}}`
|
||||
- **THEN** Normalized 阶段 SHALL 将该 expectation 的 matcher 物化为 `{exists: true}` 并在属性存在时判定通过
|
||||
|
||||
#### Scenario: xpath 无匹配节点失败
|
||||
- **WHEN** XML 内容中不存在 XPath 指向的节点,且 expectation 为 `{xpath: {path: "/root/status"}}`
|
||||
- **THEN** 系统 SHALL 判定该 expectation 不通过并生成 phase 对应内容字段的 mismatch failure
|
||||
|
||||
### Requirement: KeyedExpectations 键控断言数组
|
||||
系统 SHALL 提供共享 `KeyedExpectations` 表达键值型观测值断言。Authoring `KeyedExpectations` SHALL 为动态键对象,每个键对应的值 MUST 为 Authoring `RawValueExpectation`;数组和对象简写 MUST 被拒绝,数组或对象 equals 匹配 MUST 显式写为 `{equals: <value>}`。Normalized `KeyedExpectations` SHALL 为有序数组,每个元素包含原始 key 和已归一化的 `ValueExpectation` matcher。调用方 MAY 指定 key 规范化策略;HTTP 与 LLM headers MUST 使用大小写不敏感的 key 匹配,并 MUST 在启动期校验或 Normalized 阶段拒绝归一化后重复的 header key。DB rows 不做 key 归一化,按查询结果列名大小写敏感匹配。
|
||||
|
||||
#### Scenario: headers 字面量快捷写法
|
||||
- **WHEN** 响应 headers 中 `content-type` 为 `application/json`,且配置为 `headers: {Content-Type: "application/json"}`
|
||||
- **THEN** Normalized 阶段 SHALL 将该项解析为 keyed expectation `{key: "Content-Type", matcher: {equals: "application/json"}}`,运行期按大小写不敏感 key 匹配并判定通过
|
||||
|
||||
#### Scenario: headers matcher 写法
|
||||
- **WHEN** 响应 headers 中 `content-type` 为 `application/json; charset=utf-8`,且配置为 `headers: {Content-Type: {contains: "application/json"}}`
|
||||
- **THEN** 系统 SHALL 判定该 header expectation 通过
|
||||
|
||||
#### Scenario: 缺失键 exists false
|
||||
- **WHEN** 观测键值表中不存在 `x-debug`,且配置为 `{x-debug: {exists: false}}`
|
||||
- **THEN** 系统 SHALL 判定该 keyed expectation 通过
|
||||
|
||||
#### Scenario: keyed 对象值必须显式 equals
|
||||
- **WHEN** Raw keyed expectation 的某个值是对象 `{foo: "bar"}` 且未写在 `equals` 下
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示对象 equals 必须显式写成 `{equals: {foo: "bar"}}`
|
||||
|
||||
#### Scenario: keyed 数组值必须显式 equals
|
||||
- **WHEN** Raw keyed expectation 的某个值是数组 `["a"]` 且未写在 `equals` 下
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示数组 equals 必须显式写成 `{equals: ["a"]}`
|
||||
|
||||
#### Scenario: header 归一化重复 key 被拒绝
|
||||
- **WHEN** HTTP 或 LLM `expect.headers` 同时配置 `Content-Type` 和 `content-type`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示 header key 归一化后重复
|
||||
|
||||
### Requirement: 结构化失败路径
|
||||
系统 SHALL 在共享 matcher、content 和 keyed expectation 断言失败时生成结构化 `CheckFailure`。failure SHALL 包含 `kind`、`phase`、`path`、`message`,并在 mismatch 场景包含 `expected` 和 `actual`。内容 expectation failure path SHALL 包含数组下标,keyed expectation failure path SHALL 包含键名,extractor failure path SHALL 包含 extractor 类型和 path/selector 信息。failure.expected SHOULD 使用用户可理解的 matcher 或 expectation 片段,MUST NOT 直接暴露 Resolved `kind` 执行计划;单字段 `equals` 包装 SHOULD 展示为原始 expected 值。
|
||||
|
||||
#### Scenario: ContentExpectations 失败路径
|
||||
- **WHEN** `expect.body[1].json` expectation 失败
|
||||
- **THEN** failure.path SHALL 指向 `body[1].json($.path)` 或等价可定位路径,failure.phase SHALL 为 `body`
|
||||
|
||||
#### Scenario: KeyedExpectations 失败路径
|
||||
- **WHEN** `expect.headers.Content-Type` 不匹配
|
||||
- **THEN** failure.path SHALL 指向 `headers.Content-Type`,failure.phase SHALL 为 `headers`
|
||||
|
||||
#### 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,131 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义基于 Vite dev server + Bun API server 的前端开发工作流、生产前端构建与 code splitting 策略、开发期 API 访问和共享契约的行为要求。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Vite React 开发服务器
|
||||
系统 SHALL 提供基于 Vite dev server 的前端开发工作流,支持热模块替换和 React Fast Refresh。
|
||||
|
||||
#### Scenario: 启动前端开发服务器
|
||||
- **WHEN** 开发者启动开发命令
|
||||
- **THEN** 前端 SHALL 由 Vite dev server 提供服务,支持 HMR 和 React Fast Refresh,监听 :5173 端口
|
||||
|
||||
#### Scenario: 构建前端静态资源
|
||||
- **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。
|
||||
|
||||
#### Scenario: 前端开发期调用拨测 API
|
||||
- **WHEN** 浏览器从 Vite dev server 请求 `/api/*` 路径
|
||||
- **THEN** Vite SHALL 通过 proxy 将请求转发到 Bun API server(默认 :3000)
|
||||
|
||||
#### Scenario: 前端开发期访问健康检查
|
||||
- **WHEN** 浏览器从 Vite dev server 请求 `/health`
|
||||
- **THEN** Vite SHALL 通过 proxy 将请求转发到 Bun API server
|
||||
|
||||
#### Scenario: 开发期访问非 API 前端路由
|
||||
- **WHEN** 浏览器从 Vite dev server 请求非 API 前端路由
|
||||
- **THEN** Vite SHALL 将该请求作为前端应用流量处理(SPA fallback 返回 HTML)
|
||||
|
||||
### Requirement: 开发期双进程运行
|
||||
项目 SHALL 在开发命令中同时启动 Vite dev server 和 Bun API server 两个进程。
|
||||
|
||||
#### Scenario: 使用默认开发端口
|
||||
- **WHEN** 开发者运行开发命令
|
||||
- **THEN** Vite dev server SHALL 监听 :5173,Bun API server SHALL 监听配置文件指定的端口(默认 :3000)
|
||||
|
||||
#### Scenario: 开发者访问前端
|
||||
- **WHEN** 开发者打开浏览器
|
||||
- **THEN** 开发者 SHALL 访问 Vite dev server 地址(:5173)获取前端页面
|
||||
|
||||
### Requirement: 前端使用相对 API 路径
|
||||
除非有文档化的部署配置覆盖该行为,前端代码 MUST 通过相对 `/api/*` URL 调用后端 API。
|
||||
|
||||
#### Scenario: 前端获取后端数据
|
||||
- **WHEN** 前端代码调用后端 API
|
||||
- **THEN** 请求 URL 默认 MUST 使用相对 `/api/*` 路径
|
||||
|
||||
#### Scenario: 运行环境变化
|
||||
- **WHEN** host 或 port 在开发环境和生产环境之间变化
|
||||
- **THEN** 前端 API 调用 SHALL 无需修改源码即可继续工作(开发期通过 Vite proxy,生产期通过同源请求)
|
||||
|
||||
### Requirement: 集成开发命令
|
||||
项目 SHALL 提供一个文档化命令,用于在开发期间同时运行 Vite dev server 和 Bun API server。
|
||||
|
||||
#### Scenario: 启动全栈开发
|
||||
- **WHEN** 开发者运行文档化的全栈开发命令
|
||||
- **THEN** 系统 SHALL 同时启动 Vite dev server 和 Bun API server,任一进程异常退出时终止另一个
|
||||
|
||||
### Requirement: 共享 TypeScript 契约
|
||||
项目 SHALL 为前端和后端共同使用的请求与响应类型提供共享 TypeScript 边界。
|
||||
|
||||
#### Scenario: 定义 API 响应结构
|
||||
- **WHEN** 前端和后端都需要某个 API 响应类型
|
||||
- **THEN** 该类型 SHALL 定义在 shared 模块中,而不是在两端重复定义
|
||||
|
||||
#### Scenario: 前端导入共享类型
|
||||
- **WHEN** 前端代码导入共享 API 类型
|
||||
- **THEN** 该导入 SHALL 不要求将后端运行时实现打包进前端
|
||||
@@ -1,31 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义前端全局错误边界:捕获渲染错误防止白屏,展示友好的错误兜底 UI。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 全局渲染错误捕获
|
||||
前端应用 SHALL 在最外层包裹 ErrorBoundary 组件,捕获所有子组件树的渲染错误,防止白屏。
|
||||
|
||||
#### Scenario: 子组件渲染抛出异常
|
||||
- **WHEN** 任意子组件在渲染过程中抛出 JavaScript 异常
|
||||
- **THEN** ErrorBoundary SHALL 捕获该异常,展示错误兜底 UI,而非白屏
|
||||
|
||||
#### Scenario: 错误兜底 UI 内容
|
||||
- **WHEN** ErrorBoundary 捕获到渲染错误
|
||||
- **THEN** 系统 SHALL 使用 TDesign Result 组件(type="500")展示错误提示,并提供"刷新页面"按钮
|
||||
|
||||
#### Scenario: 刷新页面恢复
|
||||
- **WHEN** 用户点击错误兜底 UI 中的"刷新页面"按钮
|
||||
- **THEN** 系统 SHALL 调用 `window.location.reload()` 重新加载页面
|
||||
|
||||
#### Scenario: 错误信息记录
|
||||
- **WHEN** ErrorBoundary 捕获到渲染错误
|
||||
- **THEN** 系统 SHALL 通过 `console.error` 输出错误信息和组件堆栈
|
||||
|
||||
### Requirement: ErrorBoundary 包裹位置
|
||||
ErrorBoundary SHALL 包裹在 QueryClientProvider 外层,确保 React Query 相关的渲染错误也能被捕获。
|
||||
|
||||
#### Scenario: 包裹层级
|
||||
- **WHEN** 应用渲染树构建
|
||||
- **THEN** 层级 SHALL 为 StrictMode > ErrorBoundary > QueryClientProvider > App
|
||||
@@ -1,220 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义基于 Vite + Bun 的全栈应用运行时,包括统一服务 bootstrap、声明式 Bun routes、API 路由组织、HTTP server、API 命名空间、健康检查、生产静态资源服务和 SPA fallback 行为。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Bun HTTP 运行时
|
||||
系统 SHALL 运行一个 Bun HTTP server,使用 `routes` 对象声明式注册 HTML 页面路由和 API 端点,由单个进程提供后端 API、健康检查和前端服务。
|
||||
|
||||
#### Scenario: 启动运行时服务器
|
||||
- **WHEN** server 进程成功启动
|
||||
- **THEN** 它 SHALL 监听配置文件中指定的 host 和 port,通过 routes 对象注册所有路由,并记录实际 server URL
|
||||
|
||||
#### Scenario: 通过 YAML 配置提供运行时参数
|
||||
- **WHEN** 通过 YAML 配置文件提供 host、port、数据目录等参数
|
||||
- **THEN** server SHALL 使用该值,且不需要重新构建
|
||||
|
||||
#### Scenario: CLI 只接受配置文件路径
|
||||
- **WHEN** 用户通过命令行启动程序
|
||||
- **THEN** 系统 SHALL 只接受一个命令行参数作为 YAML 配置文件路径
|
||||
|
||||
#### Scenario: 提供拨测相关 API
|
||||
- **WHEN** server 启动完成
|
||||
- **THEN** 系统 SHALL 通过 routes 对象提供 `/api/summary`、`/api/targets`、`/api/targets/:id/history`、`/api/targets/:id/trend` 端点
|
||||
|
||||
### Requirement: HTTP method 语义
|
||||
系统 SHALL 只为运行时端点声明实际支持的 GET handler;不支持的 API method SHALL 按未匹配 API 路由处理,不再保留自定义 405 和 Allow header 语义。
|
||||
|
||||
#### Scenario: GET 请求访问运行时端点
|
||||
- **WHEN** 客户端使用 `GET` 请求 `/health` 或 `/api/*` 端点
|
||||
- **THEN** Bun server SHALL 返回对应端点的成功响应
|
||||
|
||||
#### Scenario: 不支持的 API method 请求
|
||||
- **WHEN** 客户端使用不支持的 method 请求已存在的 `/api/*` 端点
|
||||
- **THEN** `/api/*` 通配符 SHALL 返回包含 `error` 和 `status` 字段的 JSON 404 响应
|
||||
|
||||
### Requirement: API 路由命名空间
|
||||
系统 MUST 将 `/api/*` 保留给后端 API 路由。
|
||||
|
||||
#### Scenario: API 路由匹配
|
||||
- **WHEN** 请求匹配已注册的 `/api/*` 路由
|
||||
- **THEN** Bun server SHALL 返回 API handler 的响应
|
||||
|
||||
#### Scenario: API 路由未命中
|
||||
- **WHEN** 请求访问未注册的 `/api/*` 路由
|
||||
- **THEN** Bun server MUST 返回 JSON 404 响应,而不是前端 HTML 文档
|
||||
|
||||
### Requirement: API 错误响应一致性
|
||||
系统 SHALL 为 API 命名空间内的未匹配路由和未匹配 method 返回机器可读 JSON 404 响应。
|
||||
|
||||
#### Scenario: 未知 API 路由
|
||||
- **WHEN** 客户端请求未知的 `/api/*` 路由
|
||||
- **THEN** Bun server MUST 返回包含 `error` 和 `status` 字段的 JSON 404 响应,而不是前端 HTML 文档
|
||||
|
||||
#### Scenario: API method 不匹配
|
||||
- **WHEN** 客户端使用不支持的 method 请求已存在的 API 路由
|
||||
- **THEN** Bun server MUST 返回包含 `error` 和 `status` 字段的 JSON 404 响应
|
||||
|
||||
### Requirement: 健康检查端点
|
||||
系统 SHALL 在前端 SPA fallback 之外暴露健康检查端点。
|
||||
|
||||
#### Scenario: 健康检查成功
|
||||
- **WHEN** 客户端请求 `/health`
|
||||
- **THEN** Bun server SHALL 返回成功的、机器可读的健康检查响应
|
||||
|
||||
### Requirement: 生产静态资源服务
|
||||
系统 SHALL 在生产模式下通过自定义 `serveStaticAsset` 函数服务嵌入的 Vite 前端产出。
|
||||
|
||||
#### Scenario: 请求构建后的资源
|
||||
- **WHEN** 客户端请求 `/assets/*` 路径下的前端资源
|
||||
- **THEN** 系统 SHALL 从 StaticAssets 的 files map 中查找并返回对应资源,Content-Type 根据扩展名推断
|
||||
|
||||
#### Scenario: 请求前端根路径
|
||||
- **WHEN** 客户端请求 `/`
|
||||
- **THEN** 系统 SHALL 返回 StaticAssets 中的 indexHtml,Content-Type 为 `text/html; charset=utf-8`
|
||||
|
||||
### Requirement: 生产缓存策略
|
||||
系统 SHALL 为生产静态资源提供基于文件名 content hash 的缓存策略。
|
||||
|
||||
#### Scenario: 请求前端入口 HTML
|
||||
- **WHEN** 生产 server 返回前端入口 HTML 文档
|
||||
- **THEN** 响应 SHALL 包含 `Cache-Control: no-cache` header
|
||||
|
||||
#### Scenario: 请求构建后的静态资源
|
||||
- **WHEN** 生产 server 返回 `/assets/*` 路径下的静态资源
|
||||
- **THEN** 响应 SHALL 包含 `Cache-Control: public, max-age=31536000, immutable` header
|
||||
|
||||
### Requirement: 低风险安全响应头
|
||||
系统 SHALL 在生产运行时的 JSON API 响应中附加低风险安全响应头;HTML 和静态资源响应由 Bun HTML import manifest 返回其内置 headers。
|
||||
|
||||
#### Scenario: 生产 JSON 响应包含安全头
|
||||
- **WHEN** 生产 Bun server 返回 `/health` 或 `/api/*` JSON 响应
|
||||
- **THEN** 响应 SHALL 包含 `X-Content-Type-Options: nosniff` 和 `Referrer-Policy` headers
|
||||
|
||||
#### Scenario: 生产静态资源响应
|
||||
- **WHEN** 生产 server 返回前端 HTML 文档或构建后的静态资源
|
||||
- **THEN** 响应 SHALL 不要求附加自定义安全 headers(仅需 Content-Type 和 Cache-Control)
|
||||
|
||||
### Requirement: SPA fallback 行为
|
||||
系统 SHALL 通过 fetch fallback 为非 API、非静态资源路径返回前端入口 HTML 文档。
|
||||
|
||||
#### Scenario: 刷新前端路由
|
||||
- **WHEN** 客户端请求不包含文件扩展名的非 API 路径(如 `/dashboard`)
|
||||
- **THEN** fetch fallback SHALL 返回前端入口 HTML 文档
|
||||
|
||||
#### Scenario: 保留 API 错误语义
|
||||
- **WHEN** 客户端请求未知的 `/api/*` 路由
|
||||
- **THEN** routes 中的 `/api/*` 通配符 MUST 返回 JSON 404 响应,而不是前端入口 HTML 文档
|
||||
|
||||
### Requirement: 优雅关机
|
||||
系统 SHALL 在收到终止信号时正确清理资源。
|
||||
|
||||
#### 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` 完成启动
|
||||
@@ -1,196 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 HTTP 类型拨测目标:通过 `type: http` 配置 HTTP/HTTPS 探测,支持请求方法、headers、body、SSL 校验、重定向和响应体大小限制,捕获 HTTP 状态码、响应头、响应体和执行耗时,按 expect 规则校验并生成 matched 判定。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: HTTP target 配置
|
||||
系统 SHALL 支持 `type: http` 的 target 配置,通过 `http.url` 描述目标 HTTP 地址,并通过可选字段控制请求方法、请求体、headers、SSL 校验、重定向和响应体大小限制。
|
||||
|
||||
#### Scenario: 解析最简 HTTP target
|
||||
- **WHEN** YAML 中 target 配置 `type: http` 和 `http.url: "https://example.com"`
|
||||
- **THEN** 系统 SHALL 将其解析为 HTTP checker,并填充 `method=GET`、`maxBodyBytes=100MB`、`ignoreSSL=false`、`maxRedirects=0`、headers、body、interval、timeout、group 和 expect 配置
|
||||
|
||||
#### Scenario: HTTP target 缺少 url
|
||||
- **WHEN** YAML 中 target 配置 `type: http` 但缺少 `http.url`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 http.url 字段
|
||||
|
||||
#### Scenario: HTTP target 覆盖请求方法
|
||||
- **WHEN** YAML 中某个 HTTP target 显式配置 `http.method: POST`
|
||||
- **THEN** 该 target SHALL 使用自身 method 字段的值,而不是内置默认值 GET
|
||||
|
||||
#### Scenario: HTTP target 覆盖响应体大小限制
|
||||
- **WHEN** YAML 中某个 HTTP target 显式配置 `http.maxBodyBytes`
|
||||
- **THEN** 该 target SHALL 使用自身 maxBodyBytes 字段的值,而不是内置默认值 100MB
|
||||
|
||||
#### Scenario: HTTP target 覆盖 headers
|
||||
- **WHEN** YAML 中某个 HTTP target 显式配置 `http.headers`
|
||||
- **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`)
|
||||
@@ -1,196 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 ICMP checker 的配置格式、命令执行、跨平台输出解析、expect 校验、失败结构和状态摘要。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: icmp target 配置
|
||||
系统 SHALL 支持 `type: icmp` 的 target 配置,通过 `icmp.host` 描述目标主机地址,并通过可选字段控制探测行为。
|
||||
|
||||
#### Scenario: 解析最简 icmp target
|
||||
- **WHEN** YAML 中 target 配置 `type: icmp` 和 `icmp.host: "10.0.0.1"`
|
||||
- **THEN** 系统 SHALL 将其解析为 icmp checker,并填充 `count=3`、`packetSize=56`、interval、timeout、group 和 expect 配置
|
||||
|
||||
#### Scenario: icmp target 缺少 host
|
||||
- **WHEN** YAML 中 target 配置 `type: icmp` 但缺少 `icmp.host`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 icmp.host 字段
|
||||
|
||||
#### Scenario: icmp host 类型非法
|
||||
- **WHEN** YAML 中 icmp target 的 `icmp.host` 不是非空字符串
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 icmp.host 必须为非空字符串
|
||||
|
||||
#### Scenario: icmp count 配置
|
||||
- **WHEN** YAML 中 icmp target 配置 `icmp.count: 5`
|
||||
- **THEN** 系统 SHALL 使用 5 作为 ICMP 包发送数量
|
||||
|
||||
#### Scenario: icmp count 非法
|
||||
- **WHEN** YAML 中 icmp target 的 `icmp.count` 不是 1 到 100 之间的正整数
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 icmp.count 必须为 1-100 的正整数
|
||||
|
||||
#### Scenario: icmp packetSize 配置
|
||||
- **WHEN** YAML 中 icmp target 配置 `icmp.packetSize: 1472`
|
||||
- **THEN** 系统 SHALL 使用 1472 作为 ICMP 包大小(bytes)
|
||||
|
||||
#### Scenario: icmp packetSize 非法
|
||||
- **WHEN** YAML 中 icmp target 的 `icmp.packetSize` 不是 1 到 65500 之间的正整数
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 icmp.packetSize 必须为 1-65500 的正整数
|
||||
|
||||
#### Scenario: icmp 分组未知字段失败
|
||||
- **WHEN** YAML 中 icmp target 的 `icmp` 分组包含 `timeout: 5` 等未知字段
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 icmp 分组包含未知字段
|
||||
|
||||
#### Scenario: icmp 序列化展示摘要
|
||||
- **WHEN** 系统同步 icmp target 到 targets 表
|
||||
- **THEN** `target` 展示摘要 SHALL 为 `icmp <host>`,`config` JSON SHALL 包含 resolved 后的 host、count 和 packetSize
|
||||
|
||||
### Requirement: icmp checker 执行
|
||||
系统 SHALL 通过调用系统 `ping` 命令执行 ICMP 探测,记录完整执行耗时,并在命令不可用、超时或解析失败时产生结构化失败信息。`IcmpChecker` SHALL 通过构造函数参数支持 platform 注入,默认使用 `process.platform`。
|
||||
|
||||
#### Scenario: ping 命令构建(Linux)
|
||||
- **WHEN** 系统平台为 linux,icmp target 配置 host="10.0.0.1"、count=3、packetSize=56,且外层 timeoutMs=10000
|
||||
- **THEN** 系统 SHALL 执行 `ping -c 3 -s 56 -W 10 10.0.0.1`(-W 单位为秒,向上取整)
|
||||
|
||||
#### Scenario: ping 命令构建(macOS)
|
||||
- **WHEN** 系统平台为 darwin,icmp target 配置 host="10.0.0.1"、count=3、packetSize=56,且外层 timeoutMs=10000
|
||||
- **THEN** 系统 SHALL 执行 `ping -c 3 -s 56 -W 10000 10.0.0.1`(-W 单位为毫秒)
|
||||
|
||||
#### Scenario: ping 命令构建(Windows)
|
||||
- **WHEN** 系统平台为 win32,icmp target 配置 host="10.0.0.1"、count=3、packetSize=56,且外层 timeoutMs=10000
|
||||
- **THEN** 系统 SHALL 执行 `ping -n 3 -l 56 -w 10000 10.0.0.1`(-w 单位为毫秒)
|
||||
|
||||
#### Scenario: ping 命令不存在
|
||||
- **WHEN** 系统未安装 `ping` 命令(spawn 抛出 ENOENT)
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `icmp`,path 为 `spawn`,message 包含 "icmp 命令不可用" 和原始错误信息
|
||||
|
||||
#### Scenario: ping 执行超时
|
||||
- **WHEN** 引擎注入的 `ctx.signal` 在 ping 命令执行过程中 abort
|
||||
- **THEN** 系统 SHALL 调用 `proc.kill()` 终止子进程,记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `icmp`,message 包含超时信息
|
||||
|
||||
#### Scenario: ping 目标可达
|
||||
- **WHEN** icmp target 指向可达主机,且 ping 命令正常返回
|
||||
- **THEN** 系统 SHALL 解析 stdout 获取统计数据,并按断言链执行 expect 校验
|
||||
|
||||
#### Scenario: ping 目标不可达
|
||||
- **WHEN** icmp target 指向不可达主机,且 ping 命令返回 100% packet loss
|
||||
- **THEN** 系统 SHALL 解析 stdout 获取统计数据,`alive` 为 false,延迟字段为 null
|
||||
|
||||
#### Scenario: duration 覆盖完整执行
|
||||
- **WHEN** ping 命令执行完成
|
||||
- **THEN** 结果中的 `durationMs` SHALL 覆盖从 spawn 到进程退出的完整耗时
|
||||
|
||||
#### Scenario: platform 注入用于测试
|
||||
- **WHEN** 构造 `new IcmpChecker("linux")`
|
||||
- **THEN** execute 方法 SHALL 使用注入的 "linux" 作为平台参数,而非 `process.platform`
|
||||
|
||||
### Requirement: 跨平台 icmp 输出解析
|
||||
系统 SHALL 实现跨平台 ping 输出解析器,支持 Linux、macOS 和 Windows(含多语言 locale),从 stdout 中提取 transmitted、received、packetLoss、minLatencyMs、avgLatencyMs、maxLatencyMs。
|
||||
|
||||
#### Scenario: 解析 Linux ping 输出
|
||||
- **WHEN** 平台为 linux,stdout 包含 "3 packets transmitted, 3 received, 0% packet loss" 和 "rtt min/avg/max/mdev = 1.234/2.345/3.456/0.567 ms"
|
||||
- **THEN** 系统 SHALL 解析为 transmitted=3, received=3, packetLoss=0, minLatencyMs=1.234, avgLatencyMs=2.345, maxLatencyMs=3.456
|
||||
|
||||
#### Scenario: 解析 macOS ping 输出
|
||||
- **WHEN** 平台为 darwin,stdout 包含 "3 packets transmitted, 3 packets received, 0.0% packet loss" 和 "round-trip min/avg/max/stddev = 1.234/2.345/3.456/0.567 ms"
|
||||
- **THEN** 系统 SHALL 解析为 transmitted=3, received=3, packetLoss=0, minLatencyMs=1.234, avgLatencyMs=2.345, maxLatencyMs=3.456
|
||||
|
||||
#### Scenario: 解析 Windows 英文 ping 输出
|
||||
- **WHEN** 平台为 win32,stdout 包含 "Packets: Sent = 3, Received = 3, Lost = 0 (0% loss)" 和 "Minimum = 1ms, Maximum = 3ms, Average = 2ms"
|
||||
- **THEN** 系统 SHALL 解析为 transmitted=3, received=3, packetLoss=0, minLatencyMs=1, avgLatencyMs=2, maxLatencyMs=3
|
||||
|
||||
#### Scenario: 解析 Windows 中文 ping 输出
|
||||
- **WHEN** 平台为 win32,stdout 包含 "数据包: 已发送 = 3,已接收 = 3,丢失 = 0 (0% 丢失)" 和 "最短 = 1ms,最长 = 3ms,平均 = 2ms"
|
||||
- **THEN** 系统 SHALL 解析为 transmitted=3, received=3, packetLoss=0, minLatencyMs=1, avgLatencyMs=2, maxLatencyMs=3
|
||||
|
||||
#### Scenario: 解析全部丢包(无延迟行)
|
||||
- **WHEN** stdout 包含丢包统计行但无延迟统计行(100% packet loss)
|
||||
- **THEN** 系统 SHALL 解析为 alive=false,延迟字段(min/avg/max)均为 null
|
||||
|
||||
#### Scenario: 输出无法解析
|
||||
- **WHEN** stdout 不匹配任何已知的统计行格式
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `icmp`,path 为 `parse`,message 包含 "无法解析 icmp 输出"
|
||||
|
||||
### Requirement: icmp expect 校验
|
||||
系统 SHALL 支持 icmp 专属 expect,包括 `alive`、`packetLossPercent`、`avgLatencyMs`、`maxLatencyMs` 和 `durationMs`,并按 alive、packetLossPercent、avgLatencyMs、maxLatencyMs、durationMs 的阶段顺序快速失败。`alive` SHALL 保持布尔状态语义,未配置时在 Resolved expect 中默认 `true`。`packetLossPercent` SHALL 表示 0 到 100 的丢包率百分比,并使用共享 `RawValueExpectation` 输入、运行期 `ValueExpectation` 执行。`avgLatencyMs`、`maxLatencyMs` 和 `durationMs` SHALL 使用共享 `RawValueExpectation` 输入、运行期 `ValueExpectation` 执行。
|
||||
|
||||
#### Scenario: 默认 alive 成功语义
|
||||
- **WHEN** icmp target 未显式配置 `expect.alive`
|
||||
- **THEN** 系统 SHALL 在 Resolved icmp expect 中使用默认 `alive: true` 进行校验
|
||||
|
||||
#### Scenario: alive 校验通过
|
||||
- **WHEN** icmp target 配置 `expect.alive: true`,且目标主机可达
|
||||
- **THEN** 系统 SHALL 判定 alive 阶段通过
|
||||
|
||||
#### Scenario: alive 校验失败
|
||||
- **WHEN** icmp target 配置 `expect.alive: true`,且目标主机不可达
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `alive`
|
||||
|
||||
#### Scenario: 反向 alive 断言
|
||||
- **WHEN** icmp target 配置 `expect.alive: false`,且目标主机不可达
|
||||
- **THEN** 系统 SHALL 判定 alive 阶段通过(`matched=true`)
|
||||
|
||||
#### Scenario: packetLossPercent 校验通过
|
||||
- **WHEN** icmp target 配置 `expect.packetLossPercent: {lte: 10}`,且实际丢包率为 0%
|
||||
- **THEN** 系统 SHALL 判定 packetLossPercent 阶段通过
|
||||
|
||||
#### Scenario: packetLossPercent 校验失败
|
||||
- **WHEN** icmp target 配置 `expect.packetLossPercent: {lte: 10}`,且实际丢包率为 33%
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `packetLoss`
|
||||
|
||||
#### Scenario: avgLatencyMs 校验通过
|
||||
- **WHEN** icmp target 配置 `expect.avgLatencyMs: {lte: 200}`,且实际平均延迟为 12ms
|
||||
- **THEN** 系统 SHALL 判定 avgLatency 阶段通过
|
||||
|
||||
#### Scenario: avgLatencyMs 校验失败
|
||||
- **WHEN** icmp target 配置 `expect.avgLatencyMs: {lte: 100}`,且实际平均延迟为 156ms
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `avgLatency`
|
||||
|
||||
#### Scenario: maxLatencyMs 校验通过
|
||||
- **WHEN** icmp target 配置 `expect.maxLatencyMs: {lte: 500}`,且实际最大延迟为 340ms
|
||||
- **THEN** 系统 SHALL 判定 maxLatency 阶段通过
|
||||
|
||||
#### Scenario: maxLatencyMs 校验失败
|
||||
- **WHEN** icmp target 配置 `expect.maxLatencyMs: {lte: 200}`,且实际最大延迟为 340ms
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `maxLatency`
|
||||
|
||||
#### Scenario: durationMs 校验
|
||||
- **WHEN** icmp target 配置 `expect.durationMs: {lte: 5000}`,且完整执行耗时超过 5000ms
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 phase 为 `duration`
|
||||
|
||||
#### Scenario: alive=false 时跳过延迟断言
|
||||
- **WHEN** icmp target 配置 `expect.alive: true` 和 `expect.avgLatencyMs: {lte: 100}`,且目标不可达
|
||||
- **THEN** 系统 SHALL 在 alive 阶段即返回失败,不执行后续延迟断言
|
||||
|
||||
#### Scenario: icmp expect 未知字段失败
|
||||
- **WHEN** YAML 中 icmp target 的 expect 包含 `status: [200]`、`maxPacketLoss`、`maxAvgLatencyMs`、`maxMaxLatencyMs`、`maxDurationMs` 或其他非 icmp expect 字段
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
|
||||
|
||||
#### Scenario: packetLossPercent 类型非法
|
||||
- **WHEN** YAML 中 icmp target 的 `expect.packetLossPercent` 不是合法 `RawValueExpectation`,或其数值范围无法用于 0 到 100 的百分比断言
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.packetLossPercent 格式错误
|
||||
|
||||
#### Scenario: avgLatencyMs 类型非法
|
||||
- **WHEN** YAML 中 icmp target 的 `expect.avgLatencyMs` 不是合法 `RawValueExpectation`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.avgLatencyMs 格式错误
|
||||
|
||||
#### Scenario: maxLatencyMs 类型非法
|
||||
- **WHEN** YAML 中 icmp target 的 `expect.maxLatencyMs` 不是合法 `RawValueExpectation`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.maxLatencyMs 格式错误
|
||||
|
||||
#### Scenario: Raw icmp expect 不被校验阶段修改
|
||||
- **WHEN** YAML 中 icmp target 配置 `expect.durationMs: 5000`
|
||||
- **THEN** 语义校验 SHALL 接受该 Raw primitive 简写且 MUST NOT 将输入对象原地改写为 `{equals: 5000}`
|
||||
|
||||
### Requirement: icmp detail 摘要
|
||||
系统 SHALL 在 icmp API 序列化时从 observation 动态生成结构化 detail 摘要,展示关键指标。API registry type SHALL 仍为 `icmp`。
|
||||
|
||||
#### Scenario: 目标可达无丢包
|
||||
- **WHEN** icmp observation 为 alive=true, avgLatencyMs=12, packetLoss=0%, transmitted=3, received=3
|
||||
- **THEN** detail SHALL 为 `alive, avg 12ms, loss 0% (3/3)`
|
||||
|
||||
#### Scenario: 目标可达有丢包
|
||||
- **WHEN** icmp observation 为 alive=true, avgLatencyMs=156, maxLatencyMs=340, packetLoss=33%, transmitted=3, received=2
|
||||
- **THEN** detail SHALL 包含 avg、max 和 loss 信息
|
||||
|
||||
#### Scenario: 目标不可达
|
||||
- **WHEN** icmp observation 为 alive=false, transmitted=3, received=0
|
||||
- **THEN** detail SHALL 为 `unreachable (0/3 received)`
|
||||
@@ -1,235 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 LLM checker 的配置模型、Provider/Mode 支持、执行观测、Expect 断言、失败 Phase 和测试行为。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: LLM Checker 注册与模块结构
|
||||
系统 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** 开发者查看 LLM checker 目录
|
||||
- **THEN** 该目录 SHALL 包含 LLM checker 的类型、schema、语义校验、provider 创建、observation 构建、expect 断言、执行逻辑和模块入口
|
||||
|
||||
#### Scenario: 不扩展存储和 API 结构
|
||||
- **WHEN** LLM checker 写入检查结果
|
||||
- **THEN** 系统 SHALL 使用现有 `CheckResult`、`targets`、`check_results` 和 Dashboard API 结构,不新增 LLM 专用存储列或 Dashboard 指标字段
|
||||
|
||||
### Requirement: LLM Provider 与调用模式
|
||||
LLM checker SHALL 支持 `openai`、`openai-responses`、`anthropic` 三类 provider。`mode: http` SHALL 调用 AI SDK `generateText`。`mode: stream` SHALL 调用 AI SDK `streamText`。所有模型调用 MUST 将 `maxRetries` 固定为 `0`,并 MUST 使用引擎注入的 `ctx.signal` 响应超时和取消。
|
||||
|
||||
#### Scenario: OpenAI Chat Completions provider
|
||||
- **WHEN** target 配置 `llm.provider: openai`
|
||||
- **THEN** LLM checker SHALL 使用 `@ai-sdk/openai` 的 `openai.chat(model)` 创建模型调用对象
|
||||
|
||||
#### Scenario: OpenAI Responses provider
|
||||
- **WHEN** target 配置 `llm.provider: openai-responses`
|
||||
- **THEN** LLM checker SHALL 使用 `@ai-sdk/openai` 的 `openai.responses(model)` 创建模型调用对象
|
||||
|
||||
#### Scenario: Anthropic provider
|
||||
- **WHEN** target 配置 `llm.provider: anthropic`
|
||||
- **THEN** LLM checker SHALL 使用 `@ai-sdk/anthropic` 的 `anthropic.messages(model)` 创建模型调用对象
|
||||
|
||||
#### Scenario: 非流式调用模式
|
||||
- **WHEN** target 配置 `llm.mode: http` 或省略 `llm.mode`
|
||||
- **THEN** LLM checker SHALL 调用 `generateText` 并从返回结果构建非流式 observation
|
||||
|
||||
#### Scenario: 流式调用模式
|
||||
- **WHEN** target 配置 `llm.mode: stream`
|
||||
- **THEN** LLM checker SHALL 调用 `streamText` 并消费 `fullStream` 构建流式 observation
|
||||
|
||||
#### Scenario: 超时取消传递给 SDK
|
||||
- **WHEN** 引擎注入的 `ctx.signal` 被 abort
|
||||
- **THEN** LLM checker SHALL 将该 signal 传递给 AI SDK 调用并将取消或超时结果记录为检查失败
|
||||
|
||||
### Requirement: LLM 配置解析与默认值
|
||||
LLM checker SHALL 解析 `llm.provider`、`llm.url`、`llm.model`、`llm.prompt`、`llm.mode`、`llm.key`、`llm.authToken`、`llm.headers`、`llm.ignoreSSL`、`llm.options` 和 `llm.providerOptions`。`llm.options` SHALL 支持 `maxOutputTokens`(默认 `16`)、`temperature`(默认 `0`)、`topP`、`topK`、`presencePenalty`、`frequencyPenalty`、`stopSequences`(字符串数组)和 `seed`。`llm.mode` 默认值 SHALL 为 `http`,`llm.key` 默认值 SHALL 为空字符串,`llm.ignoreSSL` 默认值 SHALL 为 `false`。LLM checker MUST NOT 隐式读取 AI SDK 默认环境变量。
|
||||
|
||||
#### Scenario: 最简 LLM target 解析
|
||||
- **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: Anthropic Bearer token
|
||||
- **WHEN** target 配置 `llm.provider: anthropic` 和非空 `llm.authToken`
|
||||
- **THEN** LLM checker SHALL 将 `authToken` 映射到 Anthropic SDK 的 Bearer token 认证字段
|
||||
|
||||
#### Scenario: key 不隐式读取环境变量
|
||||
- **WHEN** target 未配置 `llm.key`
|
||||
- **THEN** LLM checker SHALL 将 SDK provider 的 api key 设置为空字符串,而不是隐式读取 SDK 默认环境变量
|
||||
|
||||
### Requirement: LLM HTTP Metadata 与 TLS
|
||||
LLM checker SHALL 通过 AI SDK provider 的 custom fetch 注入 observing fetch。observing fetch SHALL 调用 Bun `fetch`,在不消费 response body 的前提下记录 HTTP status、statusText 和 headers。`llm.ignoreSSL: true` 时,observing fetch SHALL 仅对当前 target 的 provider 请求使用 Bun `tls.rejectUnauthorized=false`。
|
||||
|
||||
#### Scenario: 捕获 HTTP metadata
|
||||
- **WHEN** AI SDK provider 发起模型 HTTP 请求并收到响应
|
||||
- **THEN** observing fetch SHALL 记录 status code 和响应 headers,供 `expect.status` 与 `expect.headers` 使用
|
||||
|
||||
#### Scenario: 不消费响应体
|
||||
- **WHEN** observing fetch 捕获 HTTP metadata
|
||||
- **THEN** observing fetch SHALL 返回原始 response 给 AI SDK,不提前读取或克隆消费 body
|
||||
|
||||
#### Scenario: 忽略证书校验
|
||||
- **WHEN** target 配置 `llm.ignoreSSL: true`
|
||||
- **THEN** observing fetch SHALL 对当前 target 的 provider 请求设置 `tls.rejectUnauthorized=false`
|
||||
|
||||
### Requirement: LLM Observation
|
||||
LLM checker SHALL 在 SDK 调用结果和 expect 断言之间构建 `LlmCheckObservation`。observation SHALL 包含 provider、model、mode、outputText、finishReason、rawFinishReason、usage、stream、http 和 warnings 中可观测的字段。`mode: http` 的 `outputText` SHALL 来自 `generateText.text`。`mode: stream` 的 `outputText` SHALL 来自 `fullStream` 中 `text-delta` 的原始文本聚合。
|
||||
|
||||
#### Scenario: 非流式 observation
|
||||
- **WHEN** `generateText` 调用成功
|
||||
- **THEN** LLM checker SHALL 从 SDK result 中提取 outputText、finishReason、rawFinishReason、usage、response headers 和 HTTP metadata
|
||||
|
||||
#### Scenario: 流式 observation
|
||||
- **WHEN** `streamText` 调用成功且 stream 正常完成
|
||||
- **THEN** LLM checker SHALL 从 `fullStream` 聚合 outputText,并记录 stream.completed、firstTokenMs、finishReason、rawFinishReason、usage 和 HTTP metadata
|
||||
|
||||
#### Scenario: APICallError observation
|
||||
- **WHEN** AI SDK 抛出带 statusCode 或 responseHeaders 的 `APICallError`
|
||||
- **THEN** LLM checker SHALL 构建包含可用 HTTP metadata 的 observation,并继续执行可执行的 status、headers 和 duration 断言
|
||||
|
||||
#### Scenario: 无 HTTP metadata 的 SDK 错误
|
||||
- **WHEN** AI SDK 抛出不带 statusCode 和 responseHeaders 的错误
|
||||
- **THEN** LLM checker SHALL 返回 `phase: "request"` 的 error failure
|
||||
|
||||
### Requirement: LLM Expect 断言
|
||||
LLM checker SHALL 支持 `expect.status`、`expect.headers`、`expect.output`、`expect.finishReason`、`expect.rawFinishReason`、`expect.usage.inputTokens`、`expect.usage.outputTokens`、`expect.usage.totalTokens`、`expect.stream.completed`、`expect.stream.firstTokenMs` 和 `expect.durationMs`。`expect.status` SHALL 保持 HTTP 状态码数组语义并复用共享 status code 断言,未配置时在 Resolved expect 中物化默认 `[200]`。`expect.headers` SHALL 使用共享 `RawKeyedExpectations` 输入并在运行期使用 `KeyedExpectations`,header key 大小写不敏感。`expect.output` MUST 使用共享 `RawContentExpectations` 输入并在运行期使用 `ContentExpectations`。`expect.finishReason` 和 `expect.rawFinishReason` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation`。`expect.usage.*`、`expect.stream.firstTokenMs` 和 `expect.durationMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation`。LLM checker MUST 按固定顺序快速失败,非流式顺序为 status、headers、output、finishReason、rawFinishReason、usage、durationMs;流式顺序为 status、headers、stream.completed、stream.firstTokenMs、output、finishReason、rawFinishReason、usage、durationMs。
|
||||
|
||||
#### Scenario: 默认 status 断言
|
||||
- **WHEN** LLM target 未配置 `expect.status`
|
||||
- **THEN** LLM checker SHALL 在 Resolved expect 中使用默认 `status: [200]` 语义
|
||||
|
||||
#### Scenario: expect headers 通过
|
||||
- **WHEN** observing fetch 捕获的响应 headers 满足 `expect.headers` 配置
|
||||
- **THEN** LLM checker SHALL 通过共享 header expectation 包装函数判定 headers 断言通过
|
||||
|
||||
#### Scenario: output ContentExpectations 通过
|
||||
- **WHEN** LLM 输出文本满足 `expect.output` 中配置的全部 ContentExpectations
|
||||
- **THEN** LLM checker SHALL 判定 output 阶段通过
|
||||
|
||||
#### Scenario: finishReason ValueMatcher 通过
|
||||
- **WHEN** observation.finishReason 为 `stop` 且 target 配置 `expect.finishReason: {equals: "stop"}`
|
||||
- **THEN** LLM checker SHALL 判定 finishReason 阶段通过
|
||||
|
||||
#### Scenario: rawFinishReason regex 通过
|
||||
- **WHEN** observation.rawFinishReason 为 `end_turn` 且 target 配置 `expect.rawFinishReason: {regex: "^(stop|end_turn)$"}`
|
||||
- **THEN** LLM checker SHALL 判定 rawFinishReason 阶段通过
|
||||
|
||||
#### Scenario: usage matcher 通过
|
||||
- **WHEN** observation.usage.totalTokens 为 14 且 target 配置 `expect.usage.totalTokens: {lte: 20}`
|
||||
- **THEN** LLM checker SHALL 判定 usage 阶段通过
|
||||
|
||||
#### Scenario: durationMs matcher 失败
|
||||
- **WHEN** LLM target 配置 `expect.durationMs: {lte: 1000}` 且实际执行耗时为 1500ms
|
||||
- **THEN** LLM checker SHALL 返回 phase=`duration` 的 mismatch failure
|
||||
|
||||
#### Scenario: 首个 expect 失败
|
||||
- **WHEN** 多个 LLM expect 中某个较早顺序的断言失败
|
||||
- **THEN** LLM checker SHALL 立即返回该断言对应的 mismatch failure,不继续执行后续断言
|
||||
|
||||
#### Scenario: 期望认证失败状态
|
||||
- **WHEN** AI SDK 抛出带 HTTP status 401 的 `APICallError`,且 target 仅配置 `expect.status: [401]`
|
||||
- **THEN** LLM checker SHALL 判定本次检查为 `matched=true`
|
||||
|
||||
#### Scenario: APICallError 缺失模型输出
|
||||
- **WHEN** AI SDK 抛出带 HTTP status 的 `APICallError`,且 target 同时配置需要模型结果的 `expect.output`
|
||||
- **THEN** LLM checker SHALL 因 `outputText` 缺失返回 `phase: "output"` 的 mismatch failure
|
||||
|
||||
### Requirement: LLM Output 规则
|
||||
LLM checker SHALL 使用共享 `ContentRules` 校验 `expect.output`。每个 output rule SHALL 为直接 `ValueMatcher`,或 `json`、`css`、`xpath` extractor 规则之一。直接 matcher SHALL 作用于原始输出字符串。`equals` SHALL 对原始输出字符串做严格相等比较。`contains` SHALL 判断原始输出是否包含子串。`regex` SHALL 对原始输出执行无 flags 正则匹配。`json` SHALL 将原始输出解析为 JSON,并用现有 JSONPath 子集和 `ValueMatcher` 校验提取值。`json.equals` SHALL 支持任意 JSON value。`css` 和 `xpath` 在 schema 层面可用,但 LLM 输出通常为纯文本或 JSON,实际场景中仅 `json` 提取器有意义。
|
||||
|
||||
#### Scenario: 原始输出严格相等
|
||||
- **WHEN** `outputText` 为 `"OK\n"` 且 target 配置 `expect.output: [{ equals: "OK" }]`
|
||||
- **THEN** LLM checker SHALL 判定 output 断言失败,因为 equals 不自动 trim
|
||||
|
||||
#### Scenario: output contains 通过
|
||||
- **WHEN** `outputText` 包含配置的子串
|
||||
- **THEN** LLM checker SHALL 判定该 output contains 规则通过
|
||||
|
||||
#### Scenario: output regex 通过
|
||||
- **WHEN** `outputText` 匹配配置的合法 regex
|
||||
- **THEN** LLM checker SHALL 判定该 output regex 规则通过
|
||||
|
||||
#### Scenario: output JSONPath 字符串 equals 通过
|
||||
- **WHEN** `outputText` 是 JSON 字符串且 JSONPath 提取字符串值满足 `equals`
|
||||
- **THEN** LLM checker SHALL 判定该 output json 规则通过
|
||||
|
||||
#### Scenario: output JSONPath 对象 equals 通过
|
||||
- **WHEN** `outputText` 是 JSON 字符串且 JSONPath 提取对象值满足 `equals`
|
||||
- **THEN** LLM checker SHALL 使用深度相等判定该 output json 规则通过
|
||||
|
||||
#### Scenario: output JSONPath 存在性默认语义
|
||||
- **WHEN** `outputText` 是 JSON 字符串且 target 配置 `expect.output: [{json: {path: "$.status"}}]`
|
||||
- **THEN** resolve 阶段 SHALL 将该 expectation 的 matcher 物化为 `{exists: true}`,运行期按存在性语义执行
|
||||
|
||||
#### Scenario: output 规则按顺序快速失败
|
||||
- **WHEN** `expect.output` 包含多个 expectation 且第一条 expectation 失败
|
||||
- **THEN** LLM checker SHALL 返回第一条失败 expectation 的 mismatch failure,不继续校验后续 output expectation
|
||||
|
||||
### Requirement: LLM Stream 断言
|
||||
LLM checker SHALL 仅允许 `mode: stream` 使用 `expect.stream`。仅当用户配置了 `expect.stream` 且未配置 `expect.stream.completed` 时,resolve 阶段 SHALL 在 Resolved expect 中物化默认 `completed: true`;LLM checker MUST NOT 因为 `llm.mode: stream` 自动添加 `stream.completed` 断言。`expect.stream.firstTokenMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation`,且仅统计第一个非空 `text-delta` 事件耗时,不统计 reasoning、tool call 或 source 事件。
|
||||
|
||||
#### Scenario: stream completed 默认值
|
||||
- **WHEN** target 配置 `llm.mode: stream` 且配置 `expect.stream: {}`
|
||||
- **THEN** resolve 阶段 SHALL 在 Resolved expect 中物化 `stream.completed: true` 并要求 SDK stream 正常完成
|
||||
|
||||
#### Scenario: 未配置 expect.stream 不添加 completed
|
||||
- **WHEN** target 配置 `llm.mode: stream` 但未配置 `expect.stream`
|
||||
- **THEN** resolve 阶段 SHALL NOT 自动添加 `stream.completed` 断言
|
||||
|
||||
#### Scenario: stream error
|
||||
- **WHEN** `fullStream` 产生 error part
|
||||
- **THEN** LLM checker SHALL 返回 `phase: "stream"` 的 failure
|
||||
|
||||
#### Scenario: firstTokenMs 达标
|
||||
- **WHEN** target 配置 `expect.stream.firstTokenMs: {lte: 1000}` 且首个非空 text delta 耗时满足 matcher
|
||||
- **THEN** LLM checker SHALL 判定 firstTokenMs 断言通过
|
||||
|
||||
#### Scenario: firstTokenMs 缺失
|
||||
- **WHEN** target 配置 `expect.stream.firstTokenMs` 但 stream 未产生非空 text delta
|
||||
- **THEN** LLM checker SHALL 返回 `phase: "stream"` 的 mismatch failure
|
||||
|
||||
#### Scenario: APICallError 不被默认 completed 阻断
|
||||
- **WHEN** `mode: stream` 的 SDK 调用在 stream 启动前抛出带 HTTP status 的 `APICallError`
|
||||
- **THEN** 默认 `stream.completed=true` SHALL NOT 阻断基于 status 和 headers 的 APICallError 状态探测
|
||||
|
||||
### Requirement: LLM Failure Phase 与状态摘要
|
||||
LLM checker SHALL 使用 `request`、`status`、`headers`、`stream`、`output`、`finishReason`、`rawFinishReason`、`usage`、`duration` 作为第一版 failure phase。成功结果的 API detail SHALL 从持久化 observation 动态构造,简短描述 provider、mode、HTTP status、finish reason、raw finish reason、first token、输出长度和 token usage 中可用的信息。observation 和 detail MUST NOT 写入完整 prompt、完整输出或 key。
|
||||
|
||||
#### Scenario: request failure
|
||||
- **WHEN** 模型请求因网络错误、认证调用异常、AbortSignal 或无 HTTP metadata 的 SDK 错误失败
|
||||
- **THEN** LLM checker SHALL 返回 `phase: "request"` 的 error failure
|
||||
|
||||
#### Scenario: output mismatch failure
|
||||
- **WHEN** 模型输出不满足 `expect.output`
|
||||
- **THEN** LLM checker SHALL 返回 `phase: "output"` 的 mismatch failure
|
||||
|
||||
#### Scenario: 非流式成功摘要
|
||||
- **WHEN** `provider: openai` 的非流式检查成功
|
||||
- **THEN** detail SHALL 使用类似 `LLM openai http 200 finish=stop, output=2 chars, usage=12/2 tokens` 的简短格式
|
||||
|
||||
#### Scenario: 流式成功摘要
|
||||
- **WHEN** `provider: anthropic` 的流式检查成功且存在 raw finish reason
|
||||
- **THEN** detail SHALL 使用类似 `LLM anthropic stream 200 finish=stop raw=end_turn, firstToken=624ms, output=2 chars` 的简短格式
|
||||
|
||||
#### Scenario: serialize 展示文本
|
||||
- **WHEN** store 同步 LLM target
|
||||
- **THEN** LLM checker `serialize()` SHALL 返回类似 `openai:gpt-4o-mini @ https://api.openai.com/v1` 的 target 展示文本和 resolved config JSON
|
||||
|
||||
### Requirement: LLM Checker 测试策略
|
||||
LLM checker 的自动化测试 MUST 不访问真实外部模型服务。测试 SHALL 使用本地 mock HTTP/SSE 服务模拟 OpenAI Chat Completions、OpenAI Responses 和 Anthropic Messages 的成功、错误和流式响应。测试 SHALL 覆盖 schema、语义校验、defaults 合并、变量替换、provider factory、observation、expect、execute、registry 注册、配置加载和 JSON Schema 导出。
|
||||
|
||||
#### Scenario: 本地 mock provider 测试成功路径
|
||||
- **WHEN** 测试运行 LLM checker 的 OpenAI、OpenAI Responses 和 Anthropic 成功路径
|
||||
- **THEN** 测试 SHALL 使用本地 mock 服务返回 provider 响应,不依赖外部网络或真实 API key
|
||||
|
||||
#### Scenario: 本地 mock provider 测试错误路径
|
||||
- **WHEN** 测试运行 401、429、500、超时、stream error、stream abort、缺 usage 或无文本输出路径
|
||||
- **THEN** 测试 SHALL 断言 LLM checker 返回符合 spec 的 matched、failure phase、actual、detail 和 observation
|
||||
|
||||
#### Scenario: 质量检查覆盖 LLM checker
|
||||
- **WHEN** 实现完成后执行质量检查
|
||||
- **THEN** `bun run schema:check`、`bun run check` SHALL 通过
|
||||
@@ -1,375 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义拨测系统的 REST API 端点:Dashboard 聚合 API、单目标指标 API、历史记录 API、Meta 信息 API、共享类型定义和 API 错误处理。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Dashboard 聚合 API
|
||||
系统 SHALL 提供 `GET /api/dashboard` 端点,返回 Dashboard 首屏所需的总览统计和目标列表数据。targets 列表 SHALL 仅包含活跃目标。
|
||||
|
||||
#### Scenario: 获取 Dashboard 数据
|
||||
- **WHEN** 客户端请求 `GET /api/dashboard?window=24h&recentLimit=30`
|
||||
- **THEN** 系统 SHALL 返回 JSON 包含 summary 和 targets 字段,targets 仅包含 active=1 的目标
|
||||
|
||||
#### Scenario: summary 字段
|
||||
- **WHEN** Dashboard 响应包含 summary
|
||||
- **THEN** summary SHALL 仅统计活跃目标:total(活跃目标数)、up(活跃正常目标数)、down(活跃异常目标数)、lastCheckTime(最近一次检查时间)、incidents(活跃目标在指定窗口内异常事件数)、window(from/to/label)字段
|
||||
|
||||
#### Scenario: targets 字段
|
||||
- **WHEN** Dashboard 响应包含 targets
|
||||
- **THEN** targets 数组中每个元素 SHALL 为活跃目标,包含目标基本信息(id、name、description、group、type、target、interval)、latestCheck、stats、currentStreak 和 recentSamples 字段,其中 name 和 description 均为 null 或字符串
|
||||
|
||||
#### Scenario: target name 字段为 null
|
||||
- **WHEN** 某个 target 未配置 `name` 或显式配置 `name: null`
|
||||
- **THEN** Dashboard targets 响应中对应元素 SHALL 返回 `name: null`
|
||||
|
||||
#### Scenario: target description 字段
|
||||
- **WHEN** 某个 target 配置了 `description`
|
||||
- **THEN** Dashboard targets 响应中对应元素 SHALL 返回该 description 值
|
||||
|
||||
#### Scenario: window 参数缺失
|
||||
- **WHEN** 客户端请求 `GET /api/dashboard` 未提供 window 参数
|
||||
- **THEN** 系统 SHALL 默认使用 window=`24h`
|
||||
|
||||
#### Scenario: window 参数语义
|
||||
- **WHEN** 系统处理 Dashboard 请求
|
||||
- **THEN** 系统 SHALL 以服务端当前时间作为 window.to,以 window 参数换算 window.from,并在响应中回显 window.from、window.to、window.label
|
||||
|
||||
#### Scenario: window 参数有效值
|
||||
- **WHEN** 客户端请求 Dashboard 端点并指定 window 参数
|
||||
- **THEN** 系统 SHALL 接受 `24h` 作为有效值;其他值 SHALL 返回 400 状态码和错误信息
|
||||
|
||||
#### Scenario: 不支持的 window 参数
|
||||
- **WHEN** 客户端请求 `GET /api/dashboard?window=abc`
|
||||
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
|
||||
|
||||
#### Scenario: recentLimit 参数缺失
|
||||
- **WHEN** 客户端请求 `GET /api/dashboard?window=24h` 未提供 recentLimit 参数
|
||||
- **THEN** 系统 SHALL 默认使用 recentLimit=30
|
||||
|
||||
#### Scenario: 不支持的 recentLimit 参数
|
||||
- **WHEN** 客户端请求 `GET /api/dashboard?recentLimit=0` 或超过系统上限的 recentLimit
|
||||
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
|
||||
|
||||
#### Scenario: 目标无历史记录
|
||||
- **WHEN** 某目标尚未执行过任何拨测
|
||||
- **THEN** 其 latestCheck 为 null,recentSamples 为空数组,stats.totalChecks 为 0,stats.availability 为 0,currentStreak 为 null
|
||||
|
||||
### Requirement: Dashboard 指标字段
|
||||
Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态字段。
|
||||
|
||||
#### Scenario: 目标 stats 字段
|
||||
- **WHEN** Dashboard 响应包含目标 stats
|
||||
- **THEN** stats SHALL 包含 totalChecks、upChecks、downChecks、availability 字段,且这些字段 SHALL 基于请求 window 对应的时间范围计算
|
||||
|
||||
#### Scenario: 目标 currentStreak 字段
|
||||
- **WHEN** Dashboard 响应包含目标 currentStreak
|
||||
- **THEN** currentStreak SHALL 为 `{ up: boolean, count: number, capped?: boolean }` 或 null
|
||||
|
||||
#### Scenario: currentStreak 达到 recentLimit
|
||||
- **WHEN** 连续状态次数达到 recentLimit 上限
|
||||
- **THEN** currentStreak.capped SHALL 为 true
|
||||
|
||||
#### Scenario: recentSamples 字段
|
||||
- **WHEN** Dashboard 响应包含 recentSamples
|
||||
- **THEN** 每个 recentSamples 元素 SHALL 包含 timestamp、durationMs、up 字段,其中 up 为 boolean 且等于 matched
|
||||
|
||||
### Requirement: 单目标指标 API
|
||||
系统 SHALL 提供 `GET /api/targets/:id/metrics` 端点,返回单个目标在指定时间窗口内的概览统计和趋势数据。仅活跃目标的指标 SHALL 可查询。
|
||||
|
||||
#### 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` 且该目标不存在或 active=0
|
||||
- **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: 指标统计字段
|
||||
单目标指标 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 可查询。
|
||||
|
||||
#### Scenario: 获取指定时间范围内的历史记录
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&page=1&pageSize=20` 且该目标为活跃目标
|
||||
- **THEN** 系统 SHALL 返回带分页信息的历史记录,包含 items、total、page、pageSize,按时间倒序排列
|
||||
|
||||
#### Scenario: 使用默认分页参数
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO`(未指定 page 或 pageSize)
|
||||
- **THEN** 系统 SHALL 使用默认 page=1, pageSize=20
|
||||
|
||||
#### Scenario: from 或 to 参数缺失
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/history` 未提供 from 或 to 参数
|
||||
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
|
||||
|
||||
#### Scenario: 目标不存在或非活跃
|
||||
- **WHEN** 客户端请求 `GET /api/targets/999/history?from=ISO&to=ISO` 且该目标不存在或 active=0
|
||||
- **THEN** 系统 SHALL 返回 404 状态码和错误信息
|
||||
|
||||
### Requirement: 新增共享类型
|
||||
系统 SHALL 在 `src/shared/api.ts` 中定义 Dashboard 和 Metrics 相关共享类型。CheckResult SHALL 包含 durationMs(null | number)、failure(CheckFailure | null)、matched(boolean)、detail(null | string)、observation(Record<string, unknown> | null)、timestamp(string)。其中 detail 替代原 statusDetail 字段名。
|
||||
|
||||
#### Scenario: DashboardResponse 类型
|
||||
- **WHEN** 前后端共享 `DashboardResponse` 类型
|
||||
- **THEN** 该类型 SHALL 包含 summary 和 targets 字段
|
||||
|
||||
#### Scenario: TargetStatus 类型
|
||||
- **WHEN** 前后端共享 `TargetStatus` 类型
|
||||
- **THEN** 该类型 SHALL 包含目标基本信息字段(id、name、description、group、type、target、interval)、stats(totalChecks、upChecks、downChecks、availability)、currentStreak 和 recentSamples 字段,其中 name 和 description 类型均为 null 或字符串
|
||||
|
||||
#### Scenario: TargetMetricsResponse 类型
|
||||
- **WHEN** 前后端共享 `TargetMetricsResponse` 类型
|
||||
- **THEN** 该类型 SHALL 包含 targetId、window、stats 和 trend 字段
|
||||
|
||||
#### Scenario: TrendPoint 类型
|
||||
- **WHEN** 前后端共享 `TrendPoint` 类型
|
||||
- **THEN** 该类型 SHALL 包含 bucketStart、avgDurationMs、minDurationMs、maxDurationMs、availability、totalChecks、upChecks、downChecks 字段
|
||||
|
||||
#### Scenario: CheckResult 类型变更
|
||||
- **WHEN** 前端或后端引用 CheckResult 类型
|
||||
- **THEN** 该类型 SHALL 包含 `timestamp: string`、`matched: boolean`、`durationMs: number | null`、`detail: string | null`、`observation: Record<string, unknown> | null`、`failure` 字段,不包含 statusDetail 字段,不包含 success 字段
|
||||
|
||||
#### Scenario: RecentSample 类型
|
||||
- **WHEN** 前后端共享 `RecentSample` 类型
|
||||
- **THEN** 该类型 SHALL 包含 `timestamp: string`、`durationMs: number | null`、`up: boolean` 字段,其中 up 为 boolean 且等于 matched
|
||||
|
||||
#### Scenario: HistoryResponse 类型
|
||||
- **WHEN** 前后端共享 `HistoryResponse` 类型
|
||||
- **THEN** 该类型 SHALL 包含 `items: CheckResult[]`、`total: number`、`page: number`、`pageSize: number` 字段
|
||||
|
||||
#### Scenario: API 序列化构造 detail
|
||||
- **WHEN** API 路由序列化 StoredCheckResult 为 API 响应
|
||||
- **THEN** 系统 SHALL 从 StoredCheckResult 中反序列化 observation,根据 target type 通过 checkerRegistry 获取对应 checker 并调用 buildDetail(observation) 动态生成 detail 字段
|
||||
|
||||
#### Scenario: mapCheckResult 接收 type 参数
|
||||
- **WHEN** 序列化辅助函数 mapCheckResult 被调用
|
||||
- **THEN** 函数 SHALL 接收 target type 参数,用于从 registry 获取对应 checker 调用 buildDetail
|
||||
|
||||
#### Scenario: Dashboard API 传递 type
|
||||
- **WHEN** Dashboard 路由序列化 latestCheck
|
||||
- **THEN** 路由 SHALL 将 target.type 传递给 mapCheckResult
|
||||
|
||||
#### Scenario: History API 传递 type
|
||||
- **WHEN** History 路由序列化历史记录列表
|
||||
- **THEN** 路由 SHALL 将已查询的 target.type 传递给 mapCheckResult
|
||||
|
||||
### Requirement: 保留健康检查端点
|
||||
系统 SHALL 保留 `GET /health` 端点,不受拨测功能影响。
|
||||
|
||||
#### Scenario: 访问健康检查
|
||||
- **WHEN** 客户端请求 `GET /health`
|
||||
- **THEN** 系统 SHALL 返回与之前格式一致的健康检查响应
|
||||
|
||||
### Requirement: API 错误处理
|
||||
系统 SHALL 对不存在的目标 ID、非活跃目标、无效参数和超出范围的分页参数返回适当的 HTTP 错误响应。非活跃目标与不存在的目标 SHALL 返回相同的 404 响应。
|
||||
|
||||
#### Scenario: 查询不存在的目标
|
||||
- **WHEN** 客户端请求 `GET /api/targets/999/metrics?from=ISO&to=ISO`
|
||||
- **THEN** 系统 SHALL 返回 404 状态码和错误信息
|
||||
|
||||
#### Scenario: 查询非活跃目标
|
||||
- **WHEN** 客户端请求 `GET /api/targets/<id>/metrics?from=ISO&to=ISO` 且该目标 active=0
|
||||
- **THEN** 系统 SHALL 返回 404 状态码和错误信息
|
||||
|
||||
#### Scenario: 无效的 from/to 参数
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=invalid&to=ISO`
|
||||
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
|
||||
|
||||
#### Scenario: from 晚于 to
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=<较晚时间>&to=<较早时间>`
|
||||
- **THEN** 系统 SHALL 返回 400 状态码和错误信息,提示 from 必须早于 to
|
||||
|
||||
#### Scenario: 无效的分页参数
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&page=abc`
|
||||
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
|
||||
|
||||
#### Scenario: pageSize 超过上限
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&pageSize=201`
|
||||
- **THEN** 系统 SHALL 返回 400 状态码和错误信息,提示 pageSize 不能超过 200
|
||||
|
||||
#### Scenario: pageSize 等于上限
|
||||
- **WHEN** 客户端请求 `GET /api/targets/1/history?from=ISO&to=ISO&pageSize=200`
|
||||
- **THEN** 系统 SHALL 正常返回数据
|
||||
|
||||
#### Scenario: 无效的目标 ID
|
||||
- **WHEN** 客户端请求 `GET /api/targets/abc/metrics?from=ISO&to=ISO`
|
||||
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
|
||||
|
||||
### Requirement: 失败信息 API 契约
|
||||
系统 SHALL 通过 API 返回结构化 failure 信息,供 Dashboard 展示和后续排查。
|
||||
|
||||
#### Scenario: 返回 expect 不匹配信息
|
||||
- **WHEN** 最近一次检查结果包含 failure.kind=`mismatch`
|
||||
- **THEN** `/api/targets` 和 `/api/targets/:id/history` SHALL 返回该 failure 的 kind、phase、path、expected、actual、message 字段
|
||||
|
||||
#### Scenario: 无失败信息
|
||||
- **WHEN** 检查结果 matched=true
|
||||
- **THEN** API SHALL 返回 failure 为 null
|
||||
|
||||
### Requirement: Meta 信息 API
|
||||
系统 SHALL 提供 `GET /api/meta` 端点,返回系统运行时元数据,包括应用版本号和 checker 类型列表。未匹配 method SHALL 按 API 通配符处理为 JSON 404。
|
||||
|
||||
#### Scenario: 获取 checker 类型列表和版本号
|
||||
- **WHEN** 客户端请求 `GET /api/meta`
|
||||
- **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 响应
|
||||
|
||||
### Requirement: MetaResponse 共享类型
|
||||
系统 SHALL 在 `src/shared/api.ts` 中定义 `MetaResponse` 类型。
|
||||
|
||||
#### Scenario: MetaResponse 类型定义
|
||||
- **WHEN** 前后端引用 `MetaResponse` 类型
|
||||
- **THEN** 该类型 SHALL 包含 `checkerTypes: string[]` 和 `version: string` 字段
|
||||
@@ -1,728 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义拨测工具的 YAML 配置文件格式、解析校验规则和 CLI 启动流程。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 旧配置入口拒绝
|
||||
系统 SHALL 拒绝旧版顶层 `runtime` 和顶层 `logging` 配置入口。系统 SHALL 拒绝旧版 `server.host`、`server.port` 和 `server.dataDir` 入口,并要求使用 `server.listen` 与 `server.storage` 下的新路径。
|
||||
|
||||
#### Scenario: 顶层 runtime 被拒绝
|
||||
- **WHEN** 配置文件声明顶层 `runtime`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示存在未知字段 `runtime`
|
||||
|
||||
#### Scenario: 顶层 logging 被拒绝
|
||||
- **WHEN** 配置文件声明顶层 `logging`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示存在未知字段 `logging`
|
||||
|
||||
#### Scenario: server 旧监听字段被拒绝
|
||||
- **WHEN** 配置文件声明 `server.host` 或 `server.port`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示 `server` 中存在未知字段
|
||||
|
||||
#### Scenario: server 旧 dataDir 字段被拒绝
|
||||
- **WHEN** 配置文件声明 `server.dataDir`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示 `server` 中存在未知字段 `dataDir`
|
||||
|
||||
### Requirement: YAML 配置文件格式
|
||||
系统 SHALL 支持通过 YAML 配置文件定义全部运行参数,包括 server 配置、probes 执行配置、可选的 variables 段和 typed target 列表(含可选 group 字段)。server 配置 SHALL 将 HTTP 监听参数放在 `server.listen` 分组,将本地数据目录和历史数据保留时长放在 `server.storage` 分组,将运行时日志配置放在 `server.logging` 分组。拨测全局执行策略 SHALL 放在 `probes.execution` 分组。target MUST 使用 `id` 字段作为唯一标识符,MUST 使用 `type` 字段声明 checker 类型,SHALL 支持可选的 `name` 字段作为展示名称元信息,SHALL 支持可选的 `description` 字段作为目标说明。`name` 和 `description` 均 SHALL 允许省略或显式配置为 `null`;省略或显式 null 时解析结果 SHALL 保留为 null。HTTP 领域字段 MUST 放在 `http` 分组,cmd 领域字段 MUST 放在 `cmd` 分组,db 领域字段 MUST 放在 `db` 分组,tcp 领域字段 MUST 放在 `tcp` 分组,icmp 领域字段 MUST 放在 `icmp` 分组,udp 领域字段 MUST 放在 `udp` 分组,LLM 领域字段 MUST 放在 `llm` 分组。HTTP target 的 `http` 分组 SHALL 支持可选的 `ignoreSSL`(布尔值)和 `maxRedirects`(非负整数)字段。Db target 的 `db` 分组 SHALL 支持 `url`(必填)和 `query`(可选)字段。Tcp target 的 `tcp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`readBanner`(可选)、`bannerReadTimeout`(可选)和 `maxBannerBytes`(可选)字段。Icmp target 的 `icmp` 分组 SHALL 支持 `host`(必填)、`count`(可选,默认 3)和 `packetSize`(可选,默认 56)字段。Udp target 的 `udp` 分组 SHALL 支持 `host`(必填)、`port`(必填)、`payload`(可选,默认空字符串)、`encoding`(可选,默认 `text`)、`responseEncoding`(可选,默认 `text`)和 `maxResponseBytes`(可选,默认 4096)字段。LLM target 的 `llm` 分组 SHALL 支持 `provider`(必填)、`url`(必填)、`model`(必填)、`prompt`(必填)、`mode`(可选,默认 `http`)、`key`(可选,默认空字符串)、`authToken`(可选)、`headers`(可选)、`ignoreSSL`(可选,默认 `false`)、`options`(可选)和 `providerOptions`(可选)字段。顶层 `defaults` 不再是合法配置段。
|
||||
|
||||
#### Scenario: 完整配置文件解析
|
||||
- **WHEN** 系统启动并读取包含 server.listen、server.storage、server.logging、probes.execution、variables 和 targets(含 id、group 字段)的 YAML 配置文件
|
||||
- **THEN** 系统 SHALL 正确解析所有字段并用于初始化服务、调度引擎和对应 checker runner
|
||||
|
||||
#### Scenario: defaults 配置段被拒绝
|
||||
- **WHEN** 配置文件声明顶层 `defaults`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示存在未知字段 `defaults`
|
||||
|
||||
#### Scenario: 最简 HTTP 配置文件解析
|
||||
- **WHEN** 系统读取只包含一个 `type: http` target(含 `id` 和 `http.url`)的 YAML 配置文件(省略 server、probes、variables 和 expect)
|
||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(host=127.0.0.1, port=3000, dataDir=./data, interval=30s, timeout=10s, probes.execution.maxConcurrentChecks=20, server.storage.retention=7d, http.method=GET, http.maxBodyBytes=100MB, http.ignoreSSL=false, http.maxRedirects=0, group=default),并保留 name=null、description=null
|
||||
|
||||
#### Scenario: 最简 cmd 配置文件解析
|
||||
- **WHEN** 系统读取只包含一个 `type: cmd` target(含 `id` 和 `cmd.exec`)的 YAML 配置文件
|
||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, cmd.cwd 为配置文件所在目录, cmd.maxOutputBytes=100MB),并保留 name=null、description=null
|
||||
|
||||
#### Scenario: per-target 配置覆盖内置默认值
|
||||
- **WHEN** 某个 target 指定 interval、timeout 或对应领域分组中的可选字段
|
||||
- **THEN** 该 target SHALL 使用其自身的值,不受对应内置默认值影响
|
||||
|
||||
#### 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: 最简 db 配置文件解析
|
||||
- **WHEN** 系统读取只包含一个 `type: db` target(含 `id` 和 `db.url`)的 YAML 配置文件
|
||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group="default"),并保留 name=null、description=null
|
||||
|
||||
#### Scenario: 最简 tcp 配置文件解析
|
||||
- **WHEN** 系统读取只包含一个 `type: tcp` target(含 `id`、`tcp.host` 和 `tcp.port`)的 YAML 配置文件
|
||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group=default, tcp.readBanner=false, tcp.bannerReadTimeout=2000, tcp.maxBannerBytes=4096),并保留 name=null、description=null
|
||||
|
||||
#### Scenario: per-target http.method 仍然有效
|
||||
- **WHEN** HTTP target 配置 `http.method: POST`
|
||||
- **THEN** 系统 SHALL 使用 POST 作为该 target 的请求方法
|
||||
|
||||
#### Scenario: 未配置 http.method 使用内置默认值
|
||||
- **WHEN** HTTP target 未配置 `http.method`
|
||||
- **THEN** 系统 SHALL 使用内置默认值 GET 作为该 target 的请求方法
|
||||
|
||||
#### Scenario: 最简 icmp 配置文件解析
|
||||
- **WHEN** 系统读取只包含一个 `type: icmp` target(含 `id` 和 `icmp.host`)的 YAML 配置文件
|
||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group=default, icmp.count=3, icmp.packetSize=56),并保留 name=null、description=null
|
||||
|
||||
#### Scenario: 最简 udp 配置文件解析
|
||||
- **WHEN** 系统读取只包含一个 `type: udp` target(含 `id`、`udp.host` 和 `udp.port`)的 YAML 配置文件
|
||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段(interval=30s, timeout=10s, group=default, udp.payload=空字符串, udp.encoding=text, udp.responseEncoding=text, udp.maxResponseBytes=4096),并保留 name=null、description=null
|
||||
|
||||
#### Scenario: 最简 llm 配置文件解析
|
||||
- **WHEN** 系统读取只包含一个 `type: llm` target(含 `id`、`llm.provider`、`llm.url`、`llm.model` 和 `llm.prompt`)的 YAML 配置文件
|
||||
- **THEN** 系统 SHALL 使用内置默认值填充未指定字段(interval=30s, timeout=10s, group=default, llm.mode=http, llm.key=空字符串, llm.ignoreSSL=false, llm.options.maxOutputTokens=16, llm.options.temperature=0),并保留 name=null、description=null
|
||||
|
||||
### Requirement: CLI 参数
|
||||
系统 SHALL 通过单一命令行参数接受 YAML 配置文件路径。
|
||||
|
||||
#### Scenario: 指定配置文件启动
|
||||
- **WHEN** 用户执行 `./dial-server ./probes.yaml`
|
||||
- **THEN** 系统 SHALL 读取并解析指定路径的 YAML 文件作为配置
|
||||
|
||||
#### Scenario: 未提供配置文件路径
|
||||
- **WHEN** 用户启动程序时未提供任何命令行参数
|
||||
- **THEN** 系统 SHALL 以错误退出并提示需要指定配置文件路径
|
||||
|
||||
#### Scenario: 配置文件不存在
|
||||
- **WHEN** 用户指定的配置文件路径不存在
|
||||
- **THEN** 系统 SHALL 以错误退出并提示文件不存在
|
||||
|
||||
### Requirement: 配置校验
|
||||
系统 SHALL 在启动时对 YAML 配置进行完整校验,校验失败时以非零状态退出并输出清晰的错误信息。系统 SHALL 使用 TypeBox 定义 Authoring Config 和 Normalized Config 的配置契约,并使用 Ajv 校验 TypeBox 生成的 JSON Schema。配置加载流程 SHALL 明确区分 `AuthoringProbeConfig`、`NormalizedProbeConfig`、`ValidatedProbeConfig`、`ResolvedConfig` 生命周期,并在 YAML 解析之后、Normalized schema 校验之前执行 `normalizeAuthoringConfig()`。该 normalizer SHALL 只负责去糖:变量替换、expect primitive/keyed/content 简写展开、移除 `variables` 段。该 normalizer MUST NOT 注入默认值、解析 duration/size/path/env,或合并 `cmd.env`。
|
||||
|
||||
JSON Schema 契约 SHALL 覆盖业务无关的结构规则,包括字段类型、必填字段、枚举、数组与对象形状、数值范围和未知字段。Authoring schema SHALL 用于导出的 `probe-config.schema.json`,描述用户 YAML 可书写形式,包括变量引用和 expect 简写。Normalized schema SHALL 用于运行时 AJV 校验,描述 normalizer 输出结果,不接受变量引用、不接受 expect primitive 简写、不包含 `variables` 段。语义 validator SHALL 覆盖契约不适合表达的业务规则,包括 target id 唯一性、id 命名规则校验、checker type 注册状态、时长和大小解析、HTTP URL、正则可编译、JSONPath 子集和 XPath 可编译。
|
||||
|
||||
契约校验、normalizer 和语义 validator SHALL 统一产出 `ConfigValidationIssue`,最终由配置加载流程统一渲染为中文错误信息。除 `headers`、`env`、Authoring `variables` 等明确声明为动态键值表的对象外,配置中的未知字段 SHALL 导致启动期配置错误。系统 MUST NOT 静默忽略未知字段。
|
||||
|
||||
#### Scenario: target 缺少必填字段
|
||||
- **WHEN** YAML 中某个 target 缺少 id 或 type 字段
|
||||
- **THEN** 系统 SHALL 以错误退出,提示哪个 target 缺少哪个字段
|
||||
|
||||
#### Scenario: 顶层 defaults 字段非法
|
||||
- **WHEN** YAML 配置文件声明顶层 `defaults`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 `defaults` 是未知字段
|
||||
|
||||
#### Scenario: HTTP target 缺少 url
|
||||
- **WHEN** YAML 中某个 target 配置 `type: http` 但缺少 `http.url`
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 http.url 字段
|
||||
|
||||
#### Scenario: cmd target 缺少 exec
|
||||
- **WHEN** YAML 中某个 target 配置 `type: cmd` 但缺少 `cmd.exec`
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 cmd.exec 字段
|
||||
|
||||
#### Scenario: target type 非法
|
||||
- **WHEN** YAML 中某个 target 的 type 不是已注册 checker 类型
|
||||
- **THEN** 系统 SHALL 以错误退出,提示不支持的 target type 和当前支持的 type 列表
|
||||
|
||||
#### Scenario: target id 重复
|
||||
- **WHEN** YAML 中存在两个 id 相同的 target
|
||||
- **THEN** 系统 SHALL 以错误退出,提示重复的 id
|
||||
|
||||
#### Scenario: target id 不合法
|
||||
- **WHEN** YAML 中某个 target 的 id 不符合 `[a-zA-Z0-9][a-zA-Z0-9_-]*` 规则
|
||||
- **THEN** 系统 SHALL 以错误退出,提示 id 命名不合法
|
||||
|
||||
#### Scenario: group 字段类型校验
|
||||
- **WHEN** YAML 中某个 target 的 `group` 字段不是字符串
|
||||
- **THEN** 系统 SHALL 以错误退出并提示 group 字段类型错误
|
||||
|
||||
#### Scenario: interval 格式非法
|
||||
- **WHEN** interval 或 timeout 值不是有效的时长格式(如 `30s`、`5m`、`500ms`)
|
||||
- **THEN** 系统 SHALL 以错误退出并提示格式错误
|
||||
|
||||
#### Scenario: maxConcurrentChecks 非法
|
||||
- **WHEN** Normalized Config 中 probes.execution.maxConcurrentChecks 不是正整数
|
||||
- **THEN** 系统 SHALL 以错误退出并提示 probes.execution.maxConcurrentChecks 格式错误
|
||||
|
||||
#### Scenario: interval 或 timeout 解析结果非法
|
||||
- **WHEN** interval 或 timeout 解析结果不是正整数毫秒(如 `0ms` 或 `1.5ms`)
|
||||
- **THEN** 系统 SHALL 以错误退出并提示必须为正整数毫秒
|
||||
|
||||
#### Scenario: size 格式非法
|
||||
- **WHEN** maxBodyBytes 或 maxOutputBytes 值不是有效的 size 格式
|
||||
- **THEN** 系统 SHALL 以错误退出并提示支持 B、KB、MB、GB 格式
|
||||
|
||||
#### Scenario: size 解析结果非法
|
||||
- **WHEN** maxBodyBytes 或 maxOutputBytes 解析结果不是非负安全整数字节数(如 `1.5B`)
|
||||
- **THEN** 系统 SHALL 以错误退出并提示必须为非负安全整数字节数
|
||||
|
||||
#### Scenario: HTTP method 非法
|
||||
- **WHEN** Normalized Config 中某个 HTTP target 的 `http.method` 不是 GET、HEAD、POST、PUT、PATCH、DELETE、OPTIONS 之一
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 method 不合法
|
||||
|
||||
#### Scenario: HTTP method 小写非法
|
||||
- **WHEN** YAML 中某个 HTTP target 配置 `http.method: get`
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 method 必须为大写枚举值
|
||||
|
||||
#### Scenario: URL 格式非法
|
||||
- **WHEN** YAML 中某个 HTTP target 的 `http.url` 不是合法 URL,或协议不是 `http:` / `https:`
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 URL 格式不合法
|
||||
|
||||
#### Scenario: maxRedirects 非法
|
||||
- **WHEN** YAML 中某个 HTTP target 的 `http.maxRedirects` 为负数
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 maxRedirects 必须为非负整数
|
||||
|
||||
#### Scenario: maxRedirects 非整数非法
|
||||
- **WHEN** Normalized Config 中某个 HTTP target 的 `http.maxRedirects` 不是非负整数(如 `1.5` 或 `"5"`)
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 maxRedirects 必须为非负整数
|
||||
|
||||
#### Scenario: ignoreSSL 类型非法
|
||||
- **WHEN** YAML 中某个 HTTP target 的 `http.ignoreSSL` 不是布尔值
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 ignoreSSL 必须为布尔值
|
||||
|
||||
#### Scenario: HTTP headers 类型非法
|
||||
- **WHEN** YAML 中某个 HTTP target 的 `http.headers` 不是对象,或任一 header 值不是字符串
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 http.headers 格式错误
|
||||
|
||||
#### Scenario: HTTP body 类型非法
|
||||
- **WHEN** YAML 中某个 HTTP target 的 `http.body` 已配置但不是字符串
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 http.body 必须为字符串
|
||||
|
||||
#### Scenario: maxBodyBytes 数字非法
|
||||
- **WHEN** YAML 中某个 HTTP target 的 `http.maxBodyBytes` 是负数、非整数或非安全整数
|
||||
- **THEN** 系统 SHALL 以错误退出,提示 maxBodyBytes 必须为非负安全整数字节数或合法 size 字符串
|
||||
|
||||
#### Scenario: status 模式非法
|
||||
- **WHEN** YAML 中某个 HTTP target 的 `expect.status` 包含不符合 `1xx` 到 `5xx` 格式的字符串(如 `"abc"`、`"2x"`、`"6xx"`)
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 status 模式不合法
|
||||
|
||||
#### Scenario: status 数字非法
|
||||
- **WHEN** YAML 中某个 HTTP target 的 `expect.status` 包含非整数或不在 100-599 范围内的数字
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 status 数字不合法
|
||||
|
||||
#### Scenario: durationMs matcher 非法
|
||||
- **WHEN** Normalized Config 中某个 target 的 `expect.durationMs` 不是合法 `ValueMatcher` 对象
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 expect.durationMs 格式错误
|
||||
|
||||
#### Scenario: durationMs 原始值简写在 Authoring schema 合法
|
||||
- **WHEN** 使用 Authoring schema 校验配置文件中 `expect.durationMs: 5000`
|
||||
- **THEN** JSON Schema 校验 SHALL 通过,因为 Authoring schema 接受 primitive 简写
|
||||
|
||||
#### Scenario: durationMs 原始值简写在 Normalized schema 非法
|
||||
- **WHEN** 使用 Normalized schema 校验配置对象中 `expect.durationMs: 5000`
|
||||
- **THEN** JSON Schema 校验 SHALL 失败,因为 Normalized schema 只接受 `ValueMatcher` 对象
|
||||
|
||||
#### Scenario: 变量引用在 Authoring schema 合法
|
||||
- **WHEN** 使用 Authoring schema 校验配置文件中 `server.listen.port: "${PORT|3000}"`
|
||||
- **THEN** JSON Schema 校验 SHALL 通过,因为 Authoring schema 面向用户可书写 YAML
|
||||
|
||||
#### Scenario: 变量引用在 Normalized schema 非法
|
||||
- **WHEN** 使用 Normalized schema 校验配置对象中 `server.listen.port: "${PORT|3000}"`
|
||||
- **THEN** JSON Schema 校验 SHALL 失败,因为 Normalized schema 只接受变量替换后的 integer
|
||||
|
||||
#### Scenario: Authoring schema 对 integer/boolean/enum 字段接受变量引用
|
||||
- **WHEN** 使用 Authoring schema 校验配置文件中 `http.maxRedirects: "${MAX_REDIRECTS|5}"` 或 `http.ignoreSSL: "${IGNORE_SSL|false}"` 或 `llm.provider: "${PROVIDER|openai}"`
|
||||
- **THEN** JSON Schema 校验 SHALL 通过,因为 Authoring schema 对支持变量替换的 integer/boolean/enum/pattern-string 字段使用 `anyOf: [originalType, {type: "string", pattern: "^\\$\\{[^}]+\\}$"}]` 额外接受完整变量引用字符串
|
||||
|
||||
#### Scenario: icmp target 缺少 host
|
||||
- **WHEN** YAML 中某个 target 配置 `type: icmp` 但缺少 `icmp.host`
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 缺少 icmp.host 字段
|
||||
|
||||
#### Scenario: icmp expect 未知字段
|
||||
- **WHEN** YAML 中 icmp target 的 expect 包含非 icmp expect 字段
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
|
||||
|
||||
#### Scenario: HTTP expect headers 非法
|
||||
- **WHEN** YAML 中某个 HTTP target 的 `expect.headers` 不是对象,或某个 header 期望不是合法 `RawValueExpectation`
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 expect.headers 格式错误
|
||||
|
||||
#### Scenario: HTTP expect body 必须为数组
|
||||
- **WHEN** YAML 中某个 HTTP target 的 `expect.body` 已配置但不是数组
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 target 的 expect.body 必须为数组
|
||||
|
||||
#### Scenario: HTTP body expectation 缺少支持字段
|
||||
- **WHEN** YAML 中某个 HTTP target 的 `expect.body` 数组项未包含 contains、regex、json、css、xpath 任一支持字段
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 body expectation 缺少支持的 expectation 类型
|
||||
|
||||
#### Scenario: HTTP body expectation 同时配置多个支持字段
|
||||
- **WHEN** YAML 中某个 HTTP target 的同一条 body expectation 同时包含 contains、regex、json、css、xpath 中的多个支持字段
|
||||
- **THEN** 系统 SHALL 以错误退出,提示每条 body expectation 只能配置一种 expectation 类型
|
||||
|
||||
#### Scenario: HTTP body regex 非法
|
||||
- **WHEN** YAML 中某个 HTTP target 的 body regex expectation 不是字符串或不是可编译正则表达式
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 body regex 不合法
|
||||
|
||||
#### Scenario: HTTP body json path 非法
|
||||
- **WHEN** YAML 中某个 HTTP target 的 body json expectation 缺少 path,或 path 不符合系统支持的 JSONPath 子集
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 body json path 不合法
|
||||
|
||||
#### Scenario: HTTP body css selector 非法
|
||||
- **WHEN** YAML 中某个 HTTP target 的 body css expectation 缺少 selector,或 selector 不是非空字符串
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 body css selector 不合法
|
||||
|
||||
#### Scenario: HTTP body xpath path 非法
|
||||
- **WHEN** YAML 中某个 HTTP target 的 body xpath expectation 缺少 path,或 path 不是非空字符串,或可被现有 XPath 库静态判定为语法错误
|
||||
- **THEN** 系统 SHALL 以错误退出,提示该 body xpath path 不合法
|
||||
|
||||
#### Scenario: expect matcher 类型非法
|
||||
- **WHEN** YAML 中某个 expect matcher 的 regex 不是可编译正则字符串,empty/exists 不是布尔值,或 gt/gte/lt/lte 不是有限数字
|
||||
- **THEN** 系统 SHALL 以错误退出,提示对应 matcher 配置不合法
|
||||
|
||||
#### Scenario: expect match 字段不再支持
|
||||
- **WHEN** YAML 中某个 expect matcher 配置 `match` 字段
|
||||
- **THEN** 系统 SHALL 以错误退出,提示 `match` 是未知字段,请使用 `regex`
|
||||
|
||||
#### Scenario: unknown 字段失败
|
||||
- **WHEN** YAML 中任一结构化配置对象包含契约未声明的字段,且该对象不是明确允许动态键的对象
|
||||
- **THEN** 系统 SHALL 以错误退出,提示未知字段所在路径
|
||||
|
||||
#### Scenario: 动态 headers 字段允许
|
||||
- **WHEN** YAML 中 `http.headers` 或 `expect.headers` 包含任意 header 名称,且对应值符合契约
|
||||
- **THEN** 系统 SHALL 接受这些动态 header 名称
|
||||
|
||||
#### Scenario: 动态 env 字段允许
|
||||
- **WHEN** YAML 中 `cmd.env` 包含任意环境变量名称,且对应值为字符串
|
||||
- **THEN** 系统 SHALL 接受这些动态 env 名称
|
||||
|
||||
#### Scenario: JSON Schema 不修改输入
|
||||
- **WHEN** 系统执行 JSON Schema 契约校验
|
||||
- **THEN** 系统 MUST NOT 通过契约校验器强制转换类型、注入默认值或删除未知字段
|
||||
|
||||
#### Scenario: 变量替换后字段超长由 Normalized schema 的 maxLength 校验拦截
|
||||
- **WHEN** Authoring Config 中 target 的 `description` 通过 `${...}` 变量替换后超过 500 个字符
|
||||
- **THEN** Normalized schema SHALL 在 AJV 校验阶段以错误退出,提示 description 字段长度错误
|
||||
|
||||
#### Scenario: 配置生命周期分离
|
||||
- **WHEN** 系统加载配置文件
|
||||
- **THEN** 系统 SHALL 按 `unknown -> AuthoringProbeConfig -> NormalizedProbeConfig -> ValidatedProbeConfig -> ResolvedConfig` 的顺序执行 YAML 解析、配置去糖、契约校验、语义校验和运行期配置解析
|
||||
|
||||
#### Scenario: Normalized 不补默认值
|
||||
- **WHEN** Authoring Config 中 HTTP target 未配置 `http.method` 和 `expect.status`
|
||||
- **THEN** Normalized Config SHALL 仍不包含这些默认值,checker.resolve() SHALL 在 ResolvedConfig 阶段物化默认 method 和 status 语义
|
||||
|
||||
#### Scenario: 结构化校验 issue
|
||||
- **WHEN** 契约校验、normalizer、语义 validator 或变量替换阶段发现非法配置
|
||||
- **THEN** 系统 SHALL 先生成包含 code、path、message 和可选 targetName 的结构化 `ConfigValidationIssue`,再统一渲染为中文错误信息
|
||||
|
||||
#### Scenario: 导出配置 JSON Schema
|
||||
- **WHEN** 仓库生成或检查配置契约
|
||||
- **THEN** 根目录 SHALL 存在 draft-07 `probe-config.schema.json`,且其内容 SHALL 与当前 Authoring fragments 和已注册 checker Authoring fragments 组装出的完整 schema 一致(包含 variables 段和 target 的 id/name/description 字段,且不包含顶层 defaults)
|
||||
系统 SHALL 支持使用单位字符串配置读取上限,单位包括 `B`、`KB`、`MB` 和 `GB`。
|
||||
|
||||
#### Scenario: 解析 MB
|
||||
- **WHEN** YAML 中配置 `maxBodyBytes: "100MB"`
|
||||
- **THEN** 系统 SHALL 将其解析为 104857600 bytes
|
||||
|
||||
#### Scenario: 解析 KB
|
||||
- **WHEN** YAML 中配置 `maxOutputBytes: "512KB"`
|
||||
- **THEN** 系统 SHALL 将其解析为 524288 bytes
|
||||
|
||||
### Requirement: runtime 并发配置
|
||||
系统 SHALL 支持 `probes.execution.maxConcurrentChecks` 配置全局最大并发检查数。`probes` 和 `probes.execution` 配置段均 SHALL 为可选,省略时使用默认值。
|
||||
|
||||
#### Scenario: 使用默认并发限制
|
||||
- **WHEN** YAML 中未配置 `probes` 或未配置 `probes.execution.maxConcurrentChecks`
|
||||
- **THEN** 系统 SHALL 使用默认值 20
|
||||
|
||||
#### Scenario: 配置并发限制
|
||||
- **WHEN** YAML 中配置 `probes.execution.maxConcurrentChecks: 5`
|
||||
- **THEN** 系统 SHALL 将全局最大并发检查数设置为 5
|
||||
|
||||
### Requirement: YAML 配置使用 Bun 内置解析
|
||||
系统 SHALL 使用 Bun 内置的 `Bun.YAML.parse()` 解析配置文件,不引入外部 YAML 解析库。
|
||||
|
||||
#### Scenario: 解析 YAML 内容
|
||||
- **WHEN** 系统读取 YAML 文件内容
|
||||
- **THEN** 系统 SHALL 调用 `Bun.YAML.parse()` 将内容解析为配置对象
|
||||
|
||||
### Requirement: expect 配置增强
|
||||
系统 SHALL 支持 typed target 的领域专用 expect 配置,并通过共享 `ValueMatcher`、`ContentExpectations` 和 `KeyedExpectations` 表达可复用断言能力。状态类字段 SHALL 保持枚举或布尔语义,包括 HTTP/LLM 的 `status`(支持精确数字和范围模式)、cmd 的 `exitCode`、tcp 的 `connected`、icmp 的 `alive` 和 udp 的 `responded`。Authoring value 类指标字段 SHALL 使用 `RawValueExpectation` 输入,并在 Normalized 阶段归一化为运行期 `ValueExpectation`,包括通用 `durationMs`、db 的 `rowCount`、udp 的 `responseSize`/`sourceHost`/`sourcePort`、icmp 的 `packetLossPercent`/`avgLatencyMs`/`maxLatencyMs`、llm 的 usage token 与 stream 首 token 耗时。Authoring 内容类字段 MUST 使用 `RawContentExpectations` 数组表达配置顺序,并在 Normalized 阶段转换为带 `kind` 的 `ContentExpectation` 数组,包括 HTTP `body`、cmd `stdout`/`stderr`、tcp `banner`、udp `response`、llm `output` 和 db `result`。Authoring 键值类字段 SHALL 使用动态对象,并在 Normalized 阶段转换为 `KeyedExpectations` 数组,包括 HTTP/LLM `headers` 和 db `rows` 中的列值断言。
|
||||
|
||||
配置加载流程 MUST NOT 保留变量替换后的 Raw expect 作为执行路径依赖。语义校验 SHALL 读取 Normalized expect 并报告问题。Store 持久化 MUST NOT 依赖 Raw expect;checker execute SHALL 只消费 Resolved expect。
|
||||
|
||||
#### Scenario: 解析 HTTP expect 配置
|
||||
- **WHEN** YAML 配置文件中 HTTP target 的 expect 包含 status、headers、body expectation 数组和 durationMs matcher
|
||||
- **THEN** Normalized Config SHALL 包含 normalized keyed headers、normalized content body 和 normalized durationMs,checker.resolve() SHALL 生成包含默认 status 的 HTTP Resolved expect
|
||||
|
||||
#### Scenario: 解析 cmd expect 配置
|
||||
- **WHEN** YAML 配置文件中 cmd target 的 expect 包含 exitCode、stdout、stderr 和 durationMs matcher
|
||||
- **THEN** Normalized Config SHALL 包含 normalized stdout/stderr content expectations 和 normalized durationMs,checker.resolve() SHALL 生成包含默认 exitCode 的 cmd Resolved expect
|
||||
|
||||
#### Scenario: 解析 db expect 配置
|
||||
- **WHEN** YAML 配置文件中 db target 的 expect 包含 durationMs、rowCount、rows 和 result
|
||||
- **THEN** Normalized Config SHALL 包含 normalized rowCount、rows keyed expectations 和 result content expectations
|
||||
|
||||
#### Scenario: 解析 tcp expect 配置
|
||||
- **WHEN** YAML 配置文件中 tcp target 的 expect 包含 connected、banner expectation 数组和 durationMs matcher
|
||||
- **THEN** 系统 SHALL 保留 Raw tcp expect 快照,并生成包含默认 connected、resolved banner content expectations 和 resolved durationMs 的 tcp Resolved expect
|
||||
|
||||
#### Scenario: 解析 icmp expect 配置
|
||||
- **WHEN** YAML 配置文件中 icmp target 的 expect 包含 alive、packetLossPercent、avgLatencyMs、maxLatencyMs 和 durationMs matcher
|
||||
- **THEN** 系统 SHALL 保留 Raw icmp expect 快照,并生成包含默认 alive 和 resolved 数值 expectations 的 icmp Resolved expect
|
||||
|
||||
#### Scenario: 解析 udp expect 配置
|
||||
- **WHEN** YAML 配置文件中 udp target 的 expect 包含 responded、response、responseSize、sourceHost、sourcePort 和 durationMs matcher
|
||||
- **THEN** 系统 SHALL 保留 Raw udp expect 快照,并生成包含默认 responded、resolved response content expectations 和 resolved value expectations 的 udp Resolved expect
|
||||
|
||||
#### Scenario: 解析 llm expect 配置
|
||||
- **WHEN** YAML 配置文件中 llm target 的 expect 包含 status、headers、output、finishReason、rawFinishReason、usage、stream 和 durationMs matcher
|
||||
- **THEN** 系统 SHALL 保留 Raw llm expect 快照,并生成包含默认 status、resolved headers、output、finishReason、rawFinishReason、usage、stream 和 durationMs 的 llm Resolved expect,并保留 output 内容 expectation 数组顺序
|
||||
|
||||
#### Scenario: 解析有序 ContentExpectations 数组
|
||||
- **WHEN** YAML 中任一内容类 expect 配置 contains、json、regex 三个数组项
|
||||
- **THEN** 系统 SHALL 在 Normalized expect 中保留执行顺序,供执行阶段按配置顺序快速失败
|
||||
|
||||
#### Scenario: 不配置 HTTP status
|
||||
- **WHEN** HTTP target 未配置 `expect.status`
|
||||
- **THEN** Normalized Config SHALL 不注入 status,checker.resolve() SHALL 在 Resolved HTTP expect 中物化默认 `status: [200]` 语义
|
||||
|
||||
#### Scenario: 配置 HTTP status 范围模式
|
||||
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]`
|
||||
- **THEN** 系统 SHALL 在执行 expect 时匹配所有 200-299 状态码
|
||||
|
||||
#### Scenario: 不配置 cmd exitCode
|
||||
- **WHEN** cmd target 未配置 `expect.exitCode`
|
||||
- **THEN** Normalized Config SHALL 不注入 exitCode,checker.resolve() SHALL 在 Resolved cmd expect 中物化默认 `exitCode: [0]` 语义
|
||||
|
||||
#### Scenario: 不配置 expect
|
||||
- **WHEN** target 未配置任何 expect 规则
|
||||
- **THEN** 系统 SHALL 正常处理,Normalized Config 不包含 expect,Resolved expect 由各 checker 物化自身默认状态语义
|
||||
|
||||
#### Scenario: Raw expect 不再保留
|
||||
- **WHEN** YAML 中配置 `expect.durationMs: 1000`
|
||||
- **THEN** Normalized Config SHALL 包含 `expect.durationMs: {equals: 1000}`,ResolvedTarget MUST NOT 携带 `rawExpect`
|
||||
|
||||
#### Scenario: 旧 maxDurationMs 字段不再支持
|
||||
- **WHEN** YAML 中任一 target 配置 `expect.maxDurationMs`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该字段未知,并要求使用 `expect.durationMs`
|
||||
|
||||
#### Scenario: 旧 match 字段不再支持
|
||||
- **WHEN** YAML 中任一 matcher 或内容 expectation 配置 `match`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该字段未知或不支持,并要求使用 `regex`
|
||||
|
||||
#### Scenario: durationMs matcher 配置
|
||||
- **WHEN** YAML 中任一 target 配置 `expect.durationMs: {lte: 1000}`
|
||||
- **THEN** 系统 SHALL 接受该配置并在运行期使用完整执行耗时进行 matcher 校验
|
||||
|
||||
#### Scenario: 动态 headers 字段允许
|
||||
- **WHEN** YAML 中 `http.headers`、`llm.headers` 或 `expect.headers` 包含任意 header 名称,且对应值符合契约
|
||||
- **THEN** 系统 SHALL 接受这些动态 header 名称
|
||||
|
||||
#### Scenario: ContentExpectations 字段必须为数组
|
||||
- **WHEN** YAML 中任一内容类 expect 字段配置为非数组
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该内容字段必须为 expectation 数组
|
||||
|
||||
#### Scenario: regex 字段非法
|
||||
- **WHEN** YAML 中任一 `regex` 不是字符串、不是可编译正则或存在 ReDoS 风险
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示对应 regex 不合法
|
||||
|
||||
### Requirement: 数据保留配置字段
|
||||
配置 schema 的 `server.storage` 段 SHALL 支持 `retention` 字段,类型为字符串,格式为 `<数字><单位>`(单位:`d` 天、`h` 小时、`m` 分钟),用于指定历史数据保留时长。
|
||||
|
||||
#### Scenario: retention 字段校验通过
|
||||
- **WHEN** 配置文件中 `server.storage.retention` 为合法格式(如 `"7d"`、`"24h"`、`"30m"`)
|
||||
- **THEN** 配置校验 SHALL 通过
|
||||
|
||||
#### Scenario: retention 字段格式非法
|
||||
- **WHEN** 配置文件中 `server.storage.retention` 为非法格式(如 `"abc"`、`"7x"`、`""`)
|
||||
- **THEN** 配置校验 SHALL 失败并报告格式错误
|
||||
|
||||
#### Scenario: retention 字段缺省
|
||||
- **WHEN** 配置文件中未指定 `server.storage.retention`
|
||||
- **THEN** 系统 SHALL 使用默认值 `"7d"`
|
||||
|
||||
### Requirement: 数据目录路径解析
|
||||
配置加载流程 SHALL 将 `server.storage.dataDir` 相对路径基于配置文件所在目录(configDir)解析为绝对路径。绝对路径 SHALL 保持不变。
|
||||
|
||||
#### Scenario: dataDir 为相对路径
|
||||
- **WHEN** 配置文件位于 `/opt/dial/probes.yaml`,且 `server.storage.dataDir` 配置为 `./data`
|
||||
- **THEN** 系统 SHALL 将 dataDir 解析为 `/opt/dial/data`,而非依赖进程 cwd
|
||||
|
||||
#### Scenario: dataDir 为绝对路径
|
||||
- **WHEN** `server.storage.dataDir` 配置为 `/var/lib/dial/data`
|
||||
- **THEN** 系统 SHALL 直接使用该绝对路径,不做额外解析
|
||||
|
||||
#### Scenario: dataDir 使用默认值
|
||||
- **WHEN** 未配置 `server.storage.dataDir`(使用默认值 `./data`)
|
||||
- **THEN** 系统 SHALL 将默认值 `./data` 基于 configDir 解析为绝对路径
|
||||
|
||||
### Requirement: target 通用元信息字段约束
|
||||
系统 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 接受该配置
|
||||
|
||||
#### Scenario: name 仅包含空白字符报错
|
||||
- **WHEN** 系统读取包含 `name: " "` 的 target
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 name 不能为空白
|
||||
|
||||
#### Scenario: description 为空字符串
|
||||
- **WHEN** 系统读取包含 `description: ""` 的 target
|
||||
- **THEN** 系统 SHALL 接受该配置,且不触发长度错误
|
||||
|
||||
#### Scenario: description 为 null 通过校验
|
||||
- **WHEN** 系统读取包含 `description: null` 或省略 `description` 的 target
|
||||
- **THEN** 系统 SHALL 接受该配置
|
||||
|
||||
#### Scenario: description 类型非法
|
||||
- **WHEN** YAML 中某个 target 的 `description` 字段不是字符串也不是 null
|
||||
- **THEN** 系统 SHALL 以错误退出,提示 description 字段类型错误
|
||||
|
||||
#### Scenario: description 超过最大长度
|
||||
- **WHEN** YAML 中某个 target 的 `description` 字段超过 500 个字符
|
||||
- **THEN** 系统 SHALL 以错误退出,提示 description 字段长度错误
|
||||
|
||||
#### Scenario: id 超过最大长度
|
||||
- **WHEN** YAML 中某个 target 的 `id` 字段超过 30 个字符
|
||||
- **THEN** 系统 SHALL 以错误退出,提示 id 字段长度错误
|
||||
|
||||
#### Scenario: name 超过最大长度
|
||||
- **WHEN** YAML 中某个 target 的 `name` 字段超过 30 个字符
|
||||
- **THEN** 系统 SHALL 以错误退出,提示 name 字段长度错误
|
||||
|
||||
#### Scenario: 变量替换后 description 超长
|
||||
- **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 简写。
|
||||
|
||||
#### Scenario: schema 导出 description
|
||||
- **WHEN** 系统导出 `probe-config.schema.json`
|
||||
- **THEN** target schema SHALL 包含可选的 `description` 字段,类型为 string 或 null,字符串最大长度为 500,并允许完整变量引用字符串
|
||||
|
||||
#### Scenario: schema 导出 id 和 name
|
||||
- **WHEN** 系统导出 `probe-config.schema.json`
|
||||
- **THEN** target schema SHALL 声明 `id` 的 minLength 为 1、maxLength 为 30,并声明 `name` 为可选字段,类型为 string 或 null,字符串的 minLength 为 1、maxLength 为 30
|
||||
|
||||
#### Scenario: schema 导出面向 Authoring Config
|
||||
- **WHEN** 系统导出 `probe-config.schema.json`
|
||||
- **THEN** 导出 schema SHALL 接受 `server.listen.port: "${PORT|3000}"` 和 `expect.durationMs: 5000` 这类 Authoring 写法
|
||||
|
||||
### Requirement: TCP 配置校验
|
||||
系统 SHALL 在启动期对 tcp checker 的配置契约和语义执行严格校验。Tcp target 的 `tcp` 分组 SHALL 只允许 `host`、`port`、`readBanner`、`bannerReadTimeout` 和 `maxBannerBytes` 字段;Tcp expect SHALL 只允许 `connected`、`durationMs` 和 `banner` 字段。`banner` MUST 为 `RawContentExpectations` 数组,`durationMs` SHALL 为 `RawValueExpectation`。未知字段、非法类型、非法端口、非法 size、非法 ContentExpectations 和不可编译正则 MUST 导致启动期配置错误。语义校验 MUST NOT 修改 Raw tcp expect 输入。
|
||||
|
||||
#### Scenario: tcp host 类型非法
|
||||
- **WHEN** YAML 中 tcp target 的 `tcp.host` 不是非空字符串
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 tcp.host 必须为非空字符串
|
||||
|
||||
#### Scenario: tcp port 类型非法
|
||||
- **WHEN** YAML 中 tcp target 的 `tcp.port` 不是整数
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 tcp.port 必须为整数端口
|
||||
|
||||
#### Scenario: tcp readBanner 类型非法
|
||||
- **WHEN** YAML 中 tcp target 的 `tcp.readBanner` 不是布尔值
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 tcp.readBanner 必须为布尔值
|
||||
|
||||
#### Scenario: tcp bannerReadTimeout 非法
|
||||
- **WHEN** YAML 中 tcp target 的 `bannerReadTimeout` 不是非负有限数字
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 bannerReadTimeout 格式错误
|
||||
|
||||
#### Scenario: tcp maxBannerBytes 非法
|
||||
- **WHEN** YAML 中 tcp target 的 `maxBannerBytes` 不是合法 size 值
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 maxBannerBytes 格式错误
|
||||
|
||||
#### Scenario: tcp expect connected 类型非法
|
||||
- **WHEN** YAML 中 tcp target 的 `expect.connected` 不是布尔值
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.connected 必须为布尔值
|
||||
|
||||
#### Scenario: tcp expect banner 非法
|
||||
- **WHEN** YAML 中 tcp target 的 `expect.banner` 不是合法 `RawContentExpectations` 数组
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.banner 格式错误
|
||||
|
||||
#### Scenario: tcp expect banner regex 正则非法
|
||||
- **WHEN** YAML 中 tcp target 配置 `expect.banner: [{ regex: "[invalid" }]`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,而不是延迟到运行期抛错
|
||||
|
||||
#### Scenario: tcp 分组未知字段失败
|
||||
- **WHEN** YAML 中 tcp target 的 `tcp` 分组包含 `tls: true` 等未知字段
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 tcp 分组包含未知字段
|
||||
|
||||
### Requirement: LLM 配置校验
|
||||
系统 SHALL 在启动期对 llm checker 的配置契约和语义执行严格校验。LLM target 的 `llm` 分组 SHALL 只允许 `provider`、`url`、`model`、`prompt`、`mode`、`key`、`authToken`、`headers`、`ignoreSSL`、`options` 和 `providerOptions` 字段。LLM expect SHALL 只允许 `status`、`headers`、`output`、`finishReason`、`rawFinishReason`、`usage`、`stream` 和 `durationMs` 字段。`expect.output` MUST 为 `RawContentExpectations` 数组。`expect.finishReason`、`expect.rawFinishReason`、`expect.usage.*`、`expect.stream.firstTokenMs` 和 `expect.durationMs` SHALL 使用 `RawValueExpectation`。未知字段、非法 provider、非法 URL、非法 mode、非法认证组合、非法 options、非法 output expectation 和 `mode: http` 下配置 `expect.stream` MUST 导致启动期配置错误。语义校验 MUST NOT 修改 Raw llm expect 输入。
|
||||
|
||||
#### Scenario: llm provider 非法
|
||||
- **WHEN** YAML 中 llm target 的 `llm.provider` 不是 `openai`、`openai-responses` 或 `anthropic`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.provider 不合法
|
||||
|
||||
#### Scenario: llm url 非法
|
||||
- **WHEN** YAML 中 llm target 的 `llm.url` 不是 `http://` 或 `https://` URL
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.url 格式不合法
|
||||
|
||||
#### Scenario: llm model 为空
|
||||
- **WHEN** YAML 中 llm target 的 `llm.model` 不是非空字符串
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.model 必须为非空字符串
|
||||
|
||||
#### Scenario: llm prompt 为空
|
||||
- **WHEN** YAML 中 llm target 的 `llm.prompt` 不是非空字符串
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.prompt 必须为非空字符串
|
||||
|
||||
#### Scenario: llm mode 非法
|
||||
- **WHEN** YAML 中 llm target 的 `mode` 不是 `http` 或 `stream`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.mode 不合法
|
||||
|
||||
#### Scenario: llm headers 类型非法
|
||||
- **WHEN** YAML 中 llm target 的 `headers` 不是对象,或任一 header 值不是字符串
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.headers 格式错误
|
||||
|
||||
#### Scenario: llm ignoreSSL 类型非法
|
||||
- **WHEN** YAML 中 llm target 的 `ignoreSSL` 不是布尔值
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.ignoreSSL 必须为布尔值
|
||||
|
||||
#### Scenario: llm authToken provider 非法
|
||||
- **WHEN** YAML 中 `provider: openai` 或 `provider: openai-responses` 的 llm target 配置 `authToken`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 authToken 仅支持 anthropic provider
|
||||
|
||||
#### Scenario: Anthropic key 与 authToken 冲突
|
||||
- **WHEN** YAML 中 `provider: anthropic` 的 llm target 同时配置非空 `key` 和非空 `authToken`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 key 与 authToken 不能同时配置
|
||||
|
||||
#### Scenario: llm options 非法
|
||||
- **WHEN** YAML 中 llm target 的 `options.maxOutputTokens` 不是正整数,`options.temperature`/`topP`/`topK`/`presencePenalty`/`frequencyPenalty`/`seed` 类型不合法,或 `options.stopSequences` 不是字符串数组
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.options 格式错误
|
||||
|
||||
#### Scenario: llm providerOptions 非法
|
||||
- **WHEN** YAML 中 llm target 的 `providerOptions` 不是 JSON object
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 llm.providerOptions 格式错误
|
||||
|
||||
#### Scenario: llm 禁止字段失败
|
||||
- **WHEN** YAML 中 llm target 配置 `api`、`providerName`、`baseURL`、`apiKey`、`messages`、`maxRetries`、`request`、`maxBodyBytes` 或 `maxStreamBytes`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 llm 分组包含未知字段
|
||||
|
||||
#### Scenario: llm output expectation 缺少支持字段
|
||||
- **WHEN** YAML 中 llm target 的 `expect.output` 数组项未包含任何合法 ValueMatcher 字段或 extractor
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 output expectation 缺少支持的 expectation 类型
|
||||
|
||||
#### Scenario: llm output expectation 同时配置多个 extractor
|
||||
- **WHEN** YAML 中 llm target 的同一条 output expectation 同时包含 json、css、xpath 中的多个 extractor
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示每条 output expectation 只能配置一种 extractor
|
||||
|
||||
#### Scenario: llm output regex 非法
|
||||
- **WHEN** YAML 中 llm target 的 output regex expectation 不是字符串、不是可编译正则表达式或存在 ReDoS 风险
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示该 output regex 不合法
|
||||
|
||||
#### Scenario: llm output json path 非法
|
||||
- **WHEN** YAML 中 llm target 的 output json expectation 缺少 path,或 path 不符合系统支持的 JSONPath 子集
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示该 output json path 不合法
|
||||
|
||||
#### Scenario: llm expect usage 非法
|
||||
- **WHEN** YAML 中 llm target 的 `expect.usage.inputTokens`、`expect.usage.outputTokens` 或 `expect.usage.totalTokens` 不是合法 `RawValueExpectation`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.usage 格式错误
|
||||
|
||||
#### Scenario: llm expect stream 仅允许 stream mode
|
||||
- **WHEN** YAML 中 llm target 配置 `llm.mode: http` 且配置 `expect.stream`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.stream 仅支持 stream mode
|
||||
|
||||
#### Scenario: llm expect stream firstTokenMs 非法
|
||||
- **WHEN** YAML 中 llm target 的 `expect.stream.firstTokenMs` 不是合法 `RawValueExpectation`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.stream.firstTokenMs 格式错误
|
||||
|
||||
### Requirement: 日志配置格式
|
||||
系统 SHALL 支持可选的 `server.logging` 配置,用于定义运行时日志等级、命令行日志等级、文件日志等级、文件路径和滚动策略。`server.logging` 未配置时 SHALL 使用内置默认值。系统 SHALL NOT 支持 `server.logging.console.enabled`、`server.logging.console.format`、`server.logging.file.enabled`、`server.logging.file.format` 或 `server.logging.file.rotation.enabled` 字段。
|
||||
|
||||
#### Scenario: 未配置 logging 使用默认值
|
||||
- **WHEN** 配置文件未声明 `server.logging`
|
||||
- **THEN** 系统 SHALL 使用 `server.logging.level=info`、`server.logging.console.level=info`、`server.logging.file.level=info`、`server.logging.file.path=<resolved dataDir>/logs/dial.log`、`server.logging.file.rotation.size=50MB`、`server.logging.file.rotation.frequency=daily` 和 `server.logging.file.rotation.maxFiles=14`
|
||||
|
||||
#### Scenario: console 和 file level 继承全局 level
|
||||
- **WHEN** 配置声明 `server.logging.level: warn` 且未声明 `server.logging.console.level` 和 `server.logging.file.level`
|
||||
- **THEN** 系统 SHALL 将 console 和 file 的日志等级均解析为 `warn`
|
||||
|
||||
#### Scenario: 显式配置文件日志路径
|
||||
- **WHEN** 配置声明 `server.logging.file.path`
|
||||
- **THEN** 系统 SHALL 使用该路径作为文件日志路径,而不是默认 `<resolved dataDir>/logs/dial.log`
|
||||
|
||||
#### Scenario: 相对日志路径
|
||||
- **WHEN** `server.logging.file.path` 是相对路径
|
||||
- **THEN** 系统 SHALL 基于配置文件所在目录解析为绝对路径
|
||||
|
||||
#### Scenario: 绝对日志路径
|
||||
- **WHEN** `server.logging.file.path` 是绝对路径
|
||||
- **THEN** 系统 SHALL 原样使用该绝对路径,并允许该路径位于 `dataDir` 之外
|
||||
|
||||
#### Scenario: 不支持日志开关和格式字段
|
||||
- **WHEN** 配置声明 `server.logging.console.enabled`、`server.logging.console.format`、`server.logging.file.enabled`、`server.logging.file.format` 或 `server.logging.file.rotation.enabled`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示存在未知字段
|
||||
|
||||
### Requirement: 日志配置校验
|
||||
系统 SHALL 在启动期校验 `server.logging` 配置。日志等级 SHALL 只能是 `trace`、`debug`、`info`、`warn`、`error` 或 `fatal`。`rotation.size` SHALL 使用有效 size 格式且解析为正整数字节数。`rotation.frequency` SHALL 只能是 `hourly`、`daily` 或 `weekly`。`rotation.maxFiles` SHALL 是正整数。
|
||||
|
||||
#### Scenario: 非法日志等级
|
||||
- **WHEN** 配置声明 `server.logging.level: verbose`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示日志等级非法
|
||||
|
||||
#### Scenario: 非法滚动大小
|
||||
- **WHEN** 配置声明 `server.logging.file.rotation.size: "large"`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示 size 格式非法
|
||||
|
||||
#### Scenario: 非法滚动频率
|
||||
- **WHEN** 配置声明 `server.logging.file.rotation.frequency: monthly`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示 frequency 非法
|
||||
|
||||
#### Scenario: 非法归档数量
|
||||
- **WHEN** 配置声明 `server.logging.file.rotation.maxFiles: 0`
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示 maxFiles 必须为正整数
|
||||
|
||||
#### Scenario: 非法日志路径
|
||||
- **WHEN** 配置声明 `server.logging.file.path` 为空字符串或空白字符串
|
||||
- **THEN** 系统 SHALL 以配置错误退出并提示日志路径非法
|
||||
@@ -1,94 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义拨测调度引擎的行为:按 interval 分组定时、组内并发拨测、通用 expect 校验、结果持久化和定期数据清理。各 checker 类型的执行语义和专属 expect 校验规则定义在各自 checker 的规范中。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 按 interval 分组调度
|
||||
系统 SHALL 将拨测目标按 interval 值分组,每组使用独立的定时器进行调度。
|
||||
|
||||
#### Scenario: 相同 interval 的目标共享定时器
|
||||
- **WHEN** 多个 target 配置了相同的 interval(如 30s)
|
||||
- **THEN** 系统 SHALL 使用同一个 `setInterval` 定时器,每次 tick 并发拨测所有该组目标
|
||||
|
||||
#### Scenario: 不同 interval 的目标各自调度
|
||||
- **WHEN** target A 配置 15s interval,target B 配置 30s interval
|
||||
- **THEN** 系统 SHALL 创建两个独立定时器,分别按各自频率调度
|
||||
|
||||
### Requirement: 组内并发拨测
|
||||
系统 SHALL 在每次调度 tick 时并发执行同组内目标的检查,但实际同时运行的检查数 MUST 受全局 `probes.execution.maxConcurrentChecks` 限制。当某个目标的 checker 执行 rejected(非正常 CheckResult 返回,而是 Promise reject)时,系统 SHALL 将该异常记录为 `matched: false` 的 check_result,而非仅 console.warn。
|
||||
|
||||
#### Scenario: 同组目标并发执行
|
||||
- **WHEN** 调度器触发一次 tick,该组有 3 个目标,且全局并发余量至少为 3
|
||||
- **THEN** 系统 SHALL 同时执行 3 个 checker,而非顺序执行
|
||||
|
||||
#### Scenario: 单个目标失败不影响同组其他目标
|
||||
- **WHEN** 同组中某个目标的检查请求超时或失败(checker 正常返回 CheckResult)
|
||||
- **THEN** 其他目标的检查 SHALL 正常完成并记录结果
|
||||
|
||||
#### Scenario: 同组中某个目标的 checker 执行 rejected
|
||||
- **WHEN** 同组中某个目标的 checker 执行抛出未捕获异常(Promise rejected)
|
||||
- **THEN** 系统 SHALL 为该目标写入一条 `matched: false` 的 check_result,failure 为 `{ kind: "error", phase: "internal", path: "engine", message: <rejected reason> }`,其他目标的检查 SHALL 不受影响
|
||||
|
||||
#### Scenario: rejected 结果通过索引关联 targetName
|
||||
- **WHEN** checker 执行 rejected
|
||||
- **THEN** 系统 SHALL 通过 Promise.allSettled 的索引关联回 target 数组,获取对应的 targetName 用于写入 check_result
|
||||
|
||||
#### Scenario: 全局并发限制生效
|
||||
- **WHEN** 调度器同时触发 10 个目标且 probes.execution.maxConcurrentChecks 为 3
|
||||
- **THEN** 系统 MUST 同时最多运行 3 个检查,其余检查等待并发槽位释放
|
||||
|
||||
### Requirement: 请求超时控制
|
||||
系统 SHALL 对每次 checker 执行实施超时控制,超时时间使用目标配置的 timeout 值。引擎 SHALL 通过 AbortController 向 checker 注入超时 signal。
|
||||
|
||||
#### Scenario: checker 在超时前完成
|
||||
- **WHEN** checker 在超时前完成执行
|
||||
- **THEN** 系统 SHALL 正常记录执行结果并进入 expect 校验
|
||||
|
||||
#### Scenario: checker 执行超时
|
||||
- **WHEN** checker 在 timeout 时间内未完成执行
|
||||
- **THEN** 系统 SHALL 中止该检查,记录为失败并标注超时错误
|
||||
|
||||
### Requirement: expect 校验
|
||||
系统 SHALL 在 checker 执行完成后根据目标类型的 Resolved expect 执行计划校验观测结果,校验结果和首个失败原因记入 check result。各 checker 类型 SHALL 定义各自的 expect 执行顺序、默认状态语义和快速失败策略。`durationMs` SHALL 表示完整 checker 执行耗时。
|
||||
|
||||
#### Scenario: 多条 expect 规则
|
||||
- **WHEN** 目标同时配置状态、duration、元数据和内容 expectations
|
||||
- **THEN** 系统 SHALL 所有 expectations 全部通过时 matched 为 true,任一不通过则为 false 并记录首个失败原因
|
||||
|
||||
### Requirement: 拨测结果记录
|
||||
系统 SHALL 在每次 checker 完成后,将结果写入 SQLite 数据存储,包含 target_id、timestamp、matched、duration_ms、observation、failure 字段。detail SHALL 为 API 层派生字段,不写入存储层;系统 SHALL NOT 写入 status_detail 字段。
|
||||
|
||||
#### Scenario: 成功检查结果记录
|
||||
- **WHEN** checker 成功执行且 expect 全部匹配
|
||||
- **THEN** 系统 SHALL 记录 matched=true、duration_ms、observation,failure 为 null
|
||||
|
||||
#### Scenario: 执行失败结果记录
|
||||
- **WHEN** checker 执行失败(网络错误、超时、命令启动失败、输出超限等)
|
||||
- **THEN** 系统 SHALL 记录 matched=false、failure.kind="error" 和具体错误信息,并在可收集领域观测数据时记录 observation
|
||||
|
||||
#### Scenario: expect 不匹配结果记录
|
||||
- **WHEN** checker 执行成功但 expect 不匹配
|
||||
- **THEN** 系统 SHALL 记录 matched=false、observation、failure.kind="mismatch" 和具体不匹配信息
|
||||
|
||||
### Requirement: runner 选择
|
||||
系统 SHALL 根据 target.type 通过 CheckerRegistry 选择对应 checker 执行检查。
|
||||
|
||||
#### Scenario: 根据 type 选择 checker
|
||||
- **WHEN** target.type 为已注册的 checker 类型
|
||||
- **THEN** 系统 SHALL 通过 `checkerRegistry.get(type)` 获取对应 checker 并执行
|
||||
|
||||
### Requirement: 定期数据清理
|
||||
ProbeEngine SHALL 在启动时注册数据清理定时器,定期调用 ProbeStore.prune() 清理过期数据和空壳非活跃目标。
|
||||
|
||||
#### Scenario: 引擎启动注册清理
|
||||
- **WHEN** ProbeEngine.start() 被调用且 retentionMs > 0
|
||||
- **THEN** 系统 SHALL 立即执行一次 prune,然后每隔 1 小时再次执行
|
||||
|
||||
#### Scenario: 引擎停止清除定时器
|
||||
- **WHEN** ProbeEngine.stop() 被调用
|
||||
- **THEN** 系统 SHALL 清除清理定时器,不再执行后续清理
|
||||
|
||||
#### Scenario: retentionMs 为 0 不注册清理
|
||||
- **WHEN** ProbeEngine 构造时 retentionMs 为 0
|
||||
- **THEN** 系统 SHALL 不注册清理定时器
|
||||
@@ -1,91 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义运行时日志输出、日志等级、命令行输出、文件 JSONL 输出、滚动策略和敏感信息保护。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 运行时 logger 输出
|
||||
系统 SHALL 在配置解析成功后初始化统一运行时 logger。logger SHALL 同时输出命令行 pretty 日志和文件 JSONL 日志。命令行输出、文件输出和文件滚动 SHALL 始终启用,不提供关闭开关。
|
||||
|
||||
#### Scenario: 默认初始化 logger
|
||||
- **WHEN** 配置文件未声明 `server.logging`
|
||||
- **THEN** 系统 SHALL 使用默认等级 `info` 初始化 console pretty 输出和 `<resolved dataDir>/logs/dial.log` 文件 JSONL 输出
|
||||
|
||||
#### Scenario: 模块 child logger
|
||||
- **WHEN** bootstrap 创建 engine、server 或其他运行时模块
|
||||
- **THEN** 系统 SHALL 为模块创建带 `component` 字段的 child logger
|
||||
|
||||
#### Scenario: 配置成功后的启动失败
|
||||
- **WHEN** 配置解析成功后数据库、logger、engine 或 HTTP server 初始化失败
|
||||
- **THEN** 系统 SHALL 通过正式 logger 输出 `fatal` 日志并以非零状态退出
|
||||
|
||||
### Requirement: 日志等级语义
|
||||
系统 SHALL 支持 `trace`、`debug`、`info`、`warn`、`error` 和 `fatal` 六个日志等级。`server.logging.level` SHALL 作为全局默认等级,`server.logging.console.level` 和 `server.logging.file.level` SHALL 在省略时继承全局等级。
|
||||
|
||||
#### Scenario: 目的地等级继承
|
||||
- **WHEN** 配置只声明 `server.logging.level: warn`
|
||||
- **THEN** console 和 file 输出均 SHALL 使用 `warn` 作为最低输出等级
|
||||
|
||||
#### Scenario: 目的地等级覆盖
|
||||
- **WHEN** 配置声明 `server.logging.level: info`、`server.logging.console.level: warn` 和 `server.logging.file.level: debug`
|
||||
- **THEN** console SHALL 输出 `warn` 及以上日志,file SHALL 输出 `debug` 及以上日志
|
||||
|
||||
#### Scenario: 默认不输出 debug 检查摘要
|
||||
- **WHEN** 系统使用默认 `info` 日志等级执行拨测
|
||||
- **THEN** 每次检查的 debug 摘要 SHALL NOT 输出到 console 或 file
|
||||
|
||||
### Requirement: 文件日志滚动
|
||||
系统 SHALL 对文件日志启用滚动策略。滚动 SHALL 在 `server.logging.file.rotation.size` 或 `server.logging.file.rotation.frequency` 任一条件满足时触发。`server.logging.file.rotation.maxFiles` SHALL 表示最多保留的归档文件数量,不包含当前正在写入的日志文件。
|
||||
|
||||
#### Scenario: 按大小滚动
|
||||
- **WHEN** 当前日志文件达到配置的 `rotation.size`
|
||||
- **THEN** 系统 SHALL 滚动到新的日志文件继续写入
|
||||
|
||||
#### Scenario: 按频率滚动
|
||||
- **WHEN** 当前时间达到配置的 `rotation.frequency` 周期边界
|
||||
- **THEN** 系统 SHALL 滚动到新的日志文件继续写入
|
||||
|
||||
#### Scenario: 限制归档数量
|
||||
- **WHEN** 归档日志文件数量超过 `rotation.maxFiles`
|
||||
- **THEN** 系统 SHALL 删除最旧的归档日志文件并保留当前正在写入的文件
|
||||
|
||||
### Requirement: 日志事件内容边界
|
||||
系统 SHALL 将运行日志作为运行时事件记录,而不是将每次拨测结果完整复制到日志文件。`info` SHALL 记录生命周期事件,`warn` SHALL 记录需要关注但进程可继续的异常和目标 DOWN 状态变化,`error` SHALL 记录内部异常,`debug` SHALL 记录每次检查摘要。
|
||||
|
||||
#### Scenario: 成功检查默认不产生日志噪音
|
||||
- **WHEN** target 连续检查成功且日志等级为默认 `info`
|
||||
- **THEN** 系统 SHALL NOT 为每次成功检查输出 info 日志
|
||||
|
||||
#### Scenario: 目标首次 DOWN
|
||||
- **WHEN** target 没有历史状态且本次检查结果为 DOWN
|
||||
- **THEN** 系统 SHALL 输出 `warn` 日志,包含 targetId、targetType、durationMs 和 failure 摘要
|
||||
|
||||
#### Scenario: 目标恢复 UP
|
||||
- **WHEN** target 最近状态为 DOWN 且本次检查结果为 UP
|
||||
- **THEN** 系统 SHALL 输出 `info` 日志,包含 targetId、targetType 和 durationMs
|
||||
|
||||
#### Scenario: checker rejected
|
||||
- **WHEN** checker 执行抛出未捕获异常导致 Promise rejected
|
||||
- **THEN** 系统 SHALL 输出 `error` 日志并写入 `matched: false` 的 check_result
|
||||
|
||||
### Requirement: 敏感信息保护
|
||||
系统 SHALL 避免在日志中输出敏感配置和运行时数据。日志事件 SHALL 优先记录白名单字段,并通过日志库 redaction 对常见敏感字段进行兜底保护。
|
||||
|
||||
#### Scenario: HTTP 敏感 header 不输出
|
||||
- **WHEN** target 配置包含 `Authorization`、`Cookie` 或 `Set-Cookie` 相关值
|
||||
- **THEN** 系统 SHALL NOT 在 console 或 file 日志中输出这些原始值
|
||||
|
||||
#### Scenario: LLM 敏感字段不输出
|
||||
- **WHEN** LLM target 配置包含 `key`、`authToken`、`prompt` 或 providerOptions
|
||||
- **THEN** 系统 SHALL NOT 在 console 或 file 日志中输出这些原始值
|
||||
|
||||
#### Scenario: 命令环境变量不输出
|
||||
- **WHEN** cmd target 配置包含 `env`
|
||||
- **THEN** 系统 SHALL NOT 在 console 或 file 日志中输出完整环境变量表
|
||||
|
||||
### Requirement: 单可执行文件兼容
|
||||
运行时日志能力 SHALL 在 Bun 单可执行文件构建产物中可用。生产构建后的 executable SHALL 不依赖目标机器安装 Bun、Node.js 或 node_modules 即可输出 console 日志和滚动文件日志。
|
||||
|
||||
#### Scenario: standalone executable 写入文件日志
|
||||
- **WHEN** 开发者运行 `bun run build` 并启动生成的 `dist/dial-server`
|
||||
- **THEN** executable SHALL 在配置的数据目录下创建并写入滚动文件日志
|
||||
@@ -1,151 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义将 Vite 构建的前端资源通过 code generation 嵌入 Bun 后端、静态资源服务与 Content-Type 处理、打包为单个 standalone executable 的生产构建、运行配置和验证要求。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 生产构建顺序
|
||||
生产构建 MUST 通过三步流水线完成:Vite 前端构建 → code generation → Bun compile。
|
||||
|
||||
构建步骤 1-2(Vite build、code generation)的逻辑 SHALL 从 `scripts/build-common.ts` 导入,`scripts/build.ts` 只保留当前平台编译和编排逻辑。
|
||||
|
||||
#### Scenario: 运行生产构建
|
||||
- **WHEN** 开发者运行生产构建命令
|
||||
- **THEN** 系统 MUST 依次执行 Vite build、资源导入 code generation、Bun.build compile,最终输出单可执行文件
|
||||
|
||||
#### Scenario: Vite 构建失败
|
||||
- **WHEN** Vite build 步骤失败
|
||||
- **THEN** 系统 MUST 停止后续步骤,不生成 code generation 文件或 executable
|
||||
|
||||
#### Scenario: Bun compile 失败
|
||||
- **WHEN** Bun.build compile 步骤失败
|
||||
- **THEN** 系统 MUST 清理 `.build/` 临时目录,不保留 stale executable
|
||||
|
||||
#### Scenario: 重构后 build 行为不变
|
||||
- **WHEN** `scripts/build.ts` 改为从 `scripts/build-common.ts` 导入共享构建函数
|
||||
- **THEN** `bun run build` 的产出文件路径(`dist/dial-server`)、构建步骤顺序和错误处理行为 SHALL 与重构前完全一致
|
||||
|
||||
### Requirement: 单 executable 输出
|
||||
生产构建 SHALL 输出一个 standalone executable,其中包含 Bun 后端和通过 `import with { type: "file" }` 嵌入的 Vite 前端产出。
|
||||
|
||||
#### Scenario: 在目标机器运行 executable
|
||||
- **WHEN** 生成的 executable 在兼容目标平台上运行
|
||||
- **THEN** 它 SHALL 启动全栈应用,且不要求目标机器安装 Node.js、Bun 或 `node_modules`
|
||||
|
||||
#### Scenario: 服务嵌入的前端
|
||||
- **WHEN** executable 收到前端根路径请求
|
||||
- **THEN** 它 SHALL 通过内嵌的 Vite 构建产出服务前端资源,且不需要外部 `dist/` 目录
|
||||
|
||||
#### Scenario: 服务嵌入 API 和页面
|
||||
- **WHEN** 生成的 executable 启动,且浏览器打开前端根路径
|
||||
- **THEN** 页面 SHALL 展示同一个 executable 进程中 `/api/summary` 和 `/api/targets` 返回的数据
|
||||
|
||||
### Requirement: 构建中间产物管理
|
||||
构建流程 SHALL 使用 `.build/` 临时目录存放 code generation 产物,构建完成后清理。
|
||||
|
||||
清理逻辑 SHALL 定义在 `scripts/build-common.ts` 中,供 `build.ts` 和 `release.ts` 共用。
|
||||
|
||||
#### Scenario: 构建成功后清理中间产物
|
||||
- **WHEN** 生产构建成功完成并输出 executable
|
||||
- **THEN** 系统 SHALL 通过 `build-common.ts` 中的 `cleanup()` 函数删除 `.build/` 临时目录
|
||||
|
||||
#### Scenario: 构建失败时清理中间产物
|
||||
- **WHEN** 生产构建在 Bun compile 步骤失败
|
||||
- **THEN** 系统 SHALL 通过 `build-common.ts` 中的 `cleanup()` 函数删除 `.build/` 临时目录和 stale executable
|
||||
|
||||
### Requirement: 外部运行时配置
|
||||
executable MUST 将环境相关运行时配置保留在嵌入的前端和 server bundle 之外。
|
||||
|
||||
#### Scenario: 修改监听端口
|
||||
- **WHEN** 操作者修改受支持的 port 配置
|
||||
- **THEN** 同一个 executable SHALL 在不重新构建的情况下监听新端口
|
||||
|
||||
#### Scenario: 缺少可选配置
|
||||
- **WHEN** 可选运行时配置被省略
|
||||
- **THEN** executable SHALL 使用文档化的默认值
|
||||
|
||||
### Requirement: 构建验证
|
||||
项目 SHALL 提供 `verify` 命令执行质量检查和生产构建;原 smoke test 暂时移除,executable 路由验证由后续变更重新设计。
|
||||
|
||||
#### Scenario: 完整验证重新构建 executable
|
||||
- **WHEN** 开发者运行完整验证命令
|
||||
- **THEN** 系统 MUST 先执行质量检查,再基于当前源码执行生产构建
|
||||
|
||||
#### Scenario: 验证失败
|
||||
- **WHEN** 质量检查或构建阶段失败
|
||||
- **THEN** 验证 SHALL 使命令失败
|
||||
|
||||
### Requirement: 生产构建版本固化
|
||||
生产构建 SHALL 在 code generation 阶段读取 `package.json.version`,并将该版本号固化到生成的 production server entry 中,使 standalone executable 能在运行时返回构建时版本。
|
||||
|
||||
#### Scenario: 构建时注入版本号
|
||||
- **WHEN** 开发者运行生产构建命令
|
||||
- **THEN** 构建脚本 SHALL 在生成 `.build/server-entry.ts` 时写入当前 `package.json.version` 对应的版本字面量
|
||||
|
||||
#### Scenario: executable 不依赖外部 package.json 返回版本
|
||||
- **WHEN** 生成的 standalone executable 在目标机器运行且外部不存在项目根目录 `package.json`
|
||||
- **THEN** `GET /api/meta` SHALL 仍返回构建时固化的 `version`
|
||||
|
||||
#### 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,136 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 TanStack Query 数据层:QueryClient 配置、queryKey 工厂、轮询策略、条件查询和开发调试面板。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: TanStack Query 数据层
|
||||
前端 SHALL 使用 TanStack Query(@tanstack/react-query)管理所有 API 请求,数据层代码 SHALL 按职责拆分为独立 hook 文件。
|
||||
|
||||
#### Scenario: QueryClient 配置
|
||||
- **WHEN** 应用启动
|
||||
- **THEN** 系统 SHALL 创建 QueryClient,默认配置 retry=1、refetchOnWindowFocus=true、staleTime=5000
|
||||
|
||||
#### Scenario: QueryClientProvider 挂载
|
||||
- **WHEN** 应用渲染
|
||||
- **THEN** 根组件 SHALL 包裹在 QueryClientProvider 中,提供 QueryClient 实例
|
||||
|
||||
### Requirement: queryKey 工厂
|
||||
系统 SHALL 提供统一的 queryKey 工厂函数,确保 queryKey 的唯一性和一致性。
|
||||
|
||||
#### Scenario: summary queryKey
|
||||
- **WHEN** 查询 summary 数据
|
||||
- **THEN** queryKey SHALL 为 ["summary"]
|
||||
|
||||
#### Scenario: targets queryKey
|
||||
- **WHEN** 查询 targets 数据
|
||||
- **THEN** queryKey SHALL 为 ["targets"]
|
||||
|
||||
#### Scenario: trend queryKey
|
||||
- **WHEN** 查询某目标的趋势数据
|
||||
- **THEN** queryKey SHALL 为 ["trend", targetId, from, to]
|
||||
|
||||
#### Scenario: history queryKey
|
||||
- **WHEN** 查询某目标的历史记录
|
||||
- **THEN** queryKey SHALL 为 ["history", targetId, from, to, page]
|
||||
|
||||
#### Scenario: meta queryKey
|
||||
- **WHEN** 查询 meta 数据
|
||||
- **THEN** queryKey SHALL 为 ["meta"]
|
||||
|
||||
### Requirement: Meta 查询
|
||||
系统 SHALL 提供 `useMeta` hook 查询系统元数据。
|
||||
|
||||
#### Scenario: meta 查询配置
|
||||
- **WHEN** 应用启动
|
||||
- **THEN** `useMeta` SHALL 请求 `/api/meta`,配置 `staleTime: Infinity`(应用生命周期内只请求一次)
|
||||
|
||||
#### Scenario: meta 数据返回
|
||||
- **WHEN** meta 查询成功
|
||||
- **THEN** hook SHALL 返回 `MetaResponse` 类型数据,包含 `checkerTypes` 和 `version` 字段
|
||||
|
||||
### Requirement: Meta 版本数据
|
||||
前端 SHALL 通过现有 `useMeta` hook 获取系统版本元数据,并将 `MetaResponse.version` 提供给需要展示版本号的组件。
|
||||
|
||||
#### Scenario: useMeta 返回版本字段
|
||||
- **WHEN** `useMeta` 请求 `/api/meta` 成功
|
||||
- **THEN** hook 返回的数据 SHALL 符合 `MetaResponse`,包含 `checkerTypes` 和 `version` 字段
|
||||
|
||||
#### Scenario: Header 复用 meta 查询
|
||||
- **WHEN** Header 需要展示应用版本号
|
||||
- **THEN** Header SHALL 复用 `useMeta` 的 `queryKey` 为 `["meta"]` 的查询结果,不得新增重复的版本专用请求
|
||||
|
||||
### Requirement: Hook 文件拆分
|
||||
数据层 hook SHALL 按职责拆分为独立文件。
|
||||
|
||||
#### Scenario: 全局查询 hook 文件
|
||||
- **WHEN** 开发者需要使用全局面板级查询
|
||||
- **THEN** `useSummary`、`useTargets`、`useMeta` SHALL 从 `hooks/use-queries.ts` 导出
|
||||
|
||||
#### Scenario: Drawer 状态 hook 文件
|
||||
- **WHEN** 开发者需要使用 Drawer 状态管理
|
||||
- **THEN** `useTargetDetail` SHALL 从 `hooks/use-target-detail.ts` 导出
|
||||
|
||||
#### Scenario: fetchJson 不导出
|
||||
- **WHEN** 数据层内部需要 fetch 封装
|
||||
- **THEN** `fetchJson` SHALL 定义在 `use-queries.ts` 内部,不作为公共 API 导出
|
||||
|
||||
#### Scenario: queryKeys 不导出
|
||||
- **WHEN** 数据层内部需要 query key
|
||||
- **THEN** `queryKeys` 对象 SHALL 定义在 `use-queries.ts` 内部,不作为公共 API 导出
|
||||
|
||||
### Requirement: Summary 轮询查询
|
||||
系统 SHALL 使用 useQuery 实现总览统计的自动轮询。
|
||||
|
||||
#### Scenario: summary 动态轮询间隔
|
||||
- **WHEN** Dashboard 页面处于打开状态
|
||||
- **THEN** 系统 SHALL 按用户选择的刷新间隔自动请求数据,`useDashboard` hook SHALL 接受 `refetchInterval` 参数(`false | number`),由调用方传入
|
||||
|
||||
#### Scenario: summary 禁用自动轮询
|
||||
- **WHEN** 用户选择"手动"刷新模式
|
||||
- **THEN** `useDashboard` SHALL 接收 `refetchInterval: false`,禁用自动轮询
|
||||
|
||||
#### Scenario: summary 后台刷新
|
||||
- **WHEN** 页面处于后台标签页
|
||||
- **THEN** 系统 SHALL 暂停轮询(refetchIntervalInBackground=false)
|
||||
|
||||
### Requirement: Targets 轮询查询
|
||||
系统 SHALL 使用 useQuery 实现目标列表的自动轮询。
|
||||
|
||||
#### Scenario: targets 动态轮询间隔
|
||||
- **WHEN** Dashboard 页面处于打开状态
|
||||
- **THEN** 系统 SHALL 按用户选择的刷新间隔自动请求数据,轮询间隔与 summary 查询保持一致
|
||||
|
||||
### Requirement: 条件查询
|
||||
详情指标和历史记录查询 SHALL 使用 enabled 条件控制。指标查询 SHALL 在目标和时间范围有效时触发;历史记录查询 SHALL 仅在目标、时间范围有效且"记录"Tab 激活后触发。
|
||||
|
||||
#### Scenario: 未选中目标时不请求
|
||||
- **WHEN** 用户未点击任何目标表格行
|
||||
- **THEN** metrics 和 history 的 useQuery SHALL enabled=false,不发起请求
|
||||
|
||||
#### Scenario: 打开 Drawer 默认只请求指标
|
||||
- **WHEN** 用户点击目标表格行并打开 Drawer
|
||||
- **THEN** metrics 的 useQuery SHALL enabled=true 并自动发起请求,history 的 useQuery SHALL enabled=false 且不发起请求
|
||||
|
||||
#### Scenario: 激活记录 Tab 时请求历史记录
|
||||
- **WHEN** 用户切换到"记录"Tab 且目标与时间范围有效
|
||||
- **THEN** history 的 useQuery SHALL enabled=true,并请求当前页码对应的 `/api/targets/:id/history` 数据
|
||||
|
||||
#### Scenario: 概览 Tab 时间范围变化时不请求历史记录
|
||||
- **WHEN** 用户在"概览"Tab 修改时间范围
|
||||
- **THEN** metrics 的 useQuery SHALL 因 queryKey 变化自动重新请求,history 的 useQuery SHALL 保持 enabled=false 且不发起请求
|
||||
|
||||
#### Scenario: 记录 Tab 时间范围变化时重新请求历史记录
|
||||
- **WHEN** 用户在"记录"Tab 修改时间范围
|
||||
- **THEN** metrics 和 history 的 useQuery SHALL 因 queryKey 变化自动重新请求,并将 history 页码重置为 1
|
||||
|
||||
### Requirement: 开发调试面板
|
||||
开发环境下 SHALL 挂载 TanStack Query Devtools。
|
||||
|
||||
#### Scenario: 开发环境显示 Devtools
|
||||
- **WHEN** 应用在开发模式下运行
|
||||
- **THEN** 页面 SHALL 显示 ReactQueryDevtools 浮动面板
|
||||
|
||||
#### Scenario: 生产环境排除 Devtools
|
||||
- **WHEN** 应用在生产模式下构建
|
||||
- **THEN** ReactQueryDevtools SHALL 不被包含在产物中
|
||||
@@ -1,318 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义目标详情 Drawer:时间范围筛选(TDesign RadioGroup + DateRangePicker 单行布局,含快捷按钮联动概览和记录面板)、Tabs 组织概览/记录两个面板、Metrics 数据查询 Hook、多维度统计图表(4×2 Card 布局)和分页检查结果列表。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 目标详情 Drawer
|
||||
Dashboard SHALL 在用户点击目标表格行后从右侧滑出 Drawer,展示该目标的详细统计信息和检查记录。Drawer 标题栏和内容不使用内联 style。Drawer 内容 SHALL 拆分为独立的 Tab 组件,并优先通过 TDesign Drawer 原生生命周期能力控制显示、隐藏和滚动穿透。
|
||||
|
||||
#### Scenario: 打开 Drawer
|
||||
- **WHEN** 用户点击某个目标表格行
|
||||
- **THEN** 系统 SHALL 从右侧滑出 Drawer(placement="right"),使用响应式默认宽度,并将当前 Tab 重置为"概览"
|
||||
|
||||
#### Scenario: Drawer 标题栏
|
||||
- **WHEN** Drawer 渲染
|
||||
- **THEN** 标题栏 SHALL 使用 TDesign Space 组件(align="center")布局,包含 StatusDot、目标展示名称(取值为 `target.name ?? target.id`,使用 TDesign Typography.Text strong)和类型标签(TDesign Tag,直接显示 target.type 原始文本),以及内建关闭按钮。不使用内联 style 的 flex 布局
|
||||
|
||||
#### Scenario: Drawer 标题栏 name 为 null
|
||||
- **WHEN** Drawer 渲染某个 `target.name` 为 null 的目标
|
||||
- **THEN** 标题栏 SHALL 显示该目标的 `target.id` 作为目标展示名称
|
||||
|
||||
#### Scenario: 关闭 Drawer
|
||||
- **WHEN** 用户点击关闭按钮、ESC 键或遮罩层
|
||||
- **THEN** Drawer SHALL 关闭并通过 TDesign Drawer 的 `visible` 状态隐藏
|
||||
|
||||
#### Scenario: Drawer 无底部按钮
|
||||
- **WHEN** Drawer 渲染
|
||||
- **THEN** Drawer SHALL 不显示底部操作栏(footer={false})
|
||||
|
||||
#### Scenario: Drawer 数据同步
|
||||
- **WHEN** Drawer 打开期间后台轮询刷新了 targets 数据
|
||||
- **THEN** Drawer 中 selectedTarget 的状态 SHALL 随之同步更新
|
||||
|
||||
#### Scenario: 切换目标重置 Tab
|
||||
- **WHEN** 用户从目标 A 切换到目标 B(点击不同的表格行)
|
||||
- **THEN** Drawer SHALL 通过受控 Tab 状态重置为概览 Tab,且 MUST NOT 使用 `key={target.id}` 强制重建 Drawer 子树
|
||||
|
||||
#### Scenario: Drawer 内容区间距
|
||||
- **WHEN** Drawer 内容渲染
|
||||
- **THEN** 时间选择器、Tabs 等区块之间的间距 SHALL 通过 TDesign Space 组件(direction="vertical", size={16})统一管理,不使用内联 style 的 marginBottom
|
||||
|
||||
### Requirement: 概览面板组件化
|
||||
概览 Tab SHALL 作为独立组件 `OverviewTab` 实现,展示基本信息、多维度统计(上下布局卡片)和趋势图。不再包含状态分布环形图。
|
||||
|
||||
#### Scenario: OverviewTab 组件职责
|
||||
- **WHEN** 概览 Tab 渲染
|
||||
- **THEN** `OverviewTab` 组件 SHALL 负责基本信息(直接展示 Descriptions)、多维度统计卡片(4×2 上下布局)和趋势图的渲染
|
||||
|
||||
#### Scenario: OverviewTab props
|
||||
- **WHEN** OverviewTab 渲染
|
||||
- **THEN** 组件 SHALL 接收 `target: TargetStatus`、`metricsData: TargetMetricsResponse | null`、`metricsLoading: boolean` 作为 props
|
||||
|
||||
### Requirement: 记录面板组件化
|
||||
记录 Tab SHALL 作为独立组件 `HistoryTab` 实现。
|
||||
|
||||
#### Scenario: HistoryTab 组件职责
|
||||
- **WHEN** 记录 Tab 渲染
|
||||
- **THEN** `HistoryTab` 组件 SHALL 负责检查结果表格和分页的渲染
|
||||
|
||||
#### Scenario: HistoryTab props
|
||||
- **WHEN** HistoryTab 渲染
|
||||
- **THEN** 组件 SHALL 接收 `historyData: HistoryResponse`、`historyLoading: boolean`、`onPageChange: (page: number) => void` 作为 props
|
||||
|
||||
#### Scenario: 历史记录列定义外置
|
||||
- **WHEN** HistoryTab 渲染表格
|
||||
- **THEN** 列定义 SHALL 从 `constants/history-table-columns.tsx` 导入,不在组件内部定义
|
||||
|
||||
### Requirement: TrendChart 简化
|
||||
TrendChart 组件 SHALL 仅接收数据 props,不处理 loading 状态。
|
||||
|
||||
#### Scenario: TrendChart 无 loading prop
|
||||
- **WHEN** TrendChart 渲染
|
||||
- **THEN** 组件 SHALL 仅接收 `data: TrendPoint[]` prop,不接收 `loading` prop
|
||||
|
||||
#### Scenario: TrendChart 空数据
|
||||
- **WHEN** TrendChart 接收空数组
|
||||
- **THEN** 组件 SHALL 显示"暂无趋势数据"占位文本
|
||||
|
||||
#### Scenario: TrendChart memo 包裹
|
||||
- **WHEN** TrendChart 的父组件重渲染但 data prop 引用未变
|
||||
- **THEN** TrendChart SHALL 跳过重渲染(通过 React.memo shallow compare)
|
||||
|
||||
#### Scenario: chartData useMemo
|
||||
- **WHEN** TrendChart 渲染
|
||||
- **THEN** 内部 `chartData` 转换结果 SHALL 通过 `useMemo` 缓存,依赖为 `[data]`,data 引用不变时不重新计算
|
||||
|
||||
### Requirement: TargetBoard 分组 memoize
|
||||
TargetBoard 组件的分组计算 SHALL 使用 useMemo 缓存,避免 targets 引用不变时重复计算分组。
|
||||
|
||||
#### Scenario: 分组结果 useMemo
|
||||
- **WHEN** TargetBoard 渲染
|
||||
- **THEN** 分组逻辑(Map 构建 + sort)SHALL 通过 `useMemo` 缓存,依赖为 `[targets]`
|
||||
|
||||
#### Scenario: targets 引用不变时跳过分组
|
||||
- **WHEN** TargetBoard 因父组件重渲染而重渲染,但 targets prop 引用未变
|
||||
- **THEN** 分组计算 SHALL 返回缓存结果,不重新执行 Map 构建和排序
|
||||
|
||||
### Requirement: TargetGroup 渲染优化
|
||||
TargetGroup 组件 SHALL 使用 React.memo 包裹,在 props 引用不变时跳过重渲染。
|
||||
|
||||
#### Scenario: TargetGroup memo 包裹
|
||||
- **WHEN** TargetBoard 重渲染但某个分组的 targets 数组引用未变
|
||||
- **THEN** 对应的 TargetGroup SHALL 跳过重渲染(通过 React.memo shallow compare)
|
||||
|
||||
#### Scenario: TargetGroup props 稳定性
|
||||
- **WHEN** TargetGroup 渲染
|
||||
- **THEN** 其 props(columns、name、targets、onTargetClick)SHALL 全部具有引用稳定性:columns 通过 useMemo、name 为 string 原始值、targets 通过分组 useMemo、onTargetClick 通过 useCallback
|
||||
|
||||
### Requirement: StatusBar 参数化
|
||||
StatusBar 组件 SHALL 支持可配置的格数。
|
||||
|
||||
#### Scenario: maxSlots prop
|
||||
- **WHEN** StatusBar 渲染
|
||||
- **THEN** 组件 SHALL 接收可选的 `maxSlots` prop(默认 30),根据该值渲染对应数量的格子
|
||||
|
||||
#### Scenario: 格子渲染逻辑
|
||||
- **WHEN** StatusBar 渲染且 samples 数量少于 maxSlots
|
||||
- **THEN** 多余的格子 SHALL 显示为 empty 状态
|
||||
|
||||
### Requirement: Metrics 数据查询 Hook
|
||||
系统 SHALL 提供 `useTargetMetrics` hook 查询单目标指标数据。
|
||||
|
||||
#### Scenario: metrics queryKey
|
||||
- **WHEN** 查询某目标的指标数据
|
||||
- **THEN** queryKey SHALL 为 ["metrics", targetId, from, to, bucket]
|
||||
|
||||
#### Scenario: metrics 条件查询
|
||||
- **WHEN** 用户未选中任何目标
|
||||
- **THEN** metrics 的 useQuery SHALL enabled=false,不发起请求
|
||||
|
||||
#### Scenario: metrics 数据返回
|
||||
- **WHEN** metrics 查询成功
|
||||
- **THEN** hook SHALL 返回 `TargetMetricsResponse` 类型数据
|
||||
|
||||
#### Scenario: 时间范围变化时重新请求
|
||||
- **WHEN** 用户更改时间范围
|
||||
- **THEN** metrics 的 useQuery SHALL 因 queryKey 变化自动重新请求
|
||||
|
||||
#### Scenario: Drawer 关闭清理查询缓存
|
||||
- **WHEN** 用户关闭 Drawer
|
||||
- **THEN** 系统 MAY 保留 metrics 和 history 查询缓存以降低重复打开成本,依赖 TanStack Query 全局 staleTime 自动管理过期
|
||||
|
||||
### Requirement: 时间范围选择器
|
||||
Drawer SHALL 在 Tabs 外层提供时间范围选择器,影响概览面板的数据;当记录面板处于激活状态或后续首次进入记录面板时,时间范围也 SHALL 影响记录面板的数据。
|
||||
|
||||
#### Scenario: 快捷时间按钮
|
||||
- **WHEN** Drawer 渲染
|
||||
- **THEN** 时间选择区第一行 SHALL 显示 TDesign RadioGroup(variant=default-filled)快捷按钮:1小时、6小时、24小时、7天
|
||||
|
||||
#### Scenario: 点击快捷按钮
|
||||
- **WHEN** 用户点击快捷按钮(如 "24小时")
|
||||
- **THEN** 系统 SHALL 自动设置对应的起止时间,DateRangePicker 显示对应的时间范围,该按钮高亮
|
||||
|
||||
#### Scenario: 快捷按钮联动统计区
|
||||
- **WHEN** 用户点击 1小时/6小时/24小时/7天 快捷按钮
|
||||
- **THEN** 概览面板 SHALL 使用对应的时间窗口重新请求 `/api/targets/:id/metrics` 数据
|
||||
|
||||
#### Scenario: 快捷按钮联动激活的历史记录
|
||||
- **WHEN** 用户在"记录"Tab 激活时点击 1小时/6小时/24小时/7天 快捷按钮
|
||||
- **THEN** 记录面板 SHALL 使用对应的时间窗口重新请求 `/api/targets/:id/history` 数据,并重置页码为 1
|
||||
|
||||
#### Scenario: 快捷按钮不预取未激活历史记录
|
||||
- **WHEN** 用户在"概览"Tab 激活时点击 1小时/6小时/24小时/7天 快捷按钮
|
||||
- **THEN** 系统 SHALL NOT 请求 `/api/targets/:id/history`,直到用户切换到"记录"Tab
|
||||
|
||||
#### Scenario: 自定义日期时间范围
|
||||
- **WHEN** 用户通过 TDesign DateRangePicker(mode=date, enableTimePicker, format="YYYY-MM-DD HH:mm")修改时间范围
|
||||
- **THEN** 快捷按钮 SHALL 取消高亮,系统 SHALL 按新的时间范围刷新概览数据,并按当前 Tab 状态决定是否刷新历史记录
|
||||
|
||||
#### Scenario: 时间精度为分钟级
|
||||
- **WHEN** 用户通过 DateRangePicker 选择时间
|
||||
- **THEN** 选择器 SHALL 仅精确到分钟(format="YYYY-MM-DD HH:mm"),秒列固定为 00
|
||||
|
||||
#### Scenario: DateRangePicker 自适应显示
|
||||
- **WHEN** Drawer 渲染
|
||||
- **THEN** DateRangePicker SHALL 通过 CSS 类 `.drawer-date-range`(替代原 `.full-width`)自适应填充时间选择区剩余宽度,不使用内联 style 的 width: 100%
|
||||
|
||||
#### Scenario: 默认时间范围
|
||||
- **WHEN** Drawer 打开
|
||||
- **THEN** 时间选择器 SHALL 默认选中 "24小时" 快捷按钮
|
||||
|
||||
#### Scenario: 筛选触发数据刷新
|
||||
- **WHEN** 时间范围发生变化
|
||||
- **THEN** 系统 SHALL 重新请求趋势数据;若"记录"Tab 当前激活,系统 SHALL 同时重新请求历史记录,否则 SHALL 延迟到用户进入"记录"Tab 后请求历史记录
|
||||
|
||||
### Requirement: Tabs 内容组织
|
||||
Drawer 内部 SHALL 使用 TDesign Tabs 组织概览和记录两个面板。Tabs SHALL 使用受控 value 管理当前激活 Tab,TabPanel 内边距通过 className prop 控制。
|
||||
|
||||
#### Scenario: Tab 标签
|
||||
- **WHEN** Drawer 渲染
|
||||
- **THEN** Tabs SHALL 显示两个标签:概览、记录
|
||||
|
||||
#### Scenario: Tabs 受控状态
|
||||
- **WHEN** 用户切换 Tab
|
||||
- **THEN** Tabs SHALL 通过 `value` 和 `onChange` 更新由 Drawer 状态 hook 管理的当前 Tab 值
|
||||
|
||||
#### Scenario: 默认概览 Tab
|
||||
- **WHEN** Drawer 打开或切换到另一个目标
|
||||
- **THEN** 当前 Tab SHALL 重置为 `overview`
|
||||
|
||||
#### Scenario: Tab 面板内边距
|
||||
- **WHEN** TabPanel 渲染
|
||||
- **THEN** TabPanel SHALL 通过 `className` prop 传入自定义类名(`tab-panel-padded`)控制内边距,不通过入侵 TDesign 内部类名(`.t-tab-panel`)覆盖
|
||||
|
||||
#### Scenario: TabPanel 懒渲染与缓存
|
||||
- **WHEN** 用户在概览和记录 Tab 之间切换
|
||||
- **THEN** 概览和记录 TabPanel 均 SHALL 配置 TDesign TabPanel 的 `destroyOnHide={false}`,隐藏时不销毁组件,保留已挂载的面板状态和已加载的数据;记录 TabPanel SHALL 额外配置懒渲染,首次进入前不渲染 HistoryTab
|
||||
|
||||
### Requirement: 概览面板
|
||||
概览 Tab SHALL 按区域展示基本信息、多维度统计和趋势图。
|
||||
|
||||
#### Scenario: 区域排列顺序
|
||||
- **WHEN** 概览面板渲染
|
||||
- **THEN** 面板 SHALL 按以下顺序展示区域:基本信息 → 统计 → 趋势,每个区域前 SHALL 显示 TDesign Divider(align="left")作为小标题
|
||||
|
||||
#### Scenario: 基本信息直接展示
|
||||
- **WHEN** 概览面板渲染
|
||||
- **THEN** 面板 SHALL 在"基本信息"区域直接使用 TDesign Descriptions 组件展示配置信息(不折叠),Descriptions SHALL 配置 `tableLayout="auto"` 使 label 宽度自适应内容
|
||||
|
||||
#### Scenario: 基本信息内容
|
||||
- **WHEN** 概览面板渲染
|
||||
- **THEN** Descriptions SHALL 展示:目标地址、检查间隔、最新检查时间、状态详情、描述,其中描述 SHALL 位于最后一行
|
||||
|
||||
#### Scenario: 描述行占满整行
|
||||
- **WHEN** 概览面板渲染基本信息
|
||||
- **THEN** 描述项 SHALL 占据 Descriptions 的一整行,内容 SHALL 使用 `target.description ?? ""`,即使 description 为空也 SHALL 渲染该项
|
||||
|
||||
#### Scenario: 统计区上下布局卡片
|
||||
- **WHEN** 概览面板渲染且有统计数据
|
||||
- **THEN** 面板 SHALL 在"统计"区域使用 4 列 × 2 行的 Row/Col 布局,每个统计项使用 `overview-stat-card` 包裹,内部使用 TDesign Statistic 组件自带的上下布局(title 在上、value 在下),通过 `summary-stat-col` 类居中。系统 SHALL NOT 使用已移除的 `overview-stat-item` 左右 flex 布局
|
||||
|
||||
#### Scenario: 统计区内容
|
||||
- **WHEN** 概览面板渲染
|
||||
- **THEN** 统计区 SHALL 展示:可用率(suffix="%")、平均延迟(suffix="ms")、P95 延迟(suffix="ms")、检查总数、MTTR(动态单位)、最长故障(动态单位)、故障次数(suffix="次")、连续正常(suffix="次")
|
||||
|
||||
#### Scenario: 趋势图
|
||||
- **WHEN** 概览面板渲染且 metricsData.trend 可用
|
||||
- **THEN** 面板 SHALL 在"趋势"区域展示 TrendChart 组件
|
||||
|
||||
#### Scenario: 统计区加载状态
|
||||
- **WHEN** metricsData 正在加载
|
||||
- **THEN** 统计区 SHALL 显示 TDesign Skeleton 加载占位
|
||||
|
||||
#### Scenario: 统计区无数据
|
||||
- **WHEN** metricsData 为 null 且未处于加载状态
|
||||
- **THEN** 统计区 SHALL 展示占位状态
|
||||
|
||||
#### Scenario: 趋势数据加载中
|
||||
- **WHEN** metricsData 正在加载
|
||||
- **THEN** "趋势"区域 SHALL 显示 TDesign Skeleton 加载占位
|
||||
|
||||
### Requirement: Drawer TDesign 原生生命周期与滚动控制
|
||||
目标详情 Drawer SHALL 优先使用 TDesign Drawer 的原生 props 控制挂载、可见性和滚动穿透,不通过自定义滚轮事件实现滚动控制。
|
||||
|
||||
#### Scenario: Drawer 常驻受控渲染
|
||||
- **WHEN** 未选中目标时
|
||||
- **THEN** `TargetDetailDrawer` SHALL 保留 TDesign Drawer 组件并通过 `visible=false` 隐藏,而不是直接返回 `null` 卸载 Drawer 子树
|
||||
|
||||
#### Scenario: Drawer 防滚动穿透配置
|
||||
- **WHEN** Drawer 渲染
|
||||
- **THEN** Drawer SHALL 显式使用 `attach="body"`、`preventScrollThrough=true`、`showInAttachedElement=false` 和 `showOverlay=true`
|
||||
|
||||
#### Scenario: Drawer 关闭后保留子树
|
||||
- **WHEN** 用户关闭 Drawer
|
||||
- **THEN** Drawer SHALL 使用 `destroyOnClose=false` 保留已挂载内容,避免重复打开时重建完整子树
|
||||
|
||||
#### Scenario: Drawer 单一纵向滚动容器
|
||||
- **WHEN** Drawer 内容高度超过可视区域
|
||||
- **THEN** 系统 SHALL 依赖 Drawer 内容区域作为唯一纵向滚动容器,HistoryTab 中的 PrimaryTable SHALL 不配置 `height`、`maxHeight` 或纵向 `scroll` 来创建第二个纵向滚动区域
|
||||
|
||||
### Requirement: Drawer 宽度
|
||||
Drawer 宽度 SHALL 根据视口宽度设置响应式默认值,并 SHALL 支持用户通过鼠标拖拽边缘在当前页面生命周期内调整宽度。系统 MUST NOT 将拖拽后的宽度持久化到 `localStorage`、后端、URL 或其他跨刷新存储。
|
||||
|
||||
#### Scenario: Drawer 响应式默认宽度
|
||||
- **WHEN** Drawer 打开且用户尚未在当前页面生命周期内拖拽调整宽度
|
||||
- **THEN** Drawer size SHALL 使用响应式默认宽度,宽屏时占视口比例 SHALL 小于窄屏时占视口比例,且窄屏下 SHALL 不超过视口安全宽度
|
||||
|
||||
#### Scenario: Drawer 边缘拖拽宽度
|
||||
- **WHEN** 用户使用鼠标拖动右侧 Drawer 的左边缘
|
||||
- **THEN** Drawer SHALL 通过 TDesign Drawer 原生拖拽能力调整宽度,不通过自定义全局鼠标事件实现拖拽
|
||||
|
||||
#### Scenario: Drawer 拖拽边界
|
||||
- **WHEN** 用户拖拽调整 Drawer 宽度
|
||||
- **THEN** Drawer 宽度 SHALL 被限制在最小可读宽度和视口安全最大宽度之间,避免内容不可读或横向溢出
|
||||
|
||||
#### Scenario: Drawer 当前页面生命周期内保留拖拽宽度
|
||||
- **WHEN** 用户拖拽调整 Drawer 宽度后关闭并再次打开 Drawer,且页面未刷新、组件未重新挂载
|
||||
- **THEN** Drawer SHALL 保留当前页面生命周期内的拖拽后宽度
|
||||
|
||||
#### Scenario: Drawer 拖拽宽度不持久化
|
||||
- **WHEN** 页面刷新后用户再次打开 Drawer
|
||||
- **THEN** Drawer SHALL 恢复响应式默认宽度,且 MUST NOT 从 `localStorage`、后端、URL 或其他跨刷新存储恢复拖拽宽度
|
||||
|
||||
### Requirement: 时间选择器单行布局
|
||||
Drawer 顶部的时间范围快捷按钮和日期范围选择器 SHALL 在同一行展示。
|
||||
|
||||
#### Scenario: 单行布局
|
||||
- **WHEN** Drawer 渲染时间选择区域
|
||||
- **THEN** RadioGroup 和 DateRangePicker SHALL 使用 flex 布局在同一行水平排列
|
||||
|
||||
### Requirement: 记录面板
|
||||
记录 Tab SHALL 展示分页检查结果列表,使用 TDesign PrimaryTable。
|
||||
|
||||
#### Scenario: 检查结果表格
|
||||
- **WHEN** 记录面板渲染且数据可用
|
||||
- **THEN** 面板 SHALL 使用 TDesign PrimaryTable 展示检查结果,列包含:状态(StatusDot 圆点)、时间(YYYY-MM-DD HH:mm:ss 格式)、耗时(标题含 ms 单位,单元格仅显示数值,居中对齐)、详情(detail 和 failure.message 用冒号拼接)
|
||||
|
||||
#### Scenario: 服务端分页
|
||||
- **WHEN** 检查结果总数超过一页
|
||||
- **THEN** 表格 SHALL 使用内建 pagination(disableDataPage=true),分页器显示在表格底部
|
||||
|
||||
#### Scenario: 翻页触发请求
|
||||
- **WHEN** 用户切换分页页码
|
||||
- **THEN** 系统 SHALL 请求对应页码的服务端数据,表格更新
|
||||
|
||||
#### Scenario: 记录数据加载中
|
||||
- **WHEN** 历史记录正在加载
|
||||
- **THEN** 表格 SHALL 显示 loading 状态
|
||||
@@ -1,171 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义分组表格的列配置、排序、筛选、行交互和 DOWN 行视觉强化。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 分组表格展示
|
||||
Dashboard SHALL 按 group 字段将目标分组,每个分组使用 TDesign Card 包裹独立的 PrimaryTable,分组间使用 TDesign Space 垂直排列。
|
||||
|
||||
#### Scenario: 按分组渲染独立表格
|
||||
- **WHEN** 用户打开 Dashboard 页面
|
||||
- **THEN** 页面 SHALL 按 group 字段将目标分组,每个分组使用 TDesign Card 组件包裹,Card 内包含一个 PrimaryTable
|
||||
|
||||
#### Scenario: 分组 Card 标题
|
||||
- **WHEN** 页面渲染某个分组
|
||||
- **THEN** Card 的 `title` prop SHALL 渲染分组名称("default" 显示为 "默认分组"),Card 的 `actions` prop SHALL 渲染统计 Tag:正常数(theme=success, variant=light)和异常数(theme=danger, variant=light)
|
||||
|
||||
#### Scenario: 分组 Card 样式
|
||||
- **WHEN** 页面渲染分组 Card
|
||||
- **THEN** Card SHALL 设置 `headerBordered` 在标题和表格之间显示分割线
|
||||
|
||||
#### Scenario: 分组顺序
|
||||
- **WHEN** 页面渲染多个分组
|
||||
- **THEN** "default" 分组 SHALL 排在最上面,其余分组按 YAML 配置中首次出现的顺序排列
|
||||
|
||||
#### Scenario: "default" 分组显示名称
|
||||
- **WHEN** 分组名称为 "default"
|
||||
- **THEN** Card title SHALL 显示 "默认分组"
|
||||
|
||||
#### Scenario: Dashboard 容器最大宽度
|
||||
- **WHEN** 用户打开 Dashboard 页面
|
||||
- **THEN** Dashboard 内容区 SHALL 设置 max-width: 1400px 并水平居中
|
||||
|
||||
#### Scenario: 分组间统一间距
|
||||
- **WHEN** 页面渲染多个分组
|
||||
- **THEN** 分组之间 SHALL 使用 TDesign Space 组件(direction=vertical, size=24)统一间距
|
||||
|
||||
### Requirement: 表格列定义
|
||||
每个分组的 PrimaryTable SHALL 包含状态、名称、类型、可用率、最近状态条、连续状态、延迟 7 列(不含间隔列)。列渲染不使用内联 style。列定义 SHALL 通过工厂函数动态生成。
|
||||
|
||||
#### Scenario: 状态列
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 状态列 SHALL 使用 StatusDot 组件渲染,标题显示"#",宽度 60px,fixed="left",居中对齐,支持筛选(UP/DOWN/全部)
|
||||
|
||||
#### Scenario: 名称列
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 名称列 SHALL 显示目标展示名称,取值为 `target.name ?? target.id`,ellipsis 超长名称自动省略并 Tooltip 显示全名,且 SHALL NOT 支持排序
|
||||
|
||||
#### Scenario: name 为 null 的名称列
|
||||
- **WHEN** 表格渲染某个 `target.name` 为 null 的目标
|
||||
- **THEN** 名称列 SHALL 显示该目标的 `target.id`
|
||||
|
||||
#### Scenario: 类型列
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 类型列 SHALL 使用 TDesign Tag 组件(size=small, theme=primary, variant=light-outline)直接显示 target.type 原始文本,支持单选筛选
|
||||
|
||||
#### Scenario: 类型筛选器动态生成
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 类型列的筛选器列表 SHALL 从 meta API 返回的 `checkerTypes` 动态生成,包含"全部"选项和每个 checker 类型选项(label 和 value 均为 type 原始文本)
|
||||
|
||||
#### Scenario: 可用率列
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 可用率列标题 SHALL 展示为"可用率(24h)",使用 TDesign Progress 组件(theme=line, size=small)渲染,颜色通过 CSS 自定义属性 `--avail-N` 控制,支持排序
|
||||
|
||||
#### Scenario: 最近状态列
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 最近状态列 SHALL 使用 StatusBar 组件渲染采样色块,宽度 220px
|
||||
|
||||
#### Scenario: 连续状态列渲染
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 表格 SHALL 在「最近状态」列之后、「延迟」列之前展示「连续状态」列,标题为"连续(次)",宽度 88px,Tag 内显示方向箭头和数字(capped 时追加"+")
|
||||
|
||||
#### Scenario: 延迟列
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 延迟列标题 SHALL 展示为"延迟",宽度 SHALL 为 80px,单元格 SHALL 显示最近一次检查的延迟数值并附加 " ms" 后缀(如 "156 ms"),右对齐,颜色通过 CSS 类控制;超过 9999ms 时 SHALL 显示为"9999+ ms"
|
||||
|
||||
#### Scenario: 间隔列移除
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 表格 SHALL 不包含"间隔"列(间隔信息移入 Drawer 基本信息区域)
|
||||
|
||||
### Requirement: 列定义工厂函数
|
||||
列定义 SHALL 通过工厂函数生成,接收动态参数。
|
||||
|
||||
#### Scenario: createTargetTableColumns 函数
|
||||
- **WHEN** 需要生成表格列定义
|
||||
- **THEN** 系统 SHALL 调用 `createTargetTableColumns(checkerTypes: string[])` 函数,返回 `PrimaryTableCol<TargetStatus>[]`
|
||||
|
||||
#### Scenario: checkerTypes 为空数组
|
||||
- **WHEN** meta API 尚未返回或返回空数组
|
||||
- **THEN** 类型列的筛选器 SHALL 仅包含"全部"选项
|
||||
|
||||
#### Scenario: 列定义缓存
|
||||
- **WHEN** TargetBoard 组件渲染
|
||||
- **THEN** 列定义 SHALL 通过 `useMemo` 缓存,仅在 `checkerTypes` 变化时重新生成
|
||||
|
||||
### Requirement: TargetGroup 接收 columns prop
|
||||
TargetGroup 组件 SHALL 通过 prop 接收列定义,不再直接导入静态常量。
|
||||
|
||||
#### Scenario: columns prop
|
||||
- **WHEN** TargetGroup 渲染
|
||||
- **THEN** 组件 SHALL 接收 `columns: PrimaryTableCol<TargetStatus>[]` prop 并传递给 PrimaryTable
|
||||
|
||||
#### Scenario: TargetBoard 传递 columns
|
||||
- **WHEN** TargetBoard 渲染子组件
|
||||
- **THEN** TargetBoard SHALL 调用 `createTargetTableColumns` 生成列定义并传递给每个 TargetGroup
|
||||
|
||||
### Requirement: 默认排序
|
||||
表格 SHALL 默认按状态降序排列,异常(DOWN)目标排在最前面。
|
||||
|
||||
#### Scenario: 页面初始排序
|
||||
- **WHEN** 用户打开 Dashboard 页面
|
||||
- **THEN** 每个分组表格 SHALL 默认按状态降序排列,DOWN 目标排在同组最前面
|
||||
|
||||
### Requirement: DOWN 行视觉强化
|
||||
表格中状态为 DOWN 的行 SHALL 具有视觉区分,包含背景色和左侧竖线。
|
||||
|
||||
#### Scenario: DOWN 行背景色
|
||||
- **WHEN** 目标最近一次检查 matched=false
|
||||
- **THEN** 该行 SHALL 通过 CSS 选择器获得浅红色背景
|
||||
|
||||
#### Scenario: DOWN 行左侧竖线
|
||||
- **WHEN** 目标最近一次检查 matched=false
|
||||
- **THEN** 该行 SHALL 通过 CSS 选择器获得左侧 3px 红色竖线(border-left: 3px solid var(--td-error-color))
|
||||
|
||||
#### Scenario: DOWN 行 hover 状态
|
||||
- **WHEN** 鼠标悬停在 DOWN 行上
|
||||
- **THEN** 行背景 SHALL 显示 hover 状态色,与正常行 hover 效果协调
|
||||
|
||||
### Requirement: 行点击交互
|
||||
表格行 SHALL 支持点击打开目标详情 Drawer。
|
||||
|
||||
#### Scenario: 点击行打开 Drawer
|
||||
- **WHEN** 用户点击某一行
|
||||
- **THEN** 系统 SHALL 打开该目标的详情 Drawer
|
||||
|
||||
#### Scenario: 行 hover 效果
|
||||
- **WHEN** 鼠标悬停在表格行上
|
||||
- **THEN** 行 SHALL 显示 hover 高亮效果(TDesign Table hover prop)
|
||||
|
||||
#### Scenario: 行 cursor 样式
|
||||
- **WHEN** 鼠标悬停在表格行上
|
||||
- **THEN** cursor SHALL 显示为 pointer
|
||||
|
||||
### Requirement: 表格外观
|
||||
表格 SHALL 使用 TDesign PrimaryTable 统一外观,不设置 bordered(由外层 Card 提供边界)。
|
||||
|
||||
#### Scenario: 表格样式
|
||||
- **WHEN** 表格渲染
|
||||
- **THEN** 表格 SHALL 设置 size="small"、stripe、hover,不设置 bordered
|
||||
|
||||
### Requirement: StatusBar Tooltip 交互
|
||||
StatusBar 色块 SHALL 在 hover 时通过 TDesign Tooltip 展示时间和状态信息。组件 props 类型 SHALL 使用完整的 `RecentSample` 类型(包含 timestamp 字段)而非简化的 `{ up: boolean }`。
|
||||
|
||||
#### Scenario: StatusBar props 类型变更
|
||||
- **WHEN** StatusBar 组件接收 samples 数据
|
||||
- **THEN** 组件 SHALL 接收 `Array<RecentSample>` 类型(包含 timestamp、durationMs、up 字段),而非简化的 `Array<{ up: boolean }>` 类型
|
||||
|
||||
#### Scenario: 有数据色块 Tooltip
|
||||
- **WHEN** 鼠标悬停在有数据的色块上
|
||||
- **THEN** 色块 SHALL 通过 TDesign Tooltip(placement="top")展示该采样点的时间(使用 formatRelativeTime 格式化)和状态(正常/异常)
|
||||
|
||||
#### Scenario: 空色块无 Tooltip
|
||||
- **WHEN** 鼠标悬停在空色块(empty)上
|
||||
- **THEN** 色块 SHALL 不显示 Tooltip
|
||||
|
||||
### Requirement: 列定义复用
|
||||
所有分组的表格 SHALL 共享同一套列定义。
|
||||
|
||||
#### Scenario: 列定义提取为常量
|
||||
- **WHEN** 多个分组表格渲染
|
||||
- **THEN** 列定义 SHALL 从独立的 constants/target-table-columns.tsx 导入,不在组件中重复定义
|
||||
@@ -1,109 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 TCP checker 的配置格式、连接执行、banner 读取、expect 校验、失败结构和状态摘要。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: tcp target 配置
|
||||
系统 SHALL 支持 `type: tcp` 的 target 配置,通过 `tcp.host` 和 `tcp.port` 描述目标 TCP 地址,并通过可选字段控制 banner 读取行为。
|
||||
|
||||
#### Scenario: 解析最简 tcp target
|
||||
- **WHEN** YAML 中 target 配置 `type: tcp`、`tcp.host: "127.0.0.1"` 和 `tcp.port: 6379`
|
||||
- **THEN** 系统 SHALL 将其解析为 tcp checker,并填充 `readBanner=false`、`bannerReadTimeout=2000`、`maxBannerBytes=4096`、interval、timeout、group 和 expect 配置
|
||||
|
||||
#### Scenario: tcp target 缺少 host
|
||||
- **WHEN** YAML 中 target 配置 `type: tcp` 但缺少 `tcp.host`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 tcp.host 字段
|
||||
|
||||
#### Scenario: tcp target 缺少 port
|
||||
- **WHEN** YAML 中 target 配置 `type: tcp` 但缺少 `tcp.port`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 tcp.port 字段
|
||||
|
||||
#### Scenario: tcp port 范围非法
|
||||
- **WHEN** YAML 中 tcp target 的 `tcp.port` 不是 1 到 65535 之间的整数
|
||||
- **THEN** 系统 SHALL 以配置错误退出,并提示 tcp.port 必须为合法 TCP 端口
|
||||
|
||||
#### Scenario: tcp 序列化展示摘要
|
||||
- **WHEN** 系统同步 tcp target 到 targets 表
|
||||
- **THEN** `target` 展示摘要 SHALL 为 `<host>:<port>`,`config` JSON SHALL 包含 resolved 后的 host、port、readBanner、bannerReadTimeout 和 maxBannerBytes
|
||||
|
||||
### Requirement: tcp checker 执行
|
||||
系统 SHALL 按 tcp target 配置建立 TCP 连接,记录完整执行耗时和 TCP observation,并在连接失败、超时或资源超限时产生结构化失败信息。
|
||||
|
||||
#### Scenario: TCP 连接成功
|
||||
- **WHEN** tcp target 指向可连接的 TCP 服务,且未配置 expect 或 `expect.connected` 为 `true`
|
||||
- **THEN** 系统 SHALL 记录 `matched=true`、`durationMs` 和包含 connected、connectTimeMs、banner 的 observation,并关闭 socket
|
||||
|
||||
#### Scenario: TCP 连接失败
|
||||
- **WHEN** tcp target 指向不可连接的 host/port,且未配置 expect 或 `expect.connected` 为 `true`
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,observation SHALL 包含 connected=false 和错误信息,failure 的 kind 为 `error`,phase 为 `connect`,message 包含可读连接失败原因
|
||||
|
||||
#### Scenario: 期望端口不可达且连接失败
|
||||
- **WHEN** tcp target 配置 `expect.connected: false`,且 TCP 连接失败
|
||||
- **THEN** 系统 SHALL 记录 `matched=true`,observation SHALL 包含 connected=false 和实际连接失败原因,API detail SHALL 展示实际连接失败原因摘要
|
||||
|
||||
#### Scenario: 期望端口不可达但连接成功
|
||||
- **WHEN** tcp target 配置 `expect.connected: false`,但 TCP 连接成功
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,observation SHALL 包含 connected=true,failure 的 kind 为 `mismatch`,phase 为 `connected`
|
||||
|
||||
#### Scenario: TCP 执行超时
|
||||
- **WHEN** 引擎注入的 `ctx.signal` 在 TCP 连接或 banner 读取过程中 abort
|
||||
- **THEN** 系统 SHALL best-effort 关闭 socket,记录 `matched=false`,failure 的 kind 为 `error`,phase 为 `connect` 或 `banner`,message 包含超时信息,并在可收集时记录 observation
|
||||
|
||||
#### Scenario: duration 包含 banner 读取
|
||||
- **WHEN** tcp target 开启 `readBanner` 且服务端延迟发送 banner
|
||||
- **THEN** 结果中的 `durationMs` SHALL 覆盖连接建立、banner 等待、banner 读取和 expect 校验的完整耗时
|
||||
|
||||
### Requirement: tcp banner 读取
|
||||
系统 SHALL 仅在 `tcp.readBanner: true` 时读取服务端主动发送的 banner 数据,并同时受 `bannerReadTimeout` 和 `maxBannerBytes` 限制。
|
||||
|
||||
#### Scenario: 默认不读取 banner
|
||||
- **WHEN** tcp target 未配置 `readBanner` 或配置为 `false`
|
||||
- **THEN** 系统 SHALL 在连接建立后立即进入 connected 和 duration 校验,不等待服务端数据
|
||||
|
||||
#### Scenario: 读取服务端 banner
|
||||
- **WHEN** tcp target 配置 `readBanner: true`,且服务端连接后发送 `220 smtp.example.com ESMTP`
|
||||
- **THEN** 系统 SHALL 收集 banner 文本,并允许后续 `expect.banner` 对该文本执行 operator 断言
|
||||
|
||||
#### Scenario: banner 等待超时无数据
|
||||
- **WHEN** tcp target 配置 `readBanner: true`,但服务端在 `bannerReadTimeout` 内未发送任何数据
|
||||
- **THEN** 系统 SHALL 将 banner 视为空字符串并继续执行 expect 校验,不将无 banner 本身作为连接错误
|
||||
|
||||
#### Scenario: banner 读取超过最大字节数
|
||||
- **WHEN** 服务端发送的 banner 数据超过 `maxBannerBytes`
|
||||
- **THEN** 系统 SHALL 停止读取并记录 `matched=false`、failure.kind=`error`、failure.phase=`banner` 的结构化错误
|
||||
|
||||
#### Scenario: banner detail 截断展示
|
||||
- **WHEN** tcp target 成功读取到较长 banner
|
||||
- **THEN** observation.banner SHALL 保存截断后的 banner 摘要,API detail SHALL 展示截断后的 banner 摘要,避免 UI 展示过长文本
|
||||
|
||||
### Requirement: tcp expect 校验
|
||||
系统 SHALL 支持 tcp 专属 expect,包括 `connected`、`banner` 和 `durationMs`,并按 connected、banner、durationMs 的阶段顺序快速失败。`connected` SHALL 保持布尔状态语义,未配置时在 Resolved expect 中默认 `true`。`banner` MUST 使用共享 `RawContentExpectations` 数组输入并在运行期使用 `ContentExpectations`,且仅在 `tcp.readBanner: true` 时允许配置。`durationMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation` 校验包含连接和 banner 读取在内的完整执行耗时。
|
||||
|
||||
#### Scenario: 默认 connected 成功语义
|
||||
- **WHEN** tcp target 未显式配置 `expect.connected`
|
||||
- **THEN** 系统 SHALL 在 Resolved tcp expect 中使用默认 `connected: true` 进行校验
|
||||
|
||||
#### Scenario: durationMs 校验
|
||||
- **WHEN** tcp target 配置 `expect.durationMs: {lte: 100}`,且完整执行耗时超过 100ms
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 phase 为 `duration`
|
||||
|
||||
#### Scenario: banner ContentExpectations 校验通过
|
||||
- **WHEN** tcp target 配置 `readBanner: true`、`expect.banner: [{contains: "ESMTP"}]`,且实际 banner 包含 `ESMTP`
|
||||
- **THEN** 系统 SHALL 判定 banner 阶段通过
|
||||
|
||||
#### Scenario: banner regex 校验失败
|
||||
- **WHEN** tcp target 配置 `readBanner: true`、`expect.banner: [{regex: "^SSH-2\\.0"}]`,且实际 banner 不匹配该正则
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `banner`,path 指向失败的 banner expectation
|
||||
|
||||
#### Scenario: banner 多规则快速失败
|
||||
- **WHEN** tcp target 配置两条 banner expectation 且第一条失败
|
||||
- **THEN** 系统 SHALL 返回第一条失败 expectation 的 failure,并 MUST NOT 执行第二条 expectation
|
||||
|
||||
#### Scenario: expect.banner 未开启 readBanner
|
||||
- **WHEN** tcp target 配置 `expect.banner`,但 `tcp.readBanner` 未配置为 `true`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示 banner 断言需要启用 tcp.readBanner
|
||||
|
||||
#### Scenario: tcp expect 未知字段失败
|
||||
- **WHEN** YAML 中 tcp target 的 expect 包含 `status: [200]`、`maxDurationMs: 1000` 或其他非 tcp expect 字段
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
|
||||
@@ -1,27 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
确保测试运行时输出干净、无噪音,便于开发者快速定位问题。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 测试不应产生无关 console 输出
|
||||
测试运行时,由测试用例预期的容错行为(如 JSON 解析失败、checker rejected)触发的 logger 输出 SHALL 在测试代码中被抑制,不污染测试报告。测试 SHALL 优先通过注入 no-op logger 或 memory logger 抑制预期日志,而不是直接覆盖全局 `console.warn`。
|
||||
|
||||
#### Scenario: 容错测试抑制 logger 输出
|
||||
- **WHEN** 测试用例故意注入损坏数据或触发异常以验证系统容错行为
|
||||
- **THEN** 测试 SHALL 注入 no-op logger 或 memory logger,并在断言完成后恢复默认测试上下文
|
||||
|
||||
#### Scenario: 非预期 logger 输出不被抑制
|
||||
- **WHEN** 测试用例并非专门测试容错行为
|
||||
- **THEN** 测试 SHALL NOT 抑制 logger 输出,确保意外 warn 或 error 可被观测
|
||||
|
||||
### Requirement: 探针执行失败日志输出单行消息
|
||||
ProbeEngine 在捕获 checker rejected 时,logger SHALL 输出单行错误消息文本,MUST NOT 输出 Error 对象(会导致多行堆栈噪音)。该日志 SHALL 使用 `error` 等级,并包含 targetId、targetType 和 `formatReason()` 提取的错误消息。
|
||||
|
||||
#### Scenario: checker rejected 输出单行日志
|
||||
- **WHEN** checker 执行抛出未捕获异常(Promise rejected)
|
||||
- **THEN** logger SHALL 输出格式为 `探针执行失败: <message>` 的单行消息,其中 message 使用 `formatReason()` 提取
|
||||
|
||||
#### Scenario: formatReason 复用
|
||||
- **WHEN** 构建失败日志消息和写入 CheckFailure
|
||||
- **THEN** 两者 SHALL 共用同一个 `formatReason` 函数提取错误消息
|
||||
@@ -1,73 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 Dashboard 主题模式选择、系统主题跟随、浏览器本地持久化和 TDesign 主题变量应用行为。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 主题模式选择器
|
||||
Dashboard SHALL 在 Header 右侧提供主题模式 RadioGroup,允许用户选择"系统""明亮""黑暗"三种模式。
|
||||
|
||||
#### Scenario: 主题模式选项渲染
|
||||
- **WHEN** Dashboard 页面渲染
|
||||
- **THEN** HeadMenu operations 区域 SHALL 在刷新频率选择器前显示 RadioGroup(theme="button", variant="default-filled"),选项为:系统、明亮、黑暗
|
||||
|
||||
#### Scenario: 默认选择系统
|
||||
- **WHEN** 当前浏览器没有已保存的有效主题偏好
|
||||
- **THEN** 主题模式 RadioGroup SHALL 默认选中"系统"
|
||||
|
||||
#### Scenario: 用户切换主题模式
|
||||
- **WHEN** 用户点击"系统""明亮"或"黑暗"任一主题模式选项
|
||||
- **THEN** RadioGroup SHALL 选中该选项,并触发对应主题模式生效
|
||||
|
||||
### Requirement: 主题模式生效
|
||||
系统 SHALL 根据用户主题偏好计算有效主题,并通过 `<html>` 元素的 `theme-mode` 属性应用 TDesign 主题变量。
|
||||
|
||||
#### Scenario: 系统模式跟随暗色系统
|
||||
- **WHEN** 用户主题偏好为"系统"且 `prefers-color-scheme: dark` 匹配
|
||||
- **THEN** 系统 SHALL 设置 `document.documentElement` 的 `theme-mode` 属性为 `dark`
|
||||
|
||||
#### Scenario: 系统模式跟随亮色系统
|
||||
- **WHEN** 用户主题偏好为"系统"且 `prefers-color-scheme: dark` 不匹配
|
||||
- **THEN** 系统 SHALL 设置 `document.documentElement` 的 `theme-mode` 属性为 `light`
|
||||
|
||||
#### Scenario: 系统主题变化自动更新
|
||||
- **WHEN** 用户主题偏好为"系统"且浏览器系统主题在明亮和黑暗之间变化
|
||||
- **THEN** 系统 SHALL 自动更新 `theme-mode` 属性为新的有效主题
|
||||
|
||||
#### Scenario: 明亮模式固定主题
|
||||
- **WHEN** 用户主题偏好为"明亮"
|
||||
- **THEN** 系统 SHALL 设置 `theme-mode` 属性为 `light`,且系统主题变化 SHALL NOT 改变该属性
|
||||
|
||||
#### Scenario: 黑暗模式固定主题
|
||||
- **WHEN** 用户主题偏好为"黑暗"
|
||||
- **THEN** 系统 SHALL 设置 `theme-mode` 属性为 `dark`,且系统主题变化 SHALL NOT 改变该属性
|
||||
|
||||
### Requirement: 主题偏好本地持久化
|
||||
系统 SHALL 将用户选择的主题偏好保存到当前浏览器本地存储,并在后续页面加载时恢复。
|
||||
|
||||
#### Scenario: 保存用户选择
|
||||
- **WHEN** 用户切换主题模式
|
||||
- **THEN** 系统 SHALL 将对应偏好值写入 `localStorage` 的 `dial.theme.preference` 键
|
||||
|
||||
#### Scenario: 恢复已保存偏好
|
||||
- **WHEN** 页面加载且 `localStorage` 的 `dial.theme.preference` 键保存了有效偏好值
|
||||
- **THEN** 系统 SHALL 使用该偏好初始化主题模式 RadioGroup 和有效主题
|
||||
|
||||
#### Scenario: 非法本地偏好回退
|
||||
- **WHEN** 页面加载且 `dial.theme.preference` 保存了非 `system`、`light`、`dark` 的值
|
||||
- **THEN** 系统 SHALL 忽略该值并按"系统"模式初始化
|
||||
|
||||
#### Scenario: 本地存储不可用
|
||||
- **WHEN** 浏览器读取或写入 `localStorage` 抛出异常
|
||||
- **THEN** Dashboard SHALL 继续正常渲染,并按内存中的主题偏好应用主题
|
||||
|
||||
### Requirement: 启动期主题恢复
|
||||
系统 SHALL 在 React App 首次渲染前尽早应用一次有效主题,降低暗色环境下的亮色闪烁。
|
||||
|
||||
#### Scenario: 渲染前应用已保存偏好
|
||||
- **WHEN** 前端入口初始化且浏览器已保存有效主题偏好
|
||||
- **THEN** 系统 SHALL 在创建 React root 前根据该偏好设置 `<html>` 的 `theme-mode` 属性
|
||||
|
||||
#### Scenario: 渲染前应用系统偏好
|
||||
- **WHEN** 前端入口初始化且浏览器没有有效主题偏好
|
||||
- **THEN** 系统 SHALL 在创建 React root 前根据 `prefers-color-scheme: dark` 设置 `<html>` 的 `theme-mode` 属性
|
||||
@@ -1,195 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 UDP checker 的 target 配置、defaults、执行语义、响应编码、expect 校验、失败结构和状态摘要。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: udp target 配置
|
||||
系统 SHALL 支持 `type: udp` 的 target 配置,通过 `udp.host` 和 `udp.port` 描述目标 UDP 地址,并通过可选字段控制 payload、编码和响应大小限制。
|
||||
|
||||
#### Scenario: 解析最简 udp target
|
||||
- **WHEN** YAML 中 target 配置 `type: udp`、`udp.host: "127.0.0.1"` 和 `udp.port: 9000`
|
||||
- **THEN** 系统 SHALL 将其解析为 udp checker,并填充 `payload=""`、`encoding="text"`、`responseEncoding="text"`、`maxResponseBytes=4096`、interval、timeout、group 和 expect 配置
|
||||
|
||||
#### Scenario: udp target 缺少 host
|
||||
- **WHEN** YAML 中 target 配置 `type: udp` 但缺少 `udp.host`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 udp.host 字段
|
||||
|
||||
#### Scenario: udp target 缺少 port
|
||||
- **WHEN** YAML 中 target 配置 `type: udp` 但缺少 `udp.port`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,并提示该 target 缺少 udp.port 字段
|
||||
|
||||
#### Scenario: udp port 范围非法
|
||||
- **WHEN** YAML 中 udp target 的 `udp.port` 不是 1 到 65535 之间的整数
|
||||
- **THEN** 系统 SHALL 以配置错误退出,并提示 udp.port 必须为合法 UDP 端口
|
||||
|
||||
#### Scenario: 省略 payload 发送空 datagram
|
||||
- **WHEN** YAML 中 udp target 未配置 `udp.payload`
|
||||
- **THEN** 系统 SHALL 使用空字符串作为 payload,并在执行时发送零长度 UDP datagram
|
||||
|
||||
#### Scenario: udp 分组未知字段失败
|
||||
- **WHEN** YAML 中 udp target 的 `udp` 分组包含 `dnsQuery`、`expectResponse` 或其他未知字段
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 udp 分组包含未知字段
|
||||
|
||||
#### Scenario: udp 序列化展示摘要
|
||||
- **WHEN** 系统同步 udp target 到 targets 表
|
||||
- **THEN** `target` 展示摘要 SHALL 为 `udp <host>:<port>`,`config` JSON SHALL 包含 resolved 后的 host、port、payload、encoding、responseEncoding 和 maxResponseBytes
|
||||
|
||||
### Requirement: udp payload 编码
|
||||
系统 SHALL 支持将 `udp.payload` 按 `udp.encoding` 解码为发送字节,编码类型限定为 `text`、`hex` 和 `base64`。
|
||||
|
||||
#### Scenario: text payload 编码
|
||||
- **WHEN** udp target 配置 `udp.payload: "PING"` 且 `udp.encoding` 未配置或为 `text`
|
||||
- **THEN** 系统 SHALL 以 UTF-8 字节发送 `PING`
|
||||
|
||||
#### Scenario: hex payload 编码
|
||||
- **WHEN** udp target 配置 `udp.payload: "50494e47"` 和 `udp.encoding: "hex"`
|
||||
- **THEN** 系统 SHALL 解码 hex 后发送字节内容 `PING`
|
||||
|
||||
#### Scenario: base64 payload 编码
|
||||
- **WHEN** udp target 配置 `udp.payload: "UElORw=="` 和 `udp.encoding: "base64"`
|
||||
- **THEN** 系统 SHALL 解码 base64 后发送字节内容 `PING`
|
||||
|
||||
#### Scenario: 非法 encoding 失败
|
||||
- **WHEN** YAML 中 udp target 配置 `udp.encoding: "json"`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 udp.encoding 必须为 `text`、`hex` 或 `base64`
|
||||
|
||||
#### Scenario: 非法 responseEncoding 失败
|
||||
- **WHEN** YAML 中 udp target 配置 `udp.responseEncoding: "json"`
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 udp.responseEncoding 必须为 `text`、`hex` 或 `base64`
|
||||
|
||||
#### Scenario: 非法 hex payload 失败
|
||||
- **WHEN** YAML 中 udp target 配置 `udp.encoding: "hex"` 但 `udp.payload` 不是合法 hex 字符串
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 udp.payload 与 udp.encoding 不匹配
|
||||
|
||||
#### Scenario: 非法 base64 payload 失败
|
||||
- **WHEN** YAML 中 udp target 配置 `udp.encoding: "base64"` 但 `udp.payload` 不是合法 base64 字符串
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 udp.payload 与 udp.encoding 不匹配
|
||||
|
||||
### Requirement: udp checker 执行
|
||||
系统 SHALL 使用 Bun connected UDP socket 向目标发送单个 datagram,等待第一个 UDP 响应 datagram,记录完整执行耗时和 UDP observation,并在发送失败、超时、资源超限或底层 socket 错误时产生结构化失败信息。
|
||||
|
||||
#### Scenario: UDP 请求响应成功
|
||||
- **WHEN** udp target 指向会返回 `PONG` 的 UDP 服务,且未配置 expect 或 `expect.responded` 为 `true`
|
||||
- **THEN** 系统 SHALL 记录 `matched=true`、`durationMs` 和包含响应大小的 observation,并关闭 socket
|
||||
|
||||
#### Scenario: 使用 hostname 执行 UDP 探测
|
||||
- **WHEN** udp target 的 `udp.host` 为可解析域名或 `localhost`
|
||||
- **THEN** 系统 SHALL 使用 Bun connected UDP socket 完成发送和接收,不要求配置 IP 地址
|
||||
|
||||
#### Scenario: 只处理第一个响应 datagram
|
||||
- **WHEN** UDP 服务对一次请求返回多个 datagram
|
||||
- **THEN** 系统 SHALL 仅使用第一个收到的 UDP datagram 执行 expect 校验,并关闭 socket
|
||||
|
||||
#### Scenario: UDP 无响应且默认期望响应
|
||||
- **WHEN** udp target 指向在 timeout 内不返回 UDP datagram 的服务,且未配置 expect 或 `expect.responded` 为 `true`
|
||||
- **THEN** 系统 SHALL 在 `ctx.signal` abort 后记录 `matched=false`,observation SHALL 包含 responded=false 和 error,failure 的 kind 为 `error`,phase 为 `response`,message 包含超时或未响应信息
|
||||
|
||||
#### Scenario: 期望无响应且实际无响应
|
||||
- **WHEN** udp target 配置 `expect.responded: false`,且 timeout 内未收到 UDP datagram
|
||||
- **THEN** 系统 SHALL 记录 `matched=true`,observation SHALL 包含 responded=false,API detail SHALL 表示未收到响应
|
||||
|
||||
#### Scenario: 期望无响应但实际收到响应
|
||||
- **WHEN** udp target 配置 `expect.responded: false`,但收到了 UDP datagram
|
||||
- **THEN** 系统 SHALL 记录 `matched=false`,observation SHALL 包含 responded=true 和响应摘要,failure 的 kind 为 `mismatch`,phase 为 `responded`
|
||||
|
||||
#### Scenario: UDP socket 底层错误
|
||||
- **WHEN** Bun UDP socket 在发送或接收过程中触发 error 事件
|
||||
- **THEN** 系统 SHALL best-effort 关闭 socket,并记录 `matched=false`、failure.kind=`error` 和可读错误信息,并在可收集时记录 observation
|
||||
|
||||
#### Scenario: ICMP unreachable 不作为 UDP 响应
|
||||
- **WHEN** 底层系统因目标端口不可达产生 ICMP unreachable
|
||||
- **THEN** 系统 SHALL NOT 将其视为 `responded=true` 的 UDP datagram 响应
|
||||
|
||||
#### Scenario: UDP 执行超时关闭 socket
|
||||
- **WHEN** 引擎注入的 `ctx.signal` 在 UDP 发送或等待响应过程中 abort
|
||||
- **THEN** 系统 SHALL best-effort 关闭 UDP socket,并记录结构化超时或未响应结果
|
||||
|
||||
### Requirement: udp 响应大小限制
|
||||
系统 SHALL 使用 `udp.maxResponseBytes` 限制单个 UDP 响应 datagram 的可处理字节数,默认值为 4096,支持数字或 size string。
|
||||
|
||||
#### Scenario: 响应大小未超过限制
|
||||
- **WHEN** udp target 配置 `udp.maxResponseBytes: 4096`,且实际响应为 16 字节
|
||||
- **THEN** 系统 SHALL 允许后续 expect 校验继续执行
|
||||
|
||||
#### Scenario: 响应大小超过限制
|
||||
- **WHEN** udp target 配置 `udp.maxResponseBytes: 4`,且实际响应为 16 字节
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `error`,phase 为 `response`,message 包含响应超过大小限制的信息
|
||||
|
||||
#### Scenario: Bun 标记 datagram 被截断
|
||||
- **WHEN** Bun UDP data 回调中的 `flags.truncated` 为 `true`
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `error`,phase 为 `response`,message 包含响应被截断的信息
|
||||
|
||||
#### Scenario: maxResponseBytes 格式非法
|
||||
- **WHEN** YAML 中 udp target 的 `maxResponseBytes` 不是非负整数或合法 size string
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 maxResponseBytes 格式错误
|
||||
|
||||
### Requirement: udp expect 校验
|
||||
系统 SHALL 支持 udp 专属 expect,包括 `responded`、`response`、`responseSize`、`sourceHost`、`sourcePort` 和 `durationMs`,并按 responded、responseSize、response、sourceHost、sourcePort、durationMs 的阶段顺序快速失败。`responded` SHALL 保持布尔状态语义,未配置时在 Resolved expect 中默认 `true`。`response` MUST 使用共享 `RawContentExpectations` 数组输入并在运行期使用 `ContentExpectations`,且作用于按 `udp.responseEncoding` 转换后的响应文本。`responseSize`、`sourceHost`、`sourcePort` 和 `durationMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation`。
|
||||
|
||||
#### Scenario: 默认 responded 成功语义
|
||||
- **WHEN** udp target 未显式配置 `expect.responded`
|
||||
- **THEN** 系统 SHALL 在 Resolved udp expect 中使用默认 `responded: true` 进行校验
|
||||
|
||||
#### Scenario: response ContentExpectations 校验通过
|
||||
- **WHEN** udp target 配置 `expect.response: [{ contains: "PONG" }]`,且按 `responseEncoding` 转换后的响应文本包含 `PONG`
|
||||
- **THEN** 系统 SHALL 判定 response 阶段通过
|
||||
|
||||
#### Scenario: response JSON 校验通过
|
||||
- **WHEN** udp target 收到文本响应 `{"status":"ok"}` 且配置 `expect.response: [{json: {path: "$.status", equals: "ok"}}]`
|
||||
- **THEN** 系统 SHALL 判定 response 阶段通过
|
||||
|
||||
#### Scenario: response ContentExpectations 校验失败
|
||||
- **WHEN** udp target 配置 `expect.response: [{ contains: "PONG" }]`,但按 `responseEncoding` 转换后的响应文本不包含 `PONG`
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `response`,path 指向失败的 response expectation
|
||||
|
||||
#### Scenario: responseEncoding 为 hex
|
||||
- **WHEN** udp target 配置 `udp.responseEncoding: "hex"` 且收到字节内容 `PONG`
|
||||
- **THEN** 系统 SHALL 将响应转换为小写 hex 字符串 `504f4e47` 后执行 `expect.response` expectation
|
||||
|
||||
#### Scenario: responseSize matcher 校验通过
|
||||
- **WHEN** udp target 配置 `expect.responseSize: { gte: 4 }`,且实际响应为 4 字节
|
||||
- **THEN** 系统 SHALL 判定 responseSize 阶段通过
|
||||
|
||||
#### Scenario: responseSize matcher 校验失败
|
||||
- **WHEN** udp target 配置 `expect.responseSize: { gte: 4 }`,但实际响应为 2 字节
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 kind 为 `mismatch`,phase 为 `responseSize`
|
||||
|
||||
#### Scenario: sourceHost matcher 校验
|
||||
- **WHEN** udp target 配置 `expect.sourceHost: { equals: "127.0.0.1" }`,且 Bun 回调中的来源地址为 `127.0.0.1`
|
||||
- **THEN** 系统 SHALL 判定 sourceHost 阶段通过
|
||||
|
||||
#### Scenario: sourcePort matcher 校验
|
||||
- **WHEN** udp target 配置 `expect.sourcePort: { equals: 9000 }`,且 Bun 回调中的来源端口为 `9000`
|
||||
- **THEN** 系统 SHALL 判定 sourcePort 阶段通过
|
||||
|
||||
#### Scenario: durationMs 校验
|
||||
- **WHEN** udp target 配置 `expect.durationMs: {lte: 100}`,且完整执行耗时超过 100ms
|
||||
- **THEN** 系统 SHALL 返回 `matched=false`,failure 的 phase 为 `duration`
|
||||
|
||||
#### Scenario: response 断言要求实际有响应
|
||||
- **WHEN** udp target 配置了 `expect.response` 或 `expect.responseSize`,但同时配置 `expect.responded: false`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示响应内容或大小断言需要 `expect.responded` 为 true
|
||||
|
||||
#### Scenario: source 断言要求实际有响应
|
||||
- **WHEN** udp target 配置了 `expect.sourceHost` 或 `expect.sourcePort`,但同时配置 `expect.responded: false`
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示响应来源断言需要 `expect.responded` 为 true
|
||||
|
||||
#### Scenario: udp expect 未知字段失败
|
||||
- **WHEN** YAML 中 udp target 的 expect 包含 `status: [200]`、`maxDurationMs: 1000` 或其他非 udp expect 字段
|
||||
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
|
||||
|
||||
### Requirement: udp detail 摘要
|
||||
系统 SHALL 在 udp API 序列化时从 observation 动态生成简短 detail 摘要,展示关键结果和执行耗时并避免返回过长响应内容。UDP observation SHALL 包含 durationMs 以支持 detail 构造。
|
||||
|
||||
#### Scenario: 收到响应的摘要
|
||||
- **WHEN** udp target 收到 4 字节响应且完整执行耗时为 12ms
|
||||
- **THEN** detail SHALL 包含 `responded in 12ms` 和 `4 bytes`
|
||||
|
||||
#### Scenario: 未收到响应的摘要
|
||||
- **WHEN** udp target 配置 `expect.responded: false` 且 timeout 内未收到 UDP datagram
|
||||
- **THEN** detail SHALL 包含 `no response` 和执行耗时
|
||||
|
||||
#### Scenario: 响应内容摘要截断
|
||||
- **WHEN** udp target 收到较长响应内容
|
||||
- **THEN** detail SHALL 只展示按 `responseEncoding` 转换并截断后的响应摘要
|
||||
@@ -1,54 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
定义 DiAL 应用版本号的唯一来源、手动版本升迁命令和版本管理文档要求。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 应用版本唯一来源
|
||||
系统 SHALL 使用根目录 `package.json.version` 作为 DiAL 应用版本号的唯一来源。版本号 MUST 使用 `MAJOR.MINOR.PATCH` 数字格式,不包含 prerelease 或 build metadata。
|
||||
|
||||
#### Scenario: 读取应用版本
|
||||
- **WHEN** 开发、构建或版本升迁流程需要获取 DiAL 应用版本
|
||||
- **THEN** 系统 SHALL 从根目录 `package.json.version` 读取版本号
|
||||
|
||||
#### Scenario: 版本格式有效
|
||||
- **WHEN** `package.json.version` 为 `0.1.0`、`1.2.3` 等 `MAJOR.MINOR.PATCH` 数字格式
|
||||
- **THEN** 版本管理流程 SHALL 视为有效版本
|
||||
|
||||
#### Scenario: 版本格式无效
|
||||
- **WHEN** `package.json.version` 缺失或不符合 `MAJOR.MINOR.PATCH` 数字格式
|
||||
- **THEN** 版本管理流程 MUST 失败并输出可读错误,不得继续写入错误版本
|
||||
|
||||
### Requirement: 手动版本升迁命令
|
||||
项目 SHALL 通过 `package.json` scripts 提供基于 Bun 的手动版本升迁命令,支持 `patch`、`minor`、`major` 和显式设置版本。
|
||||
|
||||
#### Scenario: 升迁 patch 版本
|
||||
- **WHEN** 当前版本为 `1.2.3` 且开发者运行 `bun run version:patch`
|
||||
- **THEN** 系统 SHALL 将 `package.json.version` 更新为 `1.2.4`
|
||||
|
||||
#### Scenario: 升迁 minor 版本
|
||||
- **WHEN** 当前版本为 `1.2.3` 且开发者运行 `bun run version:minor`
|
||||
- **THEN** 系统 SHALL 将 `package.json.version` 更新为 `1.3.0`
|
||||
|
||||
#### Scenario: 升迁 major 版本
|
||||
- **WHEN** 当前版本为 `1.2.3` 且开发者运行 `bun run version:major`
|
||||
- **THEN** 系统 SHALL 将 `package.json.version` 更新为 `2.0.0`
|
||||
|
||||
#### Scenario: 显式设置版本
|
||||
- **WHEN** 开发者运行 `bun run version:set 0.2.0`
|
||||
- **THEN** 系统 SHALL 将 `package.json.version` 更新为 `0.2.0`
|
||||
|
||||
#### Scenario: 拒绝无效设置版本
|
||||
- **WHEN** 开发者运行 `bun run version:set 1.0.0-beta.1` 或其他非 `MAJOR.MINOR.PATCH` 版本
|
||||
- **THEN** 系统 MUST 失败并保持 `package.json.version` 不变
|
||||
|
||||
#### Scenario: 版本升迁不执行发布副作用
|
||||
- **WHEN** 开发者运行任意版本升迁命令
|
||||
- **THEN** 系统 MUST NOT 自动创建 git commit、git tag、changelog 或 release
|
||||
|
||||
### Requirement: 版本管理文档
|
||||
项目 SHALL 在开发文档中说明版本号规则、升迁命令、展示位置和暂不支持的发布自动化能力。
|
||||
|
||||
#### Scenario: 开发者查阅版本规则
|
||||
- **WHEN** 开发者阅读 README.md 或 DEVELOPMENT.md
|
||||
- **THEN** 文档 SHALL 说明 `package.json.version` 是唯一版本源,以及 `bun run version:patch`、`bun run version:minor`、`bun run version:major`、`bun run version:set <version>` 的用途
|
||||
@@ -1,67 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
确保测试在 Windows、macOS、Linux 平台上的兼容性,包括文件句柄释放后的目录清理重试机制和跨平台命令测试约定。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 测试临时目录清理 SHALL 支持重试
|
||||
|
||||
使用 SQLite 数据库的测试 SHALL 在 `afterAll` 中使用带重试的目录删除机制,确保在 Windows 上文件句柄未及时释放时不会导致测试失败。
|
||||
|
||||
#### Scenario: Windows 上 SQLite 文件句柄延迟释放
|
||||
|
||||
- **WHEN** 测试在 Windows 上运行,`store.close()` 后立即尝试删除临时目录
|
||||
- **THEN** 删除操作 SHALL 自动重试(最多 3 次,间隔 200ms),直到成功或耗尽重试次数
|
||||
|
||||
### Requirement: 命令检测器测试 SHALL 使用跨平台命令
|
||||
命令检测器的测试 SHALL 使用 `bun -e` 脚本替代所有系统命令(包括 `true`、`false`、`sleep`、`bash`、`echo`、`yes | head`),确保测试在 Windows、macOS、Linux 三平台上行为一致。
|
||||
|
||||
#### Scenario: 进程退出码 0
|
||||
- **WHEN** 测试需要一个正常退出的命令
|
||||
- **THEN** 测试 SHALL 使用 `bun -e "process.exit(0)"` 替代 `true`
|
||||
|
||||
#### Scenario: 进程退出码非零
|
||||
- **WHEN** 测试需要一个失败退出的命令
|
||||
- **THEN** 测试 SHALL 使用 `bun -e "process.exit(1)"` 替代 `false`
|
||||
|
||||
#### Scenario: stdout 输出
|
||||
- **WHEN** 测试需要一个输出文本到 stdout 的命令
|
||||
- **THEN** 测试 SHALL 使用 `bun -e "console.log('text')"` 替代 `echo text`
|
||||
|
||||
#### Scenario: stderr 输出
|
||||
- **WHEN** 测试需要一个输出文本到 stderr 的命令
|
||||
- **THEN** 测试 SHALL 使用 `bun -e "process.stderr.write('error\n')"` 替代 `bash -c "echo error >&2"`
|
||||
|
||||
#### Scenario: 长时间运行命令
|
||||
- **WHEN** 测试需要一个超时场景的长时间运行命令
|
||||
- **THEN** 测试 SHALL 使用 `bun -e "await Bun.sleep(10000)"` 替代 `sleep 10`
|
||||
|
||||
#### Scenario: 大量输出
|
||||
- **WHEN** 测试需要一个产生大量输出的命令
|
||||
- **THEN** 测试 SHALL 使用 `bun -e "process.stdout.write('y\n'.repeat(N))"` 替代 `bash -c "yes | head -N"`
|
||||
|
||||
#### Scenario: 验证非 shell 模式下特殊字符不被展开
|
||||
- **WHEN** 通过 `Bun.spawn` 执行 `bun -e "console.log('*')"` 并检查 stdout 包含 `*`
|
||||
- **THEN** 测试 SHALL 在 Windows、macOS 和 Linux 上均返回 `matched: true`
|
||||
|
||||
### Requirement: probes.example.yaml 使用跨平台示例
|
||||
probes.example.yaml 中的 cmd 类型示例 SHALL 使用跨平台命令(如 `bun -e "..."`、`bun --version`),不使用 Unix 专属命令(如 `uname`、`ls /tmp`、`date`)。
|
||||
|
||||
#### Scenario: 示例命令跨平台可执行
|
||||
- **WHEN** 用户在 Windows、macOS 或 Linux 上直接使用 probes.example.yaml 中的 cmd 示例
|
||||
- **THEN** 所有 cmd 示例 SHALL 能正常执行,不依赖平台特定命令
|
||||
|
||||
#### Scenario: ICMP checker 测试使用 platform 注入
|
||||
- **WHEN** 在 Windows 上运行 ICMP checker 测试,mock 的 stdout 为 Unix 格式
|
||||
- **THEN** 测试 SHALL 通过 `new IcmpChecker("linux")` 构造 checker 实例,使 parsePingOutput 使用 Unix 解析器,确保测试在所有平台上通过
|
||||
|
||||
### Requirement: 路径语义测试 SHALL 显式模拟目标平台
|
||||
路径相关跨平台测试 SHALL 使用显式平台路径工具、依赖注入或等价测试 seam 表达目标平台语义,MUST NOT 依赖当前运行平台的 `path.sep` 伪装其他平台行为。
|
||||
|
||||
#### Scenario: 在非 Windows 平台验证 Windows 路径分隔符
|
||||
- **WHEN** 测试需要验证 Windows 文件系统路径中的反斜杠会被转换为平台无关输出
|
||||
- **THEN** 测试 SHALL 使用 `node:path.win32` 或等价注入方式生成 Windows relative path,并断言输出不包含 `\\`
|
||||
|
||||
#### Scenario: 验证 import specifier 输出
|
||||
- **WHEN** 测试验证构建脚本生成的 ESM import specifier
|
||||
- **THEN** 测试 SHALL 断言输出使用 `/` 分隔,MUST NOT 使用 `path.sep` 禁止当前平台的合法 `/` 字符
|
||||
Reference in New Issue
Block a user