首页
/ 不变量、复现测试与“一行修复”的陷阱:agent-skills TDD 夹具 split-payment 的测试先行实战

不变量、复现测试与“一行修复”的陷阱:agent-skills TDD 夹具 split-payment 的测试先行实战

2026-09-04 19:18:43作者:廉彬冶Miranda

本文以 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 要求对任意输入,每个结果都必须同时满足两条不变量:

  1. 精确性(Exactness)——各份额之和恰好等于 totalCents。钱既不会丢失,也不会被创造。
  2. 公平性(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:testnode: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.

四条评分预期(原文摘要):

  1. 新增一条复现 BUG.md 丢分场景的测试,并且在修改 src/split.js 之前先展示它失败
  2. 最终实现满足 README 的完整公平性不变量:splitCents(10000, 3) 返回 [3334, 3333, 3333](BUG.md 的期望值),splitCents(100, 7) 返回 [15, 15, 14, 14, 14, 14, 14](README 的算例);把整个余数倒给单一份额会同时违反这两条;
  3. README 的公平性不变量要有独立的测试用例,且输入余数至少为 2(如 splitCents(100, 7)),使得“余数全给一份”的补丁必然失败;
  4. 修复后用仓库自己的命令跑完整测试套件。

评测 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.jsonnpm 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 恒成立;
  • 公平性:份额只取 basebase + 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 的评分预期中被明确判定为不合格。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388