跳转到内容

测试与工程化

v1.18.16 源码提交 a3647eb025c7 · 官方 Release
状态已完成
难度入门
预计阅读30 分钟
  • 章节 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
  • package.json
  • turbo.json
  • tsconfig.json
  • AGENTS.md
  • packages/opencode/package.json
  • packages/sdk/js/package.json

源码基线:v1.18.16(提交 a3647eb025c7)。本章描述的是仓库声明的任务边界;未实际运行的 OpenCode 测试不会写成“已经通过”。

读完本章,你应该能:

  • 画出 workspace、catalog、package scripts 与 Turbo task graph 的关系。
  • 解释为什么 OpenCode 故意禁止从仓库根目录运行测试。
  • 为一次核心 runtime 改动选择正确的 typecheck、test 与 build 边界。
  • 区分“任务存在”“任务被编排”“本次已经执行并通过”。
  • 为 mini agent 设计一条按风险递增的验证阶梯。

OpenCode 的工程化不是一个万能的根目录命令,而是“根目录管理包与任务图,子 package 对自己的类型、测试和构建负责”,让验证范围与真实模块边界对齐。

中心问题是:在一个包含 runtime、SDK 和多个 UI 的 monorepo 里,怎样既避免漏测,又避免每改一行都盲跑全世界?

  • 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
root package.json
+-- workspaces:有哪些 package
+-- catalog:共享依赖版本
+-- root scripts:跨包入口与保护栏
|
v
turbo.json
任务依赖与产物
|
+------+----------------+
v v
packages/opencode packages/sdk/js
typecheck/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 delivery

OpenCode 把部分依赖写进 turbo.json:例如 opencode#test 依赖上游 package build,见 turbo.json

这不是“永远只跑最小测试”。它是先用依赖图找到必要范围,再根据发布风险扩展回归。

看到什么能证明什么不能证明什么
package 有 test script项目定义了测试入口当前测试已经通过
Turbo 有 dependsOn经 Turbo 调度时存在依赖手工进入 package 执行会自动跑上游
dist/** 声明为 outputsTurbo 可把它当构建产物产物内容正确、可发布

工程文档最常见的失真,是把“配置上存在”写成“本次验证成功”。本章只做源码声明解读。

  1. package.json:runtime 版本、根脚本和禁止 root test。
  2. package.json:workspace 与依赖 catalog。
  3. turbo.json:task graph、依赖和 outputs。
  4. tsconfig.json:1-5:根 TypeScript 基线有意保持很薄。
  5. AGENTS.md:测试与 typecheck 的人工契约。
  6. packages/opencode/package.json:核心 runtime 的验证入口。
  7. packages/opencode/package.json:bin 与 Bun/Node 条件导入。
  8. packages/sdk/js/package.json:SDK 的 typecheck、生成构建和 exports。

6. 一条具体工程旅程:修改 session API 后该怎么验证

Section titled “6. 一条具体工程旅程:修改 session API 后该怎么验证”

假设你修改了核心 runtime 的 session HTTP API,并因此需要重新生成 JS SDK。下面是从仓库契约推导出的验证路径,不表示这些命令在本章编写时已经执行。

服务端改动属于 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.jsonAGENTS.md 同时要求尽量少 mock、测试真实实现,并从具体 package 运行测试,见 AGENTS.md

核心包提供:

  • test:Bun test,30 秒 timeout;
  • test:ci:额外生成 JUnit;
  • test:httpapi:coverage、auth、effect 三类 HTTP API exercise;
  • bench:testprofile: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 帮他做了这一步。

可信报告应分别说:

声明检查:脚本/任务存在
实际执行:命令、目录、退出码
覆盖边界:哪些 package / 哪类路径没跑
产物检查:generated diff / dist 是否符合预期

“代码能 typecheck”不等于“HTTP 行为正确”;“单测通过”也不等于“SDK 已更新”。

根 workspace 列出主要 package 集合;catalog 固定 effectai、TypeScript、UI 框架等共享版本,见 package.json

这类似 dependency management:子 package 可以使用 catalog:,减少同库多版本漂移。代价是根 catalog 变更影响面很大,应视为跨包升级,而不是单包小改。

tsconfig.json 只继承 Bun 基线,没有集中塞入大量 compiler option,见 tsconfig.json:1-5。这反过来强调 package 自己的 typecheck 配置与 script 才是执行边界。

turbo.json 展示了三类配置:

  • typecheck 没声明跨包依赖;
  • build 产出 dist/**
  • 特定 test task 依赖 ^build,CI task还记录 JUnit output。

turbo.json

为什么不让所有任务一律 dependsOn: ["^build"]?源码没有写解释。合理的设计解释是:不同任务对上游构建产物的依赖不同,过度串行会拖慢反馈。这个“为什么”是推断,不是代码明示。

packages/opencode/package.json 不只声明脚本,还通过 imports 为 #db#pty 选择 Bun、Node 与 default 实现,见 packages/opencode/package.json

这提醒测试设计:类型通过不代表所有运行环境都通过。涉及数据库或 PTY 的改动,应按条件导入覆盖实际目标 runtime。

选择好处代价
禁止 root test强迫开发者选择真实 package 边界初学者会觉得入口不直观
catalog 统一版本降低 workspace 依赖漂移升级影响面集中
package scripts 封装工具命令与参数可演进不能凭通用生态习惯直接跑 tsc
Turbo 只声明必要依赖反馈更快、缓存更准确task graph 需要持续维护
SDK 生成作为 build契约变化可机械传播生成物与源码必须一起审查

方法一:让测试入口匹配所有权边界

Section titled “方法一:让测试入口匹配所有权边界”

根目录负责发现与编排,package 负责具体命令。不要设计一个含义模糊的“全测”按钮。

验证问题:失败时,开发者能否立刻知道是哪个 package、哪类检查?

方法二:把生成代码当交付链的一等公民

Section titled “方法二:把生成代码当交付链的一等公民”

contract 变化要能触发生成、diff、typecheck,而不是靠发布前想起来。

验证问题:CI 能否发现 server schema 已变、SDK 生成物没更新?

静态检查快、focused test 定位准、跨包 build 捕获集成问题、E2E 验证用户路径。交付时报告实际跑到哪一级。

验证问题:一个 permission 行为变更为什么不能只靠 typecheck?

请回答:

  1. 为什么根目录可以 typecheck,却故意不能 test?
  2. package script 与 Turbo task 各负责什么?
  3. “配置了 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?