Skip to content

Latest commit

 

History

History
111 lines (83 loc) · 44.5 KB

testing.md

File metadata and controls

111 lines (83 loc) · 44.5 KB

RainCode 测试说明

本文档描述 RainCode 的测试体系:单元测试、冒烟测试、NFR 基准、工程门禁。测试脚本变更时必须同步更新本文档(见 CONTRIBUTING 文档更新协议)。

1. 测试策略总览

RainCode 采用「单元 + 冒烟 + 基准 + 门禁」四层验证体系,全部可从仓库根目录一条命令运行:

层 目的 运行方式 密钥/外呼
单元(unit) 包内模块级正确性(node:test,零外呼) pnpm test ✅ 全程本机 mock/临时目录;cli:pinned-manager(T5.7 门禁升级轮:parsePinnedManager 纯函数面——精确 pinned pnpm@11.24.0 通过 / corepack +sha512 哈希后缀形态仍精确通过 / 范围形态 ^·~·>=·x·区间一律拒绝 / 非 pnpm 与缺失·非字符串字段拒绝)+ bundle-metafile-check(T5.7 门禁升级轮:单实例多包计数与 workspace/入口路径排除 / scoped 包名 + 还原 / 同一包多版本抛错列出实例 / 同版本不同 peer 哈希(完整 .pnpm spec 键控不做 peer 剥离)抛错 / Windows 反斜杠路径归一化判定 / 同实例多文件不误报 / 非 name@version 形态异常防御不误报)
冒烟(smoke) 功能链路端到端正确性 pnpm smoke:* ✅ 全程本机回环 mock,无外呼、无真实密钥
基准(bench) NFR 性能指标验收与防劣化 pnpm bench:* ✅ 同上(LLM 为本机 mock SSE 服务器)
门禁(gate) 代码质量与架构治理 pnpm typecheck / lint / architecture:check —

另有一个可选真实冒烟 pnpm smoke:remote(T4.8 矩阵化):走完整 CLI 路径逐条调用真实 Provider——读取 config/remote-providers.local.json(gitignored,{providers: [{name, baseURL, model, apiKey, prompt?, timeoutMs?, disabled?}]} 矩阵形态;缺失时回退旧单条形态 config/test-provider.local.json)逐条执行并矩阵汇报(PASS/FAILED + 耗时),对全部 stdout/stderr 断言无密钥泄露;网络不通时如实报告 FAILED,不影响本地冒烟结论。五 Provider 口径(07 §2.4 场景 1)密钥到位即入阵,执行留存见 m4 报告 §1。

2. 单元测试

node:test 原生测试运行器 + tsx 加载器(根 package.json pnpm test,glob 收集 packages/*/test/**/*.test.ts 与 apps/*/test/**/*.test.ts),各包 tsconfig include 已含 test 目录。

