测试
ZeroClaw 使用基于文件系统布局的五级测试分类法。每个级别都有不同的边界和成本;请选择能够证明你所需内容的最低级别。
当 PR 声称某项行为可由用户直接运行、点击、发送、安装或观察时,使用 用户边界验证 来确定能够触达该边界的最小测试或手动检查。
五个级别
| 级别 | 它测试的内容 | 边界 | 它所在的位置 |
|---|---|---|---|
| 单元 | 单个函数或结构体 | 所有功能均已模拟 | src/** 目录或同目录下的 tests.rs 文件中的 #[cfg(test)] 代码块 |
| 组件 | 一个子系统在其自身的边界内 | 子系统 real,其余全部模拟 | tests/component/ |
| 集成 | 多个内部组件通过布线连接在一起 | 真实的内部实现,外部 API 被模拟 | tests/integration/ |
| 系统 | 所有内部边界上的完整请求 → 响应 | 仅模拟外部 API | tests/system/ |
| 实时 | 使用真实外部服务的完整堆栈 | 未模拟,#[ignore] | tests/live/ |
另外两个非测试目录:
| 目录 | 目的 |
|---|---|
tests/manual/ | 人工驱动的测试脚本(shell、Python),直接运行,而非通过 cargo 运行 |
tests/support/ | 共享的模拟基础设施,并非测试二进制文件,通过 mod support; 从各层级引入 |
运行测试
sh
cargo test # 单元测试 + 组件测试 + 集成测试 + 系统测试
cargo test --lib # 仅单元
cargo test --test component # 仅组件
cargo test --test integration # 仅集成
cargo test --test system # 仅系统
cargo test --test live -- --ignored # live(需要 API 凭据)
cargo test --test integration agent # 在某个层级内过滤
cargo nextest run --locked --workspace --exclude zeroclaw-desktop # CI 运行哪些内容
./scripts/ci/parallel_runtime_test_gate.sh # 同一进程内重复执行的运行时/通道测试
./dev/ci.sh all # 完整 CI 测试套件(Docker)
./dev/ci.sh firmware-protocol # 独立固件协议主机网关(Docker)
./dev/ci.sh test-component # 特定级别的 CI 命令(Docker)
firmware-protocol 命令检查独立的 firmware/zeroclaw-fw-protocol crate,该 crate 位于根 Cargo 工作区之外。scripts/ci/firmware_protocol_gate.sh 是其格式化、严格 Clippy 及锁定测试检查的规范定义;必需的 CI 和 pre-push 钩子均调用同一辅助脚本。
并行运行时门禁会使用 16 个测试框架线程重复运行完整的 zeroclaw-runtime 和 zeroclaw-channels 库测试二进制文件。运行整个二进制文件是有意为之:它可以检测会改变状态的测试与其他看似无关的 agent 轮次之间的干扰,而这些干扰是经过筛选的测试运行无法暴露的。必需的 CI 会在单独的 job 中针对任一 crate、工作区依赖清单或该门禁自身 CI 文件的更改运行此门禁。其他 PR 会跳过它;推送到 master 和合并队列运行仍保留完整的回归兜底。使用 ZEROCLAW_PARALLEL_TEST_RUNS 覆盖重复次数,使用 ZEROCLAW_PARALLEL_TEST_THREADS 覆盖测试框架线程数。
为新测试选择一个级别
- 单独测试一个子系统?→
tests/component/ - 测试多个组件连接在一起?→
tests/integration/ - 测试完整的端到端消息流?→
tests/system/ - 需要真实的 API 密钥?→
tests/live/目录下的测试使用#[ignore]属性
创建文件后,将其添加到该层的 mod.rs 中,并使用 tests/support/ 中的共享基础设施。
共享基础设施
每个测试二进制文件都包含 mod support;,使得共享的 mock 对象可以作为 crate::support::* 使用。
| 模块 | 目录 |
|---|---|
mock_model_provider.rs | MockModelProvider(FIFO 脚本化),RecordingModelProvider(捕获请求),TraceLlmModelProvider(JSON fixture 回放) |
mock_tools.rs | EchoTool、CountingTool、FailingTool、RecordingTool |
mock_channel.rs | TestChannel(捕获发送内容,记录打字事件) |
helpers.rs | make_memory(), make_observer(), build_agent(), text_response(), tool_response(), StaticRecallMemory |
trace.rs | LlmTrace、TraceTurn、TraceStep 类型 + LlmTrace::from_file() |
assertions.rs | verify_expects() 用于声明式跟踪断言 |
典型用法:
#![allow(unused)]
fn main() {
use crate::support::{MockModelProvider, EchoTool, CountingTool};
use crate::support::helpers::{build_agent, text_response, tool_response};
}
JSON 跟踪夹具
跟踪固件(Trace fixtures)是以 JSON 文件形式存储在 tests/fixtures/traces/ 中的预设 LLM 响应脚本。它们以声明式对话脚本取代了内联模拟设置,比 mockall 链更易于阅读和编辑。
工作原理:
TraceLlmModelProvider加载一个固件并实现ModelProvidertrait。- 每次调用
provider.chat()都会按 FIFO(先进先出)顺序返回 fixture 中的下一步。 - 真实工具正常运行(
EchoTool实际处理其参数)。 - 在所有回合结束后,
verify_expects()会检查声明式断言。 - 如果代理调用的次数超过步骤数,测试将失败。
夹具格式:
{
"model_name": "测试名称",
回合: [
{
“用户输入”: “用户消息”,
步骤: [
{
响应: {
类型: "文本",
"内容": “LLM 响应”,
"input_tokens": 20,
"输出令牌": 10
}
}
]
}
],
期望: {
"response_contains": [预期文本],
"使用的工具": [`echo`],
"max_tool_calls": 1
}
}
响应类型:"text"(纯文本)或 "tool_calls"(LLM 请求工具执行)。
期望字段:response_contains、response_not_contains、tools_used、tools_not_used、max_tool_calls、all_tools_succeeded、response_matches(正则表达式)。
实时测试规范
实时测试会调用真实的外部服务并产生实际费用;它们默认带有 #[ignore] 标记,仅在显式选择启用时才会运行。
- 始终使用
#[ignore]。绝不让实时测试在普通的cargo test中运行。 - 从
env::var("ZEROCLAW_TEST_*")读取凭据。不要读取操作者的配置;实时测试应当是封闭隔离的。 - 运行
cargo test --test live -- --ignored --nocapture。
数据库测试是集成测试
对于涉及 schema 或 SQL 的测试,不要 mock SQLite;集成测试必须连接真实的数据库。“mock 通过但生产环境失败“这类 bug 是真实存在的,我们之前就吃过这个亏。
手动测试
tests/manual/ 目录包含用于人工测试的脚本,这些测试无法通过 cargo test 自动化执行。请直接运行它们。特定通道的冒烟测试位于 tests/manual/<channel>/ 下。