本文规定测试的分层、原则、写法与运行方式。做法参照 DSH 的 dsh:docs/testing.md。检查命令与完成的标准见根 AGENTS.md。
| 层 | 位置 | 内容 | 命令 |
|---|---|---|---|
| 单元 | 各包的 test/*.test.ts |
纯函数:界面逻辑、协议解析、Seatbelt 规则生成 | pnpm test |
| 集成 | packages/*/test/、apps/desktop/test/ |
Server 的 HTTP 与事务;Computer 与 Server 的往返;真实 Seatbelt 下的 OpenCode 适配器(用假 opencode) | pnpm test |
| 快照 | test/__snapshots__/*.md |
模型可见的文本,见第 4 节 | pnpm test |
| 冒烟 | apps/desktop/test/smoke.e2e.ts |
构建应用,用主进程同一份 startRuntime 启动构建好的 Server、Computer 与 shim;假 opencode 在 Seatbelt 里经 crew reply 回复,断言回复落库 |
pnpm test:smoke |
| 真实模型 | apps/desktop/test/real-opencode.e2e.ts |
与冒烟相同的构建产物与启动路径,用本机已登录的 OpenCode 调用真实模型 | CREW_E2E_MODEL=<模型> pnpm test:e2e |
| 截图 | apps/desktop/scripts/preview-shot.ts |
用临时数据库启动开发模式的应用,经 DevTools 协议截图后关闭;可以选主题,截图前可以在页面里执行脚本 | pnpm preview:shot |
| 手动 | pnpm dev |
界面:新建 Agent、发消息、看到回复与状态变化 | — |
pnpm check依次运行 lint、类型检查、pnpm test与pnpm test:smoke,全量约 17 秒。- 集成与冒烟测试需要
TEST_DATABASE_URL,由@crew/server/testing的createTestDatabase读取,环境变量没有设置时读根目录的.env。缺少时测试直接失败,不跳过。 - 依赖 Seatbelt 的测试只在 macOS 上运行。
- 改了界面后,用
pnpm preview:shot --theme light与--theme dark截图自查:- 它用临时数据库启动一个新的 Crew,不能和正在运行的
pnpm dev同时使用:单实例锁会让它直接退出。 - 截图默认保存在
apps/desktop下的out/preview/。 --eval在截图前在页面里执行一段脚本,例如点击“新建 agent”后截到对话框。
- 它用临时数据库启动一个新的 Crew,不能和正在运行的
- 真实模型测试不进
pnpm check。没有指定CREW_E2E_MODEL、没有opencode或没有登录时整组跳过。 - 改动 Engine 调用或模型可见的行为时,先问用户是否运行真实模型测试、用哪个模型:它调用付费模型,模型由运行的人选。运行后在汇报中写明模型与结果。
- 只替换昂贵或不确定的边界。 模型与时钟可以替换,数据库、Seatbelt 与 HTTP 用真实实现。例如
packages/computer/test/runner.test.ts用真实的 Server 应用与临时数据库,只把 Engine 换成按脚本回复的假 Engine。 - 验证真实世界,不相信自我报告。 断言重新读取数据库或文件,不只看被测对象返回了什么。例如 shim 测试从 Server 读回消息,而不是只看命令的输出。失败分支断言没有副作用:没有写库,没有改文件。
- 走真实入口。 冒烟测试运行构建产物,由 Electron 以 Node 方式执行,与用户运行的是同一套代码与启动路径。只跑源码测不出打包、模块解析与启动顺序的问题。
- 测试自己拥有资源。 测试创建的资源由测试释放,失败时也一样,做法见第 5 节。只在单独运行时才通过的测试,是测试本身的缺陷。
- 守卫要能失败。 新加一个防回归的测试时,先引入那个回归,看到测试失败,再恢复。例如去掉 Runner 的
PATH中bin/crew所在的目录,冒烟测试就会失败。
- 测试名用英文写行为与结果,读起来是一句话,例如
keeps the messages and reports the reason when the turn fails。禁止test 1、works、basic。 - 断言具体结果。用
toEqual断言完整的值,不只断言真值或长度。 - 禁止为了通过而放宽断言,例如把精确比较改成包含、删掉一条断言。
- 等具体条件,不
sleep固定时长。用轮询等到条件成立,超时只作为等待的上限,例如apps/desktop/test/support/built-app.ts的until。 - 前端不写只断言静态 HTML 的组件测试。逻辑抽成纯函数,写
.test.ts,例如apps/desktop/test/lib.test.ts。
- 模型可见的文本用 vitest 的
toMatchFileSnapshot逐字锁定,放在包的test/__snapshots__/中,用.md扩展名方便阅读。 - 现有快照:
AGENTS.md(Agent 的常驻规则)、turn-prompt.md(每轮输入)、shim-output.md(crew的全部输出)。 - 改动这些文本时,快照的 diff 与代码一起审阅。更新快照用
pnpm --filter <包> exec vitest run -u,然后逐行看 diff。
vitest 同时运行多个测试文件,pnpm -r test 让各包并行;它们共用同一个 PostgreSQL 与本机端口。每个测试占用的资源都要有私有的分配方式和明确的释放点。
- 数据库: 每个测试文件用
createTestDatabase建自己的临时数据库,结束时删除。 - Server: 用
createTestApp:真实的路由与数据库,监听在127.0.0.1的随机端口上,请求走真实的 HTTP 连接。t.request(路径)直接请求它;t.fetch可以交给客户端,不论 URL 写的是哪个主机都发到这个 Server。自己起服务时监听127.0.0.1:0,在“已监听”之后读取分配到的端口。 - 目录: 用
mkdtemp建私有的临时目录。不写真实的~/.crew,路径经参数注入。冒烟测试把构建产物、crew 目录都放在自己的临时目录里,同时运行的两次检查互不影响。 - 全局状态: 优先注入依赖,不改全局,例如
ServerClient的fetchFn、crew的CliIo。必须修改process.env、计时器等时,记下原值,在finally中恢复。 - 清理: 资源一创建就注册清理,并等到结束信号:
await runtime.stop()等子进程退出,awaitSSE 循环返回。只调用abort()或kill()而不等待,清理就没有完成。子进程、流与取消的写法另见 defensive-patterns.md。 - 偶发失败: 找到原因再修。加长超时、加重试、改成串行、在断言前
sleep都不是修复;重跑后通过也不算修好。
- 新行为:至少覆盖正常路径、边界值与非法输入。
- 修 bug:先写能复现的失败测试,再修。
- 模型可见的文本:在同一个改动里更新快照。
- 新的构建入口或启动路径(
electron.vite.config.ts、apps/desktop/electron/、各包的main.ts):冒烟测试要能覆盖它。 - 沙箱与路径规则:覆盖不存在的路径、符号链接、带空格与非 ASCII 的路径。
pnpm check # 提交前:lint、类型检查、测试与冒烟测试
pnpm test # 单元、集成与快照
pnpm test:smoke # 冒烟测试
CREW_E2E_MODEL=deepseek/deepseek-flash pnpm test:e2e # 真实模型,不进 pnpm check
pnpm --filter @crew/server test # 一个包的测试
pnpm --filter @crew/computer exec vitest run test/shim.test.ts # 一个测试文件开发时可以先运行改动相关的测试,改完代码运行 pnpm check。提交步骤见 crew-commit。