fix(soak): 记分板的真空成立与停止原因覆盖,两处都会把「什么都没验」显示成绿

Codex 报了两条数据不足时真空成立的不变量,顺着同类找齐了七条:零步的「步号连续」与
「动作结果与步记录一致」、不足两步的「提示词字符数单调不减」、零意图的「意图都有归宿」、
零载荷的「步记录的内部不变量」、零记录的「不串台」、零行的「日志能被读回来」。同一类
缺陷改一半,剩下那一半照样会在某天把一次什么都没验的跑显示成绿。

各条的数据下限不一样,反直觉的三处写进了说明:「步记录的内部不变量」数的是打着标签的行
不是解出来的记录(违反配对的行本来就解不出记录,按记录数当下限会把它最该判的对象数漏);
「动作结果与步记录一致」不要求那条步记录带动作结果;「不串台」两半各判各的,合成一个的话
一半的真空会被另一半的绿盖住。

「停止原因与轨迹自洽」原本只覆盖四个取值、另外六个直接放行——不是数据不足,是判据本来就
该覆盖而没覆盖,后果和真空成立一样。六个都补了规矩,llm_error 那条按库自己的判据写
(解析失败必定带说明,模型调用失败那一步压根没走到解释器,只看有没有动作结果分不开这两者)。
另加一条断言十个取值一个不漏,将来加了取值而这里没跟上会显式报「还没有规矩」。

「提示词字符数单调不减」的说明原本承诺「历史只追加」,实现只比较库自己记录的数——承诺了
它,读者看见绿就以为截断被排除了。改成只承诺它验得到的,真正的对账在故障注入那侧。

拿 193 次真实运行重跑:十一条仍然全过,而这次那 8 次解析失败连击、1 次模型调用失败、
1 次撞步数上限是被真规矩判过的。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-11 11:53:56 -04:00
parent 5a4d13b0f2
commit ff59457482
3 changed files with 1059 additions and 120 deletions
+17 -1
View File
@@ -85,6 +85,16 @@ _SUBMIT = json.dumps(
#: 两个阶段没有结束通路(提交工具只在 summarize 那一阶段),不这样它们会一路走到 50 步。
_GARBAGE = "我先想一想这道题。"
#: summarize 阶段的第一步:先读一段再提交。
#:
#: **不是可有可无的一步。** 记分板的「提示词字符数单调不减」要两步才凑得出相邻一对,
#: 一步就提交的运行在它那里是「无从判起」而不是「通过」。这条测试断言的是整批全绿,
#: 所以替身必须走够两步——否则它测到的是记分板在数据不足时的行为,不是产物合不合约定。
_READ = json.dumps(
{"tool": "read_document", "arguments": {"path": "tender.md", "start_line": 1, "end_line": 2}},
ensure_ascii=False,
)
def _audit_task(index: int) -> govdoc_scenario.AuditTask:
checkpoint = govdoc_scenario.Checkpoint(
@@ -376,7 +386,13 @@ async def test_artifacts_pass_the_scoreboard(tmp_path: Path) -> None:
两边的字段约定没有任何机器约束把它们钉在一起,这条测试就是那个约束。
"""
guard = _guard(render=lambda call: _SUBMIT if "summarize" in call.run_id else _GARBAGE)
def render(call: object) -> str:
if "summarize" not in call.run_id: # type: ignore[attr-defined]
return _GARBAGE
return _READ if call.call_index == 0 else _SUBMIT # type: ignore[attr-defined]
guard = _guard(render=render)
for index in range(2):
await run_govdoc_task(
task=_audit_task(index),