Files
PolyLoop/tools/soak/tests/test_scoreboard.py
T
iomgaa ff59457482 fix(soak): 记分板的真空成立与停止原因覆盖,两处都会把「什么都没验」显示成绿
Codex 报了两条数据不足时真空成立的不变量,顺着同类找齐了七条:零步的「步号连续」与
「动作结果与步记录一致」、不足两步的「提示词字符数单调不减」、零意图的「意图都有归宿」、
零载荷的「步记录的内部不变量」、零记录的「不串台」、零行的「日志能被读回来」。同一类
缺陷改一半,剩下那一半照样会在某天把一次什么都没验的跑显示成绿。

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 11:53:56 -04:00

1549 lines
57 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""记分板的测试:全部用合成日志,不跑模型、不起容器。
**重点是「构造出违反某条不变量的产物、记分板确实报击穿」那一组。** 一个永远返回通过的
判定器比没有判定器更糟——它会让一次什么都没验成的跑看起来全绿,而那正是最需要被看见的
情况。所以每条不变量都配一个反例,合法产物那组只是对照。
"""
import json
import re
import sys
from collections.abc import Callable, Sequence
from pathlib import Path
import pytest
_REPO_ROOT = Path(__file__).resolve().parents[3]
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
from polyloop.serialization import encode # noqa: E402
from polyloop.types import ( # noqa: E402
ActionOutcome,
ActionStatus,
Intent,
IntentKind,
ModelCallResult,
ModelReply,
ReplayPolicy,
RunFinished,
RunResult,
RunStarted,
StepCompleted,
StepRecord,
StopReason,
)
from tools.soak.scoreboard import ( # noqa: E402
_STOP_REASON_RULES,
EXIT_BREACHED,
EXIT_UNDETERMINED,
Verdict,
evaluate,
main,
render_report,
)
#: 塞进观察与最终回答里的哨兵。压测语料里有第三方的真实文档,报告里一个字都不许出现。
SENTINEL = "SENTINEL-ZZZ-第三方文档正文"
_TAGS = {
RunStarted: "run_started",
Intent: "intent",
ModelCallResult: "model_call_result",
StepCompleted: "step_completed",
RunFinished: "run_finished",
}
# ---------------------------------------------------------------------------
# 造合法产物
# ---------------------------------------------------------------------------
#: 一步可以长成的几种样子,逐条对着库里造那条步记录的那个函数。
#: `action` 走 `_action_step``parse_failure` 走 `_parse_failure_step`
#: `call_failure` 走 `_failed_call_step``final_answer` 走 `_final_answer_step`。
STEP_KINDS = (
"action",
"not_executed",
"env_error",
"parse_failure",
"call_failure",
"final_answer",
)
def build_records(
run_id: str,
*,
steps: int = 2,
step_kinds: Sequence[str] | None = None,
stop_reason: StopReason = StopReason.TASK_COMPLETED,
max_steps: int = 5,
max_actions: int = 20,
max_parse_failures: int = 2,
max_prompt_chars: int = 100000,
model_replay_policy: ReplayPolicy = ReplayPolicy.NEVER,
final_answer: str | None = None,
complete_on_last: bool = True,
tool_name: str | None = "run_code",
sink_failures: int = 0,
observation: str = "普通观察",
prompt_chars_at: Callable[[int], int] = lambda index: 100 + index * 10,
) -> tuple[list[object], RunResult]:
"""造一份自洽的记录序列。
默认每一步都是「模型意图 / 模型结果 / 动作意图 / 逐步结果」那四条,动作执行成功。
`step_kinds` 给出的话就按它逐步造,取值见 `STEP_KINDS`——那几种步在库里由不同的函数
产出,字段形状各不相同,停止原因的自洽判据分的正是这些形状。
"""
kinds = list(step_kinds) if step_kinds is not None else ["action"] * steps
for kind in kinds:
assert kind in STEP_KINDS, kind
records: list[object] = [
RunStarted(
run_id=run_id,
parameter_snapshot={
"request.max_steps": str(max_steps),
"request.max_actions": str(max_actions),
"request.max_consecutive_parse_failures": str(max_parse_failures),
"request.max_prompt_chars": str(max_prompt_chars),
"store.kind": "jsonl",
},
)
]
step_records: list[StepRecord] = []
for index, kind in enumerate(kinds):
completed = complete_on_last and index == len(kinds) - 1
text = f"{observation}#{index}"
chars = prompt_chars_at(index)
records.append(
Intent(
run_id=run_id,
kind=IntentKind.MODEL_CALL,
call_index=index,
result_id=f"m{index}",
replay_policy=model_replay_policy,
)
)
reply = ModelReply(call_id=f"c{index}", content="决策文本", thinking="")
records.append(
ModelCallResult(
run_id=run_id,
result_id=f"m{index}",
# 模型调用失败那一步:结果记录在,但它记的是失败。
reply=None if kind == "call_failure" else reply,
failure="TimeoutError: 网关没回" if kind == "call_failure" else None,
)
)
common = {
"step_idx": index,
"content_chars": 4,
"thinking_chars": 0,
"observation_truncated_chars": 0,
"prompt_chars": chars,
"step_wall_ms": 7,
}
if kind == "call_failure":
step = StepRecord(
**common, # type: ignore[arg-type]
raw_output="",
action=None,
parse_ok=False,
# 这一步压根没走到解释器,所以没有回喂给模型的说明——这正是库用来把它和
# 解析失败分开的那一对字段。
parse_error=None,
observation="[模型调用失败]",
observation_is_synthetic=True,
call_id=None,
)
elif kind == "parse_failure":
step = StepRecord(
**common, # type: ignore[arg-type]
raw_output="决策文本",
action=None,
parse_ok=False,
parse_error="解释不出有效决策,请重新输出一个 JSON 对象。",
observation="解释不出有效决策,请重新输出一个 JSON 对象。",
observation_is_synthetic=True,
call_id=f"c{index}",
)
elif kind == "final_answer":
step = StepRecord(
**common, # type: ignore[arg-type]
raw_output="决策文本",
action=None,
parse_ok=True,
parse_error=None,
observation="",
observation_is_synthetic=False,
call_id=f"c{index}",
)
else:
status = {
"action": ActionStatus.EXECUTED,
"not_executed": ActionStatus.NOT_EXECUTED,
"env_error": ActionStatus.ENV_ERROR,
}[kind]
passthrough = status is ActionStatus.EXECUTED
records.append(
Intent(
run_id=run_id,
kind=IntentKind.ACTION,
call_index=index,
result_id=f"a{index}",
replay_policy=ReplayPolicy.NEVER,
)
)
outcome = ActionOutcome(
status=status,
observation=text,
observation_is_synthetic=status is ActionStatus.NOT_EXECUTED,
env_reported_completion=completed,
observation_truncated_chars=0,
)
step = StepRecord(
**common, # type: ignore[arg-type]
raw_output="决策文本",
action="run_code",
parse_ok=True,
parse_error=None,
# 未执行与环境故障两档,库换掉回填进历史的那段观察并把合成标记立起来。
observation=text if passthrough else "[动作没有执行]",
observation_is_synthetic=not passthrough,
call_id=f"c{index}",
tool_name=tool_name,
tool_arguments="{}",
action_status=status,
env_reported_completion=completed,
)
step_records.append(step)
records.append(
StepCompleted(
run_id=run_id, result_id=f"a{index}", action_outcome=outcome, step=step
)
)
continue
step_records.append(step)
records.append(StepCompleted(run_id=run_id, result_id=None, action_outcome=None, step=step))
result = RunResult(
run_id=run_id,
stop_reason=stop_reason,
final_answer=final_answer,
steps=tuple(step_records),
event_delivery_failures=sink_failures,
)
records.append(RunFinished(run_id=run_id, result=result))
return records, result
def write_log(path: Path, records: Sequence[object]) -> None:
lines = [
json.dumps({"record": _TAGS[type(record)], **encode(record)}, ensure_ascii=False)
for record in records
]
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def materialize(
runs_dir: Path,
run_id: str = "soak-0001",
*,
write_result: bool = True,
write_events: bool = True,
write_meta: bool = True,
events: int | None = None,
meta_overrides: dict[str, object] | None = None,
**kwargs: object,
) -> RunResult:
"""把一个自洽的 run 的四个文件都写到磁盘上,返回它的运行结果。"""
runs_dir.mkdir(parents=True, exist_ok=True)
records, result = build_records(run_id, **kwargs) # type: ignore[arg-type]
write_log(runs_dir / f"{run_id}.jsonl", records)
if write_result:
(runs_dir / f"{run_id}.result.json").write_text(
json.dumps(encode(result), ensure_ascii=False), encoding="utf-8"
)
if write_events:
count = len(result.steps) if events is None else events
payload = "".join(
json.dumps({"kind": "step_finished", "run_id": run_id, "step_idx": index}) + "\n"
for index in range(count)
)
(runs_dir / f"{run_id}.events.jsonl").write_text(payload, encoding="utf-8")
if write_meta:
meta: dict[str, object] = {
"scenario": "appworld",
"task_id": "82e2fac_1",
"phase": None,
"wall_ms": 1234,
"model_calls": len(result.steps),
"sink_failures": result.event_delivery_failures,
"env_executions": len(result.steps),
"fault": None,
"success": True,
"resumed_from_step": None,
}
meta.update(meta_overrides or {})
(runs_dir / f"{run_id}.meta.json").write_text(
json.dumps(meta, ensure_ascii=False), encoding="utf-8"
)
return result
def edit_log(path: Path, edit: Callable[[list[dict]], list[dict]]) -> None:
"""按行读出来、交给 `edit` 改、再写回去。制造反例用的。"""
payloads = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines()]
changed = edit(payloads)
path.write_text(
"".join(json.dumps(item, ensure_ascii=False) + "\n" for item in changed),
encoding="utf-8",
)
def reshape_step(
path: Path,
*,
step_idx: int,
status: str,
outcome_observation: str,
step_observation: str,
outcome_is_synthetic: bool = False,
step_is_synthetic: bool = False,
outcome_truncated: int = 0,
step_truncated: int = 0,
) -> None:
"""把某一步改写成「执行器那侧与步记录那侧的观察不是同一段」的样子。
`build_records` 造出来的每一步都是 executed 且两侧逐字相同,而真实产物里被拒绝的动作
与环境故障那两档不长这样——库会替换回填进历史的观察。反例都从这里造。
"""
def edit(items: list[dict]) -> list[dict]:
for item in items:
if item["record"] != "step_completed" or item["step"]["step_idx"] != step_idx:
continue
item["action_outcome"]["status"] = status
item["action_outcome"]["observation"] = outcome_observation
item["action_outcome"]["observation_is_synthetic"] = outcome_is_synthetic
item["action_outcome"]["observation_truncated_chars"] = outcome_truncated
item["step"]["action_status"] = status
item["step"]["observation"] = step_observation
item["step"]["observation_is_synthetic"] = step_is_synthetic
item["step"]["observation_truncated_chars"] = step_truncated
return items
edit_log(path, edit)
def edit_result_steps(runs_dir: Path, run_id: str, mutate: Callable[[list[dict]], None]) -> None:
"""同时改 run_finished 内嵌的那份结果与 `.result.json` 里的步。
两边一起改,「跨进程的结果与内存里的一致」才不会跟着一起红——这里要看的是停止原因那条,
不是那条。日志里独立的 step_completed 行不动:没有任何判据拿它和结果里的步对比。
"""
def edit(items: list[dict]) -> list[dict]:
for item in items:
if item["record"] == "run_finished":
mutate(item["result"]["steps"])
return items
edit_log(runs_dir / f"{run_id}.jsonl", edit)
path = runs_dir / f"{run_id}.result.json"
payload = json.loads(path.read_text(encoding="utf-8"))
mutate(payload["steps"])
path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
def append_dangling_intent(
runs_dir: Path, run_id: str, *, replay_policy: str = "never", kind: str = "model_call"
) -> None:
"""在日志末尾补一条没有归宿的意图,模拟「意图写了、结果没写」那个断点。"""
edit_log(
runs_dir / f"{run_id}.jsonl",
lambda items: [
*items,
{
"record": "intent",
"run_id": run_id,
"kind": kind,
"call_index": 99,
"result_id": "dangling-99",
"replay_policy": replay_policy,
},
],
)
def verdict_of(scoreboard, name: str) -> Verdict:
for item in scoreboard.invariants:
if item.name == name:
return item.verdict
raise AssertionError(f"记分板里没有名为 {name!r} 的不变量")
def assert_breached(scoreboard, name: str) -> None:
assert verdict_of(scoreboard, name) is Verdict.BREACHED, (
f"{name} 应当被击穿,实际 {verdict_of(scoreboard, name)}"
)
result = next(item for item in scoreboard.invariants if item.name == name)
for evidence in result.breaches:
assert evidence.expected is not None
assert evidence.actual is not None
# ---------------------------------------------------------------------------
# 合法产物:全部通过
# ---------------------------------------------------------------------------
def test_legal_run_passes_every_invariant(tmp_path: Path) -> None:
materialize(tmp_path)
scoreboard = evaluate(tmp_path)
assert scoreboard.verdict is Verdict.PASSED, [
(item.name, item.verdict, [e.describe() for e in item.breaches + item.undetermined])
for item in scoreboard.invariants
if item.verdict is not Verdict.PASSED
]
assert scoreboard.notes == ()
def test_several_legal_runs_pass(tmp_path: Path) -> None:
materialize(tmp_path, "soak-0001", steps=3)
materialize(
tmp_path,
"soak-0002",
steps=5,
stop_reason=StopReason.STEP_BUDGET,
max_steps=5,
complete_on_last=False,
)
materialize(
tmp_path,
"soak-0003",
# 两步,不是一步:一步凑不出相邻的两个 prompt_chars,那条会报无法判定。
steps=2,
stop_reason=StopReason.AGENT_FINISHED,
complete_on_last=False,
final_answer="给出的答案",
)
scoreboard = evaluate(tmp_path)
assert scoreboard.verdict is Verdict.PASSED
assert len(scoreboard.summaries) == 3
assert scoreboard.overall_stats.stop_reasons == {
"agent_finished": 1,
"step_budget": 1,
"task_completed": 1,
}
def test_completing_tool_is_supplied_by_the_caller(tmp_path: Path) -> None:
"""task_completed 而最后一步没有环境侧证据:给了工具名就判得了,不给就是无法判定。"""
materialize(tmp_path, complete_on_last=False, tool_name="finish_task")
assert verdict_of(evaluate(tmp_path), "停止原因与轨迹自洽") is Verdict.UNDETERMINED
assert (
verdict_of(
evaluate(tmp_path, completing_tools=frozenset({"finish_task"})),
"停止原因与轨迹自洽",
)
is Verdict.PASSED
)
assert (
verdict_of(
evaluate(tmp_path, completing_tools=frozenset({"别的工具"})),
"停止原因与轨迹自洽",
)
is Verdict.BREACHED
)
# ---------------------------------------------------------------------------
# 逐条不变量的反例
# ---------------------------------------------------------------------------
def test_unreadable_line_is_a_breach(tmp_path: Path) -> None:
materialize(tmp_path)
path = tmp_path / "soak-0001.jsonl"
lines = path.read_text(encoding="utf-8").splitlines()
lines[2] = "{这不是 JSON"
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
scoreboard = evaluate(tmp_path)
assert_breached(scoreboard, "日志能被读回来")
assert any("第 3 行" in e.describe() for e in scoreboard.breached[0].breaches)
def test_unknown_record_tag_is_a_breach(tmp_path: Path) -> None:
materialize(tmp_path)
edit_log(
tmp_path / "soak-0001.jsonl",
lambda items: [{**items[0], "record": "谁知道这是什么"}] + items[1:],
)
assert_breached(evaluate(tmp_path), "日志能被读回来")
def test_torn_tail_is_reported_but_not_a_breach(tmp_path: Path) -> None:
materialize(tmp_path)
path = tmp_path / "soak-0001.jsonl"
# 最后一行只写了一半、没有换行终结:进程被杀在写入中途的样子。
path.write_text(path.read_text(encoding="utf-8") + '{"record": "step_comp', encoding="utf-8")
scoreboard = evaluate(tmp_path)
assert verdict_of(scoreboard, "日志能被读回来") is Verdict.PASSED
assert any("撕裂尾行" in note for note in scoreboard.notes)
assert "撕裂尾行" in render_report(scoreboard)
def test_torn_line_in_the_middle_is_a_breach(tmp_path: Path) -> None:
materialize(tmp_path)
path = tmp_path / "soak-0001.jsonl"
lines = path.read_text(encoding="utf-8").splitlines()
# 被换行终结、却是半截 JSON:追加写不会产生这种东西,所以是损坏。
lines[3] = lines[3][: len(lines[3]) // 2]
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
scoreboard = evaluate(tmp_path)
assert_breached(scoreboard, "日志能被读回来")
assert not any("撕裂尾行" in note for note in scoreboard.notes)
def test_result_file_disagreeing_with_the_log_is_a_breach(tmp_path: Path) -> None:
result = materialize(tmp_path)
payload = encode(result)
payload["final_answer"] = "跨进程读回来的和内存里的不一样"
(tmp_path / "soak-0001.result.json").write_text(
json.dumps(payload, ensure_ascii=False), encoding="utf-8"
)
assert_breached(evaluate(tmp_path), "跨进程的结果与内存里的一致")
def test_result_file_step_field_disagreeing_is_a_breach(tmp_path: Path) -> None:
result = materialize(tmp_path)
payload = encode(result)
payload["steps"][1]["step_wall_ms"] = 99999 # type: ignore[index]
(tmp_path / "soak-0001.result.json").write_text(
json.dumps(payload, ensure_ascii=False), encoding="utf-8"
)
scoreboard = evaluate(tmp_path)
assert_breached(scoreboard, "跨进程的结果与内存里的一致")
evidence = next(
item for item in scoreboard.invariants if item.name == "跨进程的结果与内存里的一致"
)
assert any("steps[1].step_wall_ms" in e.describe() for e in evidence.breaches)
def test_step_index_gap_is_a_breach(tmp_path: Path) -> None:
materialize(tmp_path, steps=3)
def bump(items: list[dict]) -> list[dict]:
for item in items:
if item["record"] == "step_completed" and item["step"]["step_idx"] == 1:
item["step"]["step_idx"] = 2
return items
edit_log(tmp_path / "soak-0001.jsonl", bump)
assert_breached(evaluate(tmp_path), "步号连续")
def test_dangling_intent_in_the_middle_is_a_breach(tmp_path: Path) -> None:
materialize(tmp_path, steps=3)
edit_log(
tmp_path / "soak-0001.jsonl",
lambda items: [
item
for item in items
if not (item["record"] == "model_call_result" and item["result_id"] == "m0")
],
)
assert_breached(evaluate(tmp_path), "意图都有归宿")
def test_two_dangling_intents_are_a_breach(tmp_path: Path) -> None:
materialize(tmp_path, steps=3)
edit_log(
tmp_path / "soak-0001.jsonl",
lambda items: [
item
for item in items
if not (item["record"] == "model_call_result" and item["result_id"] in {"m1", "m2"})
],
)
assert_breached(evaluate(tmp_path), "意图都有归宿")
def test_one_dangling_intent_at_the_tail_is_the_crash_point_and_passes(tmp_path: Path) -> None:
materialize(tmp_path, steps=2)
def crash(items: list[dict]) -> list[dict]:
kept = [item for item in items if item["record"] != "run_finished"]
kept.append(
{
"record": "intent",
"run_id": "soak-0001",
"kind": "model_call",
"call_index": 2,
"result_id": "m2",
"replay_policy": "never",
}
)
return kept
edit_log(tmp_path / "soak-0001.jsonl", crash)
assert verdict_of(evaluate(tmp_path), "意图都有归宿") is Verdict.PASSED
def test_step_completed_pairing_violation_is_a_breach(tmp_path: Path) -> None:
materialize(tmp_path)
def unpair(items: list[dict]) -> list[dict]:
for item in items:
if item["record"] == "step_completed" and item["step"]["step_idx"] == 0:
item["result_id"] = None
return items
edit_log(tmp_path / "soak-0001.jsonl", unpair)
assert_breached(evaluate(tmp_path), "步记录的内部不变量")
def test_outcome_disagreeing_with_step_record_is_a_breach(tmp_path: Path) -> None:
materialize(tmp_path)
def skew(items: list[dict]) -> list[dict]:
for item in items:
if item["record"] == "step_completed" and item["step"]["step_idx"] == 0:
item["step"]["action_status"] = "not_executed"
return items
edit_log(tmp_path / "soak-0001.jsonl", skew)
assert_breached(evaluate(tmp_path), "动作结果与步记录一致")
def test_executed_step_with_a_different_observation_is_a_breach(tmp_path: Path) -> None:
"""executed 档是原样透传,两侧观察必须逐字相同。"""
materialize(tmp_path)
reshape_step(
tmp_path / "soak-0001.jsonl",
step_idx=0,
status="executed",
outcome_observation="环境返回的原文",
step_observation="换了一段别的",
)
assert_breached(evaluate(tmp_path), "动作结果与步记录一致")
@pytest.mark.parametrize(
("status", "executor_text", "executor_is_synthetic"),
[
(
"not_executed",
"工具不存在:'final_answer',本次可见的是 "
"['read_document', 'grep_document', 'write_note']",
True,
),
("env_error", "容器没了:connection refused", False),
],
)
def test_replaced_observation_is_not_a_breach(
tmp_path: Path, status: str, executor_text: str, executor_is_synthetic: bool
) -> None:
"""未执行与环境故障两档,库刻意换掉回填进历史的观察,两侧文本不同是正常的。
数据形状照 `tools/soak/runs/full/govdoc-15-execute.jsonl` 第 36 行那条真实记录造:
执行器那侧留的是「工具不存在」的原文,步记录那侧是合成的那段提示。截断数也一起换掉
(库在这两档下一律填 0),所以它同样不该被比对。
"""
materialize(tmp_path)
reshape_step(
tmp_path / "soak-0001.jsonl",
step_idx=0,
status=status,
outcome_observation=executor_text,
step_observation=(
"[动作被拒绝,这一步没有执行任何工具]\n"
"请对照工具清单检查工具名与参数,然后重新输出一个 JSON 对象。"
),
outcome_is_synthetic=executor_is_synthetic,
step_is_synthetic=True,
outcome_truncated=40,
step_truncated=0,
)
assert verdict_of(evaluate(tmp_path), "动作结果与步记录一致") is Verdict.PASSED
@pytest.mark.parametrize("status", ["not_executed", "env_error"])
def test_replaced_observation_without_the_synthetic_flag_is_a_breach(
tmp_path: Path, status: str
) -> None:
"""库既然替换了观察,就必须把 observation_is_synthetic 立起来,不立才是真出了问题。"""
materialize(tmp_path)
reshape_step(
tmp_path / "soak-0001.jsonl",
step_idx=0,
status=status,
outcome_observation="执行器给的原文",
step_observation="库换上去的那一段",
outcome_is_synthetic=False,
step_is_synthetic=False,
)
assert_breached(evaluate(tmp_path), "动作结果与步记录一致")
@pytest.mark.parametrize("status", ["not_executed", "env_error"])
def test_status_still_has_to_agree_in_every_branch(tmp_path: Path, status: str) -> None:
"""状态与完成标记这两项三档下都比:观察分档,它们不分。"""
materialize(tmp_path)
reshape_step(
tmp_path / "soak-0001.jsonl",
step_idx=0,
status=status,
outcome_observation="执行器给的原文",
step_observation="库换上去的那一段",
outcome_is_synthetic=True,
step_is_synthetic=True,
)
edit_log(
tmp_path / "soak-0001.jsonl",
lambda items: [
{**item, "step": {**item["step"], "action_status": "executed"}}
if item["record"] == "step_completed" and item["step"]["step_idx"] == 0
else item
for item in items
],
)
assert_breached(evaluate(tmp_path), "动作结果与步记录一致")
def test_prompt_chars_going_backwards_is_a_breach(tmp_path: Path) -> None:
materialize(tmp_path, steps=3)
def shrink(items: list[dict]) -> list[dict]:
for item in items:
if item["record"] == "step_completed" and item["step"]["step_idx"] == 2:
item["step"]["prompt_chars"] = 1
return items
edit_log(tmp_path / "soak-0001.jsonl", shrink)
assert_breached(evaluate(tmp_path), "提示词字符数单调不减")
def test_event_count_mismatch_is_a_breach(tmp_path: Path) -> None:
materialize(tmp_path, steps=3, events=2)
assert_breached(evaluate(tmp_path), "事件条数等于本进程走完的步数")
def test_resumed_run_subtracts_the_skipped_steps(tmp_path: Path) -> None:
"""续跑:日志里有四步,本进程只真的走完后两步,所以只该有两条事件。"""
materialize(
tmp_path, steps=4, events=2, meta_overrides={"resumed_from_step": 2, "model_calls": 2}
)
assert verdict_of(evaluate(tmp_path), "事件条数等于本进程走完的步数") is Verdict.PASSED
materialize(
tmp_path,
"soak-0002",
steps=4,
events=4,
meta_overrides={"resumed_from_step": 2},
)
assert_breached(evaluate(tmp_path), "事件条数等于本进程走完的步数")
def test_delivery_failure_count_mismatch_is_a_breach(tmp_path: Path) -> None:
materialize(tmp_path, sink_failures=0, meta_overrides={"sink_failures": 3})
assert_breached(evaluate(tmp_path), "投递失败计数对得上")
def test_crosstalk_in_the_log_is_a_breach(tmp_path: Path) -> None:
materialize(tmp_path)
def swap(items: list[dict]) -> list[dict]:
for item in items:
if item["record"] == "step_completed" and item["step"]["step_idx"] == 1:
item["run_id"] = "soak-9999"
return items
edit_log(tmp_path / "soak-0001.jsonl", swap)
assert_breached(evaluate(tmp_path), "不串台")
def test_crosstalk_in_the_events_is_a_breach(tmp_path: Path) -> None:
materialize(tmp_path)
(tmp_path / "soak-0001.events.jsonl").write_text(
json.dumps({"kind": "step_finished", "run_id": "soak-0001", "step_idx": 0})
+ "\n"
+ json.dumps({"kind": "step_finished", "run_id": "soak-9999", "step_idx": 1})
+ "\n",
encoding="utf-8",
)
assert_breached(evaluate(tmp_path), "不串台")
def test_task_completed_without_evidence_is_a_breach(tmp_path: Path) -> None:
materialize(tmp_path, complete_on_last=False, tool_name="run_code")
scoreboard = evaluate(tmp_path, completing_tools=frozenset())
assert_breached(scoreboard, "停止原因与轨迹自洽")
def test_step_budget_not_matching_max_steps_is_a_breach(tmp_path: Path) -> None:
materialize(
tmp_path,
steps=3,
max_steps=9,
stop_reason=StopReason.STEP_BUDGET,
complete_on_last=False,
)
assert_breached(evaluate(tmp_path), "停止原因与轨迹自洽")
def test_agent_finished_without_final_answer_is_a_breach(tmp_path: Path) -> None:
materialize(
tmp_path,
stop_reason=StopReason.AGENT_FINISHED,
complete_on_last=False,
final_answer="",
)
assert_breached(evaluate(tmp_path), "停止原因与轨迹自洽")
def test_cancelled_without_run_finished_is_a_breach(tmp_path: Path) -> None:
materialize(tmp_path, stop_reason=StopReason.CANCELLED, complete_on_last=False)
edit_log(
tmp_path / "soak-0001.jsonl",
lambda items: [item for item in items if item["record"] != "run_finished"],
)
assert_breached(evaluate(tmp_path), "停止原因与轨迹自洽")
RULE = "停止原因与轨迹自洽"
def test_parse_failed_repeatedly_tail_matches_the_limit(tmp_path: Path) -> None:
materialize(
tmp_path,
step_kinds=["action", "parse_failure", "parse_failure"],
stop_reason=StopReason.PARSE_FAILED_REPEATEDLY,
max_parse_failures=2,
complete_on_last=False,
)
assert verdict_of(evaluate(tmp_path), RULE) is Verdict.PASSED
def test_parse_failed_repeatedly_with_a_short_tail_is_a_breach(tmp_path: Path) -> None:
"""末尾只有两步解析失败,上限却是三——那个计数撞线时不可能停在两步。"""
materialize(
tmp_path,
step_kinds=["action", "parse_failure", "parse_failure"],
stop_reason=StopReason.PARSE_FAILED_REPEATEDLY,
max_parse_failures=3,
complete_on_last=False,
)
assert_breached(evaluate(tmp_path), RULE)
def test_parse_failed_repeatedly_tail_touching_the_env_is_a_breach(tmp_path: Path) -> None:
"""解析失败那一支根本不碰环境,末尾那几步不该有动作状态。"""
materialize(
tmp_path,
step_kinds=["action", "parse_failure"],
stop_reason=StopReason.PARSE_FAILED_REPEATEDLY,
max_parse_failures=1,
complete_on_last=False,
)
def touch_env(steps: list[dict]) -> None:
steps[-1]["action_status"] = "executed"
edit_result_steps(tmp_path, "soak-0001", touch_env)
assert_breached(evaluate(tmp_path), RULE)
def test_context_overflow_with_every_step_inside_the_limit_passes(tmp_path: Path) -> None:
materialize(
tmp_path,
steps=2,
stop_reason=StopReason.CONTEXT_OVERFLOW,
max_prompt_chars=1000,
complete_on_last=False,
)
assert verdict_of(evaluate(tmp_path), RULE) is Verdict.PASSED
def test_context_overflow_with_a_step_over_the_limit_is_a_breach(tmp_path: Path) -> None:
"""超限的那次装配根本不产生步记录,所以落盘的每一步必定在线内。"""
materialize(
tmp_path,
steps=2,
stop_reason=StopReason.CONTEXT_OVERFLOW,
max_prompt_chars=105,
complete_on_last=False,
)
assert_breached(evaluate(tmp_path), RULE)
def test_context_overflow_without_any_step_is_undetermined(tmp_path: Path) -> None:
"""首次装配就超限的运行一步都没落盘,那是这个原因最典型的形态,可确实没东西可验。"""
materialize(
tmp_path,
steps=0,
stop_reason=StopReason.CONTEXT_OVERFLOW,
complete_on_last=False,
)
assert verdict_of(evaluate(tmp_path), RULE) is Verdict.UNDETERMINED
def test_action_budget_counts_only_executed_steps(tmp_path: Path) -> None:
"""未执行的那一步不加已执行动作计数,所以两步里只有一步算数。"""
materialize(
tmp_path,
step_kinds=["action", "not_executed"],
stop_reason=StopReason.ACTION_BUDGET,
max_actions=1,
complete_on_last=False,
)
assert verdict_of(evaluate(tmp_path), RULE) is Verdict.PASSED
def test_action_budget_not_matching_max_actions_is_a_breach(tmp_path: Path) -> None:
materialize(
tmp_path,
steps=2,
stop_reason=StopReason.ACTION_BUDGET,
max_actions=5,
complete_on_last=False,
)
assert_breached(evaluate(tmp_path), RULE)
def test_env_error_with_a_broken_last_step_passes(tmp_path: Path) -> None:
materialize(
tmp_path,
step_kinds=["action", "env_error"],
stop_reason=StopReason.ENV_ERROR,
complete_on_last=False,
)
assert verdict_of(evaluate(tmp_path), RULE) is Verdict.PASSED
def test_env_error_without_a_broken_last_step_is_a_breach(tmp_path: Path) -> None:
materialize(
tmp_path,
step_kinds=["env_error", "action"],
stop_reason=StopReason.ENV_ERROR,
complete_on_last=False,
)
assert_breached(evaluate(tmp_path), RULE)
def test_llm_error_with_a_failed_call_step_passes(tmp_path: Path) -> None:
materialize(
tmp_path,
step_kinds=["action", "call_failure"],
stop_reason=StopReason.LLM_ERROR,
complete_on_last=False,
)
assert verdict_of(evaluate(tmp_path), RULE) is Verdict.PASSED
def test_llm_error_ending_on_a_parse_failure_is_a_breach(tmp_path: Path) -> None:
"""解析失败那一步也没有动作结果,两者只能靠 parse_error 分开——它必须为空。"""
materialize(
tmp_path,
step_kinds=["action", "parse_failure"],
stop_reason=StopReason.LLM_ERROR,
complete_on_last=False,
)
scoreboard = evaluate(tmp_path)
assert_breached(scoreboard, RULE)
rule = next(item for item in scoreboard.invariants if item.name == RULE)
assert any("parse_error 有值" in e.describe() for e in rule.breaches)
def test_llm_error_ending_on_an_action_is_a_breach(tmp_path: Path) -> None:
materialize(
tmp_path,
steps=2,
stop_reason=StopReason.LLM_ERROR,
complete_on_last=False,
)
assert_breached(evaluate(tmp_path), RULE)
def test_resume_state_unknown_with_a_never_intent_dangling_passes(tmp_path: Path) -> None:
materialize(
tmp_path,
steps=1,
stop_reason=StopReason.RESUME_STATE_UNKNOWN,
complete_on_last=False,
)
append_dangling_intent(tmp_path, "soak-0001", replay_policy="never")
assert verdict_of(evaluate(tmp_path), RULE) is Verdict.PASSED
def test_resume_state_unknown_with_a_safe_intent_is_a_breach(tmp_path: Path) -> None:
"""声明可安全重放的意图会被直接重放,不会停在这一档。"""
materialize(
tmp_path,
steps=1,
stop_reason=StopReason.RESUME_STATE_UNKNOWN,
complete_on_last=False,
)
append_dangling_intent(tmp_path, "soak-0001", replay_policy="safe")
assert_breached(evaluate(tmp_path), RULE)
def test_resume_state_unknown_without_a_dangling_intent_is_a_breach(tmp_path: Path) -> None:
materialize(
tmp_path,
steps=1,
stop_reason=StopReason.RESUME_STATE_UNKNOWN,
complete_on_last=False,
)
assert_breached(evaluate(tmp_path), RULE)
def test_missing_budget_in_the_snapshot_is_undetermined(tmp_path: Path) -> None:
"""规矩要的上限不在参数快照里就照实说缺什么,不硬编一个默认值。"""
materialize(
tmp_path,
steps=2,
stop_reason=StopReason.ACTION_BUDGET,
max_actions=2,
complete_on_last=False,
)
def drop(items: list[dict]) -> list[dict]:
for item in items:
if item["record"] == "run_started":
del item["parameter_snapshot"]["request.max_actions"]
return items
edit_log(tmp_path / "soak-0001.jsonl", drop)
scoreboard = evaluate(tmp_path)
assert verdict_of(scoreboard, RULE) is Verdict.UNDETERMINED
rule = next(item for item in scoreboard.invariants if item.name == RULE)
assert any("request.max_actions" in e.describe() for e in rule.undetermined)
def test_every_stop_reason_has_a_rule(tmp_path: Path) -> None:
"""十个取值一个都不许落在「没有规矩」那条兜底路径上。
兜底路径本身留着,是给将来给 StopReason 加取值的人:那时它显式地报无法判定,
而不是静默地给一条绿。
"""
del tmp_path
covered = set(_STOP_REASON_RULES) | {StopReason.TASK_COMPLETED}
assert covered == set(StopReason)
def test_cancelled_with_run_finished_passes(tmp_path: Path) -> None:
materialize(tmp_path, stop_reason=StopReason.CANCELLED, complete_on_last=False)
assert verdict_of(evaluate(tmp_path), "停止原因与轨迹自洽") is Verdict.PASSED
# ---------------------------------------------------------------------------
# 缺文件:无法判定,既不是通过也不是击穿
# ---------------------------------------------------------------------------
def test_missing_result_file_is_undetermined(tmp_path: Path) -> None:
materialize(tmp_path, write_result=False)
scoreboard = evaluate(tmp_path)
assert verdict_of(scoreboard, "跨进程的结果与内存里的一致") is Verdict.UNDETERMINED
assert scoreboard.verdict is Verdict.UNDETERMINED
assert any(".result.json" in note for note in scoreboard.notes)
def test_missing_meta_file_is_undetermined(tmp_path: Path) -> None:
materialize(tmp_path, write_meta=False)
scoreboard = evaluate(tmp_path)
assert verdict_of(scoreboard, "事件条数等于本进程走完的步数") is Verdict.UNDETERMINED
assert verdict_of(scoreboard, "投递失败计数对得上") is Verdict.UNDETERMINED
assert not scoreboard.breached
def test_missing_events_file_is_undetermined(tmp_path: Path) -> None:
materialize(tmp_path, write_events=False)
scoreboard = evaluate(tmp_path)
assert verdict_of(scoreboard, "事件条数等于本进程走完的步数") is Verdict.UNDETERMINED
assert verdict_of(scoreboard, "不串台") is Verdict.UNDETERMINED
assert not scoreboard.breached
def test_sigkilled_run_keeps_only_the_log(tmp_path: Path) -> None:
"""进程被 SIGKILL:只剩日志,而且尾行撕裂。整批判成无法判定,不是通过也不是击穿。"""
materialize(tmp_path, write_result=False, write_events=False, write_meta=False)
path = tmp_path / "soak-0001.jsonl"
lines = path.read_text(encoding="utf-8").splitlines()[:-1]
path.write_text("\n".join(lines) + '\n{"record": "run_fin', encoding="utf-8")
scoreboard = evaluate(tmp_path)
assert not scoreboard.breached
assert scoreboard.verdict is Verdict.UNDETERMINED
def materialize_stepless(runs_dir: Path, run_id: str = "soak-0001") -> None:
"""造一个零步的 run:只有 run_started 与 run_finished,没有意图、没有步、没有事件。
停止原因取 `cancelled`,因为十个原因里只有它的规矩不约束轨迹——它只要求日志里有结束
记录,而这个夹具本来就有。换成别的会顺带撞出那条规矩的击穿(`task_completed` 撞
「零步不可能完成」,`llm_error` 撞「至少有一步」),把这里要看的东西盖住。
"""
materialize(
runs_dir,
run_id,
steps=0,
stop_reason=StopReason.CANCELLED,
complete_on_last=False,
)
def test_empty_log_cannot_be_judged_readable(tmp_path: Path) -> None:
"""一条被换行终结的行都没有:没有任何一行被读回来过,说「读得回来」没有依据。
造的是真实形态:`write_run_started` 先建文件、再写那一行,杀在两者之间就只剩一个空
文件,另外三个文件根本来不及写。
"""
materialize(tmp_path, write_result=False, write_events=False, write_meta=False)
(tmp_path / "soak-0001.jsonl").write_text("", encoding="utf-8")
scoreboard = evaluate(tmp_path)
assert verdict_of(scoreboard, "日志能被读回来") is Verdict.UNDETERMINED
assert not scoreboard.breached
def test_log_with_only_a_torn_half_line_cannot_be_judged_readable(tmp_path: Path) -> None:
materialize(tmp_path)
(tmp_path / "soak-0001.jsonl").write_text('{"record": "run_star', encoding="utf-8")
scoreboard = evaluate(tmp_path)
assert verdict_of(scoreboard, "日志能被读回来") is Verdict.UNDETERMINED
assert any("撕裂尾行" in note for note in scoreboard.notes)
def test_one_line_is_enough_to_judge_readability(tmp_path: Path) -> None:
"""下限是一行,别为了整齐往上抬:一行就足以判它解不解得开。"""
materialize(tmp_path)
path = tmp_path / "soak-0001.jsonl"
first = path.read_text(encoding="utf-8").splitlines()[0]
path.write_text(first + "\n", encoding="utf-8")
assert verdict_of(evaluate(tmp_path), "日志能被读回来") is Verdict.PASSED
path.write_text("{这一行解不开\n", encoding="utf-8")
assert_breached(evaluate(tmp_path), "日志能被读回来")
def test_run_without_intents_cannot_judge_their_homes(tmp_path: Path) -> None:
materialize_stepless(tmp_path)
scoreboard = evaluate(tmp_path)
assert verdict_of(scoreboard, "意图都有归宿") is Verdict.UNDETERMINED
assert not scoreboard.breached
def test_intents_without_any_result_records_are_still_judged(tmp_path: Path) -> None:
"""有意图、没有任何结果记录,正是这条要判的那种,不许赖成判不了。"""
materialize(tmp_path, steps=2)
edit_log(
tmp_path / "soak-0001.jsonl",
lambda items: [item for item in items if item["record"] in {"run_started", "intent"}],
)
assert_breached(evaluate(tmp_path), "意图都有归宿")
def test_one_intent_is_enough_to_judge_its_home(tmp_path: Path) -> None:
"""下限是一条意图:一条就足以判它悬不悬空、以及悬空的是不是最后一条。"""
materialize(tmp_path, steps=1, stop_reason=StopReason.CANCELLED, complete_on_last=False)
edit_log(
tmp_path / "soak-0001.jsonl",
lambda items: [items[0], items[1]],
)
# 唯一那条意图悬空,而它就是最后一条——那是崩溃点,判通过,不是判不了。
assert verdict_of(evaluate(tmp_path), "意图都有归宿") is Verdict.PASSED
def test_run_without_step_payloads_cannot_judge_pairing(tmp_path: Path) -> None:
materialize_stepless(tmp_path)
scoreboard = evaluate(tmp_path)
assert verdict_of(scoreboard, "步记录的内部不变量") is Verdict.UNDETERMINED
assert not scoreboard.breached
def test_pairing_counts_tagged_lines_not_decoded_records(tmp_path: Path) -> None:
"""下限数的是打着标签的行,不是解出来的记录。
唯一那条 `step_completed` 因为违反配对而解不出记录——按记录数当下限的话这条会报「判不
了」,可它要判的对象恰恰就是这一行。
"""
materialize(tmp_path, steps=1, stop_reason=StopReason.CANCELLED, complete_on_last=False)
def unpair(items: list[dict]) -> list[dict]:
for item in items:
if item["record"] == "step_completed":
item["result_id"] = None
return items
edit_log(tmp_path / "soak-0001.jsonl", unpair)
assert_breached(evaluate(tmp_path), "步记录的内部不变量")
def test_run_without_step_records_cannot_judge_agreement(tmp_path: Path) -> None:
materialize_stepless(tmp_path)
scoreboard = evaluate(tmp_path)
assert verdict_of(scoreboard, "动作结果与步记录一致") is Verdict.UNDETERMINED
assert not scoreboard.breached
def test_step_without_an_action_outcome_still_counts_as_data(tmp_path: Path) -> None:
"""下限是一条步记录,不要求它带动作结果。
没有动作结果的那一档也在这条的判定范围里——那时步记录的 `action_status` 必须为空。
所以一份全是解析失败的日志确实验到了这条的一部分,报判不了反而是假的。
"""
materialize(tmp_path, steps=1, stop_reason=StopReason.CANCELLED, complete_on_last=False)
def strip_outcome(items: list[dict]) -> list[dict]:
for item in items:
if item["record"] == "step_completed":
item["result_id"] = None
item["action_outcome"] = None
item["step"]["action_status"] = None
return items
edit_log(tmp_path / "soak-0001.jsonl", strip_outcome)
assert verdict_of(evaluate(tmp_path), "动作结果与步记录一致") is Verdict.PASSED
def relabel(items: list[dict]) -> list[dict]:
for item in items:
if item["record"] == "step_completed":
item["step"]["action_status"] = "executed"
return items
edit_log(tmp_path / "soak-0001.jsonl", relabel)
assert_breached(evaluate(tmp_path), "动作结果与步记录一致")
def test_empty_events_file_leaves_the_event_half_unjudged(tmp_path: Path) -> None:
"""两半各判各的:日志那半判过了,也不能替事件那半的真空背书。"""
materialize_stepless(tmp_path)
scoreboard = evaluate(tmp_path)
assert verdict_of(scoreboard, "不串台") is Verdict.UNDETERMINED
assert not scoreboard.breached
# 日志那半仍然是真判的:改掉一条记录的 run_id 照样击穿。
edit_log(
tmp_path / "soak-0001.jsonl",
lambda items: [
{**item, "run_id": "soak-9999"} if item["record"] == "run_finished" else item
for item in items
],
)
assert_breached(evaluate(tmp_path), "不串台")
def test_log_without_records_leaves_the_log_half_unjudged(tmp_path: Path) -> None:
"""事件那半判过了(两条事件的 run_id 都对),也不能替日志那半的真空背书。"""
materialize(tmp_path, steps=2)
(tmp_path / "soak-0001.jsonl").write_text("", encoding="utf-8")
scoreboard = evaluate(tmp_path)
crosstalk = next(item for item in scoreboard.invariants if item.name == "不串台")
assert crosstalk.verdict is Verdict.UNDETERMINED
assert crosstalk.breaches == ()
def test_run_without_steps_cannot_judge_step_indices(tmp_path: Path) -> None:
"""零步的 run 上「步号从 0 开始逐 1 递增」根本没被验过,所以不许报通过。
崩溃注入那两类产物里真的会出现零步的 run——进程在第一步落盘之前就被杀了。
"""
materialize(
tmp_path,
steps=0,
stop_reason=StopReason.CANCELLED,
complete_on_last=False,
)
scoreboard = evaluate(tmp_path)
assert verdict_of(scoreboard, "步号连续") is Verdict.UNDETERMINED
assert not scoreboard.breached
@pytest.mark.parametrize("steps", [0, 1])
def test_too_few_steps_cannot_judge_prompt_monotonicity(tmp_path: Path, steps: int) -> None:
"""零步和一步都凑不出相邻的两个值,一次比较都没发生过。"""
materialize(
tmp_path,
steps=steps,
stop_reason=StopReason.CANCELLED,
complete_on_last=False,
)
scoreboard = evaluate(tmp_path)
assert verdict_of(scoreboard, "提示词字符数单调不减") is Verdict.UNDETERMINED
assert not scoreboard.breached
def test_one_step_still_judges_the_step_index(tmp_path: Path) -> None:
"""一步凑不出单调性,但「从 0 开始」验得了——两条的数据下限不一样。"""
materialize(tmp_path, steps=1, stop_reason=StopReason.CANCELLED, complete_on_last=False)
assert verdict_of(evaluate(tmp_path), "步号连续") is Verdict.PASSED
def shift(items: list[dict]) -> list[dict]:
for item in items:
if item["record"] == "step_completed":
item["step"]["step_idx"] = 3
return items
edit_log(tmp_path / "soak-0001.jsonl", shift)
assert_breached(evaluate(tmp_path), "步号连续")
def test_two_steps_are_enough_for_both(tmp_path: Path) -> None:
"""数据够了就必须真的判,不许赖着报无法判定。"""
materialize(tmp_path, steps=2)
scoreboard = evaluate(tmp_path)
assert verdict_of(scoreboard, "步号连续") is Verdict.PASSED
assert verdict_of(scoreboard, "提示词字符数单调不减") is Verdict.PASSED
def test_empty_directory_is_undetermined_not_passed(tmp_path: Path) -> None:
scoreboard = evaluate(tmp_path)
assert scoreboard.verdict is Verdict.UNDETERMINED
assert all(item.verdict is Verdict.UNDETERMINED for item in scoreboard.invariants)
def test_events_file_is_not_mistaken_for_a_run(tmp_path: Path) -> None:
materialize(tmp_path)
scoreboard = evaluate(tmp_path)
assert [item.run_id for item in scoreboard.summaries] == ["soak-0001"]
# ---------------------------------------------------------------------------
# 报告
# ---------------------------------------------------------------------------
def _table_rows(report: str) -> list[list[str]]:
return [re.split(r"(?<!\\)\|", line) for line in report.splitlines() if line.startswith("|")]
def test_report_is_legal_markdown(tmp_path: Path) -> None:
materialize(tmp_path, "soak-0001", steps=2)
materialize(
tmp_path,
"soak-0002",
steps=4,
stop_reason=StopReason.STEP_BUDGET,
max_steps=4,
complete_on_last=False,
)
report = render_report(evaluate(tmp_path))
assert report.startswith("# PolyLoop 压测记分板")
for heading in (
"## 总判定",
"## 不变量",
"## 观察",
"## 统计",
"## 与基线的对照",
"## 逐个 run",
):
assert heading in report
# 表格的每一行都以竖线开头结尾,且同一段里的列数一致。
widths: set[int] = set()
previous_was_table = False
for line in report.splitlines():
if line.startswith("|"):
assert line.endswith("|"), line
if not previous_was_table:
widths = set()
widths.add(len(re.split(r"(?<!\\)\|", line)))
assert len(widths) == 1, f"表格列数不一致:{line}"
previous_was_table = True
else:
previous_was_table = False
assert _table_rows(report)
def test_report_carries_the_baseline_comparison(tmp_path: Path) -> None:
materialize(tmp_path, steps=2)
report = render_report(evaluate(tmp_path))
assert "937" in report
assert "915" in report # 基线的 task_completed 计数
assert "步数 p90" in report
assert "对照不是判定" in report
def test_report_never_contains_document_fragments(tmp_path: Path) -> None:
"""观察与最终回答里的哨兵一个字都不许进报告——压测语料里有第三方的真实文档。"""
result = materialize(
tmp_path,
steps=2,
observation=SENTINEL,
stop_reason=StopReason.AGENT_FINISHED,
complete_on_last=False,
final_answer=SENTINEL,
)
# 再造一处击穿,让证据渲染那条路径也被走到。
payload = encode(result)
payload["final_answer"] = SENTINEL + "-改过的"
payload["steps"][0]["observation"] = SENTINEL + "-也改过" # type: ignore[index]
payload["steps"][0]["tool_name"] = SENTINEL + "-当成工具名" # type: ignore[index]
payload["steps"][1]["tool_arguments"] = SENTINEL + "-当成参数" # type: ignore[index]
(tmp_path / "soak-0001.result.json").write_text(
json.dumps(payload, ensure_ascii=False), encoding="utf-8"
)
scoreboard = evaluate(tmp_path)
assert scoreboard.breached
report = render_report(scoreboard)
assert SENTINEL not in report
assert "SENTINEL" not in report
def test_report_does_not_echo_a_hostile_record_tag(tmp_path: Path) -> None:
materialize(tmp_path)
edit_log(
tmp_path / "soak-0001.jsonl",
lambda items: [{**items[0], "record": SENTINEL}] + items[1:],
)
scoreboard = evaluate(tmp_path)
assert_breached(scoreboard, "日志能被读回来")
report = render_report(scoreboard)
assert SENTINEL not in report
assert "SENTINEL" not in report
def test_report_does_not_echo_a_hostile_observation_through_decode_errors(
tmp_path: Path,
) -> None:
"""解码失败的消息也不许把观察带出来。库的取值函数对字符串字段只报类型不报内容。"""
materialize(tmp_path, observation=SENTINEL)
def corrupt(items: list[dict]) -> list[dict]:
for item in items:
if item["record"] == "step_completed":
# 少一个必填字段:整行解不出来,消息里只会有字段名。
del item["step"]["prompt_chars"]
return items
edit_log(tmp_path / "soak-0001.jsonl", corrupt)
scoreboard = evaluate(tmp_path)
assert_breached(scoreboard, "日志能被读回来")
report = render_report(scoreboard)
assert SENTINEL not in report
assert "SENTINEL" not in report
def test_report_keeps_tool_names_out(tmp_path: Path) -> None:
"""工具名也不进报告。
一次没通过校验的工具调用会把模型编的那串原样记进 `tool_name`,所以它和观察同档,
不是「我们自己的枚举取值」。这条被一次实测撞出来过:先前的实现把工具名过一遍字符
白名单就放行,而白名单留下的正是 ASCII 那一段,哨兵原样穿了过去。
"""
materialize(
tmp_path,
complete_on_last=False,
tool_name="工具|名里有竖线和" + SENTINEL,
)
scoreboard = evaluate(tmp_path, completing_tools=frozenset({"finish"}))
assert_breached(scoreboard, "停止原因与轨迹自洽")
report = render_report(scoreboard)
assert SENTINEL not in report
assert "SENTINEL" not in report
assert "竖线" not in report
# ---------------------------------------------------------------------------
# 命令行与退出码
# ---------------------------------------------------------------------------
def test_main_returns_zero_on_a_clean_batch(tmp_path: Path) -> None:
runs = tmp_path / "runs"
report = tmp_path / "reports" / "batch.md"
materialize(runs)
assert main(["--runs-dir", str(runs), "--report", str(report)]) == 0
assert report.read_text(encoding="utf-8").startswith("# PolyLoop 压测记分板")
def test_main_returns_nonzero_on_a_breach(tmp_path: Path) -> None:
runs = tmp_path / "runs"
materialize(runs, steps=3, events=1)
code = main(["--runs-dir", str(runs), "--report", str(tmp_path / "r.md")])
assert code == EXIT_BREACHED
def test_main_returns_nonzero_on_undetermined(tmp_path: Path) -> None:
runs = tmp_path / "runs"
materialize(runs, write_meta=False)
argv = ["--runs-dir", str(runs), "--report", str(tmp_path / "r.md")]
assert main(argv) == EXIT_UNDETERMINED
assert main([*argv, "--allow-undetermined"]) == 0
def test_allow_undetermined_does_not_forgive_a_breach(tmp_path: Path) -> None:
runs = tmp_path / "runs"
materialize(runs, steps=3, write_meta=False)
def bump(items: list[dict]) -> list[dict]:
for item in items:
if item["record"] == "step_completed" and item["step"]["step_idx"] == 1:
item["step"]["step_idx"] = 2
return items
edit_log(runs / "soak-0001.jsonl", bump)
code = main(
[
"--runs-dir",
str(runs),
"--report",
str(tmp_path / "r.md"),
"--allow-undetermined",
]
)
assert code == EXIT_BREACHED
def test_completing_tool_flag_reaches_the_invariant(tmp_path: Path) -> None:
runs = tmp_path / "runs"
materialize(runs, complete_on_last=False, tool_name="finish_task")
argv = ["--runs-dir", str(runs), "--report", str(tmp_path / "r.md")]
assert main(argv) == EXIT_UNDETERMINED
assert main([*argv, "--completing-tool", "finish_task"]) == 0
def test_baseline_flags_override_the_defaults(tmp_path: Path) -> None:
runs = tmp_path / "runs"
report = tmp_path / "r.md"
materialize(runs)
code = main(
[
"--runs-dir",
str(runs),
"--report",
str(report),
"--baseline-label",
"自造基线",
"--baseline-stop-reason",
"task_completed=7",
"--baseline-steps-p50",
"3",
]
)
assert code == 0
text = report.read_text(encoding="utf-8")
assert "自造基线" in text
assert "915" not in text
def test_bad_baseline_pair_is_rejected(tmp_path: Path) -> None:
with pytest.raises(SystemExit):
main(
[
"--runs-dir",
str(tmp_path),
"--report",
str(tmp_path / "r.md"),
"--baseline-stop-reason",
"没有等号",
]
)