命令 覆盖范围
pnpm test tools:truncate(字节预算头 70/尾 30 + 截断提示)、path-guard(workspace 越界)、executor-concurrency(只读并行上限/写串行)、ssrf(黑名单网段/localhost/scheme/重定向跳板/DNS mock)、web-fetch(HTML→文本转换/JSON 原文/非 2xx/maxBytes 截断/重定向跟随与拦截)、ask-user(通道应答/TOOL_UNAVAILABLE/TOOL_PERMISSION_DENIED)、executor(M3 T3.1/T3.2:resolveSandboxExecutor 工厂——未配置 local 零探测/docker+wsl 不可用回退告警/可用生效;DockerExecutor argv 策略——仅挂载 workspace 的越界拦截语义 + --network 隔离 + workdir 映射 + env 注入 + 退出后 rm -f 清理;WslExecutor --cd 翻译/发行版/env 前缀;SshExecutor(T3.2,T5.7 拆出 executor-ssh.test.ts)——base64 远端命令契约解码核对(echo b64

预期输出:# pass N(当前 506:T6.2 回放 lane +3——storage replay-lane.test.ts(fixture 全集独立验证 / DB 级两次 resume 收敛+对账回写 / 崩溃种子残尾修复+悬挂合成)+ T6.1 marketplace 分发基座 +4——server marketplace.test.ts 3(fs 原语哈希/逃逸防护 + RPC 全链)+ agent-core skill.test.ts 第三源优先级 1)且 # fail 0,退出码 0。提交前与门禁一起全绿。走查基线与截图基数:walkthrough:web 19/19 + walkthrough:desktop 14/14(DOM 契约零破坏);产品截图双端共 43 张(web 22 + desktop 21,pnpm shots:web / shots:desktop,本轮各 +6)。

会话录制回放 lane(T6.2):fixture = 真实录制 events.jsonl 原字节归档(packages/storage/test/fixtures/replay/,四场景 plain-chat / tool-roundtrip / compaction / crash-seed)+ expected.json(录制期幂等 resume 快照冻结的原会话终态;crash-seed 另申报文件级注入的悬挂 tool_call 与半行截尾)。录制工具 pnpm record:replay-fixtures(scripts/record-replay-fixture.mts:mock LLM 驱动真实节点;events.jsonl 不改写保 checkpoint.pos 字节偏移不变量;归档前独立验证 + 凭据扫描 fail-closed,04 §5.3)。harness 断言(录制与 CI 共用 packages/storage/test/replay-lane-lib.ts 同一实现):确定性(同路复跑一致)+ 双路重放收敛(tail-scan vs recorded O(1) 逐字段)+ 投影逐字段一致 + DB 级 resume 对账回写。

3. 冒烟测试

所有冒烟脚本共享同一模式:node:http 本机 mock OpenAI SSE 服务器(脚本化多轮回复,tool_call delta 分片下发以覆盖 llm 侧累积)→ 临时 RAINCODE_HOME → 进程内创建 Agent Service(in-memory RPC 绑定)→ RPC 驱动 → 断言落盘数据与事件流。

命令 脚本 覆盖范围
pnpm smoke:e2e scripts/smoke-e2e.mts 端到端主链路:session.send → llm 流式回复(SSE delta 累积 + usage + [DONE])→ 事件流/JSONL 落盘 → 会话列表与恢复
pnpm smoke:tools scripts/smoke-tools.mts 工具调用系统:用例 A(allow)read+write 多轮工具调用、tool_call.started/completed 事件、结果文件落盘、最终回复无工具泄露;用例 B(deny)needsApproval 工具被 always-deny 审批拒绝 → 模型收到 TOOL_PERMISSION_DENIED 并继续收束
pnpm smoke:permission scripts/smoke-permission.mts 权限判定链脚本化场景:五级判定优先级、bash argv 求值与只读白名单、grantId 审批闭环(allow once/always、deny)、规则持久化(global 重启保留)、高危根命令通配 allow 降级 ask;T2.5 合并矩阵 e 组:project deny 覆盖 global allow(首个命中层级)、global deny 收敛 + removeRule 即时生效、清空回归 default ask、project 规则 workspace 隔离(ws2 免疫 ws1)、global 跨 workspace 放行对照
pnpm smoke:p0 scripts/smoke-p0.mts M1 P0 控制面全集(并回归其余九个 smoke):① 协议注册表对照(22 方法 / 12 事件)② Provider 密钥引用制(明文 key → 密钥文件 + apiKeyRef,响应零明文;移除活跃 Provider 报错)③ session.steer 运行中注入 ④ session.setMode 模式级 deny/恢复 ⑤ session.archive 归档语义与 events.jsonl 保留 ⑥ 明文密钥不落入任何响应/落盘/日志
pnpm smoke:compact scripts/smoke-compact.mts auto-compact(M2 T2.1 / NFR-6 专项):用例 A auto 触发 + 压缩窗口内 send 不阻塞 + compaction.applied/epoch+1 提交 + 窗口期消息合并 + resume 连续性;用例 B 空摘要失败 → 保留原历史 + 阈值临时上调 90%;用例 C 手动 compact 低于阈值可用 + in-flight 幂等复用 ticket + 空历史 INVALID_PARAMS;T5.4 用例 D microcompact 预剪枝——白名单内 read 大结果(1540 cps)T13 边界 head+marker+tail 剪枝(节省 366 tokens 回落 usage 估算后 full compact 让位不触发)+ compaction.pruned 事件行 sourceMessageId 回指原文 + 重放历史按回指替换为同一剪后内容 + 下一请求模型所见一致;用例 E 白名单外不动——compactableTools 限 bash,read 大结果原样保留无剪枝事件
pnpm smoke:mcp scripts/smoke-mcp.mts MCP 接入(M2 T2.2 + M3 T3.7):与手写 JSON-RPC fixture server(mcp-fixture-stdio.mjs / mcp-fixture-http.mjs,node 直跑)真实互操作——连接状态机(Connected/Failed 失败隔离)、命名空间工具注册、控制面直调(ToolExecutor 链路)、turn 内模型调用(权限链)、进程崩溃 → M4 重连 → 工具恢复、HTTP transport add/call/remove + mcp.json 持久化;T3.7 用例 G 运行时启停 + 健康检查:health on Connected 主动 MCP ping 实测 RTT(latencyMs)/ Failed server 只读投影 ok=false 不触连接;setEnabled false → 断连 + 命名空间工具不可用 + mcp.tools.call MCP_UNAVAILABLE + mcp.json enabled: false 持久化(配置保留 ≠ 删除);setEnabled true → 受理即返重连 Connected + 工具恢复 + enabled: true 写回;未知 serverKey 两方法均 MCP_SERVER_NOT_FOUND
pnpm smoke:subagent scripts/smoke-subagent.mts 子代理(M2 T2.3):mock LLM 编排「主 → 子 → 主」请求序列——用例 A 完成链路(agent 工具派发 → 子会话独立 systemPrompt 收束 → 完成通知经 tool_call.completed 回传 → subagent.list 含 usage/turnsUsed);用例 B 事件镜像(subagent.spawned → progress started → progress done → subagent.completed 顺序与字段);用例 C profiles.list(workspace/global 双源 + frontmatter 投影 + 坏文件跳过);用例 D 校验错误族(SUBAGENT_PROFILE_NOT_FOUND / SUBAGENT_PROFILE_INVALID / INVALID_PARAMS / SESSION_NOT_FOUND / SUBAGENT_NOT_FOUND);用例 E stop 终态幂等(stopped:false);用例 F 并发排队(并发 4 下第 5 个 Pending + queuePosition=1 + FIFO 补位全数 Completed);T3.6 用例 G 内置角色模板(tester 目录未命中 → builtin 兜底 spawn 可收束 + profiles.list source:"builtin" 投影 + 用户同名遮蔽);用例 H 并行编排汇聚(同轮两个 agent tool_call index 0/1 → 子代理并发执行——请求到达间隔 < 300ms 而串行基线 ≥400ms——双完成通知合并回主循环)
pnpm smoke:memory scripts/smoke-memory.mts 项目记忆(M2 T2.4):用例 A 模板与注入(memory.read exists:false → 模板骨架 + MEMORY.md 全文进 system 提示);用例 B write(Agent 章节落盘 / 用户章节越界拦截 / mtime 并发冲突 MEMORY_WRITE_CONFLICT——经 SectionEditHooks.onBeforeRecheck 注入缝在 S2 复检前确定性直改,T3.3 收口旧竞速法的 ~1/6 时序闪失);用例 C archive 抽取(session-end 落盘 + settings 幂等键二次抽取返回空);用例 D search(≥3 字 FTS trigram / <3 字 LIKE 兜底 / kind 过滤 / confidence<0.6 不入召回但 entries.list 可见 / 无结果空数组);用例 E promote(合入指定章节 + MEMORY_ENTRY_NOT_FOUND);用例 F compact 抽取钩子(onBeforeReplace 先于历史替换,source=compact 落盘);用例 G 晋升草案待确认区(生成规则 0.9/0.8 入选、0.5 与 todo 排除 / 直管 promote 收敛 decision 草案 confirmed / confirm 合入 MEMORY.md / 终态与 MEMORY_DRAFT_NOT_FOUND 族);用例 H 全局记忆双层注入(M5 T5.3:RAINCODE_HOME/MEMORY.md 落位 → system 提示含全局层标题行与内容、且先于项目层标题行);用例 I session_search 端到端(M5 T5.3:tool.tools.list 含 session_search / 种子会话历史落盘 → 检索会话 mock LLM 发起 session_search 工具调用 → 命中计数·出处 sessionId·正文片段回传续答收束)
pnpm smoke:migrations scripts/smoke-migrations.mts 迁移回放(M2 T2.5 验收):t1 空库全量迁移 001→004(settings/permission_rules/memory_entries 功能探针);t2 重开幂等(版本不重复执行、数据保留);t3 存量库升级路径(模拟 001+002 时代库:DROP 003/004 表 + 删版本行 ≥3 → 重开重放 003→004:表恢复、002 数据行原样保留、重建表可写入)
pnpm smoke:plugin scripts/smoke-plugin.mts 插件化(M3 T3.5 + B10 重扫描):用例 A 示例插件发布与加载(examples/plugins/hello 拷入 plugins 目录 → plugins.list active + 工具全名 → tool.tools.list source=plugin → turn 内 mock 下发 plugin__hello__greet/word_count tool_call,greet 直执行、word_count 非字符串返回值 JSON 序列化);用例 B 运行时启停(setEnabled false → disabled + 工具不可见 + plugins.json 停用名单落盘 / 同态幂等 / true → active + 工具恢复 / 未知 name → PLUGIN_NOT_FOUND);用例 C 故障隔离(坏清单与 activate 抛错 → failed + lastError;execute 抛错 → 数据级 TOOL_EXEC_FAILED 回传、turn 收束;内核与会话不受影响、同目录好插件照常 active);用例 D 运行时重扫描(B10 缺陷回归 / v1.11 plugins.rescan:装配后新拷入目录 → rescan 前不可见 → rescan added=["late"] + active + 工具注册 + plugin.status_changed 到达;重复 rescan 幂等无新增,已有记录不重载)
pnpm smoke:marketplace scripts/smoke-marketplace.mts marketplace 分发(M6 T6.1,06 §2.10 v1.14):用例 A 分发全链(marketplace.add 注册 examples/marketplaces/local → list 未安装投影 → install status=active + 缓存布局 cache/// + 种子 .zcode-plugin-seed.json 落盘 → plugins.list 安装副本激活 → skills.list 第三源 plugin-demo(source=plugin)→ skills.invoke 起 turn → mock 下发 plugin__hello__greet tool_call 真实执行回传);用例 B 逃逸防护(junction 指向插件根外 → install 拒绝 MARKETPLACE_ESCAPE_BLOCKED 且越界路径入错误消息);用例 C 卸载复原(uninstall → plugins.list/skills.list 不可见 + 安装副本删除 + 台账清空 → 重复卸载 MARKETPLACE_NOT_FOUND → 重装恢复 active)
pnpm smoke:p1tools scripts/smoke-p1tools.mts P1 工具(T2.7 二阶段):用例 A web_fetch SSRF 端到端——turn 内 mock 下发 web_fetch(http://127.0.0.1:<mock 端口>) → SSRF 守卫拦截回环地址,tool_call.completed isError 且 TOOL_SSRF_BLOCKED 注明原因,模型收到错误后 turn 继续收束(正向抓取路径由单测覆盖,冒烟不断言正向网络);用例 B ask_user_question headless fail-safe——无交互通道 → TOOL_UNAVAILABLE 收敛 + 两工具经 tool.tools.list builtin 可见(web_fetch metadata network/needsApproval)
pnpm smoke:stdio scripts/smoke-stdio.mts stdio 绑定 + headless(T2.8):子进程 spawn raincode serve(无 Provider),断言 A 握手门禁(ping 前 session.list → VERSION_MISMATCH);B system.ping(protocolVersion/capabilities);C 畸形行带 id → PARSE_ERROR response 且不断开;D 畸形行无 id → 丢弃 + stderr 告警、连接继续;E session.create → session.resume 幂等快照(lastSeq/phase/model/contextUsage/messages/pendingApprovals 字段齐全);F stdin end → 进程优雅退出 code 0(帧可人工 cat 重放的等价路径,ADR-08)
pnpm smoke:hooks scripts/smoke-hooks.mts hooks 生命周期 v1(M5 T5.1 / 协议 v1.12):用例 1 PreToolUse deny 真实拦截(project hook 授信后拦截 write → TOOL_HOOK_DENIED 结果 + hook.completed(blocked),default-allow 政策下证明拦截先于权限);用例 2 additionalContext 注入下一回合(user hook → mock 请求体含 [hook:UserPromptSubmit via <hookId>] provenance 标记);用例 3 超时与坏 JSON 容错(timed_out/failed hook 与工具同 turn → 工具照常执行成功,hook.result 逐 hook 落盘且 stderr 截断 ≤500);用例 4 project hook 授信闭环(未授信 skipped_untrusted → grant 后 UserPromptSubmit block → turn failed HOOK_BLOCKED → revoke 立即失效);用例 5 审计事件对落盘(hook.invoked + hook.result 仅入 events.jsonl 不进 rpc);附加 hooks.list 双源投影 + 授信状态
pnpm smoke:web scripts/smoke-web.mts Web 界面(M3 T3.8,06 §6.3 v1.9):用例 A 鉴权握手 + 浏览器端到端会话(node ws 客户端 ≡ 浏览器帧形态——无鉴权 ping → UNAUTHORIZED、错误 token ws.auth 拒绝、正确 token → ping(capabilities 含 ws.auth)→ session.create → mock LLM 回合 message.delta/completed/done 全经 WS 到达);用例 B 断线重连快照补偿(验收项:createReconnectingRpcClient 真实客户端——mock 请求延迟 1200ms 使回合内容整体产生于断线窗口 → 客户端 terminate → 退避重连 → onRestored → done 于新连接到达 → session.resume 快照补推内容零丢失 + setSeqBaseline 回填 → 补偿后下一回合正常收束);用例 C 多连接扇出(两连接同收 session.created;断开其一后另一连接扇出不中断);用例 D 四面板服务面(T4.5,L-08 验收项:skills.list workspace+global 双层 source 投影 + skills.invoke 端到端回合;session.usage 回合后投影含 turnsCount;memory.read 空态 → write Agent 专用章节 → 回读 + drafts/entries 投影;MCP fixture 连接投影 → 健康检查实测 RTT → 启停(mcp.server_status_changed 到达)→ 重连收敛;插件 active → 停用 → 再激活(plugin.status_changed 到达)——UI 侧真浏览器走查归 T4.7/L-05);用例 E 双重连客户端回合扇出(B3 缺陷回归:两个 createReconnectingRpcClient 浏览器同构路径——B 经 resume → setSeqBaseline 选中 A 所建会话后,A 的回合 delta/completed/done 实时到达 B,基线门零误拦零缺口;基线防回退单测在 packages/rpc/test/web-client.test.ts)
node scripts/verify-agent-bundle.mjs scripts/verify-agent-bundle.mjs 桌面端打包产物(T2.9):esbuild bundle 的 agent 入口(apps/desktop/dist-electron/agent/entry.cjs,需先 pnpm --filter @raincode/desktop build:agent)以 node 直跑,stdin 注入 system.ping → 断言 pong response(验证 bundle + 随包 migrations env 注入通路)。完整 dist 打包已自动化(T4.7 L-04 核销):pnpm --filter @raincode/desktop dist(prepare-native.mjs 暂存 electron-ABI 原生模块 + dist.mjs gh-proxy 镜像与 winCodeSign 缓存兜底 → nsis 产物),安装启动冒烟经 walkthrough-desktop.mts 的 RAINCODE_WALKTHROUGH_APP_PATH 安装目标执行
node scripts/llm-fixture-http.mjs <port> scripts/llm-fixture-http.mjs OpenAI 兼容流式 fixture server(GUI 走查 / 端到端联调用,node 直跑):SSE 流式 chat/completions(role 帧 → content 或 tool_calls delta 分帧 → finish → usage → [DONE]);按请求 messages 自动分流——尾条 role=tool 输出含工具结果尾部的收束文本,否则下发 bash echo raincode-gui-fixture tool_call;用户消息 run: <command> 行可脚本化 bash 命令(触发真实审批链路);GET /health 就绪探测。桌面端 GUI 走查以临时 RAINCODE_HOME + RAINCODE_PROVIDER_CONFIG 指向本 fixture(M2 场景 5 自动化,见 PROGRESS §3)
pnpm smoke:skills scripts/smoke-skills.mts 技能与斜杠命令(M3 T3.4):global 层装入 examples/skills/ 官方示例真身 + workspace 层自定义技能 → 用例 A skills.list(无 sessionId)三示例在列、坏文件跳过不阻塞面板;用例 B 双源优先级(带 sessionId 同名技能 workspace 胜出);用例 C 示例技能端到端(skills.invoke(review) → mock 请求体末条 user 含 $ARGUMENTS 替换后文本、占位符不残留);用例 D 自定义技能展开语义(无占位符参数追加末尾 / workspace 模板胜出 / 无参调用占位替换空串);用例 E 错误族(SKILL_NOT_FOUND 未知名与路径逃逸 / SKILL_INVALID 坏文件 / SESSION_NOT_FOUND / strict INVALID_PARAMS)
pnpm smoke:ssh scripts/smoke-ssh.mts SSH 执行域端到端(T4.8 / L-02 核销):loopback 真 SSH2 协议服务端(ssh2 npm——密钥交换/公钥认证/exec 通道真实线上协议)+ 一次性 ssh-keygen 密钥对(临时目录即弃)+ SshExecutor 经真实 OpenSSH ssh.exe 客户端连接——A 探针(resolveSandboxExecutor kind=ssh 无告警 + T5.2 远端探针不可得 enforcement=partial);B 回合端到端(mock LLM 脚本化两次 bash 工具调用 → SSH 执行域 → cwd 映射线上核对 + 远端落盘文件 N-3 + T5.2 completed 首行 contentPreview 标注 sandbox: ssh (enforcement: partial));C env 注入(远端命令前缀线上核对 + 远端 shell 可见);D 失败收敛(错误密钥 → 探测失败 → local 回退 + 告警 fail-closed);E 本地审计(工具调用事件落本地数据根,T3.2 口径)。T5.7 base64 加固:远端命令契约改为 `echo
pnpm walkthrough:desktop scripts/walkthrough-desktop.mts 桌面端 GUI 回归走查(T3.9,CDP 驱动构建产物):种子临时 RAINCODE_HOME(生态形态 mcp.json → stdio fixture / 示例插件 hello / 官方示例技能 ×3 / config.json Provider——apiKeyRef null 无凭据,不落任何密钥)+ --remote-debugging-port 直启 electron.exe;CDP Runtime.evaluate 语义点击 + Input.insertText / dispatchKeyEvent 真实键盘流——扩展面板(MCP Connected 投影 / 健康检查 RTT / 插件与 server 启停双向 + mcp.server_status_changed 事件活更)、斜杠命令面板(/ 唤起 / ↑↓+Tab 补全 / skills.invoke 端到端回合)、会话用量统计行、记忆管理器可达,共 14 断言(M3 报告 §6)。运行前置:pnpm --filter @raincode/desktop build(electron 窗口会弹出)。安装产物冒烟(T4.7 L-04):RAINCODE_WALKTHROUGH_APP_PATH 指向静默安装后的 RainCode.exe(installer /S /D=<dir>)即对 nsis 产物跑同套断言(packaged main + ELECTRON_RUN_AS_NODE agent + electron-ABI better-sqlite3 NODE_PATH 解析 + mock 会话全链路)。CDP 客户端与 DOM 助手共享件在 cdp-lib.mts
pnpm walkthrough:web scripts/walkthrough-web.mts Web 工作台真浏览器走查(T4.7,L-05 核销 + L-21 复核):真实入口 raincode web --static(与 serve 同装配面,Provider 经 env 注入)+ Edge/Chrome headless --remote-debugging-port,CDP Runtime.evaluate + Input.insertText / dispatchKeyEvent 驱动——A 鉴权(错误 token → fatal「连接被拒绝」+ UNAUTHORIZED 停止重连;正确 token → 已连接徽章);B 会话回合(React 受控 input 原生 setter 写值 + 键盘发送 + message.delta 实时渲染 + session.usage 行);C 审批闭环(write 审批弹窗 → 放行后文件真实落盘 / 拒绝后被拒文件不存在——验证世界而非自述 N-3);D 重连恢复(杀宿主进程 → 重连态徽章 → 同 home/port 重启 → 退避自动重连 + history 冷重建零交互恢复 + B9 无滞留横幅);E L-21 复核(第二标签接入冷重建 → Page.setWebLifecycleState 冻结/解冻确定性注入 Edge 后台标签冻结假说:冻结期回合不达 + 解冻零交互补偿拉平 + 在线实时扇出零缺口),共 19 断言。运行前置:pnpm --filter @raincode/web build。走查 aid:web renderer 与桌面端同口径暴露 window.__raincodeStore;多标签经 /json/new?about:blank + targetId 连接 + Page.navigate 导航(/json/new 直拼带 & 的 URL 会被 Edge 截断,PROGRESS §4 P-2 条目)

运行全部:

pnpm smoke:p0   # 内部已并复 smoke:e2e / smoke:tools / smoke:permission / smoke:compact / smoke:mcp / smoke:subagent / smoke:memory / smoke:migrations / smoke:kernel / smoke:p1tools / smoke:skills / smoke:plugin / smoke:stdio / smoke:hooks / smoke:ssh / smoke:web

预期输出:各脚本末尾打印 SMOKE OK,退出码 0。

4. NFR 基准测试

实现入口 scripts/bench.mts(共享逻辑 scripts/bench-lib.mts),口径对照 01-PRD §6.1 性能指标基线表。M2 起新增 scripts/bench-mem-desktop.mts(NFR-4 桌面端空载内存:先 pnpm --filter @raincode/desktop build 构建 dist 产物,再 pnpm bench:mem:desktop,electron 窗口会真实弹出,60s 预热 + 10×30s 采样进程树 WorkingSet)。基准结果留存:docs/benchmarks/m1-2026-09-28.md(M1)与 docs/benchmarks/m2-2026-09-29.md(M2,含 NFR-1 增长原因申报与 NFR-6 单测口径)。

命令 指标 M1 实测(详见 benchmarks/m1-2026-09-28.md)
pnpm bench:start NFR-1 CLI 冷启动 ≤ 2s(20 次中位数) ✅ 509.2ms
pnpm bench:send NFR-2 输入→模型请求本地开销 ≤ 300ms(P95,n=100) ✅ 16.7ms
pnpm bench:render NFR-3 工具结果本地渲染延迟 ≤ 100ms(P95,n=100) ✅ 0.0ms
pnpm bench:resume NFR-5 万条消息会话恢复 ≤ 1s(10 次中位数) ✅ 93.3ms
pnpm bench:crash NFR-7 崩溃恢复专项(taskkill /F /T 强杀 → 四项断言) ✅ PASS
pnpm bench:all 全部五项 ✅

NFR-4(桌面端空载内存)、NFR-6(压缩异步不阻塞)属 M2 验收范围,M2 波次补齐。

里程碑验收时必须在 docs/benchmarks/ 新增留存报告(含环境说明与复现命令,格式参照 m1-2026-09-28.md)。

5. 工程门禁

命令 工具 检查内容
pnpm test node:test + tsx(§2 单元测试) 包内模块级正确性(tools/permission/agent-core),与门禁同绿提交
pnpm typecheck tsc --noEmit strict(逐 workspace 项目) 全仓类型正确性
pnpm lint oxlint Lint 规则(当前基线:0 错误,11 条既有 warning——rpc/stdio/desktop/legacy smoke 等历史条目,刻意快照不修)
pnpm architecture:check scripts/architecture-check.mjs 五项架构策略(依据 architecture/policy.yaml):跨包依赖白名单、Tarjan 循环依赖、单文件 ≤ 500 行、深导入禁令(跨包只能从 index.ts 导入)、managedOnly
pnpm smoke:bundle scripts/smoke-cli-bundle.mts(T5.7) CLI esbuild 产物冒烟(L-16 核销):脚本自跑 apps/cli/scripts/build.mjs(metafile 重复依赖校验在构建内强制)→ dist/raincode.mjs 以 node 直跑(非 tsx 源码路径)——A help 六命令齐备 / B 未知命令 exit 2 / C config dump --default-only exit 0(bundle 内配置管线)/ D 握手门禁 ping 前 session.list → VERSION_MISMATCH / E system.ping protocolVersion+capabilities / F tool.tools.list 14 工具逐项一致(13 内置 + agent,serve 装配面与源码运行一致)/ G session.create 返回 sessionId(migrations 自举生效证据:RAINCODE_MIGRATIONS_DIR banner → dist/migrations/)/ H stdin end 优雅退出 exit 0;CI 独立 cli-bundle job 同款
pnpm protocol:check scripts/gen-protocol-catalog.mts --check(T4.3) 生成式协议目录防漂移:docs/generated/protocol-catalog.md 与 shared METHOD_SCHEMAS / EVENT_SCHEMAS / *_ERROR_CODES 常量的内存再生成结果逐字节比对,不一致(手改或协议演进后未再生成)即失败;pnpm protocol:gen 一键再生成
pnpm event-matrix:check scripts/gen-event-matrix.mts --check(T5.5) 事件生产者/消费者矩阵防漂移:docs/generated/event-matrix.md(25 事件 = 协议 21 + 存储级 4,session.created 双栖)与源码扫描(声明/派发/监听分类 + 登记式排除/覆盖/豁免)的内存再生成结果逐字节比对,不一致(手改或事件面演进后未再生成)即失败;另设防漏登记守卫——事件域前缀点分字面量未登记即生成器报错;pnpm event-matrix:gen 一键再生成;与 protocol:check 同入 CI 门禁 6(双生成物)
CI GitHub Actions .github/workflows/ci.yml 六门禁 gates job 原样入 CI(windows-latest + pnpm@11.24.0 + Node 24 + frozen lockfile + pnpm store 缓存),push(master)/PR 触发——与本地门禁集一致;electron 二进制按需跳过(ELECTRON_SKIP_BINARY_DOWNLOAD=1,desktop 测试与 smoke 全程 node/tsx 路径零 electron 运行时依赖);2026-10-03 首次运行全绿(07 §10.2 T4.1),同日 T4.3 增补门禁 6。T5.7 双 job 形态(2026-10-06 门禁升级轮落地):gates job 前置 pinned 包管理器校验(pnpm check:manager 本地同款)(scripts/check-pinned-manager.mjs:packageManager 精确 pinned + lockfile 在位 + pnpm --version 实测一致,MiMo 杂散二进制教训)+ 新增 cli-bundle job 与 gates 并行(install → build:cli → smoke:bundle,产物命令集与协议断言随门禁回归)

提交前必须全绿。architecture:check 含 gate 自测(注入越权 import 可被拦截)。CI 在 push/PR 时复跑同一门禁集——本地绿为提交习惯,CI 绿为准入事实。

6. 已知边界与注意事项

  • 冒烟/基准全部使用临时 RAINCODE_HOME 与临时工作区,不污染真实用户数据;测试数据即用即弃。
  • smoke:remote 读取的 config/*.local.json 含真实密钥,已被 .gitignore 隔离(config/*.local.json 模式)——任何测试脚本不得将密钥写入被跟踪文件或输出,这是架构级安全约束(04-architecture §5.3)。
  • NFR-7 崩溃基准默认单轮专项口径;全量验收口径(强杀 ×20)可用 --times 20 扩展。
  • Windows 专用实现(如进程树终止 taskkill /F /T)在非 Windows 平台的行为未经验证。

7. 新增测试的约定

  1. 新功能合入前补对应单测(packages/<pkg>/test/*.test.ts,node:test 风格参照既有文件)与冒烟用例:功能级断言放既有脚本对应用例组,新领域新建 scripts/smoke-<domain>.mts 并在 package.json 注册 smoke:<domain>,同时更新本表。
  2. 头部注释必须说明:链路、用例清单、隔离方式(参照现有脚本格式)。
  3. 性能相关改动合入后复跑 pnpm bench:all,与上一里程碑基准对比不劣化(±10% 内)。
  4. 测试文档(本文档)与测试脚本同 PR 变更。
  5. 护栏必须演示过「变红路径」:新门禁/断言/降级路径合入时先验证其对缺陷敏感(恒真断言不是 guard);端到端断言以落盘/响应事实为准,不以被测方自述为准(详见 defensive-patterns.md N-3)。