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) 效果类似。若数组里嵌套着对象,浅拷贝只能保证外层引用不同,内部对象仍共享,此时重排不会改变内层数据但替换元素可能互相影响。若需要彻底隔离,应使用深拷贝。案例中选择在排序前复制,等于把「不可变性」作为契约写进实现,使得同一函数在连续多次调用时不会累积改变调用方的状态。
相关推荐

恒星究竟能有多大?最大恒星的物理极限
红超巨星是宇宙中体积最大的恒星,半径可达太阳的2000倍,吞没内太阳系。但恒星尺寸没有精确上限,模型预测可达15000倍太阳半径。质量最大的蓝色恒星体积远小于红超巨星——最大和最重是两回事。

苹果HomeView智能屏全功能解析:墙挂桌面两用,299美元
苹果发布299美元智能显示屏HomeView,支持壁挂和桌面两种安装方式,配备1200万像素摄像头实现面部识别,可控制智能家居、播放音乐、发送食谱到屏幕逐步跟做。本文全面解析HomeView的表盘、应用扩展、隐私保护和烹饪集成等核心功能。

承诺的新工作室去哪了?Craft Computing用五年时间给出答案
Craft Computing主理人回应观众质疑:承诺五年的新工作室因建设成本从3万暴涨至7.5万美元而搁浅,最终改为168平方英尺车库改造空间。频道收入因算法变化下滑,内容方向也将转向更务实的项目制作。