[控场AI]
· 8 分钟阅读· 4,455 字

Grok Bot⑭|排序Bug修没修好,怎么证明?

Grok Bot⑭|排序Bug修没修好,怎么证明?

用数字排序故障案例,讲清如何以可复现证据链而非口头声明验收修复。

文章以B站UP主「马上花生」Grok Bot第十四课中的数字排序故障为例,示范一套可迁移的验收方法论。核心观点是:「修好了」不构成验收,必须提供可独立复现的证据链。案例中默认排序把元素转字符串比较,结果从 `[12, 2, 1]` 得到 `[1, 12, 2]`,且会原地修改并返回同一数组,因此验收需同时覆盖顺序、原输入不变、新引用三项要求。候选修复通过复制数组与数值比较器解决前两项,但不做类型校验,未实现的需求不能算作已验收。文章还强调最小附现是收缩而非删光条件,要保留版本、输入与前提;无法复现时记录差异、交回可确认部分;报告需写明未验证项,测试通过不等于已提交或上线。

「排序坏了」「修好了」——这两句话放在一起,不构成任何有效的验收。真正能让人放心的,是一份可独立复现的证据链。B站UP主「马上花生」在 Grok Bot 系列第十四课里,用一个数字排序故障作为教学案例,拆解了什么叫「有用的最小附现」,以及如何用证据而不是口头声明来验收一个修复。

这期的价值不在于排序算法本身,而在于它示范了一套可迁移的验收方法论:从复现、收缩、断言到报告,每一步都在对抗「看起来通过了」的假象。

为什么要用「故意写的Bug」做教学

课程一开始就明确了一个边界:本课故意编写的数字排序故障,不是 Grok Bot 的产品问题。代码在讲师本机运行验证,为启动真实 Bot 或 Cloud Agent 交付的命令和结果也明确记录。

这个声明本身很重要。它把「教学代码的行为」和「真实仓库的状态」严格切开——讲师本机实测的旧失败与候选通过记录在附件里,它证明的是教学代码的行为,不证明 Bot 已经修了,也没有一份真实的线上故障因此被关闭。

换句话说,这一课教的不是「怎么修排序」,而是「怎么证明一个修复真的发生了」。

先看默认排序的真实行为

案例的核心是一个看似简单的函数调用:直接对数组 [12, 2, 1] 调用默认排序。

结果不是 [1, 2, 12],而是 [1, 12, 2]。原因在于默认规则会先把元素转换成字符串,再按字符串顺序比较,而不是按数值大小排序。

还有一个容易被忽略的副作用:这个调用会原地改变输入数组,并且返回同一个数组引用。

这才是一份有用的最小附现

这意味着需求不只是「顺序正确」。至少有三条验收要求同时成立:按数字升序排列、原数组保持不变、返回结果是一个新的数组引用。只看显示顺序,就会漏掉副作用这一层。

默认排序为什么会把数字转成字符串?

JavaScript 的 Array.prototype.sort 在不传入比较函数时,会把每个元素用 ToString 抽象操作转成字符串,然后按 UTF-16 码元顺序逐位比较。所以 [12, 2, 1] 先变成 ["12", "2", "1"],第一位字符分别是 1、2、1,"12" 因第二位 2 小于 "1" 的比较结果而被排到 "2" 前面,得到 [1, 12, 2]。这个规则对 [10, 9] 之类的数组同样会出问题,因为 "10" 排在 "9" 前。它是规范层面定义的默认行为,不是引擎 bug,但在数值场景下几乎总是错的。要按数值排序,必须显式传入 (a, b) => a - b 这样的比较函数,否则得到的是字典序。

「原地改变」与「返回同一引用」意味着什么?

sort 是少数几个既修改原数组又返回该数组本身的方法之一,许多开发者误以为它像 map、filter 那样返回新数组。这个副作用在真实代码里会导致连锁问题:调用方原本持有的数组被悄悄重排,下游逻辑读到的是已变更的顺序。因此验收时不能只看函数的返回值,还要检查输入数组在调用前后是否一致、两个引用是否相等。这也是案例中强调「顺序、原输入、新引用」三条验收要求同时成立的原因。测试可以分别断言 deepEqual(input, originalCopy) 与 input !== result,用三项独立检查覆盖完整行为。

收缩问题,而不是删光条件

讲师的建议是把问题收缩成一个函数调用与一组输入,减少干扰,再验证它仍能出现相同错误——而不是把条件删光之后说「不再失败」。

在真实项目里做收缩时,要保留版本、输入与前提,并记录删掉了什么、保留了什么,让别人能独立运行并看到同样的差异。这才是一份有用的最小附现。

前提也要写清楚。本课的前提是有限数字构成的数组,负数、零、重复值与空数组都在这个前提范围内。至于字符串、非数字值、无穷大和空值怎么处理,应该先问业务规则,不要在一个排序小修复里偷偷增加类型转换。

候选修复做了两件事,但没说做了第三件

修复候选的逻辑是:先复制数组,再用两个值相减作为比较器按数值排序。

复制保护原数组

复制保护了原数组,比较器解决了默认字符串顺序的问题。但它本身并不执行有限数字类型校验。真正要拒绝非法输入,应该另行定义规则与测试——本课没有把这项后续需求算作已经实现。

把前提和实现分别写进报告,是为了避免一种常见混淆:把「在限定范围内通过」误读成「所有输入都正确」。

12个断言,不能只打印「正确列表」

回归覆盖的用例包括原始错误、负数、0、重复值和空数组,每一例都要验证三件事:结果顺序、原输入未变、返回新引用。三者合计共12个断言。

不能只打印正确列表就说通过

