"""B-gatens rad 1, steg 4: de SJU utboks-skriverne, drevet UTENFRA som kommandoer. Kjørestien skriver sju artefakter (`outbox.write_*`) som ingen utenfor po kunne be om: veien til dem gikk gjennom `run.main`, og hver vei gjennom den bygger en chatklient. Stegene selv trenger ingen modell — de tar ferdig rendret data og legger den på disk i en byte-deterministisk form. Denne fila måler at de nå har en dør, og at det som kom ut av døren er artefaktet kjørestien ville skrevet. Hver arm er en **atferdsprobe** i gatens forstand: `portfolio_optimiser.toolbox` startes som en subprosess med underkommandoens navn i argv, og asserten leser FILA kommandoen skrev. Ingen arm importerer skriveren og kaller den in-prosess. Fasiten er hentet UTENFOR døren i hver arm: nyttelasten testen selv skrev (og teller selv), og den byte-formen kontrakten krever — `json.dumps(sort_keys=True, indent=2)` pluss avsluttende linjeskift — komponert her, ikke lest fra `outbox._dump`. En arm som sammenlignet døren med dørens egen hjelper ville vært grønn uansett hva den skrev. Artefaktene skrives ALLTID til `tmp_path`. En probe som skrev i repoets egen utboks ville lagt igjen filer en senere kjøring leser som sine egne. """ from __future__ import annotations import json import subprocess import sys from pathlib import Path from typing import Any _REPO = Path(__file__).resolve().parents[1] _MIKRO = _REPO / "shared" / "examples" / "bygg-energi-mikro" _IR = _MIKRO / "validator-input.json" _GOLDEN = json.loads((_MIKRO / "golden.json").read_text(encoding="utf-8"))["validator"] _RUN = "probe-utboks-01" def _toolbox(*args: str) -> subprocess.CompletedProcess[str]: """Døren, som en subprosess. ``-m``-formen med vilje: den virker i en ren klone uten sync.""" return subprocess.run( [sys.executable, "-m", "portfolio_optimiser.toolbox", *args], cwd=_REPO, capture_output=True, text=True, ) def _json_arg(path: Path, payload: Any) -> str: path.write_text(json.dumps(payload), encoding="utf-8") return str(path) def _deterministic(payload: Any) -> str: """Kontraktens byte-form, komponert HER. Ikke `outbox._dump` — da målte armen en likhet modulen har med seg selv.""" return json.dumps(payload, sort_keys=True, indent=2) + "\n" # --- write-run-config ------------------------------------------------------------------------- def test_write_run_config_from_outside_is_the_byte_deterministic_artefact(tmp_path: Path) -> None: """Kjørekonfigurasjonen beskriver en kjøring UTEN et modellkall — og er derfor nøyaktig det steget en kaller utenfor po har bruk for før han bestemmer seg for å betale for en.""" modeller = _json_arg(tmp_path / "modeller.json", {"proposer": "gpt-x", "checker": "gpt-y"}) ut = tmp_path / "ut" proc = _toolbox( "write-run-config", "--out-dir", str(ut), "--run-id", _RUN, "--profile", "local", "--resolved-models", modeller, "--max-rounds", "5", "--max-tokens", "600000", "--top-k", "3", ) assert proc.returncode == 0, proc.stderr skrevet = Path(json.loads(proc.stdout)["path"]) assert skrevet == ut / f"{_RUN}-runconfig.json" assert skrevet.read_text(encoding="utf-8") == _deterministic( { "run_id": _RUN, "profile": "local", "resolved_models": {"proposer": "gpt-x", "checker": "gpt-y"}, "max_rounds": 5, "max_tokens": 600000, "top_k": 3, } ) # --- write-coverage --------------------------------------------------------------------------- def test_write_coverage_from_outside_keeps_every_row_and_the_stop_reason(tmp_path: Path) -> None: """Uavhengig telling: raden-lista skrives HER, og armen teller den her. `stop_reason` er påkrevd i kjernefunksjonen fordi «kjøringen ble ferdig» og «vi fikk aldri vite» ikke må være samme verdi — døren arver det kravet framfor å defaulte til tom streng.""" rader = [ { "id": "a1", "label": "Etterisolering", "status": "validated", "detail": "", "saving_nok": 30000.0, }, { "id": "a2", "label": "Styring", "status": "not_evaluated", "detail": "budsjett", "saving_nok": None, }, ] kilde = _json_arg(tmp_path / "rader.json", rader) ut = tmp_path / "ut" proc = _toolbox( "write-coverage", "--outbox-dir", str(ut), "--run-id", _RUN, "--rows", kilde, "--stop-reason", "tokens", ) assert proc.returncode == 0, proc.stderr skrevet = Path(json.loads(proc.stdout)["path"]) assert skrevet == ut / f"{_RUN}-coverage.json" paa_disk = json.loads(skrevet.read_text(encoding="utf-8")) assert paa_disk["stop_reason"] == "tokens" assert len(paa_disk["rows"]) == len(rader) assert paa_disk["rows"] == rader assert [r["id"] for r in paa_disk["rows"]] == ["a1", "a2"] # --- write-outbox ----------------------------------------------------------------------------- def _stamp(path: Path, *, decision: str, anchored: bool) -> str: """En provenance-stempel-JSON på modellens egen form (`ProvenanceStamp`).""" return _json_arg( path, { "citations": [ { "file": "index.md", "locator": {"start_index": 0, "end_index": 12}, "snippet": "energiprofil", } ], "model": "ingen-modell", "role": "proposer", "validator_decision": decision, "token_usage": 0, "cost_baseline_anchored": anchored, "bundle_id_source": None, "code_forms": {"ENERGI-TOTAL-EL": "identifier"}, "external_calls": [], }, ) def _baseline(path: Path, codes: dict[str, tuple[float, float]]) -> str: return _json_arg( path, { "project_id": "BYGG-KONTOR-NORD", "items": {c: {"quantity": q, "unit_cost": u} for c, (q, u) in codes.items()}, }, ) def test_write_outbox_from_outside_writes_the_pair_the_run_path_writes(tmp_path: Path) -> None: """Fasiten er basens INNSJEKKEDE golden-suite — produsentens frosne utfall av den samme deterministiske porten, skrevet før verktøykassen fantes. Utfallet er ikke noe kalleren kan OPPGI: døren utleder det gjennom `validate_proposal`, ellers ville utboksen vært en samling påstander framfor resultater.""" ut = tmp_path / "ut" proc = _toolbox( "write-outbox", "--outbox-dir", str(ut), "--run-id", _RUN, "--proposal", str(_IR), "--provenance", _stamp(tmp_path / "stempel.json", decision="validated", anchored=False), "--verdict-id", "v-probe", "--checker-verdict", "approve", ) assert proc.returncode == 0, proc.stderr skrevet = json.loads(proc.stdout) forslag = Path(skrevet["proposal_path"]) utfall = Path(skrevet["outcome_path"]) assert forslag == ut / f"{_RUN}-proposal.json" assert utfall == ut / f"{_RUN}-outcome.json" ir = json.loads(_IR.read_text(encoding="utf-8")) paa_disk = json.loads(forslag.read_text(encoding="utf-8")) assert paa_disk["run_id"] == _RUN assert paa_disk["proposal"]["project_id"] == ir["project_id"] assert paa_disk["proposal"]["measure"] == ir["measure"] assert paa_disk["provenance"]["validator_decision"] == "validated" dom = json.loads(utfall.read_text(encoding="utf-8")) assert dom["p50"] == _GOLDEN["p50"] assert dom["p10"] == _GOLDEN["p10"] assert dom["p90"] == _GOLDEN["p90"] assert dom["verdict_id"] == "v-probe" assert dom["checker_verdict"] == "approve" def test_write_outbox_carries_a_blocked_proposal_out_as_a_refusal(tmp_path: Path) -> None: """Et forslag den deterministiske porten BLOKKERER er exit 3 gjennom denne døren også — artefaktene skrives (det er dem dommen bor i), men en kaller som bare leser exit-koden skal aldri se et blokkert forslag som et klarert.""" stempel = _stamp(tmp_path / "stempel.json", decision="rejected", anchored=True) ukjent = _baseline(tmp_path / "ukjent.json", {"VARME-TOTAL": (1.0, 2.0)}) ut = tmp_path / "nekt" proc = _toolbox( "write-outbox", "--outbox-dir", str(ut), "--run-id", _RUN, "--proposal", str(_IR), "--provenance", stempel, "--verdict-id", "v-nekt", "--cost-baseline", ukjent, ) assert proc.returncode == 3, proc.stdout skrevet = json.loads(proc.stdout) assert skrevet["decision"] == "rejected" dom = json.loads(Path(skrevet["outcome_path"]).read_text(encoding="utf-8")) assert dom["reason"].startswith("unknown cost code 'ENERGI-TOTAL-EL'") assert "p50" not in dom # rc-0-kontroll: SAMME forslag og SAMME stempel mot et grunnlag som priser koden. Uten den # målte armen over ingenting — den ville vært grønn mot en dør som avviste alt. priset = _baseline(tmp_path / "priset.json", {"ENERGI-TOTAL-EL": (300000.0, 1.0)}) ok = _toolbox( "write-outbox", "--outbox-dir", str(tmp_path / "ok"), "--run-id", _RUN, "--proposal", str(_IR), "--provenance", stempel, "--verdict-id", "v-ok", "--cost-baseline", priset, ) assert ok.returncode == 0, ok.stdout assert json.loads(ok.stdout)["decision"] == "validated" # --- write-prepass ---------------------------------------------------------------------------- def test_write_prepass_from_outside_records_the_cut_the_run_was_given(tmp_path: Path) -> None: """Artefaktets TILSTEDEVÆRELSE er det som skiller «tilbaketrukket med vilje» fra «regrederte» når debatt-sporet er tomt — derfor måler armen både at fila finnes og hva den sier.""" erklaering = {"bundle_id": "bygg-energi-mikro-fixture", "considered": 12, "delivered": 4} kilde = _json_arg(tmp_path / "kutt.json", erklaering) ut = tmp_path / "ut" proc = _toolbox( "write-prepass", "--outbox-dir", str(ut), "--run-id", _RUN, "--declaration", kilde ) assert proc.returncode == 0, proc.stderr skrevet = Path(json.loads(proc.stdout)["path"]) assert skrevet == ut / f"{_RUN}-prepass.json" assert skrevet.read_text(encoding="utf-8") == _deterministic( {"run_id": _RUN, "prepass": erklaering} ) # --- write-parse-failures --------------------------------------------------------------------- def test_write_parse_failures_from_outside_keeps_every_reply_that_did_not_parse( tmp_path: Path, ) -> None: """Uavhengig telling: to feil inn, to feil ut — og rekkefølgen er kjøringens, ikke sortert.""" feil = [ {"text": "{ nesten json", "error": "Expecting property name"}, {"text": "prosa uten IR", "error": "Expecting value"}, ] kilde = _json_arg(tmp_path / "feil.json", feil) ut = tmp_path / "ut" proc = _toolbox( "write-parse-failures", "--outbox-dir", str(ut), "--run-id", _RUN, "--failures", kilde ) assert proc.returncode == 0, proc.stderr skrevet = Path(json.loads(proc.stdout)["path"]) assert skrevet == ut / f"{_RUN}-parse-failures.json" paa_disk = json.loads(skrevet.read_text(encoding="utf-8")) assert len(paa_disk["parse_failures"]) == len(feil) assert paa_disk["parse_failures"] == feil # --- write-proposal-reviews ------------------------------------------------------------------- def test_write_proposal_reviews_from_outside_states_an_empty_review_list(tmp_path: Path) -> None: """Regelen er «hvis en vurderer ble gitt, OGSÅ når lista er tom» (D4): en vurderer som ble tilbudt og aldri konsultert er et faktum artefaktet skal SI, ikke noe en operatør må slutte seg til av en fil som ikke er der. Døren arver regelen ved å skrive det den får.""" ut = tmp_path / "ut" tom = _json_arg(tmp_path / "tom.json", {"reviews": []}) proc = _toolbox( "write-proposal-reviews", "--outbox-dir", str(ut), "--run-id", _RUN, "--payload", tom ) assert proc.returncode == 0, proc.stderr skrevet = Path(json.loads(proc.stdout)["path"]) assert skrevet == ut / f"{_RUN}-proposal-reviews.json" assert skrevet.read_text(encoding="utf-8") == _deterministic({"run_id": _RUN, "reviews": []}) # Kontroll mot en arm som ville vært grønn for enhver nyttelast: en ikke-tom liste må komme # ut som den gikk inn, med samme telling. en = _json_arg(tmp_path / "en.json", {"reviews": [{"verdict_id": "v1", "decision": "accept"}]}) andre = _toolbox( "write-proposal-reviews", "--outbox-dir", str(ut), "--run-id", "probe-utboks-02", "--payload", en, ) assert andre.returncode == 0, andre.stderr paa_disk = json.loads(Path(json.loads(andre.stdout)["path"]).read_text(encoding="utf-8")) assert len(paa_disk["reviews"]) == 1 assert paa_disk["reviews"][0]["verdict_id"] == "v1" # --- write-debate-tools ----------------------------------------------------------------------- def test_write_debate_tools_from_outside_writes_an_empty_trace_as_a_statement( tmp_path: Path, ) -> None: """Skrives på HVER kjøring, også en debatt som åpnet ingenting — det tomme sporet ER S2c- regresjonen, og den må kunne leses av artefaktet framfor sluttes av en fil som mangler. `requirements` defaulter til tom av samme grunn: «denne debatten erklærte ingenting» er en ærlig positiv påstand.""" ut = tmp_path / "ut" tomt = _json_arg(tmp_path / "ingen.json", []) proc = _toolbox( "write-debate-tools", "--outbox-dir", str(ut), "--run-id", _RUN, "--tool-calls", tomt ) assert proc.returncode == 0, proc.stderr skrevet = Path(json.loads(proc.stdout)["path"]) assert skrevet == ut / f"{_RUN}-debate.json" assert skrevet.read_text(encoding="utf-8") == _deterministic( {"run_id": _RUN, "tool_calls": [], "requirements": []} ) # Kontroll: en debatt som FAKTISK åpnet noe skal se annerledes ut — ellers målte armen over # bare at fila finnes. kall = [ {"tool": "retrieve", "arguments": {"query": "energi"}}, {"tool": "retrieve", "arguments": {"query": "varme"}}, ] krav = [{"id": "R1", "text": "U-verdi"}] andre = _toolbox( "write-debate-tools", "--outbox-dir", str(ut), "--run-id", "probe-utboks-02", "--tool-calls", _json_arg(tmp_path / "kall.json", kall), "--requirements", _json_arg(tmp_path / "krav.json", krav), ) assert andre.returncode == 0, andre.stderr paa_disk = json.loads(Path(json.loads(andre.stdout)["path"]).read_text(encoding="utf-8")) assert len(paa_disk["tool_calls"]) == len(kall) assert len(paa_disk["requirements"]) == len(krav) assert paa_disk["tool_calls"][1]["arguments"]["query"] == "varme" # --- formvakten ------------------------------------------------------------------------------- def test_a_payload_of_the_wrong_shape_is_refused_rather_than_written(tmp_path: Path) -> None: """Skriverne tar `Mapping`/`Sequence` og ville serialisert hva som helst de fikk: en liste der et objekt hører hjemme blir en gyldig fil med feil form, og kjøringen som leser den SENERE er den som feiler. Døren feller formen med en gang, og gjør det som en avvisning (3) — kallet parset, så 2 ville vært feil kode.""" ut = tmp_path / "ut" feil = _json_arg(tmp_path / "feil.json", ["ikke", "et", "objekt"]) proc = _toolbox( "write-proposal-reviews", "--outbox-dir", str(ut), "--run-id", _RUN, "--payload", feil ) assert proc.returncode == 3, proc.stdout assert json.loads(proc.stdout)["error"]["kind"] == "ValueError" assert not ut.exists(), "en avvist form skal ikke ha lagt igjen en katalog eller en fil" # rc-0-kontroll: SAMME dør, samme sti, riktig form. Uten den ville armen vært grønn mot en # dør som avviste alt. ok = _toolbox( "write-proposal-reviews", "--outbox-dir", str(ut), "--run-id", _RUN, "--payload", _json_arg(tmp_path / "rett.json", {"reviews": []}), ) assert ok.returncode == 0, ok.stderr assert Path(json.loads(ok.stdout)["path"]).is_file()