"""The Fase 1b full-run contract must DISCRIMINATE — proven offline, for free (Fase 1b, last step). ``tests/test_full_run_live.py`` spends real money on a real endpoint, exactly once, and cannot be run red-then-green: the paid call is the MEASUREMENT, not the proof that the measuring instrument works. This file is that proof, and it costs nothing. The repo's own rule — *"en test som ikke kan skille to implementasjoner beviser ingenting"* — has been violated by this project's own measuring instruments three times (økt 27's slide sweep read ``section``'s 100vh and reported the same number for all eleven slides; økt 37's T3 asserted on a schema the scripted client ignores). An assertion that can only ever pass is the same defect class, and a LIVE assertion is the worst place to discover it, because a green result there is precisely what the operator would act on. So both arms drive the SAME ``assert_full_run_contract`` helper the live test uses (ONE copy, in ``conftest`` — a second copy would drift, kø-(p)), over the SAME bundle and the SAME project as the live run, through the canonical ``ScriptedChatClient``'s ``reply_selector`` seam: * T1 — a run in which ONE reply failed to parse must make the contract FAIL. This is the arm that matters: it is the offline stand-in for "the live endpoint ignored ``response_format``", which is the single outcome the paid run exists to rule out. Per the repo rule for negative asserts, it first PROVES the event happened (the artefact exists) rather than inferring it from the failure. * T2 — the CONTROL: a run in which every reply parsed must make the contract PASS. Without it, a helper that raised unconditionally would satisfy T1 and the live test could then only ever be red, which is the mirror-image vacuity. Together they show the contract keys on the ACTUAL discriminator (the parse-failure artefact), not on something both runs share. """ from __future__ import annotations from collections.abc import Callable from pathlib import Path import pytest from conftest import assert_full_run_contract from portfolio_optimiser.budget import Budget, TokenMeter from portfolio_optimiser.run import RunResult, run_project from portfolio_optimiser.simulation import ScriptedChatClient from portfolio_optimiser.verdicts import VerdictStore BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro" _PROJECT_ID = "BYGG-KONTOR-NORD" _VERDICT_INPUT = {"decision": "approved", "rationale": "expert reviewed (contract probe)"} #: The line ``generate._build_messages`` puts in EVERY generation prompt and nowhere else — the one #: identifier separating a generation call from a debate turn (borrowed from #: ``test_parse_failure_capture_loadbearing``, whose seam this file shares). _GENERATION_MARK = "Respond with ONLY a JSON object" #: Prose where an object was requested: what a model that ignored ``response_format`` returns. _MALFORMED = ( "Sure! Here is what I found for this project.\n\n" "The main opportunity looks like demand-controlled lighting, worth roughly 30 000 NOK.\n" "Let me know if you want that as JSON." ) #: BYGG-KONTOR-NORD: affected total 300000 x 1.0 -> degenerate Monte Carlo P90 = 90000, so a claim #: of 30000 validates (same arithmetic as ``test_parse_failure_capture_loadbearing``). _VALID_REPLY = ( '{"measure":"Behovsstyrt belysning i fellesarealer","affected_items":' '[{"code":"ENERGI-TOTAL-EL","quantity":300000,"unit_cost":1.0}],' '"claimed_saving_nok":30000}' ) def _malformed_then_valid(failures: int) -> Callable[[str, str], str]: """Fail to parse ``failures`` times, then answer with a proposal that validates. Keyed on the GENERATION prompt only, so debate turns — which are never parsed — do not consume the counter.""" seen = {"n": 0} def _select(blob: str, _role: str) -> str: if _GENERATION_MARK not in blob: return "ok" seen["n"] += 1 return _MALFORMED if seen["n"] <= failures else _VALID_REPLY return _select async def _run(select: Callable[[str, str], str], outbox_dir: Path, run_id: str) -> RunResult: def factory(role: str) -> ScriptedChatClient: return ScriptedChatClient(role=role, reply_selector=select, default_reply="ok") result = await run_project( _PROJECT_ID, "local", docs_dir=str(BUNDLE_DIR), bundle_dir=str(BUNDLE_DIR), verdict_input=_VERDICT_INPUT, store=VerdictStore(verdicts=[]), client_factory=factory, outbox_dir=str(outbox_dir), run_id=run_id, meter=TokenMeter(Budget(max_tokens=10**9, max_rounds=8)), ) assert isinstance(result, RunResult) return result # -------------------------------------------------------------------------------------------- # T1 — the arm that matters: an unparsed reply must FAIL the contract. # -------------------------------------------------------------------------------------------- async def test_contract_fails_when_a_reply_did_not_parse(tmp_path: Path) -> None: """The offline stand-in for "the live endpoint ignored the schema". The run itself SUCCEEDS — the second reply validates — so the contract cannot be keying on the run's outcome; the only thing separating this from T2 is the parse-failure artefact. RED (i.e. this test fails) on a contract that ignores the artefact and merely checks that a RunResult came back — which is what ``test_portfolio_live.py``'s ``len(runs) == 1`` does, and the reason that existing gated test could not carry this claim.""" outbox_dir = tmp_path / "outbox" run_id = "contract-dirty" result = await _run(_malformed_then_valid(1), outbox_dir, run_id) # Prove the event happened FIRST — a negative assert that merely observes a failure cannot tell # "the contract caught the artefact" from "the contract failed for some unrelated reason". artefact = outbox_dir / f"{run_id}-parse-failures.json" assert artefact.exists(), "precondition: this arm must actually produce a parse failure" assert result.provenance.validator_decision in {"validated", "rejected"}, ( "precondition: the run must otherwise CONCLUDE, so the artefact is the only difference" ) with pytest.raises(AssertionError, match="did NOT honour the structured schema"): assert_full_run_contract(result, outbox_dir, run_id) # -------------------------------------------------------------------------------------------- # T2 — the CONTROL: a clean run must PASS. # -------------------------------------------------------------------------------------------- async def test_contract_passes_when_every_reply_parsed(tmp_path: Path) -> None: """Without this control, a helper that raised unconditionally would satisfy T1, and the live test could then only ever be red — the mirror image of a check that can only ever be green.""" outbox_dir = tmp_path / "outbox" run_id = "contract-clean" result = await _run(_malformed_then_valid(0), outbox_dir, run_id) assert not (outbox_dir / f"{run_id}-parse-failures.json").exists() assert_full_run_contract(result, outbox_dir, run_id)