0. 本章学习目标
你会理解 OpenCode 的 Bun workspace、Turbo task graph、package-level test/build,以及为什么根目录故意禁止直接跑测试。
1. 一句话讲明白
OpenCode 的工程化不是一个单体 npm test,而是 Bun workspace + Turbo task graph:根目录负责统一包、依赖版本和跨包任务,具体 package 负责自己的 typecheck、test、build。
来源:package.json:7-21、package.json:23-30、turbo.json:5-43。
2. 它在 OpenCode agent 中的位置
工程化不参与 agent loop,但它决定 agent 项目能否长期演进:runtime、Web app、UI package、SDK package 各自有脚本和测试边界。
Java 类比
这很像 Gradle multi-project:根项目管理版本和任务图,子项目分别声明自己的 test、check、assemble。
3. 生活类比
根目录像实验室行政办公室,定义实验组、采购统一版本设备;packages/opencode 是核心机器人组;packages/app 是操作台组;turbo.json 是排班表;AGENTS.md 是实验室守则。
4. Java 开发者类比
| OpenCode | Java 类比 | 源码依据 |
|---|---|---|
| Bun workspace | Gradle multi-project / Maven reactor | package.json:23-30 |
| catalog dependency | version catalog / dependencyManagement | package.json:30-87 |
| Turbo tasks | Gradle task graph | turbo.json:5-43 |
| generated SDK | OpenAPI Generator / Feign client | packages/sdk/js/script/build.ts:14-47 |
5. 最小源码路径
package.json:7-21:根脚本。package.json:23-87:workspace 与 catalog。turbo.json:5-43:跨 package task graph。AGENTS.md:119-127:测试和 typecheck 规则。packages/opencode/package.json:8-23:核心 runtime scripts/bin。packages/app/package.json:11-24:Web app test/build。packages/sdk/js/script/build.ts:14-47:SDK 生成流程。
6. 工程化整体链路
根 package.json
-> workspaces 定义 package 集合
-> catalog 固定共享依赖版本
-> turbo.json 编排 typecheck/build/test
-> package 自己定义脚本
-> package 内测试读取真实实现
-> SDK/build 脚本生成或打包产物
7. 核心源码逐段讲解
7.1 根目录脚本
路径:package.json:8-21
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"dev:desktop": "bun --cwd packages/desktop dev",
"dev:web": "bun --cwd packages/app dev",
"lint": "oxlint",
"typecheck": "bun turbo typecheck",
"test": "echo 'do not run tests from root' && exit 1"
}
根目录可以跑 typecheck,但故意禁止 root test。它把测试责任下放到 package 目录。
7.2 Workspace 与 catalog
路径:package.json:23-31
"workspaces": {
"packages": [
"packages/*",
"packages/console/*",
"packages/sdk/js",
"packages/slack"
],
"catalog": {
catalog 像 Java 的版本目录,子包可以引用统一版本。
7.3 Turbo 编排
路径:turbo.json:5-23
"tasks": {
"typecheck": {},
"build": {
"dependsOn": [],
"outputs": ["dist/**"]
},
"opencode#test": {
"dependsOn": ["^build"],
"outputs": [],
"passThroughEnv": ["*"]
}
}
opencode#test 依赖上游 build,说明核心 runtime 测试可能需要 workspace 依赖先准备好。
7.4 测试规则
路径:AGENTS.md:119-127
## Testing
- Avoid mocks as much as possible
- Test actual implementation, do not duplicate logic into tests
- Tests cannot run from repo root; run from package dirs like `packages/opencode`.
## Type Checking
- Always run `bun typecheck` from package directories, never `tsc` directly.
这解释了为什么 OpenCode 的测试更偏真实实现和状态行为,而不是把生产逻辑复制一份。
7.5 核心 runtime package
路径:packages/opencode/package.json:8-19
"scripts": {
"typecheck": "tsgo --noEmit",
"test": "bun test --timeout 30000",
"test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
"test:httpapi": "bun run script/httpapi-exercise.ts --mode coverage --fail-on-missing --fail-on-skip",
"build": "bun run script/build.ts",
"dev": "bun run --conditions=browser ./src/index.ts"
}
7.6 真实测试例子
路径:packages/app/src/context/global-sync/event-reducer.test.ts:88-104
test("upserts project.updated in sorted position", () => {
const project = [{ id: "a" }, { id: "c" }] as Project[]
let refreshCount = 0
applyGlobalEvent({
event: { type: "project.updated", properties: { id: "b" } },
project,
refresh: () => {
refreshCount += 1
},
setGlobalProject(next) {
if (typeof next === "function") next(project)
},
})
expect(project.map((x) => x.id)).toEqual(["a", "b", "c"])
})
这是典型 reducer/service 测试:构造输入,调用真实实现,断言状态变化。
7.7 SDK 生成
路径:packages/sdk/js/script/build.ts:14-47
await $`bun dev generate > ${dir}/openapi.json`.cwd(opencode)
await createClient({
input: "./openapi.json",
output: {
path: "./src/v2/gen",
tsConfigPath: path.join(dir, "tsconfig.json"),
clean: true,
},
})
await $`bun prettier --write src/gen`
await $`bun tsc`
SDK 是从 OpenAPI 生成,不是手写维护。
8. 关键 TypeScript 语法复习
import type:只导入类型,不产生运行时代码。来源:event-reducer.test.ts:2-4。Partial<State>:把 State 字段变可选,适合测试默认对象。来源:event-reducer.test.ts:60。...input:对象展开覆盖默认值。来源:event-reducer.test.ts:85。- literal union:
arch: "arm64" | "x64"。来源:script/build.ts:83-88。 - Bun shell template:
$`bun dev generate ...`,类似更强的ProcessBuilder。来源:script/build.ts:14。
9. 涉及的设计模式和架构思想
本章核心思想包括 monorepo package boundary、generated client、test real implementation、build script as application code。
10. 它如何和 Tool、Provider、Session、文件系统协作
工程化层不直接参与 runtime,但它保护 runtime:LLM/tool stream 有测试,UI/session sync 有 reducer 测试,SDK build 把 HTTP API contract 转成 client,conditional imports 支持不同运行时文件系统和 PTY 实现。
11. 如果自己实现 mini agent,这一章对应什么代码
mini-agent/
package.json
tsconfig.json
src/
cli.ts
session.ts
llm.ts
tool.ts
test/
session.test.ts
tool-stream.test.ts
第一版只需要 dev、typecheck、test 三个脚本。等拆成 CLI/UI/SDK 后,再引入 workspace。
12. 费曼复述区
请向一个 Java 同事解释:为什么 OpenCode 根目录不能直接跑测试?为什么要让每个 package 自己跑?
卡点提示:不要把根目录当单体项目;不要把 Turbo 当测试框架;不要以为 generated SDK 是手写文件。
13. 练习题
- 找出根目录所有
dev:*脚本,说出它们分别启动哪个 package。 - 解释
opencode#test为什么依赖^build。 - 阅读
packages/sdk/js/script/build.ts,画出 SDK 生成流程。 - 实现一个
tool-stream.test.ts,模拟模型分两次输出 JSON 参数并断言能合并。
14. 源码追踪任务
依次阅读 package.json、turbo.json、bunfig.toml、AGENTS.md、packages/opencode/package.json、packages/app/package.json、packages/sdk/js/script/build.ts。
15. 面试式自测
- 如果给 OpenCode 增加一个新 package,你会检查哪些根目录配置?
- 为什么根目录
test明确失败,反而是好事? catalog:解决了什么依赖管理问题?- 对 agent 项目来说,为什么 tool stream 解析值得单独测试?