Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

断言不了输出,就断言关系——蜕变测试进 agent 评测

第 1 篇立过断言危机:非确定性系统的断言目标从结果移走,当时登记的去处是路径覆盖与终态。但这两个去处都假定「什么叫对」列得出清单——对开放式输出,这个假定本身就漏。这一篇补第三种移法:**断言不了输出,就断言关系。**界面部分第 16 篇的等价性层早就把它用出来了,只是没叫这个名字;这一篇给它正名,并把它从界面层的测试基建,升级成 agent 评测里的一种断言形态。

本章将讲

  • 断言危机的第三种解法:断言目标从结果移走之后,除了路径与终态,还可以移到关系——蜕变关系为什么是非确定性系统的天然断言。
  • 旗舰案例:界面部分第 16 篇的 delta 切分等价性按断言形态重拆——事后形态、全程形态,以及双实现互证这个 oracle 难题的另一种解法。
  • 与故障注入合流:每一条蜕变关系反过来都是一张注入地图,切分方式本身就是旋钮——收回第 4 篇。

一、断言目标的三次搬家

第 1 篇把 agent 的断言难拆成三件事,第一件就是非确定性:同一输入不同输出,精确断言必死,断言目标只能从结果移走。当时登记的去处有两个:路径(不管输出是什么,该走的步骤走了没有)和终态(不管过程怎么抖,最后落库的状态对不对)。

这两个去处解了一大批问题,但都还带着 oracle 的影子:路径覆盖要求你预先知道「合法路径长什么样」,终态断言要求你预先知道「什么终态算对」。对结构化的任务——订票、改库、跑测试——这两个清单列得出;对开放式的输出,一段回答、一次渲染、一份计划,列不出。你没法为「这次回答该说什么」写预期值,正如你没法为「这段 Markdown 渲染出来该是什么样」逐帧写预期值。

还有第三种移法,这一篇的主角:移到关系。不断言输出是什么,断言输出与输出之间必须保持的关系——对输入做一个你选定的变换,输出必须满足一个你指定的关系。正确答案依然不知道,但不需要知道了。

二、蜕变关系:没有 oracle 时的断言形态

这个手法在软件测试里有正式名字:蜕变测试(metamorphic testing),核心概念是蜕变关系(metamorphic relation,MR)。它通常追溯到 Chen、Cheung、Yiu 1998 年的一篇技术报告,动机被叫作 test oracle problem:很多程序事实上没有预言机——要么正确答案不存在(画图、排序检索),要么独立算一遍正确答案和被测程序本身一样贵。教科书例子是三角函数:没有表可查,怎么测 sin 的实现?你不需要知道 sin(x) 的具体值,只需要知道 sin(x) 必须等于 sin(π − x)——同一个程序,喂 x 和喂 π − x,两个输出必须相等。变换是考题,关系是判卷标准,预期值全程没有出场。

一句话定义:与其断言 f(x) = y,不如断言 f(x) 与 f(g(x)) 之间满足关系 R——g 是你选的变换,R 是你对系统的理解。源用例跑一遍,把输入按 g 变出后继用例再跑一遍,断言的不是两个输出各自是什么,而是它们之间的关系。

这个形态和 agent 评测是天作之合,理由有三:

  1. oracle 缺位是常态,不是例外。 agent 的产出是开放文本,「正确答案」要么不存在、要么写不起;MR 从设计上就不需要它。
  2. 变换是你控制的。 模型的非确定性在输出侧,而你选 g 的手在输入侧——非确定性还在,但它被关进了「关系必须保持」的笼子:输出可以抖,关系不许破。
  3. 一个关系顶一整个用例矩阵。 切分方式是无限的,逐帧断言永远覆盖不完;关系只有一个,断言一次管所有变换——这正是界面部分第 16 篇说的「一次断言,覆盖无限切分」。

诚实义务也要尽:MR 只能证伪,不能证明。关系被破坏,一定有 bug;关系保持,只是没抓到。这个不对称和第 4 篇故障注入的检出逻辑同构——检出是证据,未检出只是没证据。蜕变测试不是 oracle 的替代品,是 oracle 缺位时的次优制度:次优,但成文。

三、旗舰案例:delta 切分是一条教科书级 MR

界面部分第 16 篇讲过 Codex TUI 的四层断言框架,这里不重复,只把第三层(等价性层)单独抽出来,用 MR 的眼光重新拆一遍。它把流式渲染的正确性钉成一句话:「任意 delta 切分下,流式渲染的可见输出必须收敛于一次性全量渲染」——这句话的结构就是教科书级的蜕变关系:

  • 源用例:模型输出整段、一次性渲染;
  • 变换 g:同一段输出的切分方式——按 token 切、按行切、按随机字节切;
  • 关系 R:可见输出收敛相等。

注意这个断言从头到尾没有说「这段 Markdown 渲染出来该是什么样」——它不需要知道。它说的是切不许改变结果。正确答案缺位的地方,关系上岗。

streaming/controller.rs 里的三个测试(2026-10-14 按快照 commit 6b9826e3 复核行号),按断言形态可以分成两类。

