21 · 测试矩阵、CI 与发布¶
一句话结论: Paperclip 的测试把“功能正确”拆成 shared contract、service 并发、workspace/provider、UI 行为、E2E 安装和 release smoke 多层;CI 通过分组/shard 和 serialized lane 控制耗时,发布脚本在打包、验证、标签和 smoke 之间形成闭环。
1. 测试规模与分布¶
快照中有约 1,229 个 test/spec 文件,粗略分布:
| 区域 | 代表内容 |
|---|---|
| Server | service、route、auth、recovery、tool、workspace、portability |
| UI | component、page、query/cache、routing、adapter parser |
| Packages | shared validators、DB helpers、adapter engines、plugin SDK |
| CLI | command/API parity、onboard、doctor、context、worktree |
| Scripts | release、migration、build、smoke、packaging contracts |
| Tests | Playwright E2E、release smoke、visual/storybook |
文件数不是质量指标,但体现了它把边界场景作为一等公民。
2. 测试层次¶
flowchart TB
UNIT["Pure unit\nconstants / parser / validator"] --> SERVICE["Service integration\nDB + transactions + locks"]
SERVICE --> ROUTE["Route/API\nauth + HTTP semantics"]
ROUTE --> UI["Component/page\nquery + realtime + optimistic"]
ROUTE --> E2E["Playwright E2E\ninstall + browser workflow"]
BUILD["Package/build tests"] --> RELEASE["Docker/release smoke"]
E2E --> RELEASE
选择层次的原则:
- 纯逻辑用快速 unit;
- 并发/数据库/权限在 service integration;
- 浏览器交互只在 E2E;
- 发布安装/容器启动必须有 smoke。
3. 不变量测试比 happy path 更重要¶
典型测试主题包括:
- Issue checkout race/409/idempotent/stale adoption;
- run status CAS 与 late failure;
- budget hard stop 与 incident idempotency;
- auth cross-company not-found;
- workspace path traversal/symlink;
- lease expiry/cleanup;
- tool approval signed arguments;
- plugin worker crash/restart/capability denial;
- skills trust/import drift;
- pipeline lease/version/automation ledger;
- hot restart adoption/lost classification。
命名如 heartbeat-process-recovery.test.ts、execution-workspaces-service.test.ts、tool-gateway.test.ts 已经在告诉读者真正的风险面。
4. Shared Contract Tests¶
packages/shared 测试确保:
- constants 与 type union 一致;
- Zod validators 接受/拒绝边界;
- config 跨字段规则;
- issue/agent/approval/tool/trust 状态;
- portability/frontmatter/zip/hash;
- telemetry event 与 retention。
它们是跨 Server/UI/CLI 的早期护栏,避免把坏输入推到更慢的 E2E。
5. 数据库与 Service Integration¶
Server tests 使用真实或测试 PostgreSQL/embedded helper 验证:
- transaction;
- unique/check/index;
FOR UPDATE/CAS;- migration/backfill;
- company isolation;
- JSONB 配置;
- filesystem/provider mock 与 DB operation ledger。
Mock DB 不能发现两个请求同时 checkout 的错误,所以并发不变量必须在有真实隔离级别的环境测。
6. UI 组件与缓存测试¶
UI 不只测“组件渲染出文字”,还测:
- query key 与 placeholder data;
- optimistic update/rollback;
- live event patch/invalidation;
- toast suppression;
- company-prefixed routing;
- adapter parser transcript;
- visibility/focus;
- mobile/responsive states。
LiveUpdatesProvider 把大量纯函数导出到 __liveUpdatesTestUtils,允许在不启动真实 WebSocket 时验证事件决策。
7. CLI Parity Tests¶
CLI 测试覆盖:
- command registration/help;
- context/profile precedence;
- API request shape;
- setup/doctor/onboard;
- company/issue/agent/approval/skills/routine/pipeline;
- secret and token behavior;
- worktree/workspace;
- output
--json。
它们防止 CLI 只在 demo 中工作而与新 API schema 漂移。
8. E2E 与 Release Smoke 的区别¶
E2E¶
从源码构建后启动测试实例,使用 Playwright 验证 UI、安装、认证、MCP fixture、Issue 和 workspace 等用户流程。
Release Smoke¶
使用已发布 dist-tag/package + Docker onboarding,再验证真实安装产物和浏览器路径。它回答“发布出去的东西能不能从零启动”,不是“源码分支的功能是否通过”。
9. CI 分组与 Shard¶
Release verify workflow 把测试分成:
- typecheck;
- general server/workspace groups;
- serialized tests 多 shard;
- build;
- 另有 E2E/visual/smoke workflow。
源仓库当前使用 Node 24、pnpm 9.15.4、actions/checkout@v7、actions/setup-node@v7 等现代 Action,并明确固定 lockfile/manifest 检查。
10. 为什么要 Serialized Tests¶
有些 Server tests:
- 共享 embedded DB/端口;
- 修改 instance settings;
- 启动/停止外部进程;
- 依赖 singleton runtime registry;
- 修改全局 fixture。
它们不能简单并行。把 serialized 组单独 shard 既保留隔离,也能利用多个 runner。
11. E2E Shard 的隔离原则¶
Playwright lane 让每个 shard 启动自己的 Server,workers 受控为 1(某些 specs 改 instance flags)。这样并行发生在 runner 层,而不是同一个实例内互相踩状态。
12. Typecheck 不是唯一契约¶
Typecheck 会漏掉:
- migration 未应用;
- SQL index 缺失;
- runtime adapter binary;
- secret/provider 配置;
- Docker 镜像/静态 UI packaging;
- WebSocket proxy;
- 真实浏览器焦点/resize。
因此 release verify 同时包含 build、test、E2E 和 smoke。
13. Release Package Map¶
Release scripts 维护 package map,检查:
- 哪些 workspace 可发布;
- package version/metadata;
- private/example 包不误发布;
- standalone/public package bootstrap;
- generated files;
- lockfile 与 manifest;
- tags/release notes。
这对有 20+ workspace package 的 Monorepo 很关键:pnpm -r publish 过于粗糙。
14. Canary 与 Stable¶
发布流程分两段:
flowchart LR
PUSH["master push"] --> VERIFY["release-verify reusable workflow"]
VERIFY --> CANARY["publish canary + tag"]
DISPATCH["manual stable dispatch"] --> VERIFY2["verify source_ref"]
VERIFY2 --> PREVIEW["optional dry-run"]
PREVIEW --> STABLE["publish stable + tag + GitHub Release"]
Canary 先验证并发布,stable 可由人工指定 ref/date,并可 dry-run 预览版本。
15. 发布前的清洁树与锁文件¶
Release script 要求 clean working tree。CI 会:
- frozen/no-frozen install 按场景;
- 检查 manifest/lockfile;
- 必要时把 regenerated lockfile 放入临时 commit;
- build/package;
- 用 release script dry-run。
这避免“CI 生成了未提交文件,但 release 仍假设 clean”的隐性差异。
16. Docker Smoke¶
Release smoke:
- 构建/拉取发布 Docker 镜像;
docker-onboard-smoke.sh创建 throwaway data dir;- 启动容器并读取 metadata;
- 用 Playwright 登录/操作;
- 上传报告、结果和 Docker logs;
- 清理容器。
它覆盖 onboarding、公开产物、默认配置和运行时镜像,不只是源码测试。
17. 失败诊断顺序¶
遇到 CI 红灯可以按层排查:
typecheck → shared/schema
service test → DB/concurrency/business invariant
UI test → cache/routing/component
build → package/generated asset
E2E → process/config/browser
release smoke → published artifact/Docker/onboard
不要把 E2E 失败优先解释为 UI bug;它可能是 migration、auth base URL 或 embedded DB 启动问题。
18. 仍需人工判断的地方¶
自动测试不能完全验证:
- 新增 capability 是否符合 threat model;
- recovery 是否会对错误任务产生副作用;
- cost provider 归因是否正确;
- UX 是否让 Board 理解 blocked/review;
- 文档/规格是否反映代码现实。
因此每个大功能仍需要代码 review、文档更新和必要的 smoke/evidence。
19. 可复用经验¶
- 测试矩阵按不变量和生命周期分层,不只按目录。
- 并发/迁移/外部副作用要用 integration,不能全 mock。
- E2E 验证用户流程,release smoke 验证可安装发布产物。
- serialized lane 是对共享状态现实的承认,不是测试失败的借口。
- 发布前同时锁定 package map、lockfile、generated assets 和 clean tree。
- Canary→stable→Docker smoke 形成真实交付闭环。
源码入口¶
package.json、vitest.config.tsserver/src/__tests__/ui/src/**/*.test.*cli/src/__tests__/scripts/__tests__/.github/workflows/pr.yml.github/workflows/release-verify.yml.github/workflows/e2e.yml.github/workflows/release-smoke.ymlscripts/release.sh、release-package-map.mjs
最后用一条任务时序把所有章节重新串起来:22 · 一次任务的端到端源码追踪。