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-21package.json:23-30turbo.json:5-43

2. 它在 OpenCode agent 中的位置

工程化不参与 agent loop,但它决定 agent 项目能否长期演进:runtime、Web app、UI package、SDK package 各自有脚本和测试边界。

Java 类比

这很像 Gradle multi-project:根项目管理版本和任务图,子项目分别声明自己的 testcheckassemble

3. 生活类比

根目录像实验室行政办公室,定义实验组、采购统一版本设备;packages/opencode 是核心机器人组;packages/app 是操作台组;turbo.json 是排班表;AGENTS.md 是实验室守则。

4. Java 开发者类比

OpenCodeJava 类比源码依据
Bun workspaceGradle multi-project / Maven reactorpackage.json:23-30
catalog dependencyversion catalog / dependencyManagementpackage.json:30-87
Turbo tasksGradle task graphturbo.json:5-43
generated SDKOpenAPI Generator / Feign clientpackages/sdk/js/script/build.ts:14-47

5. 最小源码路径

  1. package.json:7-21:根脚本。
  2. package.json:23-87:workspace 与 catalog。
  3. turbo.json:5-43:跨 package task graph。
  4. AGENTS.md:119-127:测试和 typecheck 规则。
  5. packages/opencode/package.json:8-23:核心 runtime scripts/bin。
  6. packages/app/package.json:11-24:Web app test/build。
  7. 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

第一版只需要 devtypechecktest 三个脚本。等拆成 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.jsonturbo.jsonbunfig.tomlAGENTS.mdpackages/opencode/package.jsonpackages/app/package.jsonpackages/sdk/js/script/build.ts

15. 面试式自测

  1. 如果给 OpenCode 增加一个新 package,你会检查哪些根目录配置?
  2. 为什么根目录 test 明确失败,反而是好事?
  3. catalog: 解决了什么依赖管理问题?
  4. 对 agent 项目来说,为什么 tool stream 解析值得单独测试?

16. 下一步阅读建议

下一章建议读“从 OpenCode 反推 mini coding agent”,把这些工程化规则压缩成你自己的最小项目结构。