# 测试与工程化

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

## 0. 本章学习目标

读完本章，你应该能：

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

## 1. 一句话讲明白

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

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

## v1.18.16 相比旧基线改变了什么

- monorepo 已扩展出新的 core、schema、LLM 与 UI v2 边界；共享契约下沉后，验证不能只盯 `packages/opencode`。
- `turbo.json` 的任务图仍声明 build/test/typecheck 的依赖与缓存产物，但具体可运行命令继续由各 package scripts 定义。
- 本章只说明仓库声明的验证边界；本次教程更新没有把“阅读到 test script”写成“OpenCode 全仓测试已通过”。

<!-- source-ref path="package.json" lines="1-120" title="v1.18.16 workspace 与根脚本" note="根配置描述包集合和跨包入口。" -->

<!-- source-ref path="turbo.json" lines="1-34" title="Turbo 任务图" note="任务依赖与缓存产物不等于某次任务已经执行。" -->

## 2. 先画地图：谁定义什么

```text
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. 最小机制：按影响面选择任务

一个可迁移的最小工程化策略是：

```text
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`。

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

## 4. 三种容易混淆的事实

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

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

## 5. 最小源码路径

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 后该怎么验证

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

### 第一步：先定位 package ownership

服务端改动属于 `packages/opencode`；generated client 属于 `packages/sdk/js`。根 `workspaces.packages` 明确包含 `packages/*` 与 `packages/sdk/js`，见 `package.json`。

所以这是跨两个 package 的改动，不能只对根 `tsconfig.json` 跑一次编译就结束。

### 第二步：用 package 声明的方式做 typecheck

核心包声明：

```text
typecheck = tsgo --noEmit
```

见 `packages/opencode/package.json`。SDK 也有自己的 `tsgo --noEmit`，见 `packages/sdk/js/package.json`。

仓库规范进一步要求从 package 目录运行 `bun typecheck`，不要直接调用 `tsc`，见 `AGENTS.md`。原因不是语法偏好，而是 package script 才包含仓库选择的编译器和参数。

### 第三步：重新生成 SDK 是显式步骤

SDK package 的 `build` 指向 `bun ./script/build.ts`，见 `packages/sdk/js/package.json`；根 `AGENTS.md` 也明确指出 SDK 生成入口。

因此，API contract 改了但没有更新 generated client，是一个可预见的交付缺口。生成物是否变化、类型是否对应，都应该在 diff 与 SDK typecheck 中验证。

### 第四步：核心测试必须在 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 会补哪些依赖

经 Turbo 执行 `opencode#test` 时，会先做 `^build`，即上游 workspace 依赖构建，见 `turbo.json`。CI 版本也声明 JUnit output，并同样依赖上游 build，见 `turbo.json`。

这解决了“核心测试引用了尚未构建的 workspace 包”。但如果开发者直接在 `packages/opencode` 手工运行 package test，就不能假设 Turbo 帮他做了这一步。

### 第六步：交付证据要分层报告

可信报告应分别说：

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

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

## 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 的关键选择

`turbo.json` 展示了三类配置：

- `typecheck` 没声明跨包依赖；
- `build` 产出 `dist/**`；
- 特定 test task 依赖 `^build`，CI task还记录 JUnit output。

见 `turbo.json`。

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

## 9. 运行时包的跨环境边界

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

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

## 10. OpenCode 的选择

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

## 11. 可以带走的方法

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

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

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

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

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

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

### 方法三：按风险建立验证阶梯

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

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

## 12. 费曼复述与练习

请回答：

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，防止开发者在错误目录误跑测试。

## 最后复盘：工程化是可执行的信任链

```text
文件改动
  -> package ownership
  -> package typecheck / focused test
  -> task graph 补齐上游
  -> generated/build 产物
  -> 清楚报告已验证与未验证边界
```

到这里，我们已经读完 runtime 的关键模块与支撑它的工程边界。最后一章不再继续堆 OpenCode 细节，而是做一次删减实验：哪些机制必须保留，才能亲手做出一个小而真的 coding agent？
