"""B-gatens rad 1, steg 2: de fire verktøykasse-dørene, drevet UTENFRA som en kommando. Hver arm her er en **atferdsprobe** i B-gatens forstand: den starter `portfolio_optimiser.toolbox` som en subprosess med underkommandoens navn i argv, og asserterer på det kommandoen SKREV. Ingen av dem importerer kjernefunksjonen og kaller den in-prosess — det er nettopp den forskjellen gaten finnes for å måle, og en probe som gjør begge deler beviser ingenting om døren. Nevneren for «hva kommandoen skal ha skrevet» er hentet UTENFOR døren i hver arm: filsystemet (navigate-bundle), en tabell transkribert fra prisskjema-fixturen (cost-baseline), det in-prosess API-et den deler søm med (retrieve-chunks), og produsentens egen innsjekkede nyttelast (prepass-admit). En arm som sammenlignet døren med seg selv ville vært grønn uansett hva døren gjorde. INGEN modellkall, intet nett: hele poenget med verktøykassen er at disse stegene ikke har en chatklient på veien, og gaten feller døren om de får en. """ from __future__ import annotations import ast import json import shutil import subprocess import sys from pathlib import Path from portfolio_optimiser import datasource from portfolio_optimiser.verdicts import ( ProposalFeatures, capture_verdict, verdict_key, verdict_to_dict, ) _REPO = Path(__file__).resolve().parents[1] _MIKRO = _REPO / "shared" / "examples" / "bygg-energi-mikro" _PRISSKJEMA = _REPO / "tests" / "fixtures" / "k2-prisskjema-SYNTETISK" _PREPASS_PAYLOAD = ( _REPO / "tests" / "fixtures" / "prepass" / "bygg-energi-mikro-fixture.payload.json" ) _DECLARED_ID = "bygg-energi-mikro-fixture" #: Prisskjema-fixturens tabell, transkribert fra fixturen — ikke fra derivasjonen som leser den. _PRICED_ROWS = { "21.1": (1250.0, 850.0), "24.2": (48000.0, 42.5), "36.1": (4.0, 187500.0), } 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, ) # --- navigate-bundle -------------------------------------------------------------------------- def test_navigate_bundle_from_outside_reports_every_file_the_base_holds() -> None: """Fasiten er FILSYSTEMET: mikro-basens `.md`-filer, talt her. Basen er bygget så hver av dem nås fra indeksen, så «navigert» og «finnes» skal falle sammen — og gjør de det ikke, er det dét armen skal si.""" proc = _toolbox("navigate-bundle", "--bundle-dir", str(_MIKRO)) assert proc.returncode == 0, proc.stderr payload = json.loads(proc.stdout) paa_disk = sorted(p.name for p in _MIKRO.glob("*.md")) assert sorted(f["name"] for f in payload["files"]) == paa_disk assert payload["counts"]["files"] == len(paa_disk) assert payload["bundle_dir"] == str(_MIKRO) # Sporet bæres UT av døren, ikke summert bort: mikro-basens indeks lenker med vilje til # `shared/README.md`, som ligger utenfor basen. En dør som bare ga et filtall ville latt # «dokumentet finnes ikke» og «lenken var feil» se like ut for den som skal rette det. assert payload["skipped"] == [ {"from_file": "index.md", "target": "../../README.md", "reason": "outside-bundle"} ] assert payload["counts"]["skipped"] == 1 def test_navigate_bundle_refuses_a_directory_that_is_no_bundle_with_a_named_code() -> None: """rc-0-kontrollens motstykke: en avvisning må IKKE være 0. Exit-koden er det eneste signalet en agent som kaller døren har.""" proc = _toolbox("navigate-bundle", "--bundle-dir", str(_REPO / "docs")) assert proc.returncode == 3, proc.stdout assert json.loads(proc.stdout)["error"]["kind"] # --- cost-baseline ---------------------------------------------------------------------------- def test_cost_baseline_from_outside_derives_exactly_what_the_priced_table_says() -> None: proc = _toolbox("cost-baseline", "--bundle-dir", str(_PRISSKJEMA), "--project-id", "K2") assert proc.returncode == 0, proc.stderr payload = json.loads(proc.stdout) assert payload["project_id"] == "K2" assert set(payload["items"]) == set(_PRICED_ROWS) for code, (quantity, unit_cost) in _PRICED_ROWS.items(): assert payload["items"][code]["quantity"] == quantity, code assert payload["items"][code]["unit_cost"] == unit_cost, code def test_cost_baseline_refuses_an_unpriced_schedule_and_names_the_row() -> None: proc = _toolbox( "cost-baseline", "--bundle-dir", str(_MIKRO), "--project-id", "bygg-kontor-nord" ) assert proc.returncode == 3, proc.stdout assert json.loads(proc.stdout)["error"]["message"] # --- retrieve-chunks -------------------------------------------------------------------------- def test_retrieve_chunks_from_outside_matches_the_in_process_seam_exactly() -> None: """Den ene armen der fasiten ER det in-prosess API-et: verktøykassen skal gi en ekstern kaller NØYAKTIG det MAF-agentene får gjennom sin FunctionTool, ikke en egen variant.""" proc = _toolbox( "retrieve-chunks", "--query", "LED retrofit", "--docs-dir", str(_MIKRO), "--top-k", "2" ) assert proc.returncode == 0, proc.stderr payload = json.loads(proc.stdout) assert payload == datasource.retrieve_chunks("LED retrofit", str(_MIKRO), 2) assert len(payload) == 2 assert payload[0]["snippet"] def test_retrieve_chunks_refuses_a_top_k_that_asks_for_nothing() -> None: proc = _toolbox("retrieve-chunks", "--query", "x", "--docs-dir", str(_MIKRO), "--top-k", "0") assert proc.returncode == 3, proc.stdout assert "top_k" in json.loads(proc.stdout)["error"]["message"] # --- prepass-admit ---------------------------------------------------------------------------- def _base_declaring_its_id(tmp_path: Path) -> str: """Mikro-basen kopiert med `bundle_id` erklært — `shared/` er et pull-only subtree.""" root = tmp_path / "en-annen-mount" shutil.copytree(_MIKRO, root) index = root / "index.md" lines = index.read_text(encoding="utf-8").split("\n") assert lines[0].strip() == "---", "basen åpner ikke lenger med frontmatter" lines.insert(1, f"bundle_id: {_DECLARED_ID}") index.write_text("\n".join(lines), encoding="utf-8") return str(root) def test_prepass_admit_from_outside_admits_the_producers_own_payload(tmp_path: Path) -> None: """Produsentens innsjekkede output, uendret. Uten denne rc-0-kontrollen måler avvisnings- armen under ingenting — den ville vært grønn mot en dør som avviste alt.""" proc = _toolbox( "prepass-admit", "--payload", str(_PREPASS_PAYLOAD), "--bundle-dir", _base_declaring_its_id(tmp_path), ) assert proc.returncode == 0, proc.stderr payload = json.loads(proc.stdout) assert payload["admitted"] is True assert payload["bundle_id"] == _DECLARED_ID assert payload["delivered"] == 4 def test_prepass_admit_refuses_a_payload_that_names_another_base(tmp_path: Path) -> None: raw = json.loads(_PREPASS_PAYLOAD.read_text(encoding="utf-8")) raw["bundle"]["bundle_id"] = "en-helt-annen-base" endret = tmp_path / "endret.payload.json" endret.write_text(json.dumps(raw, ensure_ascii=False), encoding="utf-8") proc = _toolbox( "prepass-admit", "--payload", str(endret), "--bundle-dir", _base_declaring_its_id(tmp_path), ) assert proc.returncode == 3, proc.stdout assert "en-helt-annen-base" in json.loads(proc.stdout)["error"]["message"] # --- døren selv --------------------------------------------------------------------------------- def test_the_toolbox_has_no_chat_client_on_any_of_its_paths() -> None: """Verktøykassens ene forutsetning, målt i kilden: ingen av de fire kommandoene drar en modell med seg. Gaten måler det samme på rad 1; her står det som en egen påstand, fordi det er DENNE modulens grunn til å finnes.""" modul = _REPO / "src" / "portfolio_optimiser" / "toolbox.py" tre = ast.parse(modul.read_text(encoding="utf-8")) kalt = { node.func.attr if isinstance(node.func, ast.Attribute) else getattr(node.func, "id", "") for node in ast.walk(tre) if isinstance(node, ast.Call) } for navn in ("create_chat_client", "client_factory", "BaseChatClient", "get_backend"): assert navn not in kalt, navn importert: set[str] = set() for node in ast.walk(tre): if isinstance(node, ast.ImportFrom): importert |= {f"{node.module}.{a.name}" for a in node.names} elif isinstance(node, ast.Import): importert |= {a.name for a in node.names} assert not any(navn.startswith("portfolio_optimiser.run") for navn in importert), importert assert "portfolio_optimiser.run" not in importert # Kontroll: armen KAN se en import — uten den ville den vært grønn mot et tomt sett. assert "portfolio_optimiser.okf" in importert or "portfolio_optimiser.prepass" in importert def test_wrong_usage_is_exit_two_and_never_a_silent_zero() -> None: assert _toolbox().returncode == 2 assert _toolbox("ingen-slik-kommando").returncode == 2 assert _toolbox("navigate-bundle").returncode == 2 # --- dommen: validate-proposal / verdict-key / capture-verdict ---------------------------------- # # Steg 3 i B-gatens kø: de tre stegene som avgjør et forslag. Samme dør, samme regel om fasit — # den hentes UTENFOR døren i hver arm: basens egen innsjekkede golden-suite (validate-proposal), # den publike myntingsregelen `verdicts.verdict_key` (verdict-key) og `verdicts.verdict_to_dict` # over en dom bygget i testen (capture-verdict). _IR = _MIKRO / "validator-input.json" _GOLDEN = json.loads((_MIKRO / "golden.json").read_text(encoding="utf-8"))["validator"] #: Mikro-basens IR, transkribert fra `validator-input.json` — ikke lest ut av den. En arm som #: leste fila ville flyttet seg med den, og da måler den ikke lenger at døren gir DISSE tallene. _IR_CODE = "ENERGI-TOTAL-EL" _IR_PROJECT = "BYGG-KONTOR-NORD" _IR_QUANTITY = 300000.0 _IR_UNIT_COST = 1.0 _IR_CLAIMED = 30000.0 def _measure() -> str: """Tiltakets tekst, lest fra basen: den ER lang og står ordrett i domsnøkkelens kanoniske form, så en kopi her ville vært en transkribering som kunne drive fra basen uten at noe ble rødt.""" return str(json.loads(_IR.read_text(encoding="utf-8"))["measure"]) def _baseline_file(path: Path, codes: dict[str, tuple[float, float]]) -> str: """En kostnadsgrunnlags-JSON på PROSJEKTETS form (`okf.load_cost_baseline_file`).""" path.write_text( json.dumps( { "project_id": _IR_PROJECT, "items": {c: {"quantity": q, "unit_cost": u} for c, (q, u) in codes.items()}, } ), encoding="utf-8", ) return str(path) def test_validate_proposal_from_outside_reproduces_the_bases_own_golden() -> None: """Fasiten er basens INNSJEKKEDE golden-suite — produsentens frosne utfall av `validate_proposal` på nøyaktig denne IR-en, med seedet Monte Carlo. Den ble skrevet før verktøykassen fantes, så den kan ikke ha blitt tilpasset døren.""" proc = _toolbox("validate-proposal", "--proposal", str(_IR)) assert proc.returncode == 0, proc.stderr payload = json.loads(proc.stdout) assert payload["decision"] == "validated" assert payload["nominal_feasible"] == _GOLDEN["nominal_feasible"] assert payload["p10"] == _GOLDEN["p10"] assert payload["p50"] == _GOLDEN["p50"] assert payload["p90"] == _GOLDEN["p90"] assert payload["claimed_saving_nok"] == _GOLDEN["claimed_saving_nok"] # Uten grunnlag kjørte stadium 0 ALDRI, og døren sier det framfor å la kalleren tro at # tallene ble holdt mot prosjektets egne kostnadslinjer. assert payload["cost_baseline_anchored"] is False def test_validate_proposal_refuses_a_code_the_cost_baseline_never_priced(tmp_path: Path) -> None: """Stadium 0 gjennom døren, med den ORDRETTE setningen løpet ellers skriver — det er den Steg 5 mater tilbake til neste forsøk, så en dør som omskrev den ville brutt sløyfa.""" ukjent = _baseline_file(tmp_path / "annen.json", {"VARME-TOTAL": (1.0, 2.0)}) proc = _toolbox("validate-proposal", "--proposal", str(_IR), "--cost-baseline", ukjent) assert proc.returncode == 3, proc.stdout payload = json.loads(proc.stdout) assert payload["decision"] == "rejected" assert payload["reason"] == ( f"unknown cost code '{_IR_CODE}': not in project {_IR_PROJECT}'s cost baseline " f"(1 known codes: 'VARME-TOTAL')" ) assert payload["stage"] == "stage0-baseline" assert payload["cost_baseline_anchored"] is True # rc-0-kontroll: SAMME forslag mot et grunnlag som priser koden. Uten den måler armen over # ingenting — den ville vært grønn mot en dør som avviste alt. priset = _baseline_file(tmp_path / "priset.json", {_IR_CODE: (_IR_QUANTITY, _IR_UNIT_COST)}) ok = _toolbox("validate-proposal", "--proposal", str(_IR), "--cost-baseline", priset) assert ok.returncode == 0, ok.stdout assert json.loads(ok.stdout)["decision"] == "validated" assert json.loads(ok.stdout)["cost_baseline_anchored"] is True def test_validate_proposal_applies_the_method_cap_through_the_same_door(tmp_path: Path) -> None: """Metodetaket er DATA (F8), og døren rekker det: et strengere tak enn den generiske P90 avviser et forslag P90-stadiet slapp gjennom — samme dør, samme ordrette setning. Taket regnes UAVHENGIG her: 0,05 × (300 000 × 1,0) = 15 000, transkribert fra basen.""" tak = tmp_path / "tak.json" tak.write_text(json.dumps({_measure(): 0.05}), encoding="utf-8") ventet_tak = 0.05 * _IR_QUANTITY * _IR_UNIT_COST assert ventet_tak == 15000.0 proc = _toolbox("validate-proposal", "--proposal", str(_IR), "--method-caps", str(tak)) assert proc.returncode == 3, proc.stdout payload = json.loads(proc.stdout) assert payload["reason"] == ( f"claimed {_IR_CLAIMED:.0f} exceeds the {_measure()} method cap {ventet_tak:.0f} " f"(stricter than the generic P90)" ) assert payload["stage"] == "stage5-method-cap" # rc-0-kontroll: et tak som IKKE er strengere enn kravet slipper samme forslag gjennom. romslig = tmp_path / "romslig.json" romslig.write_text(json.dumps({_measure(): 0.30}), encoding="utf-8") ok = _toolbox("validate-proposal", "--proposal", str(_IR), "--method-caps", str(romslig)) assert ok.returncode == 0, ok.stdout assert json.loads(ok.stdout)["decision"] == "validated" def test_verdict_key_from_outside_is_the_key_the_learning_loop_mints(tmp_path: Path) -> None: """A5: én publik domsnøkkel. Fasiten er `verdicts.verdict_key` kalt på trekk bygget HER — og armen skiller de to formene som ellers ser like ut: IR-en skriver `30000`, kjørestien sender en pydantic-FLOAT videre, og `_mint_id` hasher den rå verdien. En dør som mintet fra JSON-en slik den står ville gitt en ANNEN nøkkel enn løpet, på samme forslag.""" trekk = ProposalFeatures( affected_codes=frozenset({_IR_CODE}), measure_type=_measure(), claimed_saving_nok=_IR_CLAIMED, description=_measure(), ) som_heltall = ProposalFeatures( affected_codes=frozenset({_IR_CODE}), measure_type=_measure(), claimed_saving_nok=30000, description=_measure(), ) assert verdict_key(trekk) != verdict_key(som_heltall), "de to formene er ikke lenger ulike" proc = _toolbox("verdict-key", "--proposal", str(_IR)) assert proc.returncode == 0, proc.stderr payload = json.loads(proc.stdout) assert payload["verdict_id"] == verdict_key(trekk) assert payload["verdict_id"] != verdict_key(som_heltall) assert payload["affected_codes"] == [_IR_CODE] assert payload["claimed_saving_nok"] == _IR_CLAIMED # Nøkkelen er ingen konstant: et forslag med et annet beløp nøkles annerledes. annet = json.loads(_IR.read_text(encoding="utf-8")) annet["claimed_saving_nok"] = 12345 sti = tmp_path / "annet.json" sti.write_text(json.dumps(annet), encoding="utf-8") andre = _toolbox("verdict-key", "--proposal", str(sti)) assert andre.returncode == 0, andre.stderr assert json.loads(andre.stdout)["verdict_id"] != payload["verdict_id"] def test_capture_verdict_from_outside_is_the_verdict_the_store_would_hold() -> None: """Dommen fanget utenfra skal være NØYAKTIG den `capture_verdict` bygger i løpet — samme felter, samme id, samme serialisering (`verdict_to_dict`, som er formen `write_verdict` legger på disk).""" grunn = "realiseringsgapet er dokumentert i basen; tallene holder" ventet = verdict_to_dict( capture_verdict( ProposalFeatures( affected_codes=frozenset({_IR_CODE}), measure_type=_measure(), claimed_saving_nok=_IR_CLAIMED, description=_measure(), ), "approved", grunn, ) ) proc = _toolbox( "capture-verdict", "--proposal", str(_IR), "--decision", "approved", "--rationale", grunn, ) assert proc.returncode == 0, proc.stderr assert json.loads(proc.stdout) == ventet def test_capture_verdict_refuses_a_decision_the_feedback_contract_does_not_know() -> None: """Vokabularet er kontraktens (`FeedbackContract`), ikke dørens egen kopi: en halvgitt eller oppfunnet dom avvises med FELTNAVNET, aldri fullført på fagpersonens vegne.""" proc = _toolbox( "capture-verdict", "--proposal", str(_IR), "--decision", "kanskje", "--rationale", "x", ) assert proc.returncode == 3, proc.stdout melding = json.loads(proc.stdout)["error"]["message"] assert "decision" in melding and "kanskje" in melding # rc-0-kontroll: samme kall med en dom kontrakten KJENNER. ok = _toolbox( "capture-verdict", "--proposal", str(_IR), "--decision", "rejected", "--rationale", "x", ) assert ok.returncode == 0, ok.stdout assert json.loads(ok.stdout)["decision"] == "rejected"