"""Ingen kaller-oppgitt NAVN bærer en sti inn i et filnavn — og de sju utboks-dørene håndhever det. Hver skriver bygger filnavnet sitt som ``/-.json``. Utkatalogen er kallerens å navngi (det er en egen regel), men run-id-en gikk urørt inn i den sammensetningen: ``--outbox-dir /inni --run-id ../../ESCAPE`` skrev artefaktet TO nivåer OVER katalogen kalleren navnga, og svarte 0. Vakten sto på den ene halvdelen av stien og manglet på den andre. Klassen måles her, ikke ett tilfelle: alle SJU dørene mot fire former for stibærende run-id — relativ oppover, absolutt, med skilletegn, og den bare ``..``. Hver arm asserterer to ting, fordi én av dem alene ville vært tilfreds med feil oppførsel: at exit-koden er ikke-null, OG at filtreet under `tmp_path` er BYTE-uendret etterpå. En dør som avviste med rett kode etter å ha lagt igjen fila ville passert på den første alene. rc-0-kontrollen er obligatorisk: samme argv med en ren run-id må svare 0 og skrive. Uten den måler armene ingenting — en dør som avviste ALT ville vært like grønn som den riktige. `--run-id` var bare den ene halvdelen av SAMME sammensetning. `write_outbox` bygger ``{run_id}-{approach_id}`` når en tilnærming er navngitt, og `--approach-id` gikk urørt inn: ``--approach-id a/../../../ESCAPE`` (etter at ``/-a`` finnes) svarte 0 og la artefaktene TRE nivåer over katalogen kalleren navnga. Telt som klasse: 11 stisammensetninger i `outbox.py`, to kaller-oppgitte verdier i dem — run-id og approach-id. Begge voktes her. Og inneslutningen står igjen som SISTE ledd, men der den er OPPNÅELIG: i `outbox.py`, der stien faktisk settes sammen. Strengregelen bor i døren, så kjørestiens egne kall på `outbox.write_*` (og `run.py`s eget `--run-id`) går UTENOM den — inneslutnings-armene under kaller skriverne direkte og er derfor ikke vakuøse. Den verner også mot det NESTE flagget noen legger inn i et filnavn: sammensetningen er ett sted, og sjekken sitter i den. """ from __future__ import annotations import json import subprocess import sys from collections.abc import Callable from pathlib import Path from typing import Any import pytest from portfolio_optimiser import outbox _REPO = Path(__file__).resolve().parents[1] _IR = _REPO / "shared" / "examples" / "bygg-energi-mikro" / "validator-input.json" #: En run-id uten stibærende tegn. Den ENE verdien rc-0-kontrollen står på. _REN = "probe-containment-01" def _toolbox(*args: str) -> subprocess.CompletedProcess[str]: """Døren som subprosess, `-m`-formen — 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_file(path: Path, payload: Any) -> str: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(payload), encoding="utf-8") return str(path) def _stamp(path: Path) -> str: return _json_file( path, { "citations": [ { "file": "index.md", "locator": {"start_index": 0, "end_index": 12}, "snippet": "energiprofil", } ], "model": "ingen-modell", "role": "proposer", "validator_decision": "validated", "token_usage": 0, "cost_baseline_anchored": False, "bundle_id_source": None, "code_forms": {"ENERGI-TOTAL-EL": "identifier"}, "external_calls": [], }, ) #: Dør -> (flagget som navngir utkatalogen, de øvrige argumentene bygget mot en fikstur-mappe). #: Argumentene er de MINIMALE som ellers ville gitt 0 — det er dét som gjør nekten målbar. _DOERER: dict[str, tuple[str, Callable[[Path], list[str]]]] = { "write-run-config": ( "--out-dir", lambda f: [ "--profile", "local", "--resolved-models", _json_file(f / "modeller.json", {"proposer": "gpt-x"}), "--max-rounds", "5", "--max-tokens", "600000", "--top-k", "3", ], ), "write-coverage": ( "--outbox-dir", lambda f: [ "--rows", _json_file(f / "rader.json", []), "--stop-reason", "tokens", ], ), "write-outbox": ( "--outbox-dir", lambda f: [ "--proposal", str(_IR), "--provenance", _stamp(f / "stempel.json"), "--verdict-id", "v-probe", ], ), "write-prepass": ( "--outbox-dir", lambda f: [ "--declaration", _json_file(f / "kutt.json", {"bundle_id": "x", "considered": 1, "delivered": 1}), ], ), "write-parse-failures": ( "--outbox-dir", lambda f: ["--failures", _json_file(f / "feil.json", [])], ), "write-proposal-reviews": ( "--outbox-dir", lambda f: ["--payload", _json_file(f / "vurderinger.json", {"reviews": []})], ), "write-debate-tools": ( "--outbox-dir", lambda f: ["--tool-calls", _json_file(f / "kall.json", [])], ), } def _tree(root: Path) -> dict[str, bytes]: """Hele filtreet som innhold, ikke bare navn — en dør som overskrev en fil den ikke skulle røre ville hatt samme navneliste.""" return { str(p.relative_to(root)): p.read_bytes() for p in sorted(root.rglob("*")) if p.is_file() } @pytest.mark.parametrize("door", sorted(_DOERER)) @pytest.mark.parametrize( "form", ["relativ-opp", "med-skilletegn", "bare-punktum", "absolutt"], ) def test_a_run_id_that_carries_a_path_is_refused_and_writes_nothing( tmp_path: Path, door: str, form: str ) -> None: """Fire former, sju dører. Utkatalogen ligger TO nivåer under `tmp_path`, så en rømning oppover lander et sted armen kan se — den ville vært usynlig om utkatalogen lå på toppen.""" fikstur = tmp_path / "fikstur" ut = tmp_path / "inni" / "dypere" flagg, oevrige = _DOERER[door] argv = oevrige(fikstur) run_id = { "relativ-opp": "../../ESCAPE", "med-skilletegn": "under/ESCAPE", "bare-punktum": "..", "absolutt": str(tmp_path / "ABSOLUTT"), }[form] foer = _tree(tmp_path) proc = _toolbox(door, flagg, str(ut), "--run-id", run_id, *argv) assert proc.returncode != 0, f"{door} godtok en stibærende run-id: {proc.stdout}" assert _tree(tmp_path) == foer, f"{door} la igjen en fil for run-id {run_id!r}" assert not ut.exists(), f"{door} opprettet utkatalogen for en avvist run-id" @pytest.mark.parametrize("door", sorted(_DOERER)) def test_the_same_call_with_a_clean_run_id_writes(tmp_path: Path, door: str) -> None: """rc-0-kontrollen. Samme dør, samme argv, eneste forskjell er run-id-en — så nekten over handler om run-id-en og ikke om at kallet var mangelfullt.""" fikstur = tmp_path / "fikstur" ut = tmp_path / "inni" / "dypere" flagg, oevrige = _DOERER[door] proc = _toolbox(door, flagg, str(ut), "--run-id", _REN, *oevrige(fikstur)) assert proc.returncode == 0, proc.stdout + proc.stderr skrevet = json.loads(proc.stdout) stier = [skrevet["path"]] if "path" in skrevet else [skrevet["proposal_path"]] for sti in stier: assert Path(sti).is_file() assert Path(sti).resolve().parent == ut.resolve(), "artefaktet havnet utenfor utkatalogen" def test_write_coverage_refuses_to_invent_a_stop_reason(tmp_path: Path) -> None: """«Kjøringen ble ferdig» og «vi fikk aldri vite» må ikke være samme verdi — derfor er `--stop-reason` PÅKREVD og defaulter aldri til tom streng. Regelen er kalt bærende i hovedboken, men ingen arm felte den: en `required=True` byttet til `default=""` passerte hele suiten. Denne armen er vakten — uten flagget skal kallet være en usage-feil, ikke en kjøring som stille kaller en avbrutt runde fullført.""" ut = tmp_path / "ut" rader = _json_file(tmp_path / "rader.json", []) uten = _toolbox("write-coverage", "--outbox-dir", str(ut), "--run-id", _REN, "--rows", rader) assert uten.returncode == 2, uten.stdout + uten.stderr assert "--stop-reason" in uten.stderr, "nekten må NAVNGI flagget som mangler" assert not ut.exists(), "et kall uten stoppgrunn skal ikke ha lagt igjen noe" # rc-0-kontroll: samme argv PLUSS flagget, med den tomme strengen som er en gyldig verdi — # det er nettopp verdien mutanten ville funnet på selv, og den må kalleren kunne oppgi. med = _toolbox( "write-coverage", "--outbox-dir", str(ut), "--run-id", _REN, "--rows", rader, "--stop-reason", "", ) assert med.returncode == 0, med.stdout + med.stderr paa_disk = json.loads(Path(json.loads(med.stdout)["path"]).read_text(encoding="utf-8")) assert paa_disk["stop_reason"] == "" #: De fire formene, som approach-id-en de settes inn som. `write_outbox` bygger stammen #: ``{run_id}-{approach_id}``, så approach-id-en er et SUFFIKS i sammensetningen og ikke et eget #: ledd: `../../ESCAPE` alene ville truffet et ikke-eksisterende `-..` og gitt en #: FileNotFoundError — altså grønt av feil grunn. Derfor går den relative formen gjennom et ledd #: som FINNES (`a/../../../`), som er nøyaktig formen som ble målt 20.09. _TILNAERMING_FORMER: dict[str, str] = { "relativ-opp-gjennom-eksisterende": "a/../../../ESCAPE", "med-skilletegn": "under/ESCAPE", "bare-punktum": "..", "absolutt": "/ABSOLUTT", } @pytest.mark.parametrize("form", sorted(_TILNAERMING_FORMER)) def test_an_approach_id_that_carries_a_path_is_refused_and_writes_nothing( tmp_path: Path, form: str ) -> None: """`--approach-id` × den ene døren den når. Samme sammensetning, samme krav. Mellomkatalogen den sammensatte stien peker gjennom opprettes FØRST, og det er ikke pynt: uten den feiler skrivingen på en manglende katalog, og armen er grønn ved HEAD av en grunn som ikke er vakten. Målt 20.09 — to av de fire formene var nettopp dét. Med katalogen på plass svarer ALLE fire 0 og skriver ved HEAD, så hver av dem skiller de to implementasjonene.""" fikstur = tmp_path / "fikstur" ut = tmp_path / "inni" / "dypere" verdi = _TILNAERMING_FORMER[form] (ut / f"{_REN}-{verdi}-proposal.json").parent.mkdir(parents=True, exist_ok=True) _, oevrige = _DOERER["write-outbox"] argv = oevrige(fikstur) foer = _tree(tmp_path) proc = _toolbox( "write-outbox", "--outbox-dir", str(ut), "--run-id", _REN, "--approach-id", verdi, *argv, ) assert proc.returncode != 0, f"write-outbox godtok en stibærende approach-id: {proc.stdout}" assert _tree(tmp_path) == foer, f"write-outbox la igjen en fil for {form}" def test_the_same_call_with_a_clean_approach_id_writes(tmp_path: Path) -> None: """rc-0-kontrollen for approach-id-en. Samme argv, eneste forskjell er verdien — og artefaktene bærer den utvidede stammen inne i katalogen kalleren navnga.""" fikstur = tmp_path / "fikstur" ut = tmp_path / "inni" / "dypere" _, oevrige = _DOERER["write-outbox"] proc = _toolbox( "write-outbox", "--outbox-dir", str(ut), "--run-id", _REN, "--approach-id", "a", *oevrige(fikstur), ) assert proc.returncode == 0, proc.stdout + proc.stderr skrevet = json.loads(proc.stdout) sti = Path(skrevet["proposal_path"]) assert sti.is_file() assert sti.name == f"{_REN}-a-proposal.json" assert sti.resolve().parent == ut.resolve(), "artefaktet havnet utenfor utkatalogen" #: Skriverne bak de sju dørene, hver kalt DIREKTE — utenom strengregelen, som bor i døren. #: Det er kjørestiens egen vei (`run.py` sender sin `--run-id` rett inn her), så inneslutningen #: er oppnåelig her og armene er ikke vakuøse. _SKRIVERE: dict[str, Callable[[str, str], Path]] = { "write_run_config": lambda d, r: outbox.write_run_config( d, r, profile="local", resolved_models={"proposer": "gpt-x"}, max_rounds=5, max_tokens=600000, top_k=3, ), "write_coverage": lambda d, r: outbox.write_coverage(d, r, rows=[], stop_reason="tokens"), "write_prepass": lambda d, r: outbox.write_prepass(d, r, declaration={"bundle_id": "x"}), "write_parse_failures": lambda d, r: outbox.write_parse_failures(d, r, failures=[]), "write_proposal_reviews": lambda d, r: outbox.write_proposal_reviews( d, r, payload={"reviews": []} ), "write_debate_tools": lambda d, r: outbox.write_debate_tools(d, r, tool_calls=[]), } @pytest.mark.parametrize("writer", sorted(_SKRIVERE)) def test_the_writer_itself_refuses_a_path_that_leaves_the_directory( tmp_path: Path, writer: str ) -> None: """Inneslutningen som SISTE ledd, der sammensetningen skjer. En ferdig sti som ikke resolverer UNDER utkatalogen er en feil, ikke en fil — og ingenting skal være skrevet når den felles.""" ut = tmp_path / "inni" / "dypere" foer = _tree(tmp_path) with pytest.raises(ValueError, match="utkatalogen|outside|outenfor|leaves"): _SKRIVERE[writer](str(ut), "../../ESCAPE") assert _tree(tmp_path) == foer, f"{writer} la igjen en fil for en rømmende sti" @pytest.mark.parametrize("writer", sorted(_SKRIVERE)) def test_the_writer_itself_still_writes_a_contained_path(tmp_path: Path, writer: str) -> None: """rc-0-kontrollen for inneslutningen: samme kall med et rent navn skriver, inne i katalogen. Uten den ville en skriver som nektet ALT vært like grønn som den riktige.""" ut = tmp_path / "inni" / "dypere" sti = _SKRIVERE[writer](str(ut), _REN) assert sti.is_file() assert sti.resolve().parent == ut.resolve()