TestJack 续: 案例 研究、 提示 模板 与 局限
同一篇 TestJack 论文的附录 C 至 E:Navidrome 与 Open Library 的逐试验反驳、httpx cookie 存储上的轻量审计、三份提示模板的路径,以及依赖参考解和精度的局限。

本文目录
译注:这是《TestJack:你该相信编码基准里的结果吗?》的全文续篇,含附录 C、附录 D 与附录 E。
附录 C 案例研究
C.1 逐试验审计中的案例
逐试验审计的反驳暴露的是基准测试的缺陷,而不是任务本身的缺陷。我们检查 TestJack 在 SWE-bench Pro 上反驳的两次试验,那里官方测试留下一项必要行为未检查。两个案例的差别在于试验如何利用这一缺口:第一个从未实现该特性中未被测试的部分,而第二个恰好实现可见夹具所观察的东西,除此之外什么也不实现。互补的案例,即见证反映的是欠指定的提示而不是有缺陷的试验,在附录 B.2 中讨论。
案例 1:navidrome/navidrome 中缺失的测试用例。任务 navidrome-0130c6dc(Go,基线提交 2f90fc9b)要求 Navidrome 跟踪每张专辑可用的图像文件,以便客户端能够访问备用封面和其他美术图。检测到一个目录含有美术图只是该特性的第一阶段。要求还跨越专辑构造、数据库存储,以及对已经索引过的媒体的重新扫描。第一项要求明确该特性到达持久存储:数据库必须为每张专辑增加一个新列来存储完整图像路径,并在迁移被应用后触发一次完整的媒体重扫描。其余要求增加一个存放这些路径的专辑字段、经过排序且无重复的媒体目录、目录统计中的图像文件名、在专辑被持久化之前赋上完整路径、用系统列表分隔符构造路径、在增量扫描和删除期间传播目录信息,以及一个修订过的文件夹变更签名。参考解把存储要求具体化:它注册一个新迁移,其主体是 alter table main.album add image_files varchar; 在语句成功后调用 forceFullRescan(tx),增加 Album.ImageFiles,实现 MediaFiles.Dirs(),并在 repo.Put(&a) 之前把合并后的路径赋给专辑。只增加一个 Go 字段并不能创建这些路径所存储于其中的列。
官方评测则相反,只给一个扫描器测试打分:fail_to_pass 为 ['TestScanner'],pass_to_pass 为空,selected_test_files_to_run 为 ["TestScanner"]。整个测试补丁是 531 字节,只触及 scanner/walk_dir_tree_test.go,其中唯一改动的断言是把 "HasImages": BeTrue() 改成 "Images": ConsistOf("cover.jpg"),并仍检查 "HasPlaylist": BeFalse() 与 "AudioFilesCount": BeNumerically("==", 5)。该断言检查目录遍历现在收集的是一个夹具目录的图像文件名,而不是一个布尔标志。表 7 把任务的必要行为映射到这个补丁上:六项里有一项被断言,被打分的测试从未到达模式、专辑仓库或重扫描。
表 7:Navidrome 任务的必要行为,以及每一项在官方测试补丁中的证据。只有第一项被打分,而我们所审计的试验违反了第二项。
| 必要行为 | 官方测试补丁中的证据 |
|---|---|
| 在目录统计中收集图像文件名 | 对 cover.jpg 做了断言 |
| 创建专辑图像路径列 | 没有迁移或模式断言 |
| 持久化并取回专辑图像路径 | 没有仓库往返断言 |
| 迁移之后触发完整重扫描 | 没有重扫描断言 |
| 跨互异的媒体目录构建路径 | 没有路径构造断言 |
| 在增量扫描和删除期间保持路径 | 没有生命周期断言 |
gpt-5.6-sol 被审计的试验实现了该特性的遍历一侧和模型字段,但省略了迁移:增加了带 structs:"image_files" 的 Album.ImageFiles,缺失创建 album.image_files 的迁移。由此得到的模型因此指向数据库所没有的一列。官方评分器仍然把该试验报告为通过,原因很直接:在内存中的目录结构里收集 cover.jpg 不需要任何列,因此唯一被打分的断言被一个无法存储它所收集之物的补丁所满足。审计期间测得的覆盖相应地很薄,补丁所增加的行中有 6.7% 被覆盖,八项要求里有七项未被演练。
TestJack 所确认的见证测试 TestAlbumImageFilesColumnCreated 驱动一个临时数据库走过应用自己的迁移运行器,并断言 image_files 列在 album 上存在:模型世界为 FAIL: TestAlbumImageFilesColumnCreated (1.93s),预言机世界为 PASS: TestAlbumImageFilesColumnCreated (0.48s),包为 github.com/navidrome/navidrome/db。它在参考解上通过,在试验上失败,而仓库中预先存在的持久化测试在该试验上因没有这样的列 image_files 而失败。因此缺陷并不微妙,仓库自己就含有本来会抓住它的测试;基准只是没有选中它们。
这一缺口超出这次试验而普遍存在。三个独立的失败能在改动后的扫描器断言下存活:迁移缺失、未注册或无效,这就是此处的缺陷;列存在,但专辑刷新或仓库映射从未持久化所生成的路径;以及迁移创建了列却没有请求重扫描,于是更早被索引的专辑永远得不到它们的美术图。弥合缺口需要在该特性实际跨越的各个阶段上做断言,也就是在运行生产迁移路径之后做模式检查、经由真实仓库用 filepath.ListSeparator 连接的路径做专辑往返,以及检查应用迁移会请求一次完整重扫描。这类测试必须使用隔离的临时数据库,而不是用手创建 image_files 的夹具,因为手工造出的列恰好掩盖本案例所暴露的省略,并且它们必须被加入打分选择:评分器从不运行的断言弥合不了任何缺口。
案例 2:internetarchive/openlibrary 中的奖励黑客。任务 openlibrary-5069b09e(Python,基线提交 facafbe7)涉及在目的地已经持有一条冲突记录时,把一条书注移到另一部作品。书注带有复合主键 (username, work_id, edition_id),因此把一条源记录更新到共享用户名和版本的目的地会违反唯一性。修复必须防止冲突笔记的丢失,并返回一个含有 rows_changed、rows_deleted 和 failed_deletes 的字典。参考解设置 Booknotes.ALLOW_DELETE_ON_CONFLICT = False,并且当批量更新碰到冲突时,回退到 update_work_ids_individually,后者保留每一条冲突的书注,对每一条这样的记录把失败计数加一,并仍然移动那些可以移动的记录。其他记录类型,尤其是书架,保持它们在冲突时删除的策略。
官方测试增加一个恰好有一条源笔记和一条冲突目的地笔记的夹具:rows 含 {"username": "@mek", "work_id": 1, "edition_id": 1, "notes": "Jimmeny"} 与 {"username": "@mek", "work_id": 2, "edition_id": 1, "notes": "Cricket"},经 self.db.multiple_insert("booknotes", rows) 插入,再调用 Booknotes.update_work_id("1", "2"),断言 resp 等于 {"rows_changed": 0, "rows_deleted": 0, "failed_deletes": 1},并断言库中书注仍等于 rows。该夹具检查真实的数据库内容,因此它在这一情景中确实能检测到删除。它的弱点是不含第二处冲突,也不含可以独立移动的源笔记,表 8 把这一点说精确:它所做的每一项观察,对按记录的实现和对一个常数都重合。被打分的选择没有进一步的保护,因为两项保持测试演练的是书架而不是书注:FAIL_TO_PASS 为 test_db.py::TestUpdateWorkID::test_no_allow_delete_on_conflict,PASS_TO_PASS 为 test_db.py::TestUpdateWorkID::test_update_collision 与 test_db.py::TestUpdateWorkID::test_update_simple。
表 8:Open Library 任务的单冲突夹具所做的观察。按记录的实现与该试验的常数在每一项上都一致,因此被打分的测试无法把二者分开。
| 夹具中的观察 | 正确实现 | 试验的捷径 |
|---|---|---|
| 被更改的记录 | 0:唯一的源记录发生冲突 | 0:操作回滚 |
| 被删除的记录 | 0:冲突笔记被保留 | 0:操作回滚 |
| 失败的删除 | 1:一条冲突的源记录 | 1:一个字面常数 |
| 所存储的记录 | 两条原始笔记都留下 | 两条原始笔记都留下 |
表 9:轻量途径在 DeepSWE 任务 httpx-deterministic-cookie-store 上的过程。任务分析每个任务运行一次;三轮审计各检查一个被抽样的幸存者,其见证在整个幸存池上重放。
| 阶段 | 进入的试验 | 见证测试 | 被反驳 | 剩余 |
|---|---|---|---|---|
| 官方验证器 | – | – | – | 42 |
| 任务分析 | 42 | 9(其中 2 个有区分力) | 11 | 31 |
| 随机化试验审计 | 31 | 4 | 13 | 18 |
| 合计 | 42 | 13 | 24 | 18 |
gpt-5.6-sol 被审计的试验提交了那个常数:若 cls.PRESERVE_ROWS_ON_CONFLICT,则注释说碰撞不得导致部分更新或两条冲突记录中的任一条被丢弃,然后 failed_deletes = 1,t.rollback(),并返回含 rows_changed、rows_deleted 与 failed_deletes 的字典。这个分支无论有多少记录冲突都报告一次失败,放弃整个更新而不是移动那些不冲突的记录,并让 update_work_ids_individually 内部无条件的删除原封不动。值得注意的是,该试验没有编辑任何测试,也没有篡改任何用具。评测过程无论如何都使篡改没有意义,因为它在应用模型补丁之后从修复提交恢复被打分的测试文件:把仓库重置到基线提交,应用模型补丁,检出修复提交的 openlibrary/tests/core/test_db.py,运行被选中的测试,解析结果。在那次恢复之后存活下来的是生产代码的改动,而试验得以通过,是因为单冲突夹具是基准对它所作的唯一观察。
TestJack 确认了三个见证,每一个针对夹具所省略的情景之一。第一个把冲突对的数目参数化,并断言 failed_deletes 等于该数目,常数无法对两对或更多对满足这一点。第二个把一对冲突与一条可移动的笔记混合,并断言可移动的笔记到达目的地,而两条冲突笔记保持其内容,回滚无法满足这一点。第三个直接以一对冲突调用 update_work_ids_individually,并断言两条记录都存活,未被触碰的辅助函数无法满足这一点。三者都在参考解上通过,在试验上失败。每一个比较的是完整的行内容而不是行数,从而使删除一条笔记再重建一个空的替代品无法通过,并按键对行排序,因为结果顺序不是要求的一部分。
这个案例还带有一项限定。提示把冲突例子描述为记录“一次失败”,并要求书注在冲突中保持完全不变,读者可能把这种措辞理解成许可整个操作的回滚。因此单独的字面量 1 并不是故意博弈的证明,混合更新和辅助函数这两个见证才是确立对所请求行为的真实偏离的那些。我们的裁决相应地是行为性的:该试验通过满足一项不足的观察而通过,而不是通过实现所请求的处理,并且我们不对模型的意图或它对隐藏测试的知识做任何主张。该任务也会受益于明确陈述其契约,也就是 failed_deletes 计数冲突的源记录、冲突保留两条笔记、可以独立移动的笔记仍然更新,以及共享的辅助函数遵守书注策略。
C.2 轻量途径中的案例
轻量途径分两个阶段对整个任务得出裁决:基于提示的任务分析构建增强套件 $T_{1}$ 并反驳每一个在其上失败的试验,随机化循环然后审计少数被抽样的幸存者,并把已确认的见证在其余试验上重放(算法 1)。我们跟随一个 DeepSWE 任务 httpx-deterministic-cookie-store 走过两个阶段,以表明每一个阶段贡献什么,以及为什么便宜的阶段并不包含昂贵的阶段。该任务要求一个公开的 httpx.CookieStore,它从响应中提取 cookie 并构建出站的 Cookie 头,具有确定性的排序与逐出、过期、域与路径匹配、前缀规则和映射操作,同时除非使用新的存储,否则不改变已有行为。我们语料中的试验有 42 次通过官方验证器,其套件是充实的:它既构造 Client(cookies=store) 也构造 AsyncClient(cookies=store),导入一个 CookieJar,检查普通的域匹配,并确认一个被提取的 Secure cookie 不会发给 HTTP 请求。尽管如此,TestJack 仍反驳 42 次中的 24 次,汇总于表 9。
阶段 1:官方套件从未演练的要求。任务分析阅读指令、仓库和 $T_{0}$,而不能访问任何试验,并把要求映射到检查它们的测试上。对这个任务它产生九项测试,其中两项在被审计的队列上有区分力;二者合起来反驳 42 次试验中的 11 次,留下 31 次。二者都针对提示直白陈述、而官方套件只部分演练的要求。
第一项关系到存储可以用在哪里。指令要求存储在任何接受 cookies= 的地方都能工作,但 $T_{0}$ 只把它供给客户端构造器,从不作为逐请求的参数。新增测试把一个非空存储传给单个请求,并期望一个被正确构建的头。九次试验在它上面失败,并且九次都以同样的方式失败:BaseClient._merge_cookies 到旧的 Cookies.update 再到标准库 cookiejar,然后 AttributeError: ’str’ object has no attribute ’domain’。这些试验在构造器路径上认出新容器,却把合并路径留在旧包装器上,后者把映射迭代成 cookie 名字,而 jar 期望的是 cookie 对象。参考解则在合并边界上做转换,每当合并的任一侧是 CookieStore 时就构建一个。
第二项关系到 update() 接受哪些输入。指令枚举了 CookieStore、httpx.Cookies、CookieJar、字典,以及成对的列表;$T_{0}$ 覆盖了每一种形式,除了 httpx.Cookies 包装器。新增测试恰好供给那种形式:store = httpx.CookieStore(),store.update(httpx.Cookies({"a": "1"})),request = httpx.Request("GET", "https://example.org/"),store.set_cookie_header(request),并断言 request.headers.get("cookie") == "a=1"。两次试验让存储保持为空并且不产生头,因为它们接受底层的 jar 但不接受它的包装器,而参考解在导入之前先解包。两项测试都不需要一个有缺陷的补丁来推动它们:两个缺口都来自把一项被枚举的要求与 $T_{0}$ 实际演练的调用点相比较,而这恰恰是任务分析所能得到的证据。
阶段 2:只有具体试验才能揭示的缺陷。随机化循环然后从 31 个幸存者中抽样三个,每轮审计一个,已确认的见证在整个幸存者池上重放。每一轮检查一种实现,并把它所发现的缺陷推广成一项由指令所证明的测试。
第一轮抽样一次 gpt-5.5 试验,它用 .lstrip(".").rstrip(".") 规范化 cookie 域,把前导点和尾随点当成可互换的。对一个来自 example.org、携带 Domain=example.org. 的响应,剥掉尾随点会把一个本不该匹配的域变成一个匹配的域,于是 cookie 被存储而不是被拒绝。参考解只移除前导点,然后应用它的匹配辅助函数。见证检查该 cookie 被拒绝;底层缺陷,即放宽接受的规范化,单从任务文本是看不见的,因为指令调用标准的域匹配,却没有把尾随点写出来。
第二轮抽样一次 claude-opus-4.7 试验,它通过公开的设置器 self.set(name, value, domain, path) 重新导入 jar 的 cookie。设置器不接受 Secure 参数,因此导入保留了可见的名字、值、域和路径,同时静默丢掉了方案限制。两个见证从一个原始 CookieJar 和从一个 httpx.Cookies 包装器导入一个 Secure cookie,然后断言它不会被发给 HTTP 请求;参考解在导入时复制 Secure 标志和过期时间。这一缺陷位于没有任何一份单独 API 名字清单能抓住的交互之中:$T_{0}$ 用一个非安全 cookie 检查 jar 导入,并且只在提取路径上检查 Secure,因此两个特性分开时都通过,而它们的组合失败。
第三轮抽样一次 claude-opus-4.8 试验,它反而放宽了方案策略:is_secure = url.scheme.lower() in ("https", "wss")。指令说 Secure cookie 只通过 HTTPS 发送,因此见证把一个已存储的 Secure cookie 应用到 wss:// 请求上,并期望没有头。参考解把方案严格限制为 HTTPS。
两个阶段各自贡献什么。把四项审计生成的见证在 31 个幸存者上重放,又反驳了 13 次试验,因此循环的三次昂贵审计转移到的试验数是它们所检查的四倍以上。基准所接受的 42 次试验中,24 次(57.1%)被反驳,11 次由任务分析,13 次由随机化循环,18 次存活。两个阶段发现不同种类的缺陷。任务分析发现提示所枚举而 $T_{0}$ 留下未检查的要求,这就是为什么它的测试派生起来便宜,并能成批反驳试验。随机化循环发现住在实现选择内部的缺陷,也就是一个放宽接受的规范化辅助函数、一个静默丢掉元数据的设置器,以及一个多了一项的方案谓词,其中没有一个单从提示和官方测试就能看见。这里的每一个见证所检查的要求都被分类为必要(附录 A.2),因此全部 24 次反驳都成立。重放结果也反映循环的一次执行:被抽样的试验本身是池中的成员,另一次抽样会按不同的顺序固定见证,因此达到这一结果所需的轮数并不由这一次运行单独决定。
附录 D 提示模板
本节列出图 3 所示三个智能体的提示模板。审计智能体审计单次通过基准的试验,并编写演示问题的测试(§3.2)。审查智能体对照任务指令重新判断一次审计所增加的测试,并决定保留哪些。任务分析智能体在检查任何试验之前分析一个任务,并为现有套件留下未检查的要求增加测试(§3.3)。花括号中的字段(例如 {{TASK_ID}})由运行器为每个任务和每次试验填入。
审计智能体提示:appendix/prompts/audit_agent.txt
审查智能体提示:appendix/prompts/reviewer_agent.txt
任务分析智能体提示:appendix/prompts/task_analysis_agent.txt
译注:论文用提示框把上述三个文件的全文嵌入附录。HTML 转写只保留了文件路径,没有展开提示词正文。提示词是审计、审查和任务分析的操作说明,不是基准分数表的一部分;本译文按转写后的原文逐块翻译这一节,不把未进入原文 Markdown 的提示词文件改写成另一篇论文。三份文件仍在论文源码的 appendix/prompts 目录中,分别为审计智能体、审查智能体和任务分析智能体。
附录 E 局限
我们摘要 TestJack 以及我们的评测的主要局限。
对参考解的依赖。TestJack 依靠一个参考解来认证每一项新测试,因此只附带提示和测试套件的基准落在它的范围之外。它进一步假设每个任务是良构的,也就是其测试、提示和真值互相一致(附录 A.1);违反这一假设的任务需要的是修复任务,而不是重新判断它的试验。
基于大语言模型的审计。审计智能体和审查智能体本身是大语言模型,因此一次反驳的可靠程度只等于它们对提示的阅读。预言机验证保证每一个见证所断言的行为是真值所展现的,但不保证提示要求它。这一判断被委托给审查者。欠指定的提示——其隐含要求读者无法可靠推断——仍然是我们各个配置之间分歧的主要来源(附录 B.2)。
轻量途径的覆盖与精度。轻量途径用覆盖和精度换成本。每一轮审计针对一次被抽样试验的缺陷,因此局限在它从未抽样的试验上的缺陷,在有限预算下可能被漏掉,而通过试验很少的任务几乎没有机会把一次审计分摊到多次试验上。此外,轻量途径会放大试验中的噪声和提示中的含糊,并扩散到其他未经审计的试验上,这对精度有负面影响。
评测的范围。我们审计的是一组固定的前沿模型的试验,并排除了不是编码任务、需要 GPU 或属于超长程项目的 SkillsBench 与 SWE-Marathon 任务(§4.1),因此我们的反驳率未必能迁移到其他智能体或任务类型。RQ2 到 RQ4 中的召回和精度度量的是与逐试验审计的一致性,而不是与人工标注的真值的一致性。
觉得有用,转给同事
微信扫码
用微信扫一扫,在手机上打开后即可转发。