第十五章 从源码运行、检查和测试
15.1 先分清源码入口与发行版
从源码学习 Pi,不需要先理解整个构建系统。本章中的“仓库根目录”专指 earendil-works/pi 源码仓库,不是你正在阅读的书籍网站仓库。Pi 根目录是一个 npm 工作区:多个包在同一工程中开发,但仍各有自己的职责和 package.json。根文件规定工作区、脚本和 Node 要求。当前基准要求 Node.js 至少为 22.19.0,学习前可用 node --version 检查自己的环境。
如果只有本书,还需先取得 Pi 源码。Git 是保存代码版本的工具,clone 会下载一份仓库,cd 进入该目录,最后一行选择本书介绍的固定提交,仅供阅读和本地练习。下面命令应在准备存放源码的位置运行,目录名 pi-source 应当尚未存在:
git clone https://github.com/earendil-works/pi.git pi-source
cd pi-source
git switch --detach 16f5f0af4d85ca583d045ceb5ed6d646fcaf7761
选择固定提交后处于“分离头指针”状态,即查看某个历史快照。若以后要保存并提交自己的开发改动,应先按 Git 的使用方式创建工作分支;初次阅读无需提交。下文安装、源码启动和测试都在这个 Pi 源码目录内进行。
先完整阅读根目录 AGENTS.md,再读 README 和脚本定义。README 给出常见操作,AGENTS 进一步约束开发方式。例如,它要求直接依赖固定版本、禁止自动运行依赖生命周期脚本,并规定代码修改后做检查。不要看到 README 的构建示例,就把构建当成每次浏览工程都必须执行的动作。
本地准备依赖的参考命令是 npm install --ignore-scripts;需要按锁文件重建干净环境时,规则给出 npm ci --ignore-scripts。后者会重建依赖目录,使用前应确认自己的环境。这里的 --ignore-scripts 表示跳过安装钩子,不是跳过依赖。依赖及锁文件变化仍应作为代码审查,不能因为安装成功就随意提交。
源码启动入口是根目录 ./pi-test.sh。脚本定位仓库内的源码解析器,然后启动 packages/coding-agent/src/cli.ts。它能从其他工作目录调用,所以你可以让源码版 Pi 处理练习项目,而不是总以 Pi 自己的仓库为工作对象。以下仅是读者以后可执行的参考,本章编写时没有运行它们:
npm install --ignore-scripts
./pi-test.sh --help
./pi-test.sh
Node 原生去掉 TypeScript 类型,并不会自动理解根 tsconfig.json 中的工作区路径映射。源码解析器补上这层能力,避免内部导入悄悄落到过时的 dist 文件。基础配置还开启 erasableSyntaxOnly:代码应使用能直接去除的类型语法,不能依赖枚举等需要转换成 JavaScript 的结构。
15.2 check 检查什么,又可能改变什么
检查回答“代码是否符合规则”,测试回答“具体行为是否符合预期”。根目录 npm run check 先运行 Biome,再检查固定依赖、运行时依赖、相对导入、公开入口关系、安装锁,随后进行 TypeScript 类型检查和浏览器冒烟检查。冒烟检查是快速确认基本入口能工作的检查,不等于全面测试。
尤其注意,当前 check 的第一步带有 --write,格式化过程可能修改文件。它不是只读命令。运行前后应查看 git status 和差异,确认哪些变化由自己的工作产生。多人共享工作区时,不要一并暂存其他人的文件,也不要用清理整个工作区的办法消除检查结果。
AGENTS 要求改代码后运行 check,并修复错误、警告和提示,完整查看输出;只改文档没有这一自动要求。check 不运行测试。反过来,一条测试通过也不能替代依赖、类型和入口检查。两者覆盖的问题不同,不应该只追求终端里出现一次绿色结果。
例如,你修改工具错误的展示方式,类型检查可以发现传错字段,却未必发现错误文本没有显示。针对这种行为才需要合适的测试;单纯更正说明文字则不必另造一个重复文档的测试。验证应对应实际风险,报告也应写清“检查通过”或“某个测试通过”,不要把未运行的其余验证一并宣布成功。
15.3 测试先隔离,再缩小范围
根目录 test.sh 是日常不带用户模型凭据运行测试的入口。它创建临时用户目录、临时缓存和配置,从空环境启动,只保留必要的平台与测试变量,并关闭本地模型自动发现。脚本内部再调用根测试脚本,结束后清理自己标记的临时目录。这种设计避免测试读取你的登录记录或意外连接已有本地模型。
与它不同,pi-test.sh --no-env 只是取消一组列出的环境变量,方便观察源码 CLI 缺少这些变量时的行为;它没有建立同样的临时用户目录,也没有抹掉用户保存的 auth.json。不能把两者都简称为“无凭据模式”。隔离测试也不等于完全断网,部分工程测试仍可能使用本地服务或网络资源。
仓库规定,不要直接启动完整 Vitest 套件:某些端到端测试会因已有服务地址或鉴权变量而激活。需要广泛运行非真实模型依赖测试时,读者应按当前规则使用 ./test.sh。书稿编写没有执行测试,也没有验证当前机器是否具备所有原生组件或远端守护程序。
只改一个行为时,可以先选对应包里的特定测试。Vitest 是多数包使用的测试运行器;TUI 包使用 Node 自带的 node:test。下面两组命令应分别在指定包目录运行,文件名来自本书基准,执行前仍应核对文件存在:
cd "$(git rev-parse --show-toplevel)/packages/telemetry"
node "$(git rev-parse --show-toplevel)/node_modules/vitest/dist/cli.js" --run test/metrics.test.ts
cd "$(git rev-parse --show-toplevel)/packages/tui"
node --test test/wrap-ansi.test.ts
packages/coding-agent/test/suite 有更具体的规则:使用其中的 harness.ts 和 faux provider。测试夹具是创建可控环境、便于重复验证的辅助代码;faux provider 是模拟模型服务的实现。这样可以规定模型返回什么工具调用,再验证会话如何处理,而无需真实密钥、付费 token 或不可预测的模型回答。
15.4 诊断记录与行为评测各回答什么
packages/telemetry 提供诊断数据的公共接口。一次操作的起止记录称为 span,可以带属性、事件和状态。例如,某次请求慢了,可以查看耗时分布与重试,而不是猜测界面卡住的位置。它提供空实现和内存参考实现,但不绑定某家监控服务;这些诊断记录也不能代替 Durable 的业务存储。
packages/evals 研究更高一层的行为问题,例如提供文档后,代理完成任务的成功率是否提高。它有主机评测和隔离容器中的文档对照评测,会保存任务、会话和比较报告。这里真正调用模型,可能消耗额度或产生费用,还可能需要 Docker。因此普通单元测试与 eval 不是可随意互换的两个命令。
对新手来说,先读一个确定性测试,再看一次诊断记录结构,最后才研究真实评测。一次评测成功只能说明那次运行,不能证明稳定;缺少配对结果时,当前报告也不会把缺失当成零分后继续宣布提升。
15.5 小结与练习
本章小结:从源码启动、静态检查、确定性测试和真实模型评测,各有明确入口。先读脚本,再选择最小验证范围;理解命令是否写文件、是否接触用户资源,比照抄一串命令更重要。
练习一:check 通过后,能说全部测试通过吗?参考答案:不能。check 不运行测试,而且 Biome 可能已经改了格式,应检查差异。
练习二:要验证没有真实模型参与的会话工具编排,应选择什么?参考答案:选择 test/suite/harness.ts 与 faux provider,按相应测试入口执行,不给测试真实密钥。
练习三:删除 OPENAI_API_KEY 就能保证不会读取已保存凭据吗?参考答案:不能,用户目录中仍可能有鉴权文件。test.sh 额外隔离用户目录与环境,目的正是缩小这类影响。