10项单元测试,ruff和mypy全过,被AI代理当生产工具用了好几天,Docker镜像都推到Hub上了——然后他写了个demo脚本,第一次运行,不到一分钟,HTTP 400。
gemini-3.1-flash-lite-image 只接受 thinking_level 的 'low' 和 'high',服务器提交了个 'minimal'。
【灯光渐暗】
如果你觉得"测试全绿还能出这种丑闻"很玄幻,那是因为你还没看到更玄幻的那层:服务器里本地维护了一个允许值列表,写着 'minimal'、'low'、'medium'、'high' 四个值,而真实API只要两个。
更离谱的是默认值设成了 'medium'。这意味着所有没显式覆盖这个参数的实时调用,都会必然而确定地收到HTTP 400。
看到这个默认值的时候,我脑子里冒出来的第一个问题是:那10项单元测试到底在测什么?
在测mock。mock对所有输入都返回成功,所以测试通过只能证明一件事——服务器会忠实转发 'medium' 这个值。它转发得准确、优雅、格式完全合规。
唯一美中不足的是,'medium' 在真实API里根本不存在。
于是这套测试的结论可以翻译成:你造了一台能把错误的工件准确送达目的地的机器。工件类型是对的,协议是对的,流程是感人肺腑的。
有个细节单拎出来特别好笑:这台服务器的三个消费者里,有一位是跑在gemini-2.5-flash上的ADK agent,通过MCP导入工具,Gemini调用Gemini。整个生态全是自己人在接力,最后被自己人写的一个 'medium' 卡在半路——素材没说它到底踩没踩中,但我单方面宣布它是本案最大嫌疑人。
【关于默认值】
作者复盘的时候提到要专门测默认参数。这建议我举双手赞成,因为默认值是测试盲区中的盲区。
写测试的人总是勤勤恳恳把所有参数显式传进去,一个不落;真实用户总是能省则省,剩下的都交给API决定。默认值被夹在中间,没人特意传它,也没人特意信它——直到它把你某个平静的下午变成一个线上事故。
修复过程只花了大概十分钟,从报错到新镜像上传Docker Hub。作者说主要归功于API的报错信息直接列出了允许值是 "low, high",一句话就把路指明白了。
你品品这个对比:报错信息两秒钟给出答案,测试套件全程给出了零个有效信号。
全流程里唯一和真实API说过话的程序,是那个十分钟前刚写好的demo.sh。
它不是测试,不是静态检查,不是AI代理的日常使用——它就是一路边脚本,跑一遍就拉倒,但它成了整个CI体系里最诚实的那个。
修起来倒也不复杂:改掉允许值列表,默认值改成 'low',加上一条回归测试确保 'medium' 在本地直接被拦,然后同步三个工具的函数签名、docstrings、get_help、所有文档,重新构建并推送镜像。十分钟,收工。
【后日谈】
真正让我没法假装没看见的,是那个本地允许值列表。
在代码里维护一份远程API的允许值清单,这个动作本身就注定会过期。功能每季度悄悄换代,模型今天不打招呼就改参数——你的列表安安静静躺在源码里,没有任何更新它的欲望。
你以为你在做防御,其实你在给未来埋一个"全绿事故"。
这大概就是为什么要保留一个廉价的实时冒烟调用。换成人话说:让代码定期跟真实世界对个表,别让测试套件在真空里自嗨。demo脚本不值得尊敬,但它戳破了一个挺扎心的事实——你那一整套质量保障,没有一个环节在跟远程契约直接对话。
(友情提示:如果你的CI全绿,但代码已经三个月没连过真实API,你电脑里可能也躺着一个正在等爆的'medium'。我没查过你的代码——我只是单纯地有点担心。)
