测试与工程化
a3647eb025c7 · 官方 Release
Agent 生成档案
Section titled “Agent 生成档案”- 章节 ID:
13-testing-engineering - 章节摘要:沿一次 package 级改动的验证路径,理解 workspace、Turbo 任务图、测试根目录保护、类型检查和 SDK 生成如何形成工程信任链。
- 教程版本:
v1.18.16 - 源码基线:
a3647eb025c7615159d417dcc49fc39fdaeba65b - 章节元数据:/versions/v1-18-16/data/chapters.json
- 源码映射:/versions/v1-18-16/data/source-map.json
主要源码路径
Section titled “主要源码路径”package.jsonturbo.jsontsconfig.jsonAGENTS.mdpackages/opencode/package.jsonpackages/sdk/js/package.json
源码基线:
v1.18.16(提交a3647eb025c7)。本章描述的是仓库声明的任务边界;未实际运行的 OpenCode 测试不会写成“已经通过”。
0. 本章学习目标
Section titled “0. 本章学习目标”读完本章,你应该能:
- 画出 workspace、catalog、package scripts 与 Turbo task graph 的关系。
- 解释为什么 OpenCode 故意禁止从仓库根目录运行测试。
- 为一次核心 runtime 改动选择正确的 typecheck、test 与 build 边界。
- 区分“任务存在”“任务被编排”“本次已经执行并通过”。
- 为 mini agent 设计一条按风险递增的验证阶梯。
1. 一句话讲明白
Section titled “1. 一句话讲明白”OpenCode 的工程化不是一个万能的根目录命令,而是“根目录管理包与任务图,子 package 对自己的类型、测试和构建负责”,让验证范围与真实模块边界对齐。
中心问题是:在一个包含 runtime、SDK 和多个 UI 的 monorepo 里,怎样既避免漏测,又避免每改一行都盲跑全世界?
v1.18.16 相比旧基线改变了什么
Section titled “v1.18.16 相比旧基线改变了什么”- monorepo 已扩展出新的 core、schema、LLM 与 UI v2 边界;共享契约下沉后,验证不能只盯
packages/opencode。 turbo.json的任务图仍声明 build/test/typecheck 的依赖与缓存产物,但具体可运行命令继续由各 package scripts 定义。- 本章只说明仓库声明的验证边界;本次教程更新没有把“阅读到 test script”写成“OpenCode 全仓测试已通过”。
v1.18.16 workspace 与根脚本
package.json:1-120
根配置描述包集合和跨包入口。
1{2 "$schema": "https://json.schemastore.org/package.json",3 "name": "opencode",4 "description": "AI-powered development tool",5 "private": true,6 "type": "module",控制模块格式。7 "packageManager": "bun@1.3.14",8 "scripts": {项目脚本入口。9 "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",常用工程命令。10 "dev:desktop": "bun --cwd packages/desktop dev",11 "dev:web": "bun --cwd packages/app dev",12 "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",13 "dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",14 "dev:storybook": "bun --cwd packages/storybook storybook",15 "lint": "oxlint",常用工程命令。16 "typecheck": "bun turbo typecheck",常用工程命令。17 "upgrade-opentui": "bun run script/upgrade-opentui.ts",18 "postinstall": "bun run --cwd packages/core fix-node-pty",19 "prepare": "husky",20 "random": "echo 'Random script'",21 "sso": "aws sso login --sso-session=opencode --no-browser",22 "translate:app": "bun run script/translate-app.ts",23 "test": "echo 'do not run tests from root' && exit 1"常用工程命令。24 },25 "workspaces": {声明工作区范围。26 "packages": [27 "packages/*",28 "packages/console/*",29 "packages/stats/*",30 "packages/sdk/js",31 "packages/slack"32 ],33 "catalog": {34 "@effect/opentelemetry": "4.0.0-beta.83",35 "@effect/platform-node": "4.0.0-beta.83",36 "@effect/sql-sqlite-bun": "4.0.0-beta.83",37 "@npmcli/arborist": "9.4.0",38 "@types/bun": "1.3.13",39 "@types/cross-spawn": "6.0.6",40 "@octokit/rest": "22.0.0",41 "@hono/standard-validator": "0.2.0",42 "@hono/zod-validator": "0.4.2",43 "@opentui/core": "0.4.5",44 "@opentui/keymap": "0.4.5",45 "@opentui/solid": "0.4.5",46 "@tanstack/solid-virtual": "3.13.32",47 "@shikijs/stream": "4.2.0",48 "ulid": "3.0.1",49 "@kobalte/core": "0.13.11",50 "@corvu/drawer": "0.2.4",51 "@types/luxon": "3.7.1",52 "@types/node": "24.12.2",53 "@types/semver": "7.7.1",54 "@tsconfig/node22": "22.0.2",55 "@tsconfig/bun": "1.0.9",56 "@cloudflare/workers-types": "4.20251008.0",57 "@openauthjs/openauth": "0.0.0-20250322224806",58 "@pierre/diffs": "1.2.10",59 "opentui-spinner": "0.0.7",60 "@solid-primitives/storage": "4.3.3",61 "@tailwindcss/vite": "4.1.11",62 "diff": "8.0.2",63 "dompurify": "3.3.1",64 "drizzle-kit": "1.0.0-rc.2",65 "drizzle-orm": "1.0.0-rc.2",66 "effect": "4.0.0-beta.83",67 "ai": "6.0.168",68 "cross-spawn": "7.0.6",69 "hono": "4.10.7",70 "hono-openapi": "1.1.2",71 "fuzzysort": "3.1.0",72 "luxon": "3.6.1",73 "marked": "18.0.7",74 "marked-shiki": "1.2.1",75 "remend": "1.3.0",76 "@playwright/test": "1.59.1",77 "semver": "7.7.4",78 "typescript": "5.8.2",79 "@typescript/native-preview": "7.0.0-dev.20251207.1",80 "zod": "4.1.8",81 "remeda": "2.26.0",82 "sst": "4.13.1",83 "shiki": "4.2.0",84 "solid-list": "0.3.0",85 "tailwindcss": "4.1.11",86 "vite": "7.1.4",87 "@solidjs/meta": "0.29.4",88 "@solidjs/router": "0.15.4",89 "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",90 "@sentry/solid": "10.36.0",91 "@sentry/vite-plugin": "4.6.0",92 "solid-js": "1.9.10",93 "solid-sonner": "0.3.1",94 "vite-plugin-solid": "2.11.10",95 "@lydell/node-pty": "1.2.0-beta.12"96 }97 },98 "devDependencies": {开发期依赖。99 "@actions/artifact": "5.0.1",100 "@tsconfig/bun": "catalog:",101 "@types/mime-types": "3.0.1",102 "@typescript/native-preview": "catalog:",103 "glob": "13.0.5",104 "husky": "9.1.7",105 "oxlint": "1.60.0",106 "oxlint-tsgolint": "0.21.0",107 "prettier": "3.6.2",108 "semver": "^7.6.0",109 "sst": "catalog:",110 "turbo": "2.10.2"111 },112 "dependencies": {运行时依赖。113 "@aws-sdk/client-s3": "3.933.0",114 "@opencode-ai/plugin": "workspace:*",115 "@opencode-ai/script": "workspace:*",116 "@opencode-ai/sdk": "workspace:*",117 "heap-snapshot-toolkit": "1.1.3",118 "typescript": "catalog:"119 },120 "repository": {
Turbo 任务图
turbo.json:1-34
任务依赖与缓存产物不等于某次任务已经执行。
1{2 "$schema": "https://v2-8-13.turborepo.dev/schema.json",3 "globalEnv": ["CI", "OPENCODE_DISABLE_SHARE"],4 "globalPassThroughEnv": ["CI", "OPENCODE_DISABLE_SHARE"],5 "tasks": {6 "typecheck": {},常用工程命令。7 "build": {常用工程命令。8 "dependsOn": [],9 "outputs": ["dist/**"]10 },11 "opencode#test": {12 "dependsOn": ["^build"],13 "outputs": [],14 "passThroughEnv": ["*"]15 },16 "@opencode-ai/core#test": {17 "dependsOn": ["^build"],18 "outputs": []19 },20 "@opencode-ai/app#test": {21 "dependsOn": ["^build"],22 "outputs": []23 },24 "@opencode-ai/ui#test": {25 "dependsOn": ["^build"],26 "outputs": []27 },28 "@opencode-ai/session-ui#test": {29 "dependsOn": ["^build"],30 "outputs": []31 }32 }33}34
2. 先画地图:谁定义什么
Section titled “2. 先画地图:谁定义什么”root package.json +-- workspaces:有哪些 package +-- catalog:共享依赖版本 +-- root scripts:跨包入口与保护栏 | v turbo.json 任务依赖与产物 | +------+----------------+ v vpackages/opencode packages/sdk/jstypecheck/test/build typecheck/build根目录决定“有哪些队伍、公共版本是什么、跨队依赖怎样排”;package 决定“本队如何验证”。
Java 开发者可以类比 Gradle multi-project + version catalog + task graph。但 Bun workspace 脚本不是 Maven 生命周期,不能凭 test 这个名字猜执行范围。
3. 最小机制:按影响面选择任务
Section titled “3. 最小机制:按影响面选择任务”一个可迁移的最小工程化策略是:
change(files): owners = packagesOwning(files) run fast static checks for owners run focused tests near changed behavior run dependent builds/tests from task graph only then run broader regression required for deliveryOpenCode 把部分依赖写进 turbo.json:例如 opencode#test 依赖上游 package build,见 turbo.json。
这不是“永远只跑最小测试”。它是先用依赖图找到必要范围,再根据发布风险扩展回归。
4. 三种容易混淆的事实
Section titled “4. 三种容易混淆的事实”| 看到什么 | 能证明什么 | 不能证明什么 |
|---|---|---|
package 有 test script | 项目定义了测试入口 | 当前测试已经通过 |
Turbo 有 dependsOn | 经 Turbo 调度时存在依赖 | 手工进入 package 执行会自动跑上游 |
dist/** 声明为 outputs | Turbo 可把它当构建产物 | 产物内容正确、可发布 |
工程文档最常见的失真,是把“配置上存在”写成“本次验证成功”。本章只做源码声明解读。
5. 最小源码路径
Section titled “5. 最小源码路径”package.json:runtime 版本、根脚本和禁止 root test。package.json:workspace 与依赖 catalog。turbo.json:task graph、依赖和 outputs。tsconfig.json:1-5:根 TypeScript 基线有意保持很薄。AGENTS.md:测试与 typecheck 的人工契约。packages/opencode/package.json:核心 runtime 的验证入口。packages/opencode/package.json:bin 与 Bun/Node 条件导入。packages/sdk/js/package.json:SDK 的 typecheck、生成构建和 exports。
6. 一条具体工程旅程:修改 session API 后该怎么验证
Section titled “6. 一条具体工程旅程:修改 session API 后该怎么验证”假设你修改了核心 runtime 的 session HTTP API,并因此需要重新生成 JS SDK。下面是从仓库契约推导出的验证路径,不表示这些命令在本章编写时已经执行。
第一步:先定位 package ownership
Section titled “第一步:先定位 package ownership”服务端改动属于 packages/opencode;generated client 属于 packages/sdk/js。根 workspaces.packages 明确包含 packages/* 与 packages/sdk/js,见 package.json。
所以这是跨两个 package 的改动,不能只对根 tsconfig.json 跑一次编译就结束。
第二步:用 package 声明的方式做 typecheck
Section titled “第二步:用 package 声明的方式做 typecheck”核心包声明:
typecheck = tsgo --noEmit见 packages/opencode/package.json。SDK 也有自己的 tsgo --noEmit,见 packages/sdk/js/package.json。
仓库规范进一步要求从 package 目录运行 bun typecheck,不要直接调用 tsc,见 AGENTS.md。原因不是语法偏好,而是 package script 才包含仓库选择的编译器和参数。
第三步:重新生成 SDK 是显式步骤
Section titled “第三步:重新生成 SDK 是显式步骤”SDK package 的 build 指向 bun ./script/build.ts,见 packages/sdk/js/package.json;根 AGENTS.md 也明确指出 SDK 生成入口。
因此,API contract 改了但没有更新 generated client,是一个可预见的交付缺口。生成物是否变化、类型是否对应,都应该在 diff 与 SDK typecheck 中验证。
第四步:核心测试必须在 package 内运行
Section titled “第四步:核心测试必须在 package 内运行”根 package.json 的 test 会打印禁止信息并退出 1,见 package.json。AGENTS.md 同时要求尽量少 mock、测试真实实现,并从具体 package 运行测试,见 AGENTS.md。
核心包提供:
test:Bun test,30 秒 timeout;test:ci:额外生成 JUnit;test:httpapi:coverage、auth、effect 三类 HTTP API exercise;bench:test与profile:test:性能调查入口。
见 packages/opencode/package.json。
对于 session HTTP API 改动,focused test 与 test:httpapi 比“误跑根 test”更接近风险面。最终交付需要多大回归,仍由改动范围与项目要求决定。
第五步:理解 Turbo 会补哪些依赖
Section titled “第五步:理解 Turbo 会补哪些依赖”经 Turbo 执行 opencode#test 时,会先做 ^build,即上游 workspace 依赖构建,见 turbo.json。CI 版本也声明 JUnit output,并同样依赖上游 build,见 turbo.json。
这解决了“核心测试引用了尚未构建的 workspace 包”。但如果开发者直接在 packages/opencode 手工运行 package test,就不能假设 Turbo 帮他做了这一步。
第六步:交付证据要分层报告
Section titled “第六步:交付证据要分层报告”可信报告应分别说:
声明检查:脚本/任务存在实际执行:命令、目录、退出码覆盖边界:哪些 package / 哪类路径没跑产物检查:generated diff / dist 是否符合预期“代码能 typecheck”不等于“HTTP 行为正确”;“单测通过”也不等于“SDK 已更新”。
7. workspace 与 catalog 解决什么
Section titled “7. workspace 与 catalog 解决什么”根 workspace 列出主要 package 集合;catalog 固定 effect、ai、TypeScript、UI 框架等共享版本,见 package.json。
这类似 dependency management:子 package 可以使用 catalog:,减少同库多版本漂移。代价是根 catalog 变更影响面很大,应视为跨包升级,而不是单包小改。
根 tsconfig.json 只继承 Bun 基线,没有集中塞入大量 compiler option,见 tsconfig.json:1-5。这反过来强调 package 自己的 typecheck 配置与 script 才是执行边界。
8. task graph 的关键选择
Section titled “8. task graph 的关键选择”turbo.json 展示了三类配置:
typecheck没声明跨包依赖;build产出dist/**;- 特定 test task 依赖
^build,CI task还记录 JUnit output。
见 turbo.json。
为什么不让所有任务一律 dependsOn: ["^build"]?源码没有写解释。合理的设计解释是:不同任务对上游构建产物的依赖不同,过度串行会拖慢反馈。这个“为什么”是推断,不是代码明示。
9. 运行时包的跨环境边界
Section titled “9. 运行时包的跨环境边界”packages/opencode/package.json 不只声明脚本,还通过 imports 为 #db、#pty 选择 Bun、Node 与 default 实现,见 packages/opencode/package.json。
这提醒测试设计:类型通过不代表所有运行环境都通过。涉及数据库或 PTY 的改动,应按条件导入覆盖实际目标 runtime。
10. OpenCode 的选择
Section titled “10. OpenCode 的选择”| 选择 | 好处 | 代价 |
|---|---|---|
| 禁止 root test | 强迫开发者选择真实 package 边界 | 初学者会觉得入口不直观 |
| catalog 统一版本 | 降低 workspace 依赖漂移 | 升级影响面集中 |
| package scripts 封装工具 | 命令与参数可演进 | 不能凭通用生态习惯直接跑 tsc |
| Turbo 只声明必要依赖 | 反馈更快、缓存更准确 | task graph 需要持续维护 |
| SDK 生成作为 build | 契约变化可机械传播 | 生成物与源码必须一起审查 |
11. 可以带走的方法
Section titled “11. 可以带走的方法”方法一:让测试入口匹配所有权边界
Section titled “方法一:让测试入口匹配所有权边界”根目录负责发现与编排,package 负责具体命令。不要设计一个含义模糊的“全测”按钮。
验证问题:失败时,开发者能否立刻知道是哪个 package、哪类检查?
方法二:把生成代码当交付链的一等公民
Section titled “方法二:把生成代码当交付链的一等公民”contract 变化要能触发生成、diff、typecheck,而不是靠发布前想起来。
验证问题:CI 能否发现 server schema 已变、SDK 生成物没更新?
方法三:按风险建立验证阶梯
Section titled “方法三:按风险建立验证阶梯”静态检查快、focused test 定位准、跨包 build 捕获集成问题、E2E 验证用户路径。交付时报告实际跑到哪一级。
验证问题:一个 permission 行为变更为什么不能只靠 typecheck?
12. 费曼复述与练习
Section titled “12. 费曼复述与练习”请回答:
- 为什么根目录可以 typecheck,却故意不能 test?
- package script 与 Turbo task 各负责什么?
- “配置了 test:httpapi”和“本次 test:httpapi 通过”有什么区别?
练习阶梯:
- 入门:为 root、opencode、SDK 三层各写一句职责。
- 进阶:给“新增一个 session endpoint”列出按速度排序的验证阶梯。
- 源码追踪:从
turbo.json解释^build对直接运行 package test 有何边界。 - 迁移:为 mini agent 设计 root guard,防止开发者在错误目录误跑测试。
最后复盘:工程化是可执行的信任链
Section titled “最后复盘:工程化是可执行的信任链”文件改动 -> package ownership -> package typecheck / focused test -> task graph 补齐上游 -> generated/build 产物 -> 清楚报告已验证与未验证边界到这里,我们已经读完 runtime 的关键模块与支撑它的工程边界。最后一章不再继续堆 OpenCode 细节,而是做一次删减实验:哪些机制必须保留,才能亲手做出一个小而真的 coding agent?