21 KiB
21 KiB
NCurator-Desktop 测试架构
0. 适用范围
- 本文用于规定 NCurator-Desktop 的测试架构设计。
1. 写测试前先判断
- 如果只记一件事:写测试前先判断这是正式长期回归 case,还是一次性临时排查 case。
- 默认策略:
- 只要还在排查、验证猜想、补上下文、试运行 fixture,或尚未确定要长期维护,一律先放
tests/temp/**。 - 只有确认要长期维护、参与 gating 或进入正式 report 的 case,才进入正式测试目录。
- 只要要进入正式测试目录,必须先补对应
doc/test-plan/**;未登记的 case 不得直接写进tests/**非temp目录。
- 只要还在排查、验证猜想、补上下文、试运行 fixture,或尚未确定要长期维护,一律先放
- 测试体系最终只沉淀两层正式产物:
- 原始日志:
tests/logs/** - 正式分析文档:
tests/report/<domain>/report.md
- 原始日志:
- 原始日志统一由四类文件组成:
meta.jsontimeline.jsonlruntime-events.jsonlartifacts.json
- 测试样例只记录事实,不直接写失败原因分析、优化建议或能力判断。
2. 目录结构
tests/
harness/
fixtures/
ai/
workbench/
settings/
kb-search/
temp/
logs/
report/
ai/
report.md
workbench/
report.md
settings/
report.md
规则:
- 目录统一使用 kebab-case。
- 测试发现目录使用通用树形结构:
tests/<domain>/<suite>/<groups...>/<case>.test.ts。 suite是第一层稳定分类;groups...是 suite 下可选的任意深度子目录。- 各 domain 的
suite列表与归属,只在对应doc/test-plan/**中定义,本文不重复展开。 - 当某个
suite不需要继续细分时,正式 case 可以直接放在该suite目录下。 - 普通测试文件使用
<case>.test.ts命名。 - e2e 测试文件使用
<case>.e2e.test.ts命名。 temp/默认不参与 gating,也不参与正式 report 汇总。- 除
tests/temp/**外,tests/**中的正式测试目录只允许放已在doc/test-plan/**登记的 case,否则需要主动讨论。
3. 用例准入与编写规则
3.1 快速判断
- 正式长期回归 case:用于长期维护、参与 gating、进入正式 report,必须进入正式测试闭环。
- 临时排查 case:只用于一次性排查、验证猜想、临时复现或补充上下文,固定放在
tests/temp/**。 - 用户没有明确要求“补正式长期 case”时,默认先写
tests/temp/**,不要直接进入正式目录。 - 当临时 case 需要转为正式长期 case 时,顺序必须是:先补
doc/test-plan/**,再迁入正式目录。
3.2 正式 case 共享约束
doc/test-plan/**下每个领域文档,都是该领域正式测试用例定义的唯一规格来源。- 新增、修改或补齐正式 case 时,必须严格对齐对应文档中的
用例名称 / 实现状态 / What / Why / How / Oracle / Not。 - 如果 case 覆盖的是用户可见功能,
How / Oracle还必须映射到对应doc/prd/**的需求编号与验收编号。 How用于定义 fixture、前置条件、输入、执行路径与运行方式;Oracle用于定义必须成立的可断言结果;Not用于定义禁止扩张的范围。- 如果 PRD 中的功能入口、交互流程、状态反馈或验收口径发生变化,必须先更新对应
doc/prd/**,再更新doc/test-plan/**与测试代码。 - 如果实现行为变更导致正式 case 预期变化,必须先更新对应
doc/test-plan/**,再修改测试代码。 - 如果
tests/**非temp目录里缺少对应正式 case,必须先补doc/test-plan/**定义,再补测试代码。 - 每个
用例名称都必须稳定映射到唯一caseId、唯一测试文件和唯一日志目录。
3.3 实现状态取值约定
待检查- 已在计划中定义,但还未核对代码是否已经实现或与文档一致。
未实现- 代码中还没有对应测试用例实现。
部分实现- 已有部分实现,但与文档定义不完全一致,或缺少关键断言 / fixture / 分支覆盖。
已实现- 代码中的测试用例已与文档定义对齐。
待调整- 当前代码已有实现,但文档定义或代码实现需要进一步调整后才能对齐。
已废弃- 该 case 不再作为正式测试目标保留,但保留历史记录。
3.4 用例字段定义
用例名称- 稳定
caseId,用于映射测试文件、日志目录和报告归属。
- 稳定
实现状态- 用于标记当前代码实现与文档定义的匹配状态;新增 case 默认先写
待检查。
- 用于标记当前代码实现与文档定义的匹配状态;新增 case 默认先写
What- 这条 case 到底要验证什么。
Why- 这条 case 为什么长期存在,解决什么真实风险。
How- 这条 case 的输入、前置条件、fixture、运行方式和关键执行链路。
Oracle- 这条 case 必须成立的明确断言集合。
Not- 这条 case 明确不验证什么,防止实现时擅自扩张范围。
3.5 正式目录层级口径
suite- 第一层稳定分类,按领域内最长期稳定的阶段、模块或能力边界划分。
- 具体取值与归属,只看对应
doc/test-plan/**。
groupsuite下的可选子目录。- 只有在同一
suite内确实需要继续分层时才创建,命名必须直接表达业务语义,不再默认使用固定四分组。
case- 最终测试文件,对应唯一
caseId、唯一日志目录和唯一 report 归属。
- 最终测试文件,对应唯一
type- 测试类型只通过文件名体现。
- 普通测试默认视为
backend;文件名包含.e2e.test.ts时视为e2e。
3.6 长期价值与 temp 规则
- 进入正式 domain 目录的 case 默认都应具备长期回归价值。
- 一次性排查、临时验证、待定样例统一放在
tests/temp/**。 tests/temp/**默认不参与 gating,也不进入正式 report。
3.7 常见正反例
- 反例:为了排查某个猜想,直接在
tests/ai/**、tests/workbench/**或tests/settings/**新增正式 case。- 这类需求如果还没有进入长期维护范围,应先写进
tests/temp/**。
- 这类需求如果还没有进入长期维护范围,应先写进
- 正例:为了确认某个上下文拼接问题是否真实存在,先写
tests/temp/**做一次性验证。 - 正例:某条真实用户场景已经确定要长期回归,先补对应
doc/test-plan/**,再补正式测试代码。
4. 正式测试闭环流程
4.1 固定顺序
- 先判断这是正式长期回归 case,还是临时排查 case。
- 如果只是临时排查,直接写入
tests/temp/**,不进入正式闭环。 - 如果要进入正式目录,先更新对应
doc/test-plan/**。 - 再写或调整
tests/**中的正式测试代码。 - 通过
scripts/test-workflow.mjs执行并产生日志。 - 先看
timeline.jsonl,不足时再看runtime-events.jsonl、meta.json与artifacts.json。 - 最后基于
tests/logs/<domain>/**更新tests/report/<domain>/report.md。
4.2 放入测试目录
- 正式 case 固定放在
tests/<domain>/<suite>/<groups...>/<case>.test.ts。 - e2e case 固定放在
tests/<domain>/<suite>/<groups...>/<case>.e2e.test.ts。 - 临时 case 固定放在
tests/temp/**。 - 未在
doc/test-plan/**登记的验证,不得进入正式测试目录。 - 临时 case 转正式 case 时,先补
doc/test-plan/**,再迁入正式目录。
4.3 执行与产生日志
- 测试统一通过
scripts/test-workflow.mjs执行。 - runner 只负责收集测试文件、执行测试并落
tests/logs/**。 - 不带
--type时,runner 会先执行普通测试,再执行.e2e.test.ts;带--type backend或--type e2e时,只执行对应类型;除此之外不负责失败分析、建议整理或 report 更新。
4.4 读取与分析 raw logs
- 默认先读取目标 run 的
timeline.jsonl,理解语义步骤、断言事实和失败位置。 - 如果
timeline.jsonl仍不足以解释结果,再继续读取runtime-events.jsonl与关键附件。 - 必要时回看
meta.json与artifacts.json,确认 run 摘要、统计值和附件索引。 - 所有分析结论都必须可回溯到 raw logs 与 artifacts,不得脱离证据做推断。
4.5 更新正式 report
- 正式 report 固定更新
tests/report/<domain>/report.md。 - report 只能基于
tests/logs/<domain>/**及其附件生成;tests/temp/**不进入正式 report。 - report 负责沉淀当前现状分析、失败模式和下一步建议,但不能替代 raw logs 本身。
4.6 结论回流
- 如果问题出在 case 定义、验收口径或阶段边界,必须先改
doc/test-plan/**,再改测试代码。 - 如果只是实现缺陷或回归问题,可在保持 case 规格不变的前提下修实现并重跑。
- report 中已有结论若被新 run 推翻,应以最新 raw logs 重新分析并更新,不保留无证据的旧判断。
5. runner 与命令
统一入口默认是 scripts/test-workflow.mjs。
当前命令面:
pnpm test:aipnpm test:ai:backendpnpm test:workbenchpnpm test:workbench:backendpnpm test:workbench:e2epnpm test:settingspnpm test:settings:backendpnpm test:settings:e2e
runner 支持的过滤参数:
--domain--type--suite--group,可重复传入--case--runs
补充规则:
- 不带
--type时,runner 会先收集普通测试,再收集.e2e.test.ts。 --type取值为backend或e2e。- 如果需要继续收窄到某个
suite、group或单条case,统一通过 runner 参数过滤,不额外在本文展开具体模块命令。 --group只在目标suite确实存在额外子目录时使用;如果 case 直接位于suite根目录,则不需要--group。
runner 的职责只有三件事:
- 找到目标测试文件并执行。
- 落原始日志到
tests/logs/**。 - 在不带
--type时按“普通测试 -> e2e”顺序执行。
runner 不负责:
- 写事实汇总稿
- 写失败原因分析
- 写优化建议
- 更新正式
report.md
6. 原始日志契约
每个 run 固定目录:
tests/logs/<domain>/<suite>/<groups...>/<caseId>/<runId>/
meta.json
timeline.jsonl
runtime-events.jsonl
artifacts.json
artifacts/
其中:
meta.json- 单次 run 的摘要索引。
- 用来回答“本次 run 是谁、结果是什么、统计值是什么”。
timeline.jsonl- 单次 run 的事实事件流。
- 用来回答“本次 run 按顺序发生了什么”。
runtime-events.jsonl- 单次 run 的完整运行时原始事件流。
- 用来回答“底层 runtime 真实发出了哪些
sendEvent事件”。
artifacts.json- 单次 run 的附件索引。
- 用来回答“本次 run 产生了哪些可回看的附件”。
artifacts/- 可选目录。
- 只有确实生成 trace、截图、HTML 诊断页等实体附件时才创建。
目录与命名规则
<domain>/<suite>/<groups...>- 目录层级体现业务域与 suite 树。
- 当前 runner 通过目录过滤测试范围。
<caseId>- 对应用例稳定标识。
- 通常与测试文件名去掉
.test.ts后一致;e2e case 通常以.e2e结尾。
<runId>- 单次执行唯一标识。
- 当前格式:
YYYYMMDD-HHmmss-SSS。
meta.json
固定字段:
schemaVersionrunIdcaseIdscopestatusstartedAtfinishedAtstatsruntime
字段含义:
schemaVersion- 日志 schema 版本号。
- 固定值为
4。
runId- 本次执行唯一标识,对应目录名。
caseId- 说明这次 run 属于哪条用例。
scope- 说明这次 run 属于哪个稳定测试范围。
- 格式为
<domain>/<suite>/<groups...>,不包含caseId。
status- 本次 run 的最终结果。
- 当前值域是
passed/failed。
startedAt- 本次 run 的开始时间。
finishedAt- 本次 run 的结束时间。
stats- 本次 run 的结构化统计。
- 当前固定字段:
durationMsstepsassertionserrorsartifacts
runtime- 本次 run 的运行时摘要。
- 当前固定字段:
drivertargetmodelName
约束:
meta.json顶层只保留上述固定字段。- 如果测试需要保留 fixture 依赖信息,应写在
timeline.jsonl的run/start事件data中。
timeline.jsonl
固定字段:
tstypenamestatusidmessagedata
字段含义:
ts- 当前事件发生时间。
type- 事件大类。
- 当前值域:
run- 只用于单次 case 的运行生命周期。
step- 只用于测试作者显式定义的语义步骤,不用于底层工具噪音。
assertion- 只用于
Oracle或核心验收断言。
- 只用于
artifact- 只用于登记一个已经写出的附件。
signal- 只用于外部可观察信号,例如路由变化、IPC 调用、真实 runtime event、console。
error- 只用于 runner 或测试执行层面的未恢复错误。
name- 事件的稳定语义名,不是脚本名。
- 同类事实在不同 run、不同 case 中应尽量复用同一命名。
status- 当前事件状态。
- 当前值域:
infookfailedskipped
id- 可选稳定事件标识。
- 当前主要用于
step和assertion,便于关联截图等附件。
message- 当前事件的一句事实性自然语言。
- 只写“发生了什么”,不写失败原因、优化建议或能力判断。
data- 结构化补充信息,可空。
- 只放按需原始观测值,不要求统一业务 schema。
约束:
timeline.jsonl只记录高价值语义事件,不记录底层噪音。signal用于记录可观察到的外部信号,例如路由变化、IPC 调用、AI 事件。- 当失败事件存在明确证据时,优先把证据放进
data或 artifact,不要在message中写大段解释。 - e2e 下不把所有 Playwright 底层调用逐条倾倒进 timeline;底层重放依赖
trace.zip,timeline 只保留语义 step / assertion / signal。 run之外的业务状态不要塞进status。- 例如
blocked、completed、cancelled应保留在runtime-events.jsonl或写进data,不要扩充timeline.status。
- 例如
runtime-events.jsonl
- 文件内容是逐行
AiRunEventJSON,不包额外 wrapper,不做 schema 投影。 - 范围固定为完整
sendEvent原始事件流。- 包括 tool、guard、scope 校验、审批、产物、runtime issue、AI 运行等所有通过
sendEvent发出的事件。 - 不包含本地 harness 自己维护但未经过
sendEvent的临时数组,例如某些工具测试里的activities[]。
- 包括 tool、guard、scope 校验、审批、产物、runtime issue、AI 运行等所有通过
runtime-events.jsonl始终写出;如果本次 run 没有 runtime 事件,则写空文件。- 普通测试与 e2e 的采集路径可以不同,但最终落盘契约必须一致。
- 当
Oracle、断言data、timeline.jsonl或 report 仍不足以解释失败原因时,必须继续回看runtime-events.jsonl。
artifacts.json
固定字段:
schemaVersionitems
其中 items 每项固定字段:
idkindpathrefs
字段含义:
schemaVersion- 附件索引 schema 版本号。
- 固定值为
4。
id- 单个附件的稳定标识。
kind- 附件类型。
- 当前值域:
tracescreenshotvideoplaywright-reportjsonlother
path- 相对当前 run 根目录的稳定路径。
refs- 可选关联索引。
- 当前字段:
stepIdassertionId
约束:
artifacts.json必须始终写出,即使items为空数组。artifacts/目录只有在确实存在实体附件文件时才创建。
不同测试类型的统一约束
- 普通测试与 e2e 共用同一份
meta.json/timeline.jsonl/runtime-events.jsonl/artifacts.json基础契约。 - 两类测试的差异只体现在:
meta.runtime.drivermeta.runtime.targetartifacts.json.items[].kindruntime-events.jsonl的采集路径timeline.jsonl中出现的语义事件名与信号类型
e2e 默认附件策略
- 成功 run:
- 始终保留 Playwright trace。
- 只有测试或 runtime 显式要求保留的关键截图才写入。
- 默认不生成 Playwright HTML 诊断页。
- 失败 run:
- 始终保留 Playwright trace。
- 始终生成失败截图,并通过
refs.assertionId或refs.stepId绑定。 - 生成位于
artifacts/playwright/report/index.html的 HTML 诊断页。
- 默认不保留 video。
7. 正式 report 契约
report是正式测试闭环中的最终分析产物;前提是对应 domain 已先生成可回看的 raw logs。
当前正式分析文档固定放在:
tests/report/<domain>/
report.md
含义:
report.md- 当前业务域唯一正式分析报告。
- 是基于
tests/logs/<domain>/**写出的当前现状分析。
report.md 建议结构
总览总览至少包含:分析范围总体结论下一阶段建议
- 按 suite / groups 展开
- 每个 suite / groups 节点按需要继续下钻
- 每个节点至少包含:
证据通过率失败分析当前评估下一步建议
report.md 格式约束
- 顶层标题使用
# <Domain> 域测试报告。 - 顶层必须先有
## 总览。 总览下必须先包含以下固定小节,顺序保持一致:### 分析范围### 总体结论### 下一阶段建议
总体结论中的统计口径固定拆成两类:历史总数据:基于当前 analysis window 内全部 raw runs 统计最新总数据:每个 case 只看最新一轮 run 的修复后统计
总览之后按 domain 下实际存在的 suite 展开。- suite 之下按目录树继续展开:
- suite 使用
## - suite 下第一层 group 使用
### - 更深层 group 继续按 Markdown 标题层级递增
- suite 使用
- 每个实际分析节点都必须包含以下固定小节,顺序保持一致:
证据通过率失败分析当前评估下一步建议
证据默认只列出能定位该条证据的最小信息,例如caseId、runId、status。- 不要在 report 正文里机械展开
meta.json、timeline.jsonl、runtime-events.jsonl、artifacts.json这些文件名。 - 只有当失败分析或结论明确依赖某个关键附件时,才额外补充附件名称或用途说明。
通过率只写当前节点覆盖范围内的统计,不混入其他节点数据。通过率固定拆成两组:历史总数据:totalRuns、passedRuns、failedRuns、historicalSuccessRate最新总数据:latestCaseCount、latestPassedCases、latestFailedCases、latestSuccessRate
历史总数据与最新总数据不得混写成同一个 successRate 口径。失败分析在没有 failed run 时应明确写“当前没有 failed run,因此无法给出失败模式结论”。当前评估负责总结当前状态和主要不足。下一步建议只写基于现有证据能支撑的下一步建议。
report.md 最小示例
# Workbench 域测试报告
## 总览
### 分析范围
- raw logs 范围: 当前仓库内 `tests/logs/workbench/**`
- 已分析 scope: `workbench/<suite-a>`、`workbench/<suite-b>`
### 总体结论
- 历史总数据: `2` 次 raw runs,其中 `2` 次 passed、`0` 次 failed,historicalSuccessRate=`100.0%`
- 最新总数据: `2 / 2` 个 case 的最新一轮 run 为 passed,latestSuccessRate=`100.0%`
- 当前结论: 当前已覆盖一个普通样例和一个 e2e smoke,但剩余 suite 仍待补齐。
### 下一阶段建议
- 先补 `suite-a` 的边界收敛
- 再补 `suite-b` 的主线闭环
## <suite-a>
### 证据
- `workbench-suite-a-basic` | type=`backend` | run=`20260315-101530-112` | status=`passed`
### 通过率
- 历史总数据:
- totalRuns: 1
- passedRuns: 1
- failedRuns: 0
- historicalSuccessRate: 100.0%
- 最新总数据:
- latestCaseCount: 1
- latestPassedCases: 1
- latestFailedCases: 0
- latestSuccessRate: 100.0%
### 失败分析
- 当前没有 failed run,因此无法给出失败模式结论。
### 当前评估
- suite-a 的基础路径已稳定产出 raw logs。
- 当前不足是边界场景覆盖仍不足。
### 下一步建议
- 增加 `workbench-suite-a-edge-1`
- 增加 `workbench-suite-a-edge-2`
## <suite-b>
### 证据
- `workbench-suite-b-smoke.e2e` | type=`e2e` | run=`20260315-103002-008` | status=`passed`
- 关键附件: playwright trace
### 通过率
- 历史总数据:
- totalRuns: 1
- passedRuns: 1
- failedRuns: 0
- historicalSuccessRate: 100.0%
- 最新总数据:
- latestCaseCount: 1
- latestPassedCases: 1
- latestFailedCases: 0
- latestSuccessRate: 100.0%
### 失败分析
- 当前没有 failed run,因此无法给出失败模式结论。
### 当前评估
- suite-b 的 e2e smoke 主链路已可用。
- 当前不足是剩余验收分支仍未纳入。
### 下一步建议
- 补充 `suite-b` 的验收分支 1
- 补充 `suite-b` 的验收分支 2
约束:
- report 只引用 raw logs 与 artifacts 作为证据来源。
- report 可以做分析与建议,但不得写入没有日志依据的推断。
8. 明确禁止
- 测试样例直接输出失败原因分析。
- runner 自动调用测试所用 AI 模型生成 report。
- report 写入没有日志依据的推断。
- 为单个 runner 或单种工具单独再设计一套平行 raw log 契约。