网易首页 > 网易号 > 正文 申请入驻

三行0.30的发票,新旧代码差了一分钱

0
分享至

三行净额0.30、税率19%的发票,旧代码每行先四舍五入到分再相加,账单是1.08;新代码把原始金额加总后只舍入一次,账单变成1.07。测试全绿,账单少了一分钱。


Agent 的最后一条消息读起来很漂亮:“把发票总额计算从 InvoiceController 挪进了 InvoiceTotals 用例。无行为变更。所有测试通过。”你去查,测试确实通过。问题在于,测试可能漏掉了那一处变化。

旧控制器把每行的毛额舍入到分,再累加。新类把原始金额相加,最后舍入一次。三行 0.30 净额、19% 税:每行是 0.357,舍入成 0.36,旧代码收 1.08;新代码加出 1.071,舍入成 1.07。如果你的测试夹具里每张发票都只有一行,两个版本结果一致,套件保持绿色,而账单悄悄掉了一分钱。

绿色的套件只证明这次重构没有破坏测试所检查的东西,它对没人写测试的输入一无所知。GitHub 的 Scientist 是一个“用于谨慎重构关键路径”的库,它的 README 里写着:“测试可以帮助引导你的重构,但你真正想要的是在负载下比较当前行为与重构后的行为。”

今天就能做一个本地的小型版本:把一个 worktree 检出到起点,另一个放 agent 的分支,把同一批录制好的输入分别回放给两边,遮掉每次运行都会变的部分,再 diff 输出。下面这套脚本大约 50 行 bash,适用于大多数 CLI 或 HTTP 服务,只需要 git、jq 和 curl。

第一步:把起点放进独立 worktree

git worktree 是链接到同一仓库的第二个工作目录。你需要一个给 agent 的分支(可能已经有了),另一个给 agent 起步时所在的那个提交。

要跟那个起始提交比,而不是跟今天的 main 比。如果 agent 干活期间 main 动过,跟它 diff 会把 agent 的改动和别人的改动混在一起。git merge-base 能找到分支分叉的那个提交:

  • cd ~/code/app
  • base=$(git merge-base main hermes/extract-invoice-totals)
  • git worktree add --detach ../app-base "$base"
  • cp .env ../app-base/.env
  • (cd ../app-base && composer install -q)

传提交哈希而不是分支名,可以绕开 git 的一条规则:一个分支同一时间只能在一个 worktree 里检出。文档说,当 commit-ish“是分支名且已被另一个 worktree 检出”时,add 会拒绝。新建的 worktree 只含被跟踪的文件,所以 .env 和 vendor/ 这类被忽略的东西得复制或安装,也就是上面最后两行做的事。

同样的输入,两个版本的代码,一份 diff。用完 git worktree remove ../app-base 清理。按文档,除非加 --force,它拒绝删除有修改或未跟踪文件的 worktree。

第二步:录制像生产环境的输入

cases 文件每行一条 shell 命令。每行在每个 worktree 里各跑一次,HTTP 用例的 APP_URL 指向那一侧的服务器。以 # 开头的行跳过。

  • php artisan invoice:show 1001 --json
  • php artisan invoice:show 1002 --json
  • php artisan invoice:show 1003 --json --currency=EUR
  • php artisan invoice:show 999999 --json
  • curl -s -w "\n%{response_code}\n" "$APP_URL/api/invoices/1001"
  • curl -s -w "\n%{response_code}\n" "$APP_URL/api/invoices/abc"

invoice:show 命令代表你的应用暴露的任何 Artisan 命令或 CLI。把无聊的用例、多行的用例、不存在的 ID 和格式错误的 ID 都放进去。curl 的 -w 在传输结束后写出文本和信息,所以 %{response_code} 把状态码放进输出,一个变成 500 的 200 就会在 diff 里现形。

HTTP 输入不必自己编。nginx 默认的 combined 日志格式把方法和路径放在第 6、7 个字段,真实的 GET 路径可以直接从访问日志里捞出来:

  • awk '$6 == "\"GET" { print $7 }' access.log \
  • | sort -u | head -200 \
  • | sed 's|.*|curl -s -w "\\n%{response_code}\\n" "$APP_URL&"|' \
  • >> tests/diff/cases.txt

提交之前先读一遍结果。查询字符串可能带着 token 和个人数据,路径里有 $ 会被 shell 展开。除非两边各有自己的数据库副本,否则只保留 GET:回放一个 POST 会改掉下一个用例读到的行。

第三步:脚本

存成 scripts/difftest.sh,chmod +x,在 agent 开始之前连同 cases 文件一起提交到 main。

脚本开头用 set -euo pipefail,用 mktemp -d 建临时目录并用 trap 清理。它定义了几条正则:日期、时钟、时区拼成的时间戳,以及 UUID。如果 cases 里有非注释行带 APP_URL,而 BASE_URL 或 CAND_URL 没设,脚本直接报错退出。

