Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

测试

ZeroClaw 使用基于文件系统布局的五级测试分类法。每个级别都有不同的边界和成本;请选择能够证明你所需内容的最低级别。

当 PR 声称某项行为可由用户直接运行、点击、发送、安装或观察时,使用 用户边界验证 来确定能够触达该边界的最小测试或手动检查。

五个级别

级别它测试的内容边界它所在的位置
单元单个函数或结构体所有功能均已模拟src/** 目录或同目录下的 tests.rs 文件中的 #[cfg(test)] 代码块
组件一个子系统在其自身的边界内子系统 real,其余全部模拟tests/component/
集成多个内部组件通过布线连接在一起真实的内部实现,外部 API 被模拟tests/integration/
系统所有内部边界上的完整请求 → 响应仅模拟外部 APItests/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-runtimezeroclaw-channels 库测试二进制文件。运行整个二进制文件是有意为之:它可以检测会改变状态的测试与其他看似无关的 agent 轮次之间的干扰,而这些干扰是经过筛选的测试运行无法暴露的。必需的 CI 会在单独的 job 中针对任一 crate、工作区依赖清单或该门禁自身 CI 文件的更改运行此门禁。其他 PR 会跳过它;推送到 master 和合并队列运行仍保留完整的回归兜底。使用 ZEROCLAW_PARALLEL_TEST_RUNS 覆盖重复次数,使用 ZEROCLAW_PARALLEL_TEST_THREADS 覆盖测试框架线程数。

为新测试选择一个级别

  1. 单独测试一个子系统?→ tests/component/
  2. 测试多个组件连接在一起?→ tests/integration/
  3. 测试完整的端到端消息流?→ tests/system/
  4. 需要真实的 API 密钥?→ tests/live/ 目录下的测试使用 #[ignore] 属性

创建文件后,将其添加到该层的 mod.rs 中,并使用 tests/support/ 中的共享基础设施。

共享基础设施

每个测试二进制文件都包含 mod support;,使得共享的 mock 对象可以作为 crate::support::* 使用。

模块目录
mock_model_provider.rsMockModelProvider(FIFO 脚本化),RecordingModelProvider(捕获请求),TraceLlmModelProvider(JSON fixture 回放)
mock_tools.rsEchoToolCountingToolFailingToolRecordingTool
mock_channel.rsTestChannel(捕获发送内容,记录打字事件)
helpers.rsmake_memory(), make_observer(), build_agent(), text_response(), tool_response(), StaticRecallMemory
trace.rsLlmTraceTraceTurnTraceStep 类型 + LlmTrace::from_file()
assertions.rsverify_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 链更易于阅读和编辑。

工作原理:

  1. TraceLlmModelProvider 加载一个固件并实现 ModelProvider trait。
  2. 每次调用 provider.chat() 都会按 FIFO(先进先出)顺序返回 fixture 中的下一步。
  3. 真实工具正常运行(EchoTool 实际处理其参数)。
  4. 在所有回合结束后,verify_expects() 会检查声明式断言。
  5. 如果代理调用的次数超过步骤数,测试将失败。

夹具格式:

{
  "model_name": "测试名称",
  回合: [
    {
      “用户输入”: “用户消息”,
      步骤: [
        {
          响应: {
            类型: "文本",
            "内容": “LLM 响应”,
            "input_tokens": 20,
            "输出令牌": 10
          }
        }
      ]
    }
  ],
  期望: {
    "response_contains": [预期文本],
    "使用的工具": [`echo`],
    "max_tool_calls": 1
  }
}

响应类型:"text"(纯文本)或 "tool_calls"(LLM 请求工具执行)。

期望字段:response_containsresponse_not_containstools_usedtools_not_usedmax_tool_callsall_tools_succeededresponse_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>/ 下。