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

三行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-19 12:54:29
45岁仍拍三级片还债,36E巨乳,被称香港妖女,60岁直播翻身破亿

45岁仍拍三级片还债,36E巨乳,被称香港妖女,60岁直播翻身破亿

轩逸阿II
2026-10-17 07:39:17
涨了95.7万倍的老登股,又创新高!

涨了95.7万倍的老登股,又创新高!

格隆汇
2026-10-19 16:04:23
只赔偿蒙古,无视中国损失!中蒙边境大火竟是日本人所造成的!

只赔偿蒙古,无视中国损失!中蒙边境大火竟是日本人所造成的!

故事终将光明磊落
2026-10-19 12:36:35
墨西哥女孩不听亲友劝阻远嫁中国,断了往来,5年后全家求援助

墨西哥女孩不听亲友劝阻远嫁中国,断了往来,5年后全家求援助

雅俗共赏1
2025-09-03 14:00:28
二十届五中全会10月26日召开!议程只有一个重点

二十届五中全会10月26日召开!议程只有一个重点

兰妮搞笑分享
2026-10-19 18:54:05
国产手机集体降价,力度有点大啊!

国产手机集体降价,力度有点大啊!

果粉俱乐部
2026-10-17 13:55:06
“这就是穿碳板鞋的下场”,初中男孩走路姿势给人看懵了,家长全然不知

“这就是穿碳板鞋的下场”,初中男孩走路姿势给人看懵了,家长全然不知

熙熙说教
2026-10-13 16:28:55
上海服务员月入4000,社保个人缴纳792,企业缴纳1924,谁是赢家.

上海服务员月入4000,社保个人缴纳792,企业缴纳1924,谁是赢家.

慧翔百科
2026-10-19 11:40:00
日本老人的“极简晚餐”,看完后才明白,日本为何平均寿命这么高

日本老人的“极简晚餐”,看完后才明白,日本为何平均寿命这么高

白米饭怎么吃
2026-10-19 16:48:25
英国小两口在拼多多买了个花园拱门,收到包裹懵逼了!网友:这就是为什么有些人不该获得选举权的原因...

英国小两口在拼多多买了个花园拱门,收到包裹懵逼了!网友:这就是为什么有些人不该获得选举权的原因...

悦居英国
2026-10-16 18:52:20
西方媒体:就算中国全力以赴,也不可能按时建成这么庞大的工程

西方媒体:就算中国全力以赴,也不可能按时建成这么庞大的工程

游文刀
2026-10-16 23:36:56
男子在高速上故意开80km/h 与大货车并排压速堵路

男子在高速上故意开80km/h 与大货车并排压速堵路

闪电新闻
2026-10-19 12:45:17
北控欲掀翻上海,斩获两连胜,杀招已就位

北控欲掀翻上海,斩获两连胜,杀招已就位

体育篮球弟
2026-10-19 04:31:44
被CBA菜鸟教练拿捏!球迷斥李楠:李昂有你这阵容能虐你35分

被CBA菜鸟教练拿捏!球迷斥李楠:李昂有你这阵容能虐你35分

弄月公子
2026-10-19 11:32:05
医生警告:每天吃一块红薯,就等于给肺结节 “踩刹车”? 真相来了

医生警告:每天吃一块红薯,就等于给肺结节 “踩刹车”? 真相来了

路医生健康科普
2026-10-19 09:11:10
不查不知道,坐拥北京山庄,有4个孙子孙女的陈佩斯,私下有多壕

不查不知道,坐拥北京山庄,有4个孙子孙女的陈佩斯,私下有多壕

仙味少女心
2026-10-13 01:54:48
伊朗终于找到美国的痛处!不打航母不炸油田,直接盯上一个命门

伊朗终于找到美国的痛处!不打航母不炸油田,直接盯上一个命门

扬子的故事屋
2026-10-18 11:23:53
屡教不改?韦世豪现身海港看台,怒指场内骂不停,此前被禁赛5场

屡教不改?韦世豪现身海港看台,怒指场内骂不停,此前被禁赛5场

球场没跑道
2026-10-19 09:01:20
国防部:中方将以有力行动遏制任何祸乱南海的行径

国防部:中方将以有力行动遏制任何祸乱南海的行径

新京报
2026-10-16 15:10:20
2026-10-19 21:55:00
爬虫饲养员
爬虫饲养员
业余养了只叫“龙虾”的AI爬虫,主业是给互联网打工。
243文章数 86关注度
往期回顾 全部

科技要闻

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

头条要闻

女子随口一句"姐过得不太好" 8年前帮过的小伙飞来送钱

头条要闻

女子随口一句"姐过得不太好" 8年前帮过的小伙飞来送钱

体育要闻

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

娱乐要闻

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

财经要闻

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

汽车要闻

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

态度原创

教育
时尚
家居
本地
数码

教育要闻

全国精神残疾高中在读仅 723 人:6 条容易被忽略的升学路径,很多家长都不知道

秋天衣服别穿全黑,看看这些卡其色的单品,简单大方又百搭

家居要闻

2026建博会(广州) 公装联探展交流活动

本地新闻

因新荣记出圈,这座浙东小城才是吃货天堂

数码要闻

VGN联合腾讯天美工作室推出V98Pro V4 TiMi限定款机械键盘,首发价419元

无障碍浏览 进入关怀版