claw-code Mock Parity Harness:用 Anthropic 兼容 Mock 服务对 Rust CLI 做确定性端到端验证
claw-code 仓库内置了一套 Mock Parity Harness(模拟一致性测试装置):一个确定性的 Anthropic 兼容 mock 服务,加上一组在干净环境中运行 Rust claw 二进制的可复现测试脚本。本文基于 rust/MOCK_PARITY_HARNESS.md 展开,逐条覆盖文档中的产物清单、12 个脚本化场景与运行方式,并结合 mock-anthropic-service 源码 与 harness 测试实现 说明场景如何被识别、SSE 流如何被构造、干净环境如何被隔离,帮助你既能在本地跑通整套验证,也能理解每个断言背后的请求级证据。
一、这套装置解决什么问题,产物在哪
在 Agent CLI 这类产品中,模型输出是不可控变量:真实 API 的回复文本、工具调用参数、token 用量都会随时间和模型版本漂移,导致端到端测试无法稳定断言。claw-code 的做法是把"模型"也变成一个可脚本化的组件:mock 服务按预设剧本应答(文本流、工具调用、usage 数据),CLI 侧的行为(工具执行、权限拦截、权限提示、自动压缩、成本统计)就可以被精确断言。
文档列出的三个产物及其在仓库中的位置:
| 产物 | 路径 | 作用 |
|---|---|---|
Mock /v1/messages 服务 |
rust/crates/mock-anthropic-service/ | 实现确定性的 Anthropic Messages API 应答 |
| 端到端干净环境 harness | rust/crates/rusty-claude-cli/tests/mock_parity_harness.rs | 在隔离工作区中运行 claw 二进制并断言行为 |
| 便捷运行脚本 | rust/scripts/run_mock_parity_harness.sh | 一行命令触发整个 harness |
二、Mock 服务的内部机制:场景探测、剧本应答与请求捕获
场景如何被探测
mock 服务并不需要额外的路由参数。它从 CLI 发来的 /v1/messages 请求体中"读剧本":用户消息文本里会带一个 PARITY_SCENARIO: 前缀的 token。常量定义在 lib.rs 第 13 行(SCENARIO_PREFIX),而 detect_scenario(lib.rs 第 244-L254 行)从最后一条消息的文本块倒序扫描,提取前缀并解析为 12 种 Scenario 之一。harness 侧正是在构造 prompt 时拼上这个前缀的(format!("{SCENARIO_PREFIX}{}", case.name),见 mock_parity_harness.rs 第 339 行)。
状态感知:同一个场景会应答两轮
多数工具类场景需要"先要工具调用、拿到工具结果后再给最终文本"。mock 通过 latest_tool_result 判断当前请求里是否已带 tool_result 块:
- 第一轮(无工具结果):返回
stop_reason: "tool_use"的消息,携带固定 ID 和固定参数的工具调用块。例如read_file_roundtrip会要求读取fixture.txt,grep_chunk_assembly会要求grep_search pattern=parity output_mode=count; - 第二轮(有工具结果):从
tool_result的 JSON 里提取关键字段(extract_read_content读file.content、extract_num_matches读numMatches、extract_bash_stdout读stdout等,均位于 lib.rs 第 1067-L1123 行),拼进最终助语文本,如read_file roundtrip complete: alpha parity line。
SSE 流与"JSON 分块"边界情况
当请求带 stream: true 时,服务按标准 Anthropic SSE 事件序列应答:message_start → content_block_start → 若干 content_block_delta → content_block_stop → message_delta → message_stop(见 streaming_text_sse)。一个值得注意的细节:grep_chunk_assembly 和 multi_tool_turn_roundtrip 场景下,工具的 input_json_delta.partial_json 被故意拆成 3 个碎片下发(如 "{\"pattern\":\"par"、"ity\",\"path\":\"fixture.txt\""、",\"output_mode\":\"count\"}",见 lib.rs 第 355-L359 行)。这是在专门验证 CLI 端对"工具输入 JSON 需要跨 chunk 拼装"这一边界条件的处理能力——这正是该场景名 "chunk assembly" 的含义。
请求捕获与默认响应参数
每个请求都会被记录进 CapturedRequest(method、path、headers、scenario、stream、raw_body,lib.rs 第 16-L24 行),供测试事后做请求级审计。非流式文本响应的默认 usage 为 input_tokens: 10 / output_tokens: 6;而 auto_compact_triggered 与 token_cost_reporting 场景会返回被放大的 usage(50000/200 与 1000/500),用来验证 CLI 的压缩阈值触发与成本上报路径。所有响应的 model 字段固定为 claude-sonnet-4-6(DEFAULT_MODEL)。
三、干净环境:harness 的隔离设计
harness 的核心价值在于"clean-environment"。每个场景都会:
- 在临时目录创建独立 workspace(
claw-mock-parity-{场景名}-{pid}-{毫秒}-{序号}),并预留config-home与home两个子目录; - 按场景准备 fixture(如写入
fixture.txt、构造外部插件目录); - 以完全清空的环境变量启动
claw二进制。
环境清空的细节在 run_case(mock_parity_harness.rs 第 310-L366 行):env_clear() 之后仅注入 6 个变量——ANTHROPIC_API_KEY=test-parity-key(mock 服务不校验密钥,任意非空值即可)、ANTHROPIC_BASE_URL(指向本进程内启动的 mock)、CLAW_CONFIG_HOME、HOME、NO_COLOR=1、PATH=/usr/bin:/bin。CLI 启动参数为 --model sonnet --permission-mode <场景模式> --output-format=json,可选 --allowedTools 与 --resume。权限提示类场景还会通过管道写入 stdin(y\n / n\n)来模拟人工批准或拒绝。
每个场景结束后 workspace 立即被删除,保证场景之间零串扰;测试末尾还会对整个 mock 服务捕获到的请求序列做全量断言(见下节)。
四、12 个脚本化场景逐项解析
文档列出的 12 个场景与 mock_parity_scenarios.json 清单一一对应。下表汇总了每个场景在 harness 中的权限模式、工具白名单、stdin 注入及核心断言(均取自 mock_parity_harness.rs):
| 场景 | 权限模式 | 验证点 |
|---|---|---|
streaming_text |
read-only | 纯文本流式回复;iterations=1,无工具调用;最终文本精确匹配 "Mock streaming says hello from the parity harness." |
read_file_roundtrip |
read-only | read_file 执行并回传内容,最终文本含 alpha parity line,tool result 含 fixture 绝对路径 |
grep_chunk_assembly |
read-only | 跨 chunk 拼装的 grep_search 输入可正确解析,输出 2 occurrences(fixture 中 2 行含 parity),is_error=false |
write_file_allowed |
workspace-write | 写 generated/output.txt 成功,文件实际落盘且内容为 created by mock service\n |
write_file_denied |
read-only | write_file 被拒,tool result 含 requires workspace-write permission、is_error=true,且 generated/denied.txt 不存在 |
multi_tool_turn_roundtrip |
read-only | 单轮内并行发起 read_file + grep_search 两个工具调用,两条结果均回传,最终文本同时包含两处证据 |
bash_stdout_roundtrip |
danger-full-access | bash 执行 printf 'alpha from bash',stdout JSON 字段被断言 |
bash_permission_prompt_approved |
workspace-write | 触发 "Permission approval required / Approve this tool call? [y/N]:" 提示,stdin 注入 y,命令实际执行 |
bash_permission_prompt_denied |
workspace-write | 同样触发提示,stdin 注入 n,tool result 含 denied by user approval prompt 且 is_error=true |
plugin_tool_roundtrip |
workspace-write | 从 external-plugins/parity-plugin 加载外部插件工具 plugin_echo(.claude-plugin/plugin.json 声明 + shell 脚本实现),经运行时工具注册表执行,回显 JSON 中含插件 ID parity-plugin@external |
auto_compact_triggered |
read-only | JSON 输出必须含 auto_compaction 键(低于阈值时可为 null),且 usage.input_tokens >= 50000(来自 mock 放大的 usage) |
token_cost_reporting |
read-only | usage 的 input/output tokens 均非零,estimated_cost 为 $ 开头的字符串 |
插件场景的 fixture 构造很能说明加载路径:prepare_plugin_fixture 会创建 external-plugins/parity-plugin/.claude-plugin/plugin.json 与可执行的 tools/echo-json.sh,并在 config-home/settings.json 中写入 enabledPlugins: {"parity-plugin@external": true} 与 plugins.externalDirectories(mock_parity_harness.rs 第 413-L478 行)。
请求级总量断言:21 次 /v1/messages
测试结束时并非只信各场景的局部断言,而是对 mock 捕获的全量请求做序列校验(mock_parity_harness.rs 第 185-L232 行):
- 12 个场景共产生 21 次
/v1/messages请求(9 个单轮场景各 1 次,8 个两轮回调场景各 2 次……精确展开为:1 次请求的 4 个场景 + 2 次请求的 8 个场景 = 4 + 16 = 20,加上multi_tool_turn_roundtrip等按实际剧本合计 21); - 21 次请求全部带
stream: true,证明 CLI 始终走流式通道; - 请求的 scenario 序列被逐一对齐到一个硬编码的 21 元素数组,任何一次请求顺序或剧本漂移都会让测试失败。
源码中有一段注释解释了历史演变:自 be561bf 引入 count_tokens 预检后,每个回合会在 /v1/messages 之前多发一次 POST /v1/messages/count_tokens,因此现在的断言先过滤出 /v1/messages 子集再比对(mock_parity_harness.rs 第 186-L201 行)。另外,harness 还会把每个场景的 name / category / description / parity_refs / iterations / request_count / tool_uses / tool_error_count / final_message 汇总为报告,当环境变量 MOCK_PARITY_REPORT_PATH 存在时写入指定 JSON 文件(maybe_write_report,第 802-L817 行)——这正是 parity diff 脚本的数据来源。
五、运行方式:harness、parity diff 与清单对齐
一键运行 harness
文档给出的标准命令:
cd rust/
./scripts/run_mock_parity_harness.sh
run_mock_parity_harness.sh 的实现只有三行有效逻辑:切到 rust/ 目录后执行
cargo test -p rusty-claude-cli --test mock_parity_harness -- --nocapture
行为清单 / parity diff
cd rust/
python3 scripts/run_mock_parity_diff.py
run_mock_parity_diff.py 的工作分三步:
- 引用完整性检查:读取 mock_parity_scenarios.json,确认每个场景的每条
parity_refs字符串都能在仓库根目录的 PARITY.md 中找到,缺失则直接报错退出(退出码 1); - 运行 harness:以临时目录中的
report.json作为MOCK_PARITY_REPORT_PATH触发测试,拿到结构化报告; - 输出检查单:逐场景打印
[PASS|MAPPED|MISSING]状态、描述、parity 引用、迭代/请求/工具调用/错误数与最终文本,最后附一张 "PARITY coverage map"(每个 PARITY.md 条目被哪些场景覆盖)与 harness 汇总统计。
加 --no-run 参数可跳过实际执行,只做清单与 PARITY.md 的对齐检查:
python3 scripts/run_mock_parity_diff.py --no-run
三方一致性约束
文档特别强调:mock_parity_scenarios.json 必须与 mock_parity_harness.rs 和 PARITY.md 保持对齐。这个约束不是靠约定,而是靠断言强制执行——测试启动时先加载 manifest,再断言"硬编码场景数组名序列 == manifest 名序列"(assert_eq!(case_names, manifest_names, "manifest and harness cases must stay aligned"),mock_parity_harness.rs 第 153-L161 行)。也就是说,新增一个场景如果只改清单不改 harness,cargo test 会直接红掉。
六、手动启动 Mock 服务器
除了被 harness 内嵌使用(测试进程内 MockAnthropicService::spawn() 起在 127.0.0.1:0 随机端口),mock 服务也提供独立二进制,便于手工调试:
cd rust/
cargo run -p mock-anthropic-service -- --bind 127.0.0.1:0
服务器启动后打印一行 MOCK_ANTHROPIC_BASE_URL=...(见 main.rs 第 30 行)。把 ANTHROPIC_BASE_URL 指向该 URL、ANTHROPIC_API_KEY 设为任意非空值,即可用真实 claw CLI 或任何 Anthropic 兼容客户端对接这个 mock。参数解析支持 --bind HOST:PORT 与 --bind=HOST:PORT 两种写法,--help/-h 打印用法(main.rs 第 7-L27 行);注意端口为 0 时由 OS 分配随机端口,具体值以打印的 MOCK_ANTHROPIC_BASE_URL 为准。服务器会持续等待请求直到 Ctrl-C 后干净退出。
七、小结
Mock Parity Harness 把"模型行为"降级为可复现的输入剧本,把"CLI 行为"升格为可断言的输出契约:SSE 事件序列、工具 JSON 分块拼装、权限三档(read-only / workspace-write / danger-full-access)与提示批准、外部插件加载、自动压缩的 usage 门槛、成本字符串格式,全部落在 21 次被逐条捕获与排序的 HTTP 请求上。配合 mock_parity_scenarios.json ↔ harness ↔ PARITY.md 的三方一致性断言和 run_mock_parity_diff.py 的覆盖度报告,它构成了一套可重复、可审计、不依赖外部网络的行为一致性验证体系,是理解 claw-code Rust 实现如何对标上游行为的最佳入口。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00