当你写一个 AI agent 的测试时,你会怎么测?
传统的软件测试教你"先写单元测试,再写集成测试"。但 Codex 团队把这个金字塔倒过来了 :集成测试调用 harness 超过 500 处(grep -r 'test_codex()' codex-rs/core/tests/ | wc -l),snapshot 文件 426 个(find . -name '*.snap' | wc -l),测试基础设施代码超过 6.5 万行------远超单元测试规模。
为什么?因为 AI agent 的核心价值不在于"每个函数正确",而在于"整个交互流程符合预期"。一个函数可能 100% 正确,但放在完整的 agent turn 中可能产生完全错误的行为。
本文带你走进 Codex 的测试体系,看他们如何用集成测试捕获 AI 系统的真实行为。
场景触发条件:什么情况下 Codex 团队会决定增加一种测试类型?
从代码库结构和测试目录组织可以合理推断 Codex 团队扩展测试策略的动机:
集成测试成为主力:当 agent 逻辑越来越复杂时,团队发现单元测试无法覆盖"用户输入 → 模型推理 → 工具调用 → 结果返回"的完整链条。一个函数在隔离环境下正确,不代表在完整流程中能正常工作。
Snapshot 测试引入 :当 TUI 的渲染输出频繁变化,而肉眼审查每次 PR 的 UI 差异不现实时,团队引入了 cargo insta------把"看起来对"变成"机器能验证"。
TUI PTY 测试:当 TUI 在无头环境(CI)中频繁崩溃时,团队开发了基于伪终端的测试框架------模拟真实终端的交互。
Docker 远程测试:当本地测试无法复现远程执行环境的问题时,团队构建了基于 Docker 的远程测试环境。
一个可行的判断标准:当某类 bug 反复在"集成环境"中出现,但在单元测试中无法复现时,就值得新增一种集成测试。
核心基础设施:test_codex harness
Codex 测试体系的核心不是某个测试框架,而是一个自定义的测试 harness :TestCodexBuilder。
它让你可以用几行代码测试完整的 AI 交互:
rust
let codex = test_codex()
.with_model("gpt-4")
.with_config(...)
.build(&server).await;
codex.submit_turn("write a hello world program").await;
let output = codex.function_call_stdout().await;
assert!(output.contains("hello"));
这个 harness 在内部做了很多事情:
- 启动
wiremock::MockServer作为模型 provider,模拟 OpenAI API 响应 - 创建临时目录作为
CODEX_HOME和工作区 - 设置确定性的环境变量和进程 ID
- 提供便捷方法:
submit_turn()、write_file()、read_file_text()、request_bodies()
整个代码库里有 超过 500 处调用 这个 harness。它不是"一个测试工具",而是集成测试的标准写法。
harness 如何模拟模型响应
test_codex 在底层使用 wiremock 来构造假模型响应:
rust
pub async fn start_mock_server() -> MockServer {
let server = MockServer::builder()
.body_print_limit(BodyPrintLimit::Limited(80_000))
.start()
.await;
// 提供默认的 /models 响应,确保测试自包含
let _ = mount_models_once(&server, ModelsResponse { models: Vec::new() }).await;
server
}
ResponseMock 会捕获所有请求,让你可以验证 agent 发送了什么:
rust
let mock = mount_response_once(&server, response_template).await;
// ... 运行 agent ...
let req = mock.single_request();
assert!(req.body_contains_text("write a hello world"));
这意味着测试不仅能验证"agent 输出了什么",还能验证"agent 向模型发送了什么"------这对于调试提示词工程和工具调用顺序至关重要。
关键设计决策一:集成测试优先覆盖 Agent 变更
.codex/skills/code-review-testing/SKILL.md 明确规定:
"For agent changes prefer integration tests over unit tests. Integration tests are under
core/suiteand usetest_codexto set up a test instance of codex." "Features that change the agent logic MUST add an integration test."
这不是建议,而是强制要求 。集成测试本身必须覆盖"主要逻辑变化和用户可见的行为"(Provide a list of major logic changes and user-facing behaviors that need to be tested)。Codex 的 contributing.md 也要求 invited PR 必须包含测试:bug fix 要有"改之前挂、改之后过"的测试覆盖。
为什么集成测试对 AI agent 更重要?因为 agent 的行为是涌现的(emergent):
- 提示词工程的影响无法通过单元测试捕获
- 工具调用的顺序和组合只有在完整流程中才能验证
- 上下文窗口的管理(压缩、截断)是全局行为
单元测试验证"组件正确",集成测试验证"系统正确"。从 Codex 的实践来看,对于 AI agent,系统正确比组件正确更重要。
关键设计决策二:用 Snapshot 锁定 UI 与上下文形态
Codex 仓库里有 426 个 .snap 文件 ,用于 insta snapshot 测试。
Snapshot 测试解决了一个传统测试无法解决的问题:TUI 渲染输出的验证。
当你修改了 TUI 的颜色、布局或文本时,传统的断言无法表达"渲染输出应该看起来怎样"。Snapshot 测试的做法是:
- 运行测试,捕获渲染输出的文本表示
- 新生成的输出与已批准的 snapshot 比较
- 如果不同,生成
.snap.new文件 - 人工审查
.snap.new,确认变化符合预期 - 执行
cargo insta accept批准新的 snapshot
一个真实的 .snap 文件长这样:
yaml
---
source: tui/src/history_cell.rs
expression: rendered
---
╭───────────────────────────────────────╮
│ >_ OpenAI Codex (vtest) │
│ │
│ model: gpt-5 /model to change │
│ directory: /tmp/project │
│ permissions: YOLO mode │
╰───────────────────────────────────────╯
这不仅是测试,更是代码审查的一部分。reviewer 不需要运行代码就能看到 UI 变化的效果------因为变化被捕获成了可 diff 的文本文件。
Codex 还扩展了 snapshot 的概念,实现了上下文 snapshot:把模型可见的上下文结构(消息序列、工具调用、输出)格式化为标准化的文本,验证"模型看到了什么"。例如,它可以验证在一次 agent turn 中,系统消息、用户输入、工具调用结果是否按预期顺序出现在上下文中------这对于调试上下文压缩和注入逻辑至关重要。
关键设计决策三:PTY 测试验证真实终端交互
TUI(文本用户界面)测试是最难做的测试之一。你需要:
- 在没有真实终端的 CI 环境中运行
- 验证光标位置、颜色、布局
- 处理终端的交互式输入(键盘事件、鼠标事件)
Codex 的解决方案是 PTY(伪终端)测试:
rust
let mut pty = spawn_pty_process("codex", &["--some-flag"]).await;
// 发送输入
pty.write("hello\n").await;
// 读取输出并验证
let output = pty.read_until("prompt").await;
assert!(output.contains("expected text"));
关键技术点:
- 在测试中 spawn 真实的 Codex CLI 进程
- 模拟终端光标位置查询(
ESC[6n),返回假位置(ESC[1;1R) - 这样 TUI 可以在没有真实终端的环境下初始化
这不是 mock,而是真实进程在模拟环境中的运行。它验证了从 CLI 参数解析到 TUI 渲染的完整链条。
关键设计决策四:Docker 远程测试覆盖真实执行环境
Codex 的 exec-server 可以在远程环境中执行命令(如 Docker 容器)。本地测试无法覆盖这种场景,因为:
- 远程执行涉及 WebSocket 通信
- 沙箱行为在容器中和在本地不同
- 权限和网络配置在远程环境中有差异
Codex 的解决方案是 scripts/test-remote-env.sh。核心逻辑如下:
bash
# 构建 codex 二进制
cargo build -p codex-cli --bin codex
# 启动特权容器(bubblewrap 需要 mount propagation)
docker run -d \
--name "${container_name}" \
--privileged \
--security-opt seccomp=unconfined \
ubuntu:24.04 sleep infinity
# 在容器中启动 exec-server,暴露 WebSocket
docker exec "${container_name}" sh -lc \
"nohup ${remote_codex_path} exec-server --listen ws://0.0.0.0:31987 > /tmp/exec-server.stdout 2>&1 &"
# 测试通过环境变量连接
export CODEX_TEST_REMOTE_EXEC_SERVER_URL="ws://${container_ip}:31987"
这个脚本不仅在 CI 中运行,也可以在本地开发时通过 source scripts/test-remote-env.sh 运行。它让开发者可以在与生产环境接近的条件下测试远程执行功能。
关键设计决策五:主动管理 Flaky 测试生命周期
集成测试比单元测试更容易出现 flaky(不稳定)的情况,因为:
- 异步操作的时序不确定
- 外部依赖(网络、文件系统)有噪声
- 多线程竞争条件
Codex 有三层 flaky test 管理策略:
第一层:标记和隔离
- 用
#[ignore = "flaky"]标记已知不稳定的测试 - 在
nextest.toml中用test-groups序列化有资源竞争的测试组:
toml
[test-groups.app_server_integration]
max-threads = 1
[[profile.default.overrides]]
filter = 'package(codex-app-server) & kind(test)'
test-group = 'app_server_integration'
第二层:超长超时容忍
rust-ci-full.yml的 tests job 使用 45 分钟超时,给大型集成测试足够的时间(其他 job 为 30 分钟)nextest.toml中的slow-timeout会在测试卡住时自动终止:连续 15 秒无进展则警告,警告 2 次后(累计 30 秒)强制终止
第三层:PR Babysitter 诊断
babysit-prskill 会自动分析 CI 失败- 区分"分支相关失败"(代码问题,需要修复)和"flaky 失败"(环境问题,需要重试)
- 对 flaky 失败通过
python3 .codex/skills/babysit-pr/scripts/gh_pr_watch.py --pr auto --retry-failed-now重跑失败 job
这不是"接受 flaky 测试存在",而是主动管理 flaky 测试的生命周期。
核心洞察:测试是行为契约,不是覆盖率数字
Codex 的测试实践体现出的哲学是:测试不是为了证明代码没有 bug,而是为了锁定行为契约。
.snap 文件锁定的是"UI 应该长什么样"。test_codex harness 锁定的是"agent 在面对这个输入时应该做什么"。Docker 远程测试锁定的是"远程执行在这个环境下应该成功"。
这些测试的价值不在于覆盖率百分比,而在于它们捕获了无法被单元测试表达的系统行为。
对于 AI 原生系统,这个洞察尤为重要:
- 模型行为是非确定性的,但系统行为应该是有边界的
- 你无法测试"模型会输出什么",但可以测试"无论模型输出什么,系统都会正确路由"
- 你无法测试"上下文压缩后模型是否理解",但可以测试"压缩后的上下文结构是正确的"
可学习点
- 集成测试优先:对于 agent/AI 系统,验证完整交互流程比验证单个函数更重要
- 投资测试基础设施 :
test_codexharness 让写集成测试和写单元测试一样简单,这是集成测试能被广泛采用的关键 - Snapshot 测试捕获 UI 行为:把"看起来对"变成"机器能验证"
- 真实环境测试:用 PTY 测试 TUI,用 Docker 测试远程执行------不要依赖 mock
- 主动管理 flaky 测试:标记、重试、自动诊断,而不是忽视
- 关键判断标准:当某类 bug 反复在"集成环境"中出现,但在单元测试中无法复现时,就需要新增一种集成测试