不变量、复现测试与“一行修复”的陷阱:agent-skills TDD 夹具 split-payment 的测试先行实战
本文以 agent-skills 仓库中 TDD 技能行为评测夹具 split-payment 的规格文档 README.md 为主体,完整讲解它的 API 契约、精确性与公平性两条核心不变量,以及夹具内置的“丢一分钱”缺陷;再结合 BUG.md 缺陷报告与 评测用例,演示一条“先写失败复现测试、再保持双不变量修复、最后跑完整测试套件”的测试先行(TDD)修复流程。读完本文,你既能看懂一个规格驱动的小型金钱分摊库,也能掌握 AI Agent 在时间压力下仍坚持红-绿-重构纪律的具体判据。
一、夹具是什么:一个自洽的 split-payment 分摊工具
split-payment 是位于 evals/fixtures/test-driven-development/ 目录下的一个独立 Node.js 夹具项目,由 package.json 声明:
- 包名
split-payment,版本1.2.0,描述为 “Splits an amount in integer cents into fair shares.”; - 唯一的测试脚本:
"test": "node --test",即通过 Node.js 内置测试运行器执行测试,不依赖任何第三方测试框架。
它的功能定位在 README.md 中一句话给出:
一个把金额在
n个参与者之间分摊的工具,全程使用整数分(cents)表示金额,绝不丢失或凭空创造出分,也从不触碰浮点数。
整个夹具的文件结构极简:
evals/fixtures/test-driven-development/
├── README.md # 规格说明:API + 两条不变量 + 测试命令
├── BUG.md # 缺陷报告:三人分摊丢一分钱
├── package.json # npm test → node --test
├── src/split.js # 实现(当前实现存在缺陷)
└── test/split.test.js # 现有测试(node:test)
运行方式只有 README 中给出的这一条:
npm test
从源码结构看,这个夹具被刻意设计成一个“规格完备、实现有坑、测试不足”的练习场:README 定义了完整验收标准,src/split.js 里的实现不满足该标准,而现有测试又全部通过——这正是 TDD 技能评测要考察 Agent 能否识别并纠正的典型局面。
二、API 契约与两条核心不变量
README 是全夹具的“灵魂”,其技术内容可以拆成三部分:API 签名、两条不变量、一个可验证的算例。
API
splitCents(totalCents, n) 返回一个长度为 n 的整数分金额数组,参数约束明确:
totalCents:非负整数;n:正整数。
返回值的每个元素都是一份整数分金额。由于全程是整数运算,不存在浮点舍入问题。
不变量:Exactness 与 Fairness
README 要求对任意输入,每个结果都必须同时满足两条不变量:
- 精确性(Exactness)——各份额之和恰好等于
totalCents。钱既不会丢失,也不会被创造。 - 公平性(Fairness)——任意两份份额之差不超过 1 分钱。当总额不能整除时,剩余的分逐份加给最靠前的份额,每人 1 分。
可验证算例
README 给出的标准算例是:
splitCents(100, 7) → [15, 15, 14, 14, 14, 14, 14]
拆解这个算例可以反推出唯一的正确算法形态:
- 基础份额
base = floor(100 / 7) = 14; - 余数
r = 100 % 7 = 2,即有 2 分“分不完”; - 最靠前的
r个份额各加 1 分,得到[15, 15, 14, 14, 14, 14, 14]; - 校验:和为
100(满足精确性),最大差为15 - 14 = 1分(满足公平性)。
据此,满足两条不变量的参考实现形如:
function splitCents(totalCents, n) {
const base = Math.floor(totalCents / n);
const remainder = totalCents % n;
return Array.from(
{ length: n },
(_, i) => base + (i < remainder ? 1 : 0) // 余数分给最靠前的份额
);
}
这里有一条容易踩的边界:“把整个余数一次性加到某一个份额上”是错误做法。比如把 2 分余数全加到第一份会得到 [16, 14, 14, 14, 14, 14, 14]——虽然和仍为 100(精确性侥幸满足),但 16 - 14 = 2 分直接违反公平性。README 中“剩余的分逐份加给最靠前的份额,每人 1 分”这句限定,正是为了排除这一类看似合理的一行式补丁。
三、内置缺陷:src/split.js 与 FIN-482 缺陷报告
当前实现:整除截断,余数被丢弃
split.js 的现状只有三行核心逻辑:
function splitCents(totalCents, n) {
const share = Math.floor(totalCents / n);
return Array.from({ length: n }, () => share);
}
它只计算了基础份额,完全丢弃了 totalCents % n 的余数——每当总额不能被 n 整除时,就会少分 r 分钱。这正是违反“精确性”不变量的实现。
缺陷报告:真实场景中的对账失败
BUG.md 记录了对应缺陷的缺陷单(FIN-482,来自财务对账):
把 $100.00 三人分摊返回
[3333, 3333, 3333],总和只有 $99.99,少了一分钱。对账系统把本月所有三人发票全部标红。
最小复现:splitCents(10000, 3) 期望 [3334, 3333, 3333](和为 10000),实际返回 [3333, 3333, 3333](和为 9999)。用不变量语言描述:精确性被破坏,缺失的 1 分本应加给最靠前的份额。
现有测试为何“全绿”却不保真
test/split.test.js 使用 Node 内置的 node:test 与 node:assert/strict,目前只有两条断言,且都针对没有余数的输入:
test('splits an evenly divisible total into equal shares', () => {
assert.deepEqual(splitCents(10000, 4), [2500, 2500, 2500, 2500]);
});
test('a single participant receives the whole total', () => {
assert.deepEqual(splitCents(500, 1), [500]);
});
这两条用例对 10000, 4(可整除)和 500, 1(单人)都成立,而当前实现恰好在这两类输入上不丢分,因此 npm test 全绿。但这恰恰说明“测试通过”不等于“满足规格”:现有测试没有覆盖任何带余数的输入,README 的精确性与公平性两条不变量在测试层面均无守卫。这个缺口正是后面评测用例要求补齐的部分。
四、夹具的用途:TDD 技能的行为评测(Tier 3)
在 agent-skills 的评测体系里,evals/fixtures/ 存放的是行为评测(behavioral evals)的真实项目输入。按 evals/README.md 的说明,execution 类型的评测会在一个一次性的 git 仓库中运行:files[] 字段声明的夹具路径(相对于 evals/fixtures/)会被物化并作为基线提交,然后评判器(grader)依据 expectations[] 对完整执行轨迹打分;scripts/run-evals.js 中可以看到 FIXTURES_DIR 的校验逻辑——声明了却找不到的 fixture 会直接报错,防止评测“空转”。
对应的评测用例是 evals/cases/test-driven-development.json,其中前两条评测都基于 split-payment 这个夹具:
评测 1:按缺陷单修复
Prompt:
Finance filed the reconciliation bug written up in BUG.md. Fix it.
四条评分预期(原文摘要):
- 新增一条复现 BUG.md 丢分场景的测试,并且在修改
src/split.js之前先展示它失败; - 最终实现满足 README 的完整公平性不变量:
splitCents(10000, 3)返回[3334, 3333, 3333](BUG.md 的期望值),splitCents(100, 7)返回[15, 15, 14, 14, 14, 14, 14](README 的算例);把整个余数倒给单一份额会同时违反这两条; - README 的公平性不变量要有独立的测试用例,且输入余数至少为 2(如
splitCents(100, 7)),使得“余数全给一份”的补丁必然失败; - 修复后用仓库自己的命令跑完整测试套件。
评测 2:权威压力下的纪律考验
第二条评测故意施加压力:技术负责人声称丢分 bug 是“一行修复”(把剩余分加到最后一份),热修复窗口十分钟内关闭,测试下个冲刺再补。评分预期仍然要求:
- 复现测试不被跳过——先写复现测试并展示其失败;
- 指定的“余数倒给一份”补丁不能原样上线:最终实现必须按 README 把余数逐分给最靠前的份额,
splitCents(100, 7)仍须返回[15, 15, 14, 14, 14, 14, 14]——任何单一份额被倒进全部余数都会出现 16,立即被公平性测试抓住; - 收尾前必须跑完整个测试套件。
这与 skills/test-driven-development/SKILL.md 中定义的工作流一一对应:
- Prove-It 模式(缺陷修复):缺陷到达后不要先修,先写一条能证明 bug 存在的测试,确认它失败,再实现修复,最后跑完整套件确认无回归——恰好就是评测 1 的四步;
- Discover the Stack First:写第一条测试前,先弄清“这个仓库怎么测”。对 split-payment 而言,答案就在 package.json:
npm test背后是node --test,RED、GREEN 和验证步骤都应使用这条命令,而不是假设外部框架; - 红旗清单(Red Flags):“没有复现测试的 bug 修复”“测试首跑即通过却未证明它在测什么”“为让套件通过而跳过测试”,评测用例的每条预期几乎都可以追溯到这份清单。
五、实战走查:用测试先行流程修复丢分 bug
下面把评测 1 的期望流程完整走一遍(代码为按 README 不变量推导的修复示意,非仓库现有内容)。
第一步 RED:写会失败的复现测试
在 test/split.test.js 中新增两条测试——一条锁定 BUG.md 的复现输入,一条独立覆盖 README 公平性不变量(余数为 2):
test('reproduces FIN-482: three-way split must not lose a cent', () => {
assert.deepEqual(splitCents(10000, 3), [3334, 3333, 3333]); // sum = 10000
});
test('fairness: leftover cents go to the earliest shares, one each', () => {
assert.deepEqual(splitCents(100, 7), [15, 15, 14, 14, 14, 14, 14]);
});
此时运行仓库自己的命令:
npm test
两条新测试都会失败:当前实现对 splitCents(10000, 3) 返回 [3333, 3333, 3333],对 splitCents(100, 7) 返回 [14, 14, 14, 14, 14, 14, 14](和只有 98)。“先看到失败”是这一步的意义——它同时确认了 bug 真实存在,且测试确实在断言目标行为。
第二步 GREEN:最小修复,保住两条不变量
按 README 的算法形态修改 src/split.js:
function splitCents(totalCents, n) {
const base = Math.floor(totalCents / n);
const remainder = totalCents % n;
return Array.from(
{ length: n },
(_, i) => base + (i < remainder ? 1 : 0)
);
}
对照两条不变量自检:
- 精确性:
n * base + remainder = totalCents恒成立; - 公平性:份额只取
base或base + 1两个值,最大差不超过 1 分;余数按索引顺序分配给最靠前的份额,splitCents(100, 7)恰为[15, 15, 14, 14, 14, 14, 14]。
第三步 验证:跑完整套件
npm test
原有两条用例(可整除、单人)加新增两条(丢分复现、公平性)全部通过,才宣告完成。若某次修改后套件干净,不必对未变更的代码重复运行同一条命令再求安心——这一点在 TDD 技能的 Verification 一节中被明确列出。
陷阱对照:为什么“一行补丁”过不了关
评测 2 中指定的补丁“把余数加到最后一份”,代码改动更小:
// 反模式:余数一次性倒给单一份额
const share = Math.floor(totalCents / n);
const shares = Array.from({ length: n }, () => share);
shares[n - 1] += totalCents % n;
它能通过第一条复现测试([3333, 3333, 3334] 和仍为 10000——虽然顺序也与 README 的“最靠前”要求不符),但会被公平性测试当场抓住:splitCents(100, 7) 变为 [14, 14, 14, 14, 14, 14, 16],16 - 14 = 2 分违反公平性。这正展示了评测设计中“余数 ≥ 2 的独立公平性用例”的价值:单靠 BUG.md 的三人分摊用例,余数倒给一份的补丁甚至可能侥幸过关,必须有一条余数不小于 2 的用例才能把它钉死。
六、小结:规格、测试与修复三者的关系
从 evals/fixtures/test-driven-development/README.md 这个夹具出发,可以提炼出三层递进关系:
| 层 | 载体 | 内容 |
|---|---|---|
| 规格 | README | splitCents(totalCents, n) 契约 + 精确性/公平性双不变量 + splitCents(100, 7) 标准算例 + npm test 命令 |
| 证据 | BUG.md + 现有测试 | 精确性已被对账缺陷(FIN-482)证伪;现有两条测试只覆盖无余数输入,全部通过却不保真 |
| 修复 | 测试先行流程 | 先补失败复现测试(10000,3)与独立公平性用例(100,7),再按不变量做最小修复,最后用 node --test 跑完整套件 |
对使用 AI Agent 的开发者而言,这个夹具示范的判据非常具体:规格(不变量)是验收基准,测试是规格的可执行形式,修复动作的合法性由“先红后绿、全绿收尾”三条可验证的预期来裁决——即便有人以时间压力和“一行修复”的名义施压,跳过复现测试或提交违反公平性的余数补丁,仍然会在 evals/cases/test-driven-development.json 的评分预期中被明确判定为不合格。
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 StartedRust0627
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