Skip to content

Commit 0cc0246

Browse files
Armando ShkambiArmando Shkambi
authored andcommitted
fix(bench): report.json records whether the run completed
1 parent 5e35c37 commit 0cc0246

3 files changed

Lines changed: 106 additions & 3 deletions

File tree

tests/benchmarks/_framework/runner.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,12 @@ def _run_inner(self, *, dev_mode: bool) -> RunOutcome:
306306

307307
# Persist a JSON sidecar to output_dir/report.json regardless of validation
308308
(output_dir / "report.json").write_text(
309-
json.dumps(_report_to_dict(report, self.cost), ensure_ascii=False, indent=2) + "\n",
309+
json.dumps(
310+
_report_to_dict(report, self.cost, aborted=aborted, abort_reason=abort_reason),
311+
ensure_ascii=False,
312+
indent=2,
313+
)
314+
+ "\n",
310315
encoding="utf-8",
311316
)
312317

@@ -824,10 +829,26 @@ def _cell_to_dict(case: BenchmarkCase, run: RunResult, score: CaseScore) -> dict
824829
}
825830

826831

827-
def _report_to_dict(report: BenchmarkReport, cost: CostTracker) -> dict[str, Any]:
828-
"""Serializable shape for report.json."""
832+
def _report_to_dict(
833+
report: BenchmarkReport,
834+
cost: CostTracker,
835+
*,
836+
aborted: bool = False,
837+
abort_reason: str | None = None,
838+
) -> dict[str, Any]:
839+
"""Serializable shape for report.json.
840+
841+
``aborted`` / ``abort_reason`` mirror the same fields on ``RunOutcome``.
842+
The CLI already refuses to let a halted run exit 0, but that exit code
843+
is not part of the run directory the bench container uploads to S3, so
844+
the artifact carries the fact too. The pre-registration's
845+
``stopping_rules.partial`` clause depends on telling a partial run from
846+
a complete one after the fact.
847+
"""
829848
return {
830849
"run_id": report.run_id,
850+
"aborted": aborted,
851+
"abort_reason": abort_reason,
831852
"config_hash": report.config_hash,
832853
"started_at": report.started_at,
833854
"ended_at": report.ended_at,

tests/benchmarks/_framework/tests/test_reporting.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ def _write_report_json(run_dir: Path) -> dict:
2424
cases_dir.mkdir(parents=True, exist_ok=True)
2525
report = {
2626
"run_id": "dev-2026-01-01T00-00-00Z_cloudopsbench",
27+
"aborted": False,
28+
"abort_reason": None,
2729
"config_hash": "abc123",
2830
"started_at": "2026-01-01T00:00:00+00:00",
2931
"ended_at": "2026-01-01T00:05:00+00:00",

tests/benchmarks/_framework/tests/test_runner_budget.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from __future__ import annotations
1818

19+
import json
1920
from collections.abc import Iterator
2021
from pathlib import Path
2122
from typing import Any
@@ -85,6 +86,27 @@ def _runner(tmp_path: Path) -> BenchmarkRunner:
8586
return BenchmarkRunner(config=config, adapter=_TinyAdapter())
8687

8788

89+
def _two_llm_runner(tmp_path: Path) -> tuple[BenchmarkRunner, Path]:
90+
"""Runner over two model arms, so a halt can land between them."""
91+
out_dir = tmp_path / "out"
92+
config = BenchmarkConfig.model_validate(
93+
{
94+
"benchmark": "tiny",
95+
"modes": ["opensre+llm"],
96+
"llms": ["claude-4-sonnet", "gpt-5"],
97+
"model_versions": {
98+
"claude-4-sonnet": "claude-sonnet-4-5-20250929",
99+
"gpt-5": "gpt-5-2025-08-07",
100+
},
101+
"seed": 42,
102+
"cost_budget_usd": 10.0,
103+
"output_dir": str(out_dir),
104+
"report_formats": ["json"],
105+
}
106+
)
107+
return BenchmarkRunner(config=config, adapter=_TinyAdapter()), out_dir
108+
109+
88110
def _call_run_one_cell(runner: BenchmarkRunner, tmp_path: Path) -> None:
89111
"""Drive _run_one_cell with a minimal valid arg set."""
90112
case = BenchmarkCase(case_id="c1", benchmark_name="tiny")
@@ -174,3 +196,61 @@ def test_run_one_cell_catches_other_exceptions_as_cell_failure(tmp_path: Path) -
174196
with patch("tools.investigation.capability.run_investigation", _raises_runtime):
175197
# Should NOT raise — cell-level failure recorded in the _CellResult
176198
_call_run_one_cell(runner, tmp_path)
199+
200+
201+
# --------------------------------------------------------------------------- #
202+
# report.json must disclose that a run halted #
203+
# --------------------------------------------------------------------------- #
204+
205+
_INVESTIGATION_OK = {"root_cause": "ok", "report": "ok", "evidence_entries": []}
206+
207+
208+
def _read_report_json(out_dir: Path, run_id: str) -> dict[str, Any]:
209+
return json.loads((out_dir / run_id / "report.json").read_text(encoding="utf-8"))
210+
211+
212+
def test_report_json_records_a_halted_run(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
213+
"""A run halted part-way through the grid must say so in report.json.
214+
215+
The exit-code path already refuses to let a halt pass as success
216+
(``cli.py``: "a halted run that exits 0 is silently lost"), but the exit
217+
code is not part of the run directory the bench container uploads to S3.
218+
The pre-registration's ``stopping_rules.partial`` clause turns on this
219+
distinction: "Numbers from a partial run are NEVER promoted to a
220+
baseline; only complete runs are baselines", so the artifact has to
221+
carry it.
222+
"""
223+
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
224+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
225+
runner, out_dir = _two_llm_runner(tmp_path)
226+
with patch(
227+
"tools.investigation.capability.run_investigation",
228+
return_value=_INVESTIGATION_OK,
229+
):
230+
outcome = runner.run_without_integrity()
231+
232+
assert outcome.aborted, "second model arm should not have been reachable"
233+
report = _read_report_json(out_dir, outcome.report.run_id)
234+
assert report["aborted"] is True
235+
assert "OPENAI_API_KEY" in (report["abort_reason"] or "")
236+
237+
238+
def test_report_json_records_a_complete_run(
239+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
240+
) -> None:
241+
"""The flip side: a run that finished the grid records that plainly, so
242+
the field distinguishes the two rather than only ever appearing on
243+
failure."""
244+
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
245+
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
246+
runner, out_dir = _two_llm_runner(tmp_path)
247+
with patch(
248+
"tools.investigation.capability.run_investigation",
249+
return_value=_INVESTIGATION_OK,
250+
):
251+
outcome = runner.run_without_integrity()
252+
253+
assert not outcome.aborted, outcome.abort_reason
254+
report = _read_report_json(out_dir, outcome.report.run_id)
255+
assert report["aborted"] is False
256+
assert report["abort_reason"] is None

0 commit comments

Comments
 (0)