事后形态:controller_loose_vs_tight_with_commit_ticks_matches_full(L1143)。关系只钉在终点——全部喂完才和全量渲染比一次。一次断言覆盖整个变换,便宜;但失败时你只知道「破了」,不知道在哪一步破的。

全程形态:controller_live_view_matches_render_during_interleaved_table_streaming(L1619)。关系钉在路径上的每一步——每喂一个 delta 就断言一次「已上屏内容 + 待放行 tail ≡ 全量渲染」。我尤其喜欢它断言失败时的报错设计:报出来的是出事的那个 delta,不是预期画面。失败定位的单位从「哪一帧错了」变成了「哪种切法把它切错了」——变换成了第一公民。

两种形态之间是一个工程旋钮:断言密度。事后形态便宜、定位粗;全程形态贵、定位到 delta。写 MR 测试时这不是实现细节,是设计决策——关系钉得越密,抓得越早,跑得越贵。和第 1 篇那句「成本是一等约束」接得上:关系的断言密度,同样要用预算裁决。

四、双实现互证:oracle 难题的另一种解法

第三个测试要单独立一类,因为它把 MR 推到了更激进的位置。

incremental_holdback_matches_stateless_scan_per_chunk(L1852):一份有状态的增量实现、一份无状态的全量实现,逐 chunk 断言两者状态相等。用 MR 的语言说:变换 g 是「换一份独立实现」,关系 R 是「两份实现输出相等」。这个形态在文献里有自己的名字,叫伪 oracle(pseudo-oracle);界面部分第 16 篇的大白话更直接:用实现验证实现。

它回答的是 oracle 难题的另一面。MR 的主形态解的是「正确答案不存在」;双实现解的是「正确答案存在,但你信不过任何一份计算」——把绝对正确性问题降级为一致性问题。降级的收益来自独立性:bug 必须同时以同样的方式污染两份独立实现,才能漏网。

残余风险也要写明:两份实现共享的错误理解会双双漏过——同一个作者对 Markdown 表格规则的同一个误读,会同时长进两份代码里,一致性全绿,错误隐身。所以双实现不取代真 oracle,它取代的是「手工列预期值」那一层;规格级的误读,最终得靠第 7 篇那种裁判校准去抓。

顺手和第 6 篇划一下界,免得两种「参照物」混为一谈:双臂对照(with / without)回答「是不是它的功劳」,参照物是被测物自己的另一臂;双实现互证回答「没有 oracle 拿什么当尺子」,参照物是另一份实现。两者的共同祖先是同一条原则——没有绝对答案时,用参照物替代绝对答案——分工只在参照物站在哪一侧。

五、与故障注入合流:关系是断言,也是注入地图

界面部分第 16 篇末段留过一个反向启发:等价性层指明了最敏感的注入点——delta 切分方式。这一篇把它兑现,而且可以说得更强:每一条蜕变关系,反过来都是一张故障注入的地图。

逻辑是这样的。MR 断言「变换 g 下关系 R 必须保持」,等于宣布了系统在哪类扰动下许过承诺;而注入测试要找的最敏感的注入点,恰恰是被许下承诺的地方——承诺越硬,破坏承诺的注入越毒。既然「切分不许改变结果」是承诺,切分方式就是旋钮:按 token 切、按行切、按随机字节切;再往毒里做——在多字节 UTF-8 字符中间切、在表格分隔行中间切、在代码围栏的围栏线中间切。每一种切法都是一颗指名道姓的炸弹,炸的都是同一个不变量。随机扰动只能告诉你「系统可能会坏」;MR 指导的注入告诉你「系统承诺过不会这样坏」——后者才是回归套件里该常驻的东西。

这给了故障注入一个和第 4 篇互补的选题方法。第 4 篇的内容完整性炸弹测的是「坏了会不会察觉」,伤型来自对物理失效模式的枚举;蜕变注入测的是「变换下还守不守承诺」,伤型来自对系统承诺的枚举。两份清单最后往往指向同一批部件,但后者多了一层「违约」的语义——检出的 bug 自带呈堂证供:不是「这里坏了」,是「你承诺过这里不会坏」。不知道往哪注入的时候,先问系统承诺过什么。

所以合流是字面意义的:造得出变换,才写得出 MR;写得出 MR,变换才不只是随机扰动,而是带着承诺条款的考题。第 4 篇从注入一侧到过这个交界,这一篇从断言一侧再走一遍——交界还在原地:断言不了输出,就断言关系;而关系画出的地图,测试与注入共用。


本章源码事实基于 Codex 本地源码 2026-10-14 的复核:codex-rs/tui/src/streaming/controller.rs 三个测试分别位于 L1143、L1619、L1852,快照 main 6b9826e3(2026-09-09);四层断言框架与三个测试的行为细节见界面部分第 16 篇,本章只取断言形态视角,不重复展开。蜕变测试的概念定性(property-based 测试技术、应对 test oracle problem)2026-10-14 经 Wikipedia 词条核验;具体出处(Chen、Cheung、Yiu 1998 年技术报告)为教科书通识,按转述口径引用,未逐字核对原文。