|
16 | 16 |
|
17 | 17 | from __future__ import annotations |
18 | 18 |
|
| 19 | +import json |
19 | 20 | from collections.abc import Iterator |
20 | 21 | from pathlib import Path |
21 | 22 | from typing import Any |
@@ -85,6 +86,27 @@ def _runner(tmp_path: Path) -> BenchmarkRunner: |
85 | 86 | return BenchmarkRunner(config=config, adapter=_TinyAdapter()) |
86 | 87 |
|
87 | 88 |
|
| 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 | + |
88 | 110 | def _call_run_one_cell(runner: BenchmarkRunner, tmp_path: Path) -> None: |
89 | 111 | """Drive _run_one_cell with a minimal valid arg set.""" |
90 | 112 | 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) - |
174 | 196 | with patch("tools.investigation.capability.run_investigation", _raises_runtime): |
175 | 197 | # Should NOT raise — cell-level failure recorded in the _CellResult |
176 | 198 | _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