测试一个非确定性系统
确定性回放、性质测试、专门骗 LLM 客户端的故障服务器。核心材料:docs/testing.zh.md 与 packages/test-support/。
先玩再讲。情景 A 是一台真的 HTTP 故障服务器:行为排成队,每接一个请求消耗一个,看客户端怎么接招。情景 B 是确定性回放:拿一份真实会话日志,一键推导回放脚本重跑,再动手篡改一行,看 diff 怎么当场作证。
测非确定性系统的思路只有一条:把不确定的部分圈起来,让其余一切确定。DSH 的测试分层(docs/testing.zh.md)就是围着这条思路搭的。单元测试盯边界情况、错误路径、事件顺序和并发竞态;CI 的覆盖率门禁对 packages/*/*/src 按文件要求 100%(AGENTS.md 第 65 行 Commands 一节),文档同时把话说死:行覆盖率是必要条件,永远不是充分条件,没跑过的行往往是该删的死代码,而非该补的测试。
带密钥的真实 API 测试是另一层。这里有句很有身份特色的话:
docs/testing.zh.md「带密钥策略」一节,核对日期 2026-08-13
推理对自家便宜,冒烟测试就往真里做:启动真实示例、发一条提示词、检查外部世界。断言也有讲究:e2e 要重新读文件、重跑命令来验证结果,对 agent 自身输出做关键词探测会让作弊的 agent 通过。缺密钥的环境自动跳过,不阻塞任何人。
重头戏是回放。上一课讲过(见 LLM 适配层),每个流式分片都以 assistant/chunk 事件原样落进会话日志。dsh-llm-replay 插件把这件事反过来用:拿一份录好的 session.jsonl,把 chunk 事件按 (turn, step) 分组,每组就是当年一次模型调用的完整分片序列。测试时真实 agent 照常跑,只是模型那头换成回放适配器,逐帧吐回录制的分片。不确定性只存在于录制那一次,之后每次重跑都逐字节一致,不需要 API Key。
这就是日志即测试资产的意思:fixture 不用手写 mock 数据,直接就是生产格式的会话日志本身。快照测试拿它固定整个组装后的行为,改一行代码导致行为分叉,diff 当场标红。还有个精巧的细节在 fork(分叉会话):子会话的日志开头继承了父会话的种子事件,回放推导脚本时必须从 seedLength 边界之后开始切,否则父会话的分片会被误当成子会话的调用重放:
const text = readFileSync(childFile, 'utf8')
const header = parseSessionHeader(text)
// Derive the child's script from its own events only — events AT OR after the seed
// boundary.
const ownEvents = parseSessionLog(text).slice(header.seedLength)
children.push({
recordedId: header.id,
createdAt: header.createdAt,
entries: deriveReplayScript(ownEvents),
primary: false,
})
packages/test-support/llm-replay/src/index.ts,核对日期 2026-08-13。代码块保留源码原文。跨平台纪律也从这里来:签入的 fixture 必须在 macOS 和 Linux 上都能回放,录出来的快照哪个平台挂了就修 fixture 本身。AGENTS.md 第 123 行的原话是「fix fixtures, not normalizers」:修 fixture,别写归一化器。归一化器是在测试和现实之间垫棉花,垫多了就测不到现实了。
回放测的是行为不变,还差一块:传输层的花式死法。连接被拒、发一半 socket 重置、正常关闭但没发 [DONE]、限流带 Retry-After、干脆停滞不动,每一种在适配器和恢复层眼里都是不同的东西,用进程内 mock 全测不到,因为 mock 绕过了 fetch、SSE 分帧、socket 终止和空闲看门狗这些真实边界。所以 DSH 造了 dsh-llm-mock-server:一台真的 Node HTTP 服务器,说 OpenAI 方言,行为完全由脚本控制,每接一个请求消耗一个行为,脚本耗尽就明确报错(设计动机见 Agent Note 2026-07-25-scriptable-llm-wire-fault-server.zh.md)。开发者想手动复现故障,改一下 base URL 和 key 就能把任何应用接上来。
它还有个 random 模式,按权重随机抽故障做压力测试,seed 公开且可复现。默认权重表本身就是一份清单,列着 LLM 客户端在野外会遇到什么:正常成功占 48、慢速成功 10、半途掐线(partial_disconnect)10,然后是各占 5 的连接重置、断流、空回复和限流,服务器错误 4,触顶 max_tokens、停滞挂起和 503 各 2,最刁的两种各占 1:流正常收尾却没发完的 partial_eof,和吐出坏 JSON 的 malformed_json。源码注释还专门提醒:这是可调的测试压力配置,并非生产事故频率的估算。
出处:packages/test-support/llm-mock-server/src/index.ts 第 56 至 70 行的 DEFAULT_MOCK_LLM_RANDOM_WEIGHTS,核对日期 2026-08-13。
服务器的操守很克制:只报告协议层事实,不判断可不可以重试,策略归 harness 自己。真实组合测试让请求依次穿过 DeepSeek 适配器、agent loop 和重试插件,验证的东西很具体:请求次数精确、重试步骤带编号、失败的半截分片不泄漏进历史、正常 EOF 的半截输出归类为 STREAM_CLOSED 且默认不重试。
最后一件武器对付的是没人想得到的交错。协议形态的代码(分片流、事件日志、收件箱调度)输入空间是组合爆炸的,示例测试只能固定想到的用例。DSH 给每个协议形态的包配一个 fast-check 驱动的性质测试:生成器造出逼真但对抗性的输入(重复索引、滞后分片、缺 block-start 的畸形流),断言的不是具体输出,是不变式,比如组装出的块数不能超过见过的不同索引数、重复调用的结果必须稳定。失败自动打印可复现的 seed。
它的战绩写在 Agent Note 的第一行:
block-end 的真实 bug。」同一索引处重复的 block-end 会改写已经完成的块,而这个 bug 是在 happy path 100% 行覆盖率之下存活的。
出处:.agents/notes/implemented/testing/2026-06-11-property-based-testing.zh.md,核对日期 2026-08-13。修复后的「首次关闭优先」防御见 LLM 适配层一课的源码面板。
这套基础设施还有个副产品:BENCHMARK.md 给的官方基准测试路径就是 Python SDK 加 minimal 变体,每个任务独立 workspace。测试体系搭扎实了,跑评测只是换个输入。
覆盖率是必要不充分按文件 100% 是 CI 门禁,但它只证明行被执行过。真 bug 藏在交错序列里,那是性质测试的地盘;藏在传输边界里,那是故障服务器的地盘。
fixture 就是会话日志回放 fixture 不是手搓的 mock,是生产格式的 session.jsonl 本身。录一次,处处重放,跨平台必须过,挂了修 fixture 不修归一化器。
故障服务器不做策略它只诚实地按脚本掐线、限流、停滞,重不重试是 harness 的事。测试基础设施保持中立,才能同时给适配器、loop 和重试层作证。
Grok Build:也有脚本化 mock,但停在 HTTP 层
Grok Build 的 xai-grok-test-support 里有一台 MockInferenceServer(crates/codegen/xai-grok-test-support/src/mock_server.rs):默认 echo 模式回声最后一条用户消息,支持按路径排队的脚本化响应(精确控制状态码、body 和 SSE 事件),一台服务器同时伺候 chat-completions、responses、messages 三种 API 方言,所有请求带 header 全量记录供断言。思路和 DSH 的故障服务器同源。差距在覆盖面:从已核对的源码看,它做的是 HTTP 响应层面的脚本化;DSH 的故障服务器往下多打了一层,socket 重置、发一半掐线、停滞挂起这些传输层死法都进了行为词汇,还配了可复现的加权随机模式。回放这块,Grok 用 xai-sqlite-journal 做持久化,但基于已公开证据,未见把生产日志直接推导成回放脚本的等价机制。
Claude Code:闭源产品的测试黑箱
还原源码(restored-src)里可见的测试痕迹有限,这符合还原的性质:从产物反推出来的是产品代码,测试代码本来就不随产物发布。所以这里能下的结论只有一条:基于已公开证据,外界无法评估 Claude Code 的测试体系长什么样。这恰好反衬出开源 harness 的一个价值:DSH 的测试策略、覆盖门禁、fixture 纪律全部写在仓库里,测试基础设施本身也是可以被学习和复用的交付物。
设计一条属于你的不变式
假设你要给 BlockAssembler 再补一条性质测试。生成器会随机吐出合法与畸形交错的分片流(重复 block-end、缺 block-start、滞后 delta)。参考本课讲的组装块数不变式,再写出两条你认为值得断言的不变式,并说明每条防的是哪类真实故障。然后推演:录制的快照 fixture 在 macOS 通过、Linux 上因为路径分隔符 diff 挂了,按「fix fixtures, not normalizers」的纪律,你改哪里,为什么不在比对器里把路径统一替换掉?