normalize 函数先判断文件是不是 JSON,是就用 jq -S 排序输出(可加 JQ_FILTER),不是就原样 cat,然后用 sed 把时间戳和 UUID 替换成空格。

run_case 函数进入指定目录,把 APP_URL 设成对应侧地址,用 bash -c 执行命令,输出重定向到文件,失败时追加 exit= 状态码。

主循环逐行读 cases,跳过空行和注释,给每个用例编号,在 base 和 cand 各跑一次。如果这一行带 APP_URL 且两边输出里都有 exit=,打印“NO RESPONSE”并计为失败。否则用 diff -u 比较两份输出,有差异就打印“DIFF”和具体差异,并计失败数。

脚本跑在分支自己的 worktree 里,任务作为 agent 的第一个提示词。agent 可以是 Claude Code、Codex 或其他 CLI agent,那就是第一步里已经建好的第二个 worktree。

Hermes 会从你的仓库读 .hermes/worktree.toml 配方。它的 done_when 列表存放任务的默认检查,按 README 的说法,“你自己的检查可以决定任务何时完成”。copy 只接收 git 忽略的文件,所以 .env 必须在里面。

  • setup = ["composer install -q"]
  • copy = [".env"]
  • done_when 里放 composer test,以及一条带 BASE_URL、CAND_URL 和 difftest.sh 的多行命令

三引号字符串是 TOML 的多行形式:行尾的反斜杠会去掉换行和其后的缩进,所以第二条检查是一条命令。

在开始任务之前先建好 base worktree,否则第一条检查没有脚本可跑。此时任务起始分支的顶端就是 agent 将要分叉的提交,所以 git worktree add --detach ../app-base main 给出的就是第一步那个文件夹。

不传第三个参数时,脚本从 base 文件夹读 cases。脚本和 cases 都来自 base worktree,它位于 agent 的 worktree 之外。否则一个被告知“让这个检查通过”的 agent 可以改 cases.txt 直到 diff 安静下来。那个 . 假定检查从任务的 worktree 运行,也就是 composer test 必须运行的地方。如果你的检查从别处启动,就传 worktree 的路径。Review Desk 的 Worktrees 标签页会列出任务 worktree 及其分支。

HTTP 用例需要两个服务器分别跑在那两个 URL 的端口上。Hermes 既能开普通 shell 也能开 agent,分屏把两者并排放:一边 base 服务器,一边分支的服务器。检查通过后,Review Desk(⌘G 或 Ctrl+Shift+G)按轮次展示改动,Land 会提交、开 pull request 或合并回任务起始的分支,并支持撤销。

只对承诺无行为变更的任务把 diff 放进 done_when。功能任务上 diff 本来就该非空,一个见差异就失败的检查会挡住你要求的那个改动。这类任务手动跑脚本,把 diff 当审查辅助来读。

这套东西会在哪里断

diff 只覆盖你录下来的输入,仅此而已。如果 cases 文件里没有多行发票,开头那个舍入变化照样溜过去。把精力花在 cases 文件上,每有一个 bug 进到生产就补一条。

有副作用的东西需要隔离。邮件、队列任务、支付和写共享数据库都会跑两次,每边一次。把两个 worktree 指向假实现或各自的副本,或者把这些路径留给测试套件。

遮不掉的不确定性会让检查一直红。数据库返回的无序列表可以在 JQ_FILTER 里排序,随机采样需要固定种子。如果结果依赖墙上时钟,删掉这个用例。时间戳正则也很粗:它遮掉输出里每一个日期时间,包括重构可能弄坏的那个。如果日期对被改的代码很重要,就在 JQ_FILTER 里按字段名遮。

开头那个重构——把逻辑从 Laravel 控制器挪到端口后面的用例——正是 Decoupled PHP 讲的动作:一次一片地把领域从框架底下挪出来。像这样的差分检查,就是用来证明每一片挪动之后行为没变的,不管是你挪的还是 agent 挪的。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
人肉盾牌的铁证:哈马斯计划在加沙难民营引爆炸弹,制造混乱掩护恐怖分子逃脱

人肉盾牌的铁证:哈马斯计划在加沙难民营引爆炸弹,制造混乱掩护恐怖分子逃脱

老王说正义
2026-10-20 00:48:04
至今健在的开国将帅只剩一位,虽然已102岁高龄,但好在身体健朗

至今健在的开国将帅只剩一位,虽然已102岁高龄,但好在身体健朗

听风喃
2026-10-16 15:22:21
唏嘘,郎平再赴上海看望汤淼,19年没断过,周苏红也有情有义!

唏嘘,郎平再赴上海看望汤淼,19年没断过,周苏红也有情有义!

体坛侃侃球
2026-10-19 09:50:17
上海又捡到宝了?麦基从质疑到统治内线,这么下去内线实力暴涨!

上海又捡到宝了?麦基从质疑到统治内线,这么下去内线实力暴涨!

篮球资讯达人
2026-10-19 22:07:03
1939年,李德返回苏联后,对在中国的老婆和年幼的儿子不管不顾

