19 · 测试、日志与故障定位
约 771 个测试文件如何按纯核心、协议、adapter、reactor 和平台边界分层。
测试数量不是重点,边界才是
在 apps、packages、infra、native 范围内,本快照约有 771 个 .test/.spec 文件。数字会变化,真正值得学习的是命名与分层:
纯 decider/projector、协议 framing、Provider adapter、reactor、数据库迁移、客户端 supervisor、Electron lifecycle 都有各自测试。
测试金字塔按架构切片
层次不是“越上越好”。每类 failure 应在最窄边界被发现:一个 event mapping bug 不需要启动 Electron,安装器漏文件则纯函数测试无法发现。
纯核心测试
Decider
输入 read model 与 command,断言 events/rejection。应覆盖每个 invariant 的正反面、幂等语义所依赖的 command shape、 archive/pin/snooze/settled/title regeneration 等状态组合。
Projector / client reducers
给定 event sequence,断言新 state;还要验证:
- replay 全序与逐条增量结果相同;
- 重复/旧 sequence 的策略;
- 流式 segment append;
- session stop 结算 turn;
- 2,000 messages / 500 checkpoints 等边界;
- structural sharing 不误改旧 state。
这些测试快、可穷举,是事件系统最重要的回归网。
编排引擎测试
Engine 测试使用临时数据库/Effect test layers 验证跨组件不变量:事务原子性、单队列顺序、receipt 幂等、commit 后 publish、 失败对账。不要只断言最终 thread;还要查询 event sequence、receipt 和 persisted projection,确认它们一致。
并发测试应使用 barrier/Deferred 构造确定交错,而不是起两个 Promise 后祈祷竞态出现。
Reactor 测试与 drain
Provider/Checkpoint ingestion 是后台 worker。makeDrainableWorker 暴露 drain(),测试能在 enqueue 后等待所有 outstanding handler 完成。
反模式:
emit(event)
await sleep(100)
expect(state).toEqual(...)正确模式:
emit(event)
await worker.drain()
expect(state).toEqual(...)drain 把“工作完成”变成契约,CI 负载不再决定测试是否 flaky。
Provider 测试的四层
- Wire/Schema:截断 frame、多消息、未知字段、request ID、EOF;
- Runtime mapping:原生 notification/update → canonical event;
- Adapter contract:start/send/interrupt/approval/stop/list/has;
- Ingestion:canonical sequence → domain messages/activities/lifecycle。
真实 CLI 登录的端到端测试昂贵且不稳定,所以仓库使用 mock peer、wire fixture 与 SDK mock 覆盖大多数协议。少量 integration 验证 process/runtime 组合。
数据库迁移测试
迁移不只要在空数据库通过,还要从旧 shape 构造数据:非法 pending approval、旧 model selection、archive/index/backfill 等。 Canonicalization 测试应包括未知值与重复执行。索引迁移可检查 query/Schema,而不是仅“没有 throw”。
由于 SQLite 是权威存储,一次 migration bug 可能比 UI bug 更难恢复;迁移测试应和当前 Schema 测试同级对待。
Git 与文件系统测试
VCS 测试用临时目录创建真实 Git 仓库,覆盖 staged/unstaged/untracked、无 HEAD、worktree、diff、restore。关键断言之一是捕获 checkpoint 前后 用户 index 和 HEAD 不变。文件/PTY 测试必须只操作明确临时目录,不能依赖开发者真实仓库或 userdata。
Client connection 测试
supervisor.test.ts 使用 TestClock/可控 connectivity 驱动 3/4/8/16 秒退避、offline、stable reset、wakeup 和 scope close。
用虚拟时间能把分钟级状态机在毫秒内确定验证,也避免真实网络。
Registry 测试关注同环境 lease 串行化、最后释放、删除清 cache;snapshot/reducer 测试关注 cursor gap 与重建。
Electron 与 Mobile 边界测试
Electron 模块将 app/window/updater/protocol/safeStorage 包在 service 后,测试使用 fake adapter 验证调用顺序。test:desktop-smoke
再对打包组合做轻量验证。Mobile 原生 module 边界有 JS contract/static check,终端 buffer replay 等逻辑保持纯 TS 测试。
可观测性分层
结构化 Effect logs/spans
Server 在 command ID、environment、thread、session、provider instance 等维度关联日志。日志要记录状态转移和耗时,不复制完整 prompt/code。
Provider NDJSON
可选原生 event logger 用于协议诊断。它是高敏本地调试资料,应有开关、路径与清理策略,禁止默认上传。
Relay tracing
cloud/relayTracing.ts / traceRelayRequest.ts 关联控制面请求;应记录 route、status、latency 和 trace ID,移除 Authorization、ticket、DPoP proof。
Desktop observability / diagnostics
主进程、backend process、renderer connection 是不同故障域。Diagnostics 页面应展示版本、endpoint/状态、最近安全化错误和日志位置, 让“UI 空白”“backend 未 ready”“远端 auth 失败”可区分。
Resource telemetry
CPU/内存采样用于产品监控,不替代日志/trace。异常资源可以定位 provider session,再用关联 ID 查 runtime event。
故障定位手册
| 症状 | 第一证据 | 下一层 |
|---|---|---|
| 消息重复 | command ID/receipt | event sequence、客户端是否重生成 ID |
| UI 永远 running | projected session/turn | provider stream end、ingestion 结算 |
| 断线后少消息 | snapshot sequence/cursor | gap 上限、subscription 建立 |
| Provider 启动失败 | instance snapshot/launch args | executable/home、stderr、protocol handshake |
| 审批点了没反应 | pending request ID | response intent event、adapter callback |
| 回退破坏暂存区 | checkpoint test/log | GIT_INDEX_FILE、restore scope |
| 手机恢复后请求风暴 | supervisor state | 是否有多个 retry owner/重复 wakeup |
| Desktop 空白 | backend readiness + renderer log | IPC/preload/router history |
可迁移经验
- 测试围绕状态所有权和协议边界组织,而不是围绕文件大小。
- 后台 worker 必须可 drain,时间状态机必须可注入虚拟时钟。
- Event system 的测试要同时断言 log、projection、receipt 和 publish 顺序。
- 协议原始日志非常有用也非常敏感;默认本地、可控、去凭据。
- Diagnostics 首先区分故障域,再提供细节,不要只展示一段综合 stack trace。