portfolio-optimiser/tests/test_toolbox_outbox_doors.py
Kjell Tore Guttormsen 6375ce5af3
feat(toolbox): the seven outbox writers get their own door -- row 1 moves 8 -> 15 of 17
Every one of the run path's seven `outbox.write_*` steps was reachable only through
`run.main`, and every path through that builds a chat client. The steps need no model:
they take already-rendered data and put it on disk in a byte-deterministic form.

Seven subcommands, seven thin adapters. The outbox directory is always the caller's to
name -- never a default, never the repository's own, because a step that wrote into a
folder the framework also reads as an inbox would bypass the Step-8 promotion gate.

`write-outbox` is the one that is not purely mechanical: `outbox.write_outbox` branches
on the outcome TYPE, so a door that took the outcome as an argument would let anyone
author an outbox of claims and hand it to Step 8 as results. The door DERIVES it through
`validate_proposal` -- the run path's own composition -- and a blocked proposal exits 3
with the artefacts still written, since that is where the rejection is recorded.
`verdict_id` stays an argument: `verdict-key` already owns that minting.

`--stop-reason` is required rather than defaulted to the empty string, inheriting the
core writer's measured reason: "the run finished" and "we never found out" must not be
the same value.

Eight probes, each a subprocess with the subcommand in argv, each asserting on the FILE
the command wrote. The ground truth is composed in the test -- the payload it wrote and
counted itself, and the byte form the contract requires -- never `outbox._dump`, which
would have measured the module against itself. The refusal arm carries its rc-0 control.

Measured, own run of the B gate: row 1 8 of 17 -> 15 of 17, exit 1 unchanged, no other
row moved. 0 chat-client names reachable from the toolbox (known-positive control: 24 in
run.py).

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

393 lines
15 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"