1939年,李德返回苏联后,对在中国的老婆和年幼的儿子不管不顾

古书记史
2026-01-12 21:59:25
“天生的主任医师料子!”女儿挑芝麻视频火了,网友:前途亮得睡不着

“天生的主任医师料子!”女儿挑芝麻视频火了,网友:前途亮得睡不着

熙熙说教
2026-10-18 14:24:05
一个家庭最大悲哀,就是人到70多岁,却还在和外人随便说6件事

一个家庭最大悲哀,就是人到70多岁,却还在和外人随便说6件事

游戏收藏指南
2026-10-20 00:34:31
出轨女子第二波聊天记录曝光,虎狼之词惊呆吃瓜网友,她嫌丢人开始批量投诉了

出轨女子第二波聊天记录曝光,虎狼之词惊呆吃瓜网友,她嫌丢人开始批量投诉了

汉史趣闻
2026-09-01 17:55:01
记者假扮俄军工买家,一开口要500台,中国公司开价1950万元

记者假扮俄军工买家,一开口要500台,中国公司开价1950万元

桂系007
2026-10-15 16:31:21
美国话音刚落,巴铁宣布封关,双向封锁已成型?就等伊朗认输

美国话音刚落,巴铁宣布封关,双向封锁已成型?就等伊朗认输

民间胡扯老哥
2026-10-19 06:59:05
为什么说“下一个伯德”全都失败了?因为根本没人能复制这种怪物

为什么说“下一个伯德”全都失败了?因为根本没人能复制这种怪物

贵州小娟
2026-10-19 18:45:12
不管你信不信,能吃能喝的老人,基本在71岁,已经不做这4件事了

不管你信不信,能吃能喝的老人,基本在71岁,已经不做这4件事了

新时代的两性情感
2026-10-19 03:06:11
李小冉节目上首谈前夫,坦诚公开离婚的原因,谈及过往委屈到落泪

李小冉节目上首谈前夫,坦诚公开离婚的原因,谈及过往委屈到落泪

铁锤妹妹是只猫
2026-09-12 05:20:00
雷佳音近况公开,留及肩长卷发,网友:与早年“陈俊生”形象判若两人

雷佳音近况公开,留及肩长卷发,网友:与早年“陈俊生”形象判若两人

韩小娱
2026-10-17 16:11:30
太突然,风向变了?很多人开始批评孙颖莎,再次印证了樊振东的话

太突然,风向变了?很多人开始批评孙颖莎,再次印证了樊振东的话

探长影视解说
2026-10-19 19:28:32
德米凯利斯:曾和迪奥曼德谈过几次,他明确表示想再进一步

德米凯利斯:曾和迪奥曼德谈过几次,他明确表示想再进一步

懂球帝
2026-10-19 22:02:18
1日内双喜临门!76人2大悍将即将复出,詹姆斯和恩比德能松口气了

1日内双喜临门!76人2大悍将即将复出,詹姆斯和恩比德能松口气了

包饺子ai剪辑
2026-10-19 19:10:33
欧洲领导人:如果我们想早上吃牛角包、喝咖啡,就必须支持乌克兰

欧洲领导人:如果我们想早上吃牛角包、喝咖啡,就必须支持乌克兰

麓谷隐士
2026-10-19 00:10:04
新型穷人正席卷中国社会:有房子,身价千万,对城市没有归属感

新型穷人正席卷中国社会:有房子,身价千万,对城市没有归属感

铭记历史呀
2026-10-18 10:38:20
看看加拿大老人退休后的生活,再看看中国老人,真的没法比!

看看加拿大老人退休后的生活,再看看中国老人,真的没法比!

江启
2026-10-15 23:33:12
2026-10-20 03:16:49
爬虫饲养员
爬虫饲养员
业余养了只叫“龙虾”的AI爬虫,主业是给互联网打工。
188文章数 86关注度
往期回顾 全部

科技要闻

王晟:近千亿资金涌入后 具身智能开始退烧

头条要闻

中国留学生披旭日旗事件最新进展:目前已被遣返

头条要闻

中国留学生披旭日旗事件最新进展:目前已被遣返

体育要闻

从NBA退役10年后,他真去警局上班了

娱乐要闻

奚梦瑶回应重返维密:还是很开心的

财经要闻

黄奇帆之子往事:从广微系到魏桥的跳跃

汽车要闻

不知道二代海鸥多好玩? 上赛道跑两圈 很过瘾的

态度原创

艺术
教育
亲子
时尚
数码

艺术要闻

俄罗斯90后摄影师比莉娅娃英这组写真,看完我直接跪了!最后一张绝了!

教育要闻

1359 个市级指标藏在哪里?初中三年铺路地图

亲子要闻

无论和谁相处,这八个忠告一定要牢记!

中国第一位女指挥:97岁,不退休,不退场

数码要闻

2026网易未来公开课开启:第五期对话王晟

无障碍浏览 进入关怀版