Share content-addressed parsing and embedding caches across isolated test runs, fingerprint relevant dependencies and model assets, and remove implicit UI warm-up. Add realistic two-round Skill delivery evaluation and cache regressions.
21 KiB
21 KiB
NCurator-Desktop 测试架构
1. 测试体系总览
ai-test专门覆盖 AI 架构及其工具,全部是 backend;knowledge-test专门覆盖知识库的功能测试(如搜索和索引等),优化设计测试(如召回率、检索与索引优化等),全部是 backend;settings-test和workbench-test专门覆盖对应 PRD 功能点,全部是 E2E;Unit 只补足前述范围未覆盖的 backend 函数、类和底层逻辑;showcase是独立官网样例采集,不进入领域测试计划。
测试体系只沉淀一层长期执行产物:
- 原始日志:
tests/logs/**
测试样例只记录事实,不直接写失败原因分析、优化建议或能力判断。
1.1 测试分类
- 测试用例
- 列入
doc/test-plan/**,放在tests/case/<domain>/<suite>/**。 - 用于长期维护、批量执行和生成可追溯运行产物。
- 列入
unit- 放在
tests/case/unit/**。 - 全部是 backend 单元测试,不允许导入 Renderer、React、jsdom 或 Electron UI。
- 不进入
doc/test-plan/**,由 unit runner 独立发现和执行。
- 放在
showcase- 放在
tests/case/showcase/**,文件后缀.showcase.test.ts。 - 独立官网样例采集,走真实 E2E 链路,用
pnpm test:showcase单独执行。
- 放在
- 不落测试文件的排查不属于测试文档治理对象;新增测试文件前必须先确认归入 AI、Knowledge、Settings、Workbench、Showcase 或 Unit。
1.2 目录结构
tests/
harness/
fixtures/
case/
ai/
knowledge/
unit/
settings/
workbench/
showcase/
logs/
规则:
- 目录使用 kebab-case。
- 正式测试发现目录使用通用树形结构:
tests/case/<domain>/<suite>/<groups...>/<case>.test.ts。 - 正式 E2E 测试文件使用
tests/case/<domain>/<suite>/<groups...>/<case>.e2e.test.ts。 suite是第一层稳定分类;groups...是 suite 下可选的子目录。- 各测试 domain 的
suite列表与归属,只在对应doc/test-plan/**中定义。 - 当某个
suite不需要继续细分时,测试 case 可以直接放在该suite目录下。 tests/case/{ai,knowledge,settings,unit,workbench,showcase}/**只允许放已在doc/test-plan/**登记的正式测试 case(unit、showcase例外,不进入测试计划)。tests/fixtures/workspace/是共享工作区 fixture 根目录;凡是依赖“工作区里有什么文件”的测试,以及 backend/unit 直接引用的固定文件输入,统一复用该目录。tests/fixtures/sqlite/放数据库迁移 fixture。tests/fixtures/**全部是只读测试输入:测试禁止对其中任何文件原地写入、覆盖或修改;需要写入时,必须通过fixtures: []声明由 runner/harness 复制到隔离临时 workspace(或按 E2E 规则复制到临时目录),测完删除。公开真实文档保留发布方原始字节;异常格式仅在隔离副本注入,不改写检入原件。fixture 需要更新时直接提交新二进制,禁止引入任何 fixture 生成器脚本(生成器不得以.test.ts形态进入tests/case/**自动发现)。- 当 case 直接传入
tests/fixtures/workspace目录时,runner 应把该目录下的子内容原样复制到临时 workspace 根目录,而不是保留tests/fixtures/**中间层路径。
1.3 稳定映射模型
测试按 domain / suite / groups... / caseId 映射到目录和日志;普通 .test.ts 为 backend,.e2e.test.ts 为 E2E,.showcase.test.ts 为独立样例采集。固定边界为:ai、knowledge、unit 只能是 backend,settings、workbench 只能是 E2E。
2. 测试用例规则
2.1 准入规则
- 新增任何测试文件前,必须先与用户确认目标分类。
doc/test-plan/**是测试 case 的唯一规格来源;新增、修改或补齐测试 case 时,必须先更新对应测试计划,再修改测试代码。tests/case/<domain>/**只能放已登记的测试 case;tests/case/unit/**不进入领域测试计划,但仍必须满足 Unit backend 边界。- 每个测试
caseId都必须稳定映射到唯一测试定义与唯一日志目录。一个测试文件可以承载多个紧密相关的 case,但--case <caseId>必须能只执行目标 case。
2.2 测试 case 共享字段
测试 case 在 doc/test-plan/** 中统一使用以下字段:
测试类型backend或e2e;需要时以/追加执行方式,如backend / live、e2e / live、e2e / exclusive。
What- 这条 case 到底要验证什么。
Why- 这条 case 为什么长期存在,解决什么真实风险。
How- 输入、前置条件、fixture、运行方式与关键执行链路。
Oracle- 必须成立的明确断言集合。
Settings / Workbench 每项 PRD 验收必须通过完整 AC 编号登记到具体 case 的 Oracle,或本域计划的人工验收 / 自动化覆盖边界;可使用同需求的 AC1/AC2 缩写与明确的闭区间。仅引用需求编号不能代替逐项登记。跨 AI / Unit 的证据须指明实际 case / 文件和覆盖层次。架构校验只检查验收登记及 case 文件关系,不证明行为已执行或断言充分。
2.3 backend 正式 case 规则
ai/knowledge正式 backend 必须打到页面或 IPC 会调用的生产实现(真实 store、真实服务、真实文件、真实工具)。- 禁止用假 executor、假 store、假
assert*、vi.fn/vi.mock/vi.spyOn顶替被测依赖。 - 纯函数和假对象只属于
tests/case/unit/**,不能充当正式计划里的功能 case。 - How 必须写明生产入口。vitest 起不了 Electron
utilityProcess/ Worker 时,允许同一份生产实现 in-process,缺口写进 How,禁止另写假类。 - 新增任何模拟路径必须先说明必要性、取得用户确认,并写入该 case 所在测试计划。
2.4 UI E2E 测试规则
- UI E2E 必须通过真实 UI 和产品链路完成被测行为;禁止 fake、mock、stub、直接写状态或内部写 IPC 绕过。
- fixture 和 helper 只能准备真实输入、执行用户可见操作、等待结果和收集证据,不能替代产品输出。
- Settings / Workbench E2E 默认使用
serviceMode: real;仅自动更新、数据库版本恢复等受控状态的 case 可使用fixture,并在对应doc/test-plan/**说明原因。 - 允许的模拟、种子或受控注入只写在对应
doc/test-plan/**case 的 How;新增假数据或模拟路径须先说明必要性、取得用户确认,并写入该 case 所在测试计划。 - observer、只读 IPC 和截图只能收集证据,不能触发被测行为或改变运行结果。
- 产品缺少必要 testability 入口时,应记录缺口,不能改用模拟路径绕过。不为 E2E 增加生产环境变量、故障开关、全局探针或其他生产代码入口;确需修改生产代码时,必须先取得用户确认。
2.5 真实 AI / live 断言规则
- backend AI 与 Workbench AI E2E 共用 live 环境配置(第 3.2 节);各域维护其 opt-in 命令与真实前置。
- 涉及真实 AI 调用的测试,硬断言只验证完成态、关键事实、关键工具或事件、关键 artifact,以及用户诉求是否落地;最终自然语言回答不得作为主要正确性证据。
- 不对最终文案做逐字断言,不断言固定工具顺序,不断言固定 prompt 原文,不因表达差异判失败。
- 如果用户诉求已落地但断言过严,应先调整
Oracle和测试代码,再根据 raw logs 记录完成度评分。 - 完成度评分由执行者或 agent 写入运行结论;测试代码不得在执行过程中修改文档。
3. 测试执行闭环
3.1 固定顺序
- 先确认是否新增或调整测试,以及目标分类。
- 如果是测试用例,先更新对应
doc/test-plan/**,再修改tests/case/<domain>/**。 - 如果是 Unit,放入
tests/case/unit/**,保持 backend-only,不更新测试计划。 - 通过
scripts/test/workflow.mjs执行并产出日志。 - 先看
timeline.jsonl;不足时再看runtime-events.jsonl、meta.json和artifacts.json。 - 最后基于 raw logs 回流结论,决定是否需要更新测试计划或实现代码。
3.2 runner 与常用命令
backend ai/human/live 与 Workbench AI E2E 共用:
- Key 池:
AI_LIVE_KEY_<PROVIDER>,服务商为DEEPSEEK/DASHSCOPE/MIMO/ZHIPU。 AI_LIVE_PROVIDER可选,默认dashscope。AI_LIVE_MODEL可选,默认该服务商的测试模型;通义为qwen3.8-flash。- baseUrl/providerKind 由产品 registry 推导;领域计划定义如何通过真实 executor 或 Settings UI 准备服务配置。
统一入口默认是 scripts/test/workflow.mjs。
runner 的职责:
- 找到目标测试文件并执行。
- 落原始日志到
tests/logs/**。 - 在不带
--type时按“普通测试 -> 源码 E2E”顺序执行。 - E2E 执行前清理
tests/.tmp/e2e中超过 24 小时未活动的残留ncurator-*目录,避免异常终止的 run 遗留临时数据。 - 按
--target准备并构建源码应用,或校验并安装已构建的正式包;正式包构建与审计遵循doc/deploy/release.md。
runner 不负责:
- 写事实汇总稿。
- 写失败原因分析。
- 写优化建议。
运行目标与隔离
source构建当前源码并直接启动应用。package校验既有 release bundle,安装后运行所选 E2E,不重新构建。macOS 经 Launch Services 打开隔离安装的.app;Windows 经安装器创建的快捷方式启动,保留其目标与起始位置。工作目录由系统入口决定,日志记录实际程序路径、版本与工作目录。- runner 统一准备 native 依赖、临时应用和知识库缓存;各 case 使用隔离的用户数据与工作区。
- 正式包结果记录实际完成的 case、状态、原始日志位置和被验证的安装包身份;仅进程退出成功或用例被跳过不算通过。发布入口的缓存门槛与失败处理按
doc/deploy/release.md执行,普通测试命令仍按所选范围运行。 - 功能断言已通过且无页面崩溃时,若 teardown 先记录框架主动终止进程,再观察到 Unix
SIGKILL或 Windowstaskkill的退出码 1,保留强制退出警告,不记为产品崩溃;无主动终止证据、退出早于清理或功能失败时不能沿用此分类。
调度与缓存
- E2E 的
execution只控制调度:默认normal;真实 AI / 服务调用为live;需要桌面焦点或原生对话框为exclusive。原有 Settings 索引 UI case 独立执行,不再作为批量测试的自动前置。 - case 通过
knowledge: 'none' | 'cached' | 'cold'声明知识库需求,默认none;仅需直接读取文件的 Skill / 文档任务不预热知识库。需要真实索引的 case 声明cached,模型安装/损坏、冷启动、线程预算与嵌入专项声明cold。 - 预热随已选 case 的真实索引请求按文件执行,统一检查并补齐缺失的解析/向量结果,无需另一份 fixture 清单。命中时直接复用,跨命令、跨日期有效;相同输入互斥并在锁内复查,原子发布已完成条目,失败后只补缺失部分。
- 解析缓存由文件内容、扩展名、影响解析的名称信息及解析/切分依赖指纹决定;向量缓存由实际嵌入文本序列、模型资产和嵌入依赖指纹决定。构建时自动收集生产入口依赖及相关库版本;Main/Preload 整体构建哈希与日期仅为运行证据,不参与缓存有效性判断。
- 各 case 保留独立工作区、SQLite、LanceDB、job 和权限;只复用纯计算结果,查询向量仍按真实检索计算。日志分别记录解析/向量命中、实际计算量与失效原因;模型初始化、索引写入和资料向量化分别取证。
- backend 普通测试并发上限为 6;E2E 总并发上限为 3,其中
live上限为 3;exclusive用例串行独占。case 失败后不自动重跑,继续收集其余 case 的结果。 --fresh-knowledge-cache强制本次冷缓存运行;缓存完整性与指纹始终校验,不提供绕过有效性校验的复用方式。source/package 使用各自实际构建携带的索引指纹;没有指纹的旧正式包禁用计算缓存,安全冷跑。
常用命令与参数
当前常用命令按域收口如下。
- AI
pnpm test:ai:backendpnpm test:ai:non-livepnpm test:ai:toolpnpm test:ai:human:mock-llmpnpm test:ai:human:live
- Knowledge
pnpm test:knowledge:backend
- Unit
pnpm test:unitpnpm test:unit:aipnpm test:terminal:直接vitest运行终端相关 unit 文件。
- E2E(源码 / 正式包)
pnpm test:e2e:source:构建当前源码并运行tests/case/**中的全部 E2E。pnpm test:e2e:package --bundle <release-bundle.json>:校验并安装已经构建的正式包,再对安装后的可执行程序运行所选正式 E2E。安装落在本机NCurator-E2E目录;Windows 可能覆盖本机已安装的正式包。测完删除 E2E 安装目录及指向它的入口。
- Settings
pnpm test:settings:e2e
- Workbench
pnpm test:workbench:e2e
- Showcase
pnpm test:showcase:独立执行官网样例采集。
- 更新
pnpm test:update:backend、pnpm test:update:e2epnpm test:update:real:win:Windows 真实覆盖升级测试,独立脚本,不属于 runner。
runner 支持的过滤参数:
--domain--type--suite--group,可重复传入--exclude-group,可重复传入--case--runs--target,取值为source或package,默认source。--bundle,正式包 E2E 必填,指向已构建候选包的release-bundle-*.json。--fresh-knowledge-cache,清理当前平台的索引计算缓存,强制从零验证完整索引。--showcase,执行 showcase 采集类别。--result,showcase/发布流程用的结果输出路径。--help(或-h),打印 runner 用法后直接退出;传入未识别的参数会立即报错并退出(退出码 2),防止未知参数被静默忽略而误触发全量执行。
3.3 读取与分析 raw logs
- 默认先读取目标 run 的
timeline.jsonl,理解语义步骤、断言事实和失败位置。 - 如果
timeline.jsonl仍不足以解释结果,再继续读取runtime-events.jsonl与关键附件。 - 压缩切点先看
runtime-events.jsonl的context_compaction_*字段;prompt 原文看llm-io.jsonl;伙伴 bookmark 看可选artifacts/message-log-annotations.json。 - 必要时回看
meta.json与artifacts.json,确认 run 摘要、统计值和附件索引。 - 所有分析结论都必须可回溯到 raw logs 与 artifacts,不得脱离证据做推断。
4. runner 与原始产物契约
4.1 run 目录
每个 run 固定目录:
tests/logs/<domain>/<suite>/<groups...>/<caseId>/<runId>/
meta.json
timeline.jsonl
runtime-events.jsonl
llm-io.jsonl
artifacts.json
cache-summary.json
seed-manifest.json
artifacts/
固定原始文件:
meta.json- 单次 run 的摘要索引。
- 用来回答“本次 run 是谁、结果是什么、统计值是什么”。
timeline.jsonl- 单次 run 的事实事件流。
- 用来回答“本次 run 按顺序发生了什么”。
data只放按需原始观测值;压缩等长文本回看llm-io.jsonl,不要把整段 prompt 再拷进 assertiondata。
runtime-events.jsonl- 单次 run 的完整运行时原始事件流。
- 用来回答“底层 runtime 真实发出了哪些事件”。
- 压缩切点以
context_compaction_started/completed上的throughSeq、firstKeptSeq、historyFloorSeq、historyFloorKind、isSplitTurn、foldedCount、tailCount为准。
llm-io.jsonl- 发给模型的 conversation / system / usage / requestWire。
- backend 始终写入;e2e 由 observer 桥接生产
recordLlmIo写入llm-io.jsonl(无 LLM 调用时为空文件)。
artifacts.json- 单次 run 的附件索引。
- 用来回答“本次 run 产生了哪些可回看的附件”。
artifacts/- 可选目录。
- 只有确实生成 trace、截图、HTML 诊断页、可选 human bookmark 快照等实体附件时才创建。
cache-summary.json- 本次 run 的知识库/向量缓存摘要。
seed-manifest.json- 本次 run 的 fixture seed 清单。
目录与命名规则:
<domain>/<suite>/<groups...>- 目录层级体现业务域与 suite 树。
- runner 通过目录过滤测试范围。
<caseId>- 对应用例稳定标识。
- 通常与测试文件名去掉
.test.ts后一致;e2e case 通常以.e2e结尾。
<runId>- 单次执行唯一标识。
- E2E 格式为
YYYYMMDD-HHmmss-SSS;backend 在该时间戳后追加 8 位 UUID 前缀,以隔离并发运行。
- backend 正式 case 运行开始后,runner 应尽快创建对应 run 目录,并对
timeline.jsonl / runtime-events.jsonl / llm-io.jsonl采用渐进式落盘;不得要求等到 run 结束后才首次出现日志目录。 - backend raw logs 的完整记录以磁盘上的 JSONL 文件为唯一真相源;测试进程内允许不再长期保留这些日志的全量副本。
4.2 meta.json
固定字段:
schemaVersionrunIdcaseIdscopestatusstartedAtfinishedAtstatsruntime
关键约束:
schemaVersion固定值为5。scope格式为<domain>/<suite>/<groups...>,不包含caseId。status当前值域是passed/failed。- backend run 在执行中允许临时写出
running;正式结束后必须收口为passed或failed。 stats当前固定字段:durationMsstepsassertionserrorsartifacts
runtime当前固定字段:drivertargetmodelName
meta.json顶层只保留上述固定字段。- 如果测试需要保留 fixture 依赖信息,应写在
timeline.jsonl的run/start事件data中。 - 为降低长 live run 的内存压力,
stats可以来自运行中计数器或基于磁盘日志的回读统计;不要求依赖内存里的全量 timeline 副本。
4.3 timeline.jsonl
固定字段:
tstypenamestatusidmessagedata
字段规则:
type当前值域:runstepassertionartifactsignalerror
status当前值域:infookfailedskipped
message只写“发生了什么”,不写失败原因、优化建议或能力判断。data只放按需原始观测值,不要求统一业务 schema。
约束:
timeline.jsonl只记录高价值语义事件,不记录底层噪音。signal用于记录可观察到的外部信号,例如路由变化、IPC 调用、AI 事件。- 当失败事件存在明确证据时,优先把证据放进
data或 artifact,不要在message中写大段解释。 - e2e 下不把所有 Playwright 底层调用逐条倾倒进 timeline;底层重放依赖
trace.zip,timeline 只保留语义 step / assertion / signal。 run之外的业务状态不要塞进timeline.status。- 例如
blocked、completed、cancelled应保留在runtime-events.jsonl或写进data。
- 例如
4.4 runtime-events.jsonl
- 文件内容是逐行原始运行时事件 JSON,不包额外 wrapper,不做 schema 投影。
- 范围固定为完整 runtime 原始事件流。
- 普通测试与 e2e 的采集路径可以不同,但最终落盘契约必须一致。
runtime-events.jsonl始终写出;如果本次 run 没有 runtime 事件,则写空文件。E2E observer 为每条事件分配独立标识,重复采集按该标识去重;同毫秒的并行事件或流式片段均保留。- 当
Oracle、断言data或timeline.jsonl仍不足以解释失败原因时,必须继续回看runtime-events.jsonl。
4.5 artifacts.json
固定字段:
- 顶层:
schemaVersionitems
items每项固定字段:idkindpathrefsfileName(可选)capability(可选)
关键约束:
schemaVersion固定值为5。kind当前值域:tracescreenshotvideoplaywright-reportjsonlother
path是相对当前 run 根目录的稳定路径。refs当前支持:stepIdassertionId
artifacts.json必须始终写出,即使items为空数组。artifacts/目录只有在确实存在实体附件文件时才创建。- human live 可按 case 选择写出
artifacts/message-log-annotations.json(kind=other):只含compaction_checkpoint的 seq、throughSeq、compactBlockId与摘要长度,不含摘要全文。默认关闭。
4.6 不同测试类型的统一约束
- 普通测试与 e2e 共用同一份
meta.json/timeline.jsonl/runtime-events.jsonl/artifacts.json基础契约。 - 两类测试的差异只体现在:
meta.runtime.drivermeta.runtime.targetartifacts.json.items[].kindruntime-events.jsonl的采集路径timeline.jsonl中出现的语义事件名与信号类型
4.7 e2e 默认附件策略
- 成功 run:
- 始终保留 Playwright trace。
- 只有测试或 runtime 显式要求保留的关键截图才写入。
- 默认不生成 Playwright HTML 诊断页。
- 失败 run:
- 始终保留 Playwright trace。
- 始终生成失败截图,并通过
refs.assertionId或refs.stepId绑定。 - 生成位于
artifacts/playwright/report/index.html的 HTML 诊断页。
- 默认不保留 video。