portfolio-optimiser/tests/test_toolbox_outbox_doors.py
Kjell Tore Guttormsen c5aa5fcfde
test(b-gate): the pin moves 8 -> 15, and names the two steps that still share a door
`_EXTERNAL_TODAY` and `_WITH_A_DOOR_TODAY` are the arm's own count, not the gate's, so
the seven new doors have to be written down here before the suite agrees they exist. The
two assertions that pinned `kjørekonfig`/`utboks`/`prepass-artefakt`/`parse-feil` as
door-LESS are inverted rather than deleted: they were the arm's discriminator, and a
door that silently went away must still turn this red.

The manifest count moves 514 -> 515 for the one new probe file.

Also a probe for the shape guard: the writers take `Mapping`/`Sequence` and would
serialize whatever they were handed, so a list where an object belongs would become a
valid file with the wrong shape and the run that reads it later would be the one to
fail. The door refuses it as a 3 (the call parsed, so 2 is the wrong code) and leaves no
directory behind. Carries its rc-0 control.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 10:48:16 +02:00

424 lines
16 KiB
Python

"""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()