ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

DeepSeek Harness 属性测试实践:为协议形态代码编写 fast-check 属性套件

2026/9/20 3:34:00 拓冰建站 浏览量
DeepSeek Harness 属性测试实践:为协议形态代码编写 fast-check 属性套件 DeepSeek Harness 属性测试实践为协议形态代码编写 fast-check 属性套件【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness协议形态的代码——chunk 流、事件日志、Schema 转换、inbox 调度——其输入空间是组合式的真正的 bug 往往藏在没人写过示例的交错路径里。DeepSeek Harness 用 fast-check 为这类代码建立了每包一份的属性测试套件并在首次运行时就捕获了 BlockAssembler 的一个真实缺陷重复block-end覆盖已完成的 block。本文基于仓库内的实现笔记与落地源码完整还原这一决策的动机、四组属性测试的生成器设计与不变量契约以及属性测试 示例测试双轨并行的工程经验。问题示例测试钉住了我们想到的用例属性测试的引入源于一个明确的测试范式缺口。示例测试example-based tests的价值在于把我们当时想到的用例钉死为回归基线但它们的覆盖能力受限于写测试的人能想象出的场景。DeepSeek Harness 的核心是一批**协议形态protocol-shaped**的代码chunk 流模型流式输出被拆成block-start/text-delta/block-end/usage/finish等 chunk 类型需要被增量装配成完整消息事件日志会话的每次追加都记录为带序号的事件派生消息、回放、压缩都建立在这份日志之上Schema 转换工具定义的ParameterSchemaSpecDSL 需要编译为 JSON Schema、再被运行时参数校验消费inbox 调度Agent 循环的收件箱在任意时刻可能涌入多条消息与 turn 的启动/结束交错。这类代码的共同特征是输入空间是组合式的有趣的缺陷生活在没有人为此写过示例的交错路径中。笔记中记录的最有力的动机证据是一个 block-assembly 排序 bug 曾经通过了 happy path 的 100% 行覆盖。逐文件的 100% 覆盖只能证明每一行代码都执行过证明不了每一种交错都是正确的——覆盖率与正确性之间的鸿沟正是属性测试要填补的。决策fast-check 驱动的每包一测属性套件针对上述问题实现笔记记录的技术决策状态为implemented即已在仓库落地可以归纳为四点引入 fast-check 作为根 devDependency在仓库根 package.json 中声明为fast-check: ^4.8.0。每个协议形态包一个tests/properties.spec.ts目前仓库中存在四个实现分别对应笔记中列出的四个包packages/llm/llm/tests/properties.spec.tsdsh-llm / BlockAssemblerpackages/core/session/tests/properties.spec.tsdsh-sessionpackages/core/tools/tests/properties.spec.tsdsh-toolspackages/core/agent-loop/tests/properties.spec.tsdsh-agent-loop。生成器面向真实但对抗性realistic-but-adversarial输入不是均匀随机噪声而是偏向小索引池、短字符串让碰撞与交错更容易出现这一点下文结合源码展开。numRuns受控保证整套属性测试在本地运行时间远低于约 10 秒失败时打印可复现的 seed。笔记还记录了一个明确的未交付项一个运行 100 倍迭代量的 nightly CI job 尚未实现——属性套件目前只跑在常规的push/pull_requestCI 中高迭代量的定时任务被列为可能的未来工作。这意味着当前套件对运行时长是刻意收紧的。四个包的属性测试落地生成器 不变量每一份properties.spec.ts的源码头注释都直接引用了本文档the property-testing Agent Note可以确认四组测试与决策一一对应。dsh-llm / BlockAssembler任意 chunk 流的不变量契约生成器的设计充分体现了对抗性// 4 个索引的小池子让重复索引的碰撞 bug 更常见 const indexArb fc.integer({ min: 0, max: 4 }) const chunkArb indexArb.chain(index fc.oneof( fc.constantStreamChunk({ type: block-start, index, blockType: text }), // ... reasoning / tool-call block-start fc.string().map((text): StreamChunk ({ type: text-delta, index, text })), // ... reasoning-delta / tool-call-delta blockEndArb(index), // 三种 block 类型随机选择 fc.constantStreamChunk({ type: usage, usage: { inputTokens: 1, outputTokens: 1 } }), // ... stop / tool-calls / error 三种 finish )) // 流不强制以 finish 结尾 const streamArb fc.array(chunkArb, { maxLength: 30 })对照 packages/llm/llm/tests/properties.spec.ts 的完整实现可以看到这个任意流同时覆盖合法与畸形输入重复索引、block-end之后的迟到数据straggler、缺失block-start、只有 delta 没有结尾。四条被断言的不变量是blocks()的数量 ≤ 出现过的不同索引数装配器不会凭空多产出 block重装配幂等blocks()在重复调用间稳定且message().content与之一致blocks()永不抛异常且只产出合法的内容块类型text/reasoning/tool-call/tool-resultfinish反映最后一个finishchunk没有任何finish时默认{ kind: stop }。这些不变量正是 Agent 循环对装配器依赖的契约。对照实现 packages/llm/llm/src/assembler.ts可以看到push()对text-delta/tool-call-delta的 straggler 处理if (partial.block) return、finish的 last-write-wins 逻辑this._finish chunk.reason以及缺省{ kind: stop }的 getter与属性断言语义完全一致。dsh-session任意事件日志的派生与回放不变量事件日志测试在 packages/core/session/tests/properties.spec.ts 中其生成器把事件分成两类消息事件user/message、assistant/message、tool/result携带surfaceOp: append意图会影响派生历史非消息事件turn/start、turn/end、step/start、step/end、assistant/chunktrace/回放数据不得影响派生历史。代码中刻意原样转发生成的 intentif (e.intent ! undefined) session.append(e.type, e.data, e.intent)注释明确说明build绝不能自行合成 intent否则属性测试就无法覆盖畸形的 fixture 选择。断言的不变量包括deriveMessages是确定性的同一份日志 → 相同的派生结果回放一致性Session.create(id, [...events])重建的会话派生结果与原始一致且seq从 0 连续严格递增非消息事件在任意交错下都不影响派生历史——这是最有代表性的属性测试它用fc.infiniteStream(fc.boolean())生成一个随机布尔流把消息流与噪声流按该流保持各自相对顺序地随机交错再断言withNoise的派生结果等于clean派生消息角色已知且被冻结frozenObject.isFrozen(m)为真向m.content追加元素会抛TypeError——用共享冻结投影取代每次调用的深拷贝隔离。dsh-toolsSchema DSL 与运行时校验的组合闭环工具 Schema 测试在 packages/core/tools/tests/properties.spec.ts 中是四个套件里生成器最精巧的一个。它用带深度的递归生成器构造任意ParameterSchemaSpec叶子属性占权重 3嵌套 object/array 各占权重 1见propArb(depth)再基于 spec 反向生成恰好满足该 spec 的参数validArgsForSpecrequired 键必出现可选键约一半概率出现。关键的不变量分两层纯编译层JSON Schema 输出的required字段在每个层级都等于 spec 中required: true的键集合parameterSchemaSpecToJsonSchema对任意 spec 转换总是不抛异常的totalityvalidateArgs对任意 spec 与任意输入也不抛异常。属性测试 ↔ 运行时参数校验的组合对应文档链接的 运行时参数校验笔记满足 spec 的生成参数必须通过validateArgs期望返回[]定向破坏——删除一个 required 键——必须被拒绝且错误信息包含该键名非对象顶层string / number / boolean / null / 数组必须被拒绝。这正是两条笔记互相引用的闭环defineTool的InferArgsS是编译期承诺而validateArgs是运行期现实属性测试用生成 args → 校验通过 / 破坏 → 校验拒绝机械地封死了编译器 / 校验器 /InferArgs之间的漂移风险。dsh-agent-loop无墙钟睡眠的确定性调度属性Agent 循环测试在 packages/core/agent-loop/tests/properties.spec.ts 中其特殊之处在于确定性构造一个永不枯竭的EchoAdapter每次模型调用都返回同一段短回复发送调度通过监听agent/status到达idle的 settle 信号来驱动完全不依赖 wall-clock sleep。因此测试天然确定任何超时都不是时序噪声而是真实缺陷。生成器产出任意长度的用户消息文本数组分为三种调度模式同步突发burst所有消息在同一 tick 内涌入队列顺序发送每条消息等到 idle 后再发下一条混合模式每一步可选地在发送前 settle。断言的不变量是没有消息丢失且顺序保持userMessageTexts(agent)与输入数组相等、turn 号严格递增且每条消息独占一个 turn、状态迁移只发生在合法的idle/running机器上assertLegalStatusTrace断言相邻状态不重复且只含idle/running。异步属性显式收紧了numRuns20–25并设置每次运行 2–3 秒的超时——因为每一轮都会拉起一整套 Cordis 插件栈LlmRuntime、SessionStore、SystemPrompt、ToolRuntime、AgentRegistry、AgentLoop这也是套件本地运行远低于约 10 秒约束的直接体现。首跑即立功重复 block-end bug 的发现、修复与回归笔记最有力的论据是实践结果属性套件首次运行就在 BlockAssembler 上发现了一个真实 bug——相同索引的重复block-end会重写一个已经完成的 block。对照修复后的源码 packages/llm/llm/src/assembler.tsblock-end分支现在的逻辑是case block-end: { const partial this.ensure(chunk.index, chunk.block.type) // First close wins; ignoring re-close stragglers keeps streamed output // and the final assembled block in agreement. if (partial.block) return partial.block chunk.block return }即first-close-wins对已关闭的索引忽略后续的重复block-end这与既有的 straggler 规则text-delta/tool-call-delta在partial.block已设置时提前返回保持一致。修复同时配了一个专门的回归测试位于 packages/llm/llm/tests/assembler.spec.tsit(first block-end wins: a duplicate block-end for a closed index is ignored, () { const chunks: StreamChunk[] [ { type: block-end, index: 0, block: { type: reasoning, text: first } }, { type: block-end, index: 0, block: { type: text, text: second } }, ] const assembler new BlockAssembler() for (const chunk of chunks) assembler.push(chunk) expect(assembler.blocks()).toEqual([{ type: reasoning, text: first }]) })这正是属性测试发现问题、示例测试钉死回归的标准工作流坏交错由生成器在无人编写过的路径中暴露修复后由显式用例保证该交错永远不再发生。工程经验与边界笔记的 Consequences 部分总结了四条值得复用的工程经验生成器质量是价值的杠杆属性测试的威力不来自随机量而来自生成器的偏向性——偏向小索引池和短字符串让碰撞与交错高频出现。均匀噪声只会浪费numRuns。属性测试的 flake 是 finding不是需要重试掉的对象Agent 循环的属性测试由于构造上确定settle 在agent/status上一旦超时挂起就代表真实缺陷不允许用 retry 掩盖。属性测试补充而非替代示例测试仓库对每个包设有 100% 行覆盖门禁example tests 钉死特定分支属性测试在其之上补足交错空间两者缺一不可。未交付的高迭代 CI 是明确留白的夜间 100× 迭代 job 被记录为可能的未来工作不在当前 CI 中运行——这是对运行时长与反馈速度的有意取舍。在仓库中运行与深入探索本主题相关的关键文件都集中在三处读者可以按需深入决策文档本文依据的实现笔记位于 .agents/notes/implemented/testing/2026-06-11-property-based-testing.md含 中文版与之互相关联的 运行时参数校验笔记 记录了validateArgs的完整语义。属性测试实现上文四个properties.spec.ts文件即为全部落地代码可在仓库内pnpm环境下运行对应包的 vitest 用例复现例如针对工具 Schema 套件pnpm exec vitest run packages/core/tools/tests/properties.spec.ts失败时 fast-check 会打印可复现 seed。被测实现与回归BlockAssembler 的完整装配算法见 packages/llm/llm/src/assembler.ts其interruptedBlocks()、replayState、max-token 截断等访问器都围绕同一组不变量设计回归测试见 packages/llm/llm/tests/assembler.spec.ts。对于任何输入空间组合化、缺陷集中在交错路径的协议代码这套小池子对抗生成器 不变量断言 首次运行即捕获真实缺陷 回归测试钉死的实践路径都可以直接复制。【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考