portfolio-optimiser/tests/test_toolbox_doors.py
Kjell Tore Guttormsen 368367e1c5
feat(toolbox): the judgement through the same door -- validate-proposal, verdict-key, capture-verdict
B's premise applied to the three steps that DECIDE a proposal: a proposal authored outside po --
by a human, or by an agent that is not po -- now meets the blocking deterministic gate, mints the
learning key, and is captured as a Verdict, all without a chat client on the way.

Three thin adapters, no second implementation. The reason is the one the first four doors were
built on, but it bites harder here: the refusal SENTENCE is fed back verbatim into the next
attempt by step 5, so a door that reworded it would break the repair loop while still looking
correct. The probes assert the sentence, not a substring two stages share.

One measurement decided a design detail. The IR writes whole magnitudes as JSON integers
(30000), the run path carries the pydantic float, and verdicts._mint_id hashes the raw value --
so minting from the undeclared JSON would hand out a DIFFERENT verdict id than the debate does
for the same proposal. The door therefore reads the proposal through SavingsProposal and feeds
model_dump() to the public features_from_ir; the probe pins both forms and asserts they differ,
so the shortcut cannot come back silently.

A blocked proposal exits 3, carrying the verdict rather than an exception envelope. "You asked
right and the answer is no" is the same fact whether a file was missing or a claim was
infeasible, and a caller that only reads the exit code must not see a blocked proposal as a
cleared one.

Fasit outside the door in every arm: the base's own checked-in golden suite (written before the
toolbox existed, so it cannot have been fitted to it), a cost baseline authored in the test, a
method cap computed by hand from the fixture, and the public minting rule. Each refusal arm has
an rc-0 control on an argv that would otherwise be accepted.

STATED LIMIT: the input-grounding stage (P7, stage 0b) has no flag here. It falsifies a proposal
against the rendered prompt the model received, and an outside caller has no such prompt.

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

417 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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