"""Value-report seam in run.py — LOAD-BEARING (S5.4-analog; §11; paritetsrad 25; K11). The seam this file keeps alive: the deliverable entrance can produce the value report as an OPT-IN side product of a run, projecting the layers that are on disk when the run finishes — including the outbox pair the run itself just filed. The report is a projection, never a second source of truth, so it can neither change the run's verdict nor be produced without an outbox to project. Detach proofs: * Report wired: an opted-in run writes the JSON, and it carries THIS run's proposal as modelled value. Detach point: drop the ``write_value_report`` call in ``main`` → no file → ``test_run_writes_the_value_report`` RED. * Opt-in requires an outbox, refused BEFORE any spend: ``--value-report`` without ``--outbox`` exits via ``parser.error`` and the scripted client is never constructed. Detach point: remove the fail-fast → the run spends and only then discovers it has nothing to project → ``test_value_report_without_outbox_ refuses_before_any_spend`` RED. * Both outcomes: a budget-stopped run still writes the report AND still exits 3. Detach point: produce the report only on success, or let a report failure overwrite the run's exit code → ``test_budget_stop_still_reports_and_keeps_ exit_code`` RED. """ from __future__ import annotations import json from pathlib import Path import pytest from _scripted import ScriptedClient, reply from portfolio_optimiser_claude.contracts import Contracts from portfolio_optimiser_claude.ir import load_validator_input from portfolio_optimiser_claude.loop import ModelClient from portfolio_optimiser_claude.run import main as run_main BUNDLE = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro" def _scripted_factory(replies: list[object]) -> tuple[object, list[ScriptedClient]]: created: list[ScriptedClient] = [] def factory(contracts: Contracts, max_budget_usd_per_call: float) -> ModelClient: client = ScriptedClient(replies=list(replies)) # type: ignore[arg-type] created.append(client) return client return factory, created def _happy_replies() -> list[object]: return [ reply("debate reasoning"), reply("VERDICT: APPROVE"), reply(json.dumps(load_validator_input(BUNDLE).model_dump())), ] def _run_args(tmp_path: Path, *extra: str) -> list[str]: return [ "--bundle", str(BUNDLE), "--out", str(tmp_path / "out"), "--outbox", str(tmp_path / "outbox"), "--run-id", "r-001", *extra, ] def test_run_writes_the_value_report(tmp_path: Path) -> None: """LOAD-BEARING (report wired): the run's own pair shows up as modelled value.""" destination = tmp_path / "reports" / "value.json" factory, _ = _scripted_factory(_happy_replies()) code = run_main(_run_args(tmp_path, "--value-report", str(destination)), client_factory=factory) assert code == 0 payload = json.loads(destination.read_text(encoding="utf-8")) assert payload["n_proposals"] == 1 assert payload["modelled_nok"] == load_validator_input(BUNDLE).claimed_saving_nok # No inbox, no ledger → the expert stages are UNMARKED, never mirrored from modelled. assert payload["expert_corrected_nok"] is None assert payload["n_pending"] == 1 assert payload["projects"][0]["realized_nok"] is None def test_value_report_without_outbox_refuses_before_any_spend(tmp_path: Path) -> None: """LOAD-BEARING (fail-fast): nothing to project → refuse BEFORE constructing a client.""" factory, created = _scripted_factory(_happy_replies()) with pytest.raises(SystemExit): run_main( [ "--bundle", str(BUNDLE), "--out", str(tmp_path / "out"), "--value-report", str(tmp_path / "value.json"), ], client_factory=factory, ) assert created == [] # refused before the client existed → no spend assert not (tmp_path / "value.json").exists() def test_budget_stop_still_reports_and_keeps_exit_code(tmp_path: Path) -> None: """LOAD-BEARING (both outcomes): a stop is still reported, and stays exit 3.""" destination = tmp_path / "value.json" factory, _ = _scripted_factory([reply("debate reasoning", usage_tokens=10)]) code = run_main( _run_args(tmp_path, "--max-tokens", "5", "--value-report", str(destination)), client_factory=factory, ) assert code == 3 # the run's own verdict is untouched by reporting payload = json.loads(destination.read_text(encoding="utf-8")) assert payload["n_proposals"] == 0 # a stop files no pair — an honest empty picture def test_report_failure_never_rewrites_a_budget_stop(tmp_path: Path) -> None: """A malformed ledger fails the report, but a budget stop still reports as a stop.""" broken_ledger = tmp_path / "ledger.json" broken_ledger.write_text('{"entries": {}}', encoding="utf-8") factory, _ = _scripted_factory([reply("debate reasoning", usage_tokens=10)]) code = run_main( _run_args( tmp_path, "--max-tokens", "5", "--value-report", str(tmp_path / "value.json"), "--ledger", str(broken_ledger), ), client_factory=factory, ) assert code == 3 # NOT rewritten to 1 — the budget stop is the bigger news assert not (tmp_path / "value.json").exists() def test_report_failure_fails_an_otherwise_clean_run(tmp_path: Path) -> None: """The operator asked for a report and did not get one → the command is non-zero.""" broken_ledger = tmp_path / "ledger.json" broken_ledger.write_text("[]", encoding="utf-8") factory, _ = _scripted_factory(_happy_replies()) code = run_main( _run_args( tmp_path, "--value-report", str(tmp_path / "value.json"), "--ledger", str(broken_ledger) ), client_factory=factory, ) assert code == 1 assert (tmp_path / "outbox" / "r-001-proposal.json").exists() # the run itself still landed