候选在本机 12 个断言通过。原始用例中的输入仍是 [12, 2, 1],返回正确结果与保持旧输入是两件同时成立的事,验收记录要分别体现。

验收记录要包含:函数、候选代码、环境与运行命令,保存实际失败和通过输出。换了代码之后,旧日志就不是新版本的输出,而是新版本的证据——拿一个历史截图不能完成当前验收。

什么是「断言」?它和打印结果有何区别?

断言是一段可自动判定真假、并在为假时明确报错的语句,例如 assert.deepEqual(result, [1, 2, 12]) 或 assert.notEqual(result, input)。打印只是把值写到控制台,需要人眼比对,且无法在回归时自动失败。打印「正确列表」尤其危险:看到一列看似正确的输出,容易误以为覆盖了所有条件,而实际上顺序、原输入不变、新引用这三类检查往往只覆盖了其中一项。把每条要求写成独立断言,失败时能定位到具体属性,且能纳入持续集成重复执行。示例中 4 个输入用例乘以 3 条验收要求,得到 12 条断言,这个数字本身就是覆盖度的量化说明。

回归测试为什么强调「原始错误」也要保留?

修复之后如果只跑新增的边界用例,原始失败可能因为环境差异而仍然存在。把最初的 [12, 2, 1] 作为回归用例之一,等于把「已修复的 bug」钉在测试集里,防止未来重构时再次引入相同的字符串比较问题。这也解释了「旧日志不等于新证据」:修复前的失败输出只能说明旧代码失败,修复后的通过输出必须来自当前代码版本,两者不能互相替代。

不可复现时,别把问题关掉

如果相同输入在你的环境没有失败,先记录代码版本、命令和现象,对比用户条件哪里不同。无法复现不等于用户错了,也不能随手关闭问题。

可以补具体版本、最少步骤或脱敏输入;对需要真实账号的路径,说明缺少权限,交回当前可确认的部分,避免扩大范围或用猜测填上原因。

交回当前可确认部分

这一段的实操建议是:可复制任务要求先整理报告,再给修复候选与回归,返回代码、命令、实际输出和未验证项,限定文件范围,停在可审查结果,不合并或上线。有真实环境才填写实际运行记录,只有课程材料就不要写教师参考。

写报告时,把「没验证的」也写进去

报告的结构大致是:环境、代码版本、原始输入与准确调用步骤,然后写期望和实际。日志、截图或文本输出要能对应这次运行。另列影响范围与尚未核实的条件。

附在报告里的测试范围也要说清楚:事例通过并不覆盖网络、权限、真实页面或所有非法输入。没有验证的条件保留为未验证,不因为能运行就写「全部没问题」。

官方支持案例建议附现步骤与坏状态证据,但这不代表任意内部网站都无需访问权限。需要账号、环境或数据时先确认;没有材料就列缺口,不伪造截图。这份案例不需要页面、数据库或网络。

「坏状态证据」与「脱敏输入」分别指什么?

坏状态证据是在问题发生现场捕获的可观察材料,例如出错时的截图、控制台日志、网络响应或数据库快照。它记录的是系统在特定条件下的真实行为,而不是事后根据记忆复述的描述。附上它可以让接收方独立判断现象,不必依赖报告者的主观解释。脱敏输入则是在提交可复现材料时,把真实数据替换成结构等价但去除敏感信息的内容,例如把手机号、用户名、订单号替换成占位符,同时保持数据类型、长度与边界特征一致,使问题仍能复现而隐私不泄露。

「限定文件范围、停在可审查结果」是什么意思?

这指的是在交付修复候选时,只改动与问题直接相关的文件和函数,不顺手重构、不扩大改动面,并把工作停在「可被审查」的状态——附上代码、命令与输出,等待评审,而不是自行合并或上线。这样做的目的是让审查者能在受控范围内确认修复,防止一笔改动连带引入未经验证的变更,也避免把「已通过本地测试」错误地等同于「可以进入生产」。

这套方法能迁移到哪里

把这一课的流程抽象出来,是一份通用的验收模板:输入与步骤明确,旧版证明失败,同一断言证明候选变化,再补临近边界和副作用核对。

交回的不是一句「已经修好」,而是一份可独立检查的证据。下一集,讲师会把这份证据带到 PR 与持续集成环节,检查当前提交和必须检查项——测试通过不能代替新提交验收,绿色状态也要说明究竟运行了什么。

背景补充

为什么用 a - b 而不是 a > b 做比较器?

比较函数的契约要求返回一个数字:负数表示 a 应排在 b 前,正数表示 a 应排在 b 后,零表示两者相对顺序不变。a - b 天然满足这个契约,并且在数值范围内返回正确的正负与零。若写成 a > b,返回的是布尔值,大多数引擎会把它强制转成 1 或 0,永远不会返回负值,因此排序结果会随实现细节漂移,无法稳定。比较器还必须满足自反性与传递性,否则不同引擎可能产生不同顺序。对含 NaN 的输入,a - b 会返回 NaN,排序行为变成未定义,这属于案例中划出的前提之外,应由单独规则处理。

复制数组有哪些常见写法?

复制保护原数组最直接的是 [...arr] 或 arr.slice(),两者都产生浅拷贝,对数字数组已经足够;Array.from(arr) 效果类似。若数组里嵌套着对象,浅拷贝只能保证外层引用不同,内部对象仍共享,此时重排不会改变内层数据但替换元素可能互相影响。若需要彻底隔离,应使用深拷贝。案例中选择在排序前复制,等于把「不可变性」作为契约写进实现,使得同一函数在连续多次调用时不会累积改变调用方的状态。

#最小复现#Bug修复验收#单元测试断言#排序Bug#代码审查#Grok Bot#回归测试#副作用
分享:

相关推荐