portfolio-optimiser/tests/test_scripted_cli_door_loadbearing.py
Kjell Tore Guttormsen 56f4f6d084 feat(hitl): ekspertdommen kan ikke oppstaa av stillhet (F2, ORDRE 20260825T214801Z)
run_project KREVDE verdict_input og kjorte capture_verdict ubetinget; CLI-en
defaultet det til {"approved", "reviewed by expert"} og hosting listet det som
PAAKREVD. Netto: hver flaggloes kjoering myntet en ekspertgodkjenning ingen ga,
den gikk inn i den delte storen, og run_portfolio bar den inn i neste prosjekts
hypotese-prompt som en prior expert verdict -- paa flaten som ble overlevert
14.08. Non-goal 3, brutt i en soem.

RunResult.verdict er naa Verdict | None, og None er hva stillhet produserer:
ingenting myntes, ingenting lagres, ingenting varsles. Prinsippet sto allerede i
repoet -- RunFailure sin docstring: aa fylle et felt med en dummy legger
FABRIKKERT proveniens inn i aggregatet.

Traceability koster ingenting: RunResult.verdict_key (property, derivert fra
kandidaten) er verdicts.verdict_key sitt alt dokumenterte formaal -- identisk
med verdict.id naar en dom BLE gitt, og fortsatt meningsfull naar ingen ble det.
Det er den outboxen og den hostede responsen stempler.

Halv dom NEKTES paa begge doerer (FeedbackContract er eneste sted formen
valideres; CLI-en nekter ved navn FOER enhver mode-dispatch). Validering, aldri
reparasjon. De to mode-partisjonene fikk --decision/--rationale inn: kommentarene
sa ordrett at en aerlig nekt var uimplementerbar fordi de non-None
argparse-defaultene gjorde en eksplisitt verdi uskillbar fra defaulten -- med
defaultene borte er den implementerbar.

Hosting er WIDENING, ikke bryting: verdict_input flyttet fra _REQUIRED_FIELDS
til _OPTIONAL_FIELDS. Ingen ekstern kaller brekker.

AERLIGHETS-GRENSE: referanse-fixturens SYNTETISKE verdict_input-rader staar
uroert -- de er merket SYNTETISK paa fire steder og er reviewens F5 (maaling av
misjonspaastanden), ikke F2. Project.verdict_input er naa valgfri.

Load-bearing MAALT (tests/test_ungiven_verdict_loadbearing.py, 15 armer), aatte
mutasjoner alle roede mot HELE suiten + gronn kontroll 1080/5 og golden
demo-transcript.stdout BYTE-UENDRET (ea8c534773acdbe41ae68f2c55724d69aaf8be4f).
En mutasjon falsifiserte testen foerst (vakuoes-gate-klassen, ellevte gang):
--report-armen brukte et bart --report, som nekter rc 1 uansett fordi --ledger
mangler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 01:22:07 +02:00

169 lines
7.3 KiB
Python

"""The offline CLI door: run the WHOLE loop over your OWN bundle with zero model calls.
Before this, an adopter without an API budget had two half-doors and no whole one:
``--live-dry-run`` takes their own bundle but STOPS before the first model call
(``run.py`` returns a ``DryRunReport``), while ``portfolio_optimiser.simulation`` runs the
complete loop but only over ITS bundle with ITS scripted answers. The seam for the missing
third case already existed — ``run_project(client_factory=...)``, the test-injection seam —
with no CLI exposure at all. ``--scripted-replies <file.json>`` is that door.
**The honesty condition is part of the feature, not decoration** (målbilde §1): a scripted run
that reads like a model run is worse than no offline mode, so the banner asserted here is
load-bearing in the same sense the dataflow is. It mirrors ``simulation.py``'s honesty banner.
Load-bearing (each blade measured by detaching exactly one thing):
1. the door drives a FULL run offline over a caller-supplied bundle (RED without the flag);
2. the run is genuinely model-free (RED if a real factory is built);
3. the banner is emitted and unmistakable (RED when detached);
4. control: no banner without the flag — so blade 3 cannot pass on a constant;
5. the two offline modes are mutually exclusive rather than one silently winning.
"""
from __future__ import annotations
import json
import shutil
from pathlib import Path
import pytest
from portfolio_optimiser import run
from portfolio_optimiser.ledger import SavingsLedger
BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
# The same scripted answers the simulation uses, re-declared here as CALLER-supplied input —
# which is the whole point of the door: these arrive as a file the adopter writes, not as a
# module constant only the shipped simulation can reach.
_VALID_PROPOSAL = (
'{"measure":"LED-retrofit av kontorbelysning","affected_items":'
'[{"code":"ENERGI-TOTAL-EL","quantity":300000,"unit_cost":1.0}],"claimed_saving_nok":30000}'
)
_CHECKER_APPROVE = "Tallene er innenfor feasibelt område og resonnementet holder. VERDICT: APPROVE"
@pytest.fixture(autouse=True)
def _isolate_model_env(monkeypatch: pytest.MonkeyPatch) -> None:
"""Hermetic env (mirrors ``test_live_dry_run.py``): the operator's Foundry overrides must not
reach these assertions."""
monkeypatch.delenv("PORTFOLIO_MODEL_MAP", raising=False)
monkeypatch.delenv("PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT", raising=False)
@pytest.fixture()
def bundle(tmp_path: Path) -> Path:
"""A throwaway COPY — the shared fixture is commons-owned and is never mutated by a test."""
dst = tmp_path / "bundle"
shutil.copytree(BUNDLE_DIR, dst)
return dst
@pytest.fixture()
def replies_file(tmp_path: Path) -> Path:
path = tmp_path / "replies.json"
path.write_text(
json.dumps({"proposer": _VALID_PROPOSAL, "checker": _CHECKER_APPROVE}),
encoding="utf-8",
)
return path
def _argv(bundle: Path, replies_file: Path) -> list[str]:
return [
"BYGG-KONTOR-NORD",
"--docs-dir",
str(bundle),
"--bundle-dir",
str(bundle),
"--scripted-replies",
str(replies_file),
]
def test_scripted_door_runs_the_whole_loop_offline(bundle, replies_file, capsys) -> None:
"""Blade 1 — the door exists and completes a FULL run (not a dry-run report) over the
caller's own bundle. RED before the flag: argparse exits 2 on an unrecognized argument."""
rc = run.main(_argv(bundle, replies_file))
out = capsys.readouterr().out
assert rc == 0, out
# A full run reports its outcome type + the candidate's verdict key; a dry-run never gets this
# far. Since F2 this argv records NO expert verdict (no --decision/--rationale), so the line
# says so and quotes the key one would arrive under — asserting "verdict id=" here would be
# asserting that a run nobody reviewed minted an approval.
assert "BYGG-KONTOR-NORD:" in out
assert "no expert verdict given; verdict key=" in out
assert "LIVE-DRY-RUN" not in out
def test_scripted_door_makes_no_real_client(bundle, replies_file, monkeypatch, capsys) -> None:
"""Blade 2 — genuinely model-free: the production factory must never be built. Detonates if
the scripted factory is ignored and the run falls back to ``_default_factory``."""
def _boom(_profile: str): # pragma: no cover - the point is that it never runs
raise AssertionError("a real client factory was built on the scripted path")
monkeypatch.setattr(run, "_default_factory", _boom)
rc = run.main(_argv(bundle, replies_file))
assert rc == 0, capsys.readouterr().out
def test_scripted_door_says_so_unmistakably(bundle, replies_file, capsys) -> None:
"""Blade 3 — the honesty banner. A scripted run that looks like a model run is the failure
mode this asserts against; RED the moment the banner is detached."""
run.main(_argv(bundle, replies_file))
out = capsys.readouterr().out.upper()
assert "SCRIPTED" in out
assert "NO MODEL" in out or "INGEN MODELLKALL" in out
def test_no_banner_without_the_flag(bundle, capsys) -> None:
"""Blade 4 (control) — the banner must be CAUSED by the flag, not printed unconditionally.
Uses --live-dry-run so the control needs no model call of its own."""
run.main(
[
"BYGG-KONTOR-NORD",
"--docs-dir",
str(bundle),
"--bundle-dir",
str(bundle),
"--live-dry-run",
]
)
assert "SCRIPTED" not in capsys.readouterr().out.upper()
def test_the_two_offline_modes_are_exclusive(bundle, replies_file, capsys) -> None:
"""Blade 5 — ``--live-dry-run`` stops before the first call and ``--scripted-replies`` runs
every call; together they are a contradiction. Refuse, never let one silently win (mirrors
S5.3's 'refused, never ignored' partition)."""
rc = run.main([*_argv(bundle, replies_file), "--live-dry-run"])
assert rc == 1
assert "refused" in capsys.readouterr().err.lower()
def test_report_mode_refuses_the_scripted_flag(tmp_path, replies_file, capsys) -> None:
"""Blade 5b — ``--report`` is mode-exclusive by ALLOWLIST; a new flag must join the refusal
set rather than be silently dropped.
The ledger here is VALID and saved on purpose. Measured: with a non-existent ledger path this
test passed even after the allowlist entry was removed — the load failure refused first and
masked the gate entirely. With a loadable ledger the report would otherwise print and return
0, so rc 1 can only come from the mode-exclusivity check itself.
"""
ledger_path = tmp_path / "ledger.json"
SavingsLedger().save(str(ledger_path))
rc = run.main(
["--report", "--ledger", str(ledger_path), "--scripted-replies", str(replies_file)]
)
assert rc == 1
assert "mode-exclusive" in capsys.readouterr().err.lower()
def test_malformed_replies_file_is_refused(bundle, tmp_path, capsys) -> None:
"""A replies file that exists but cannot serve the run is refused with rc 1, never a
traceback and never a partial run — the same fail-fast posture the loaders take."""
bad = tmp_path / "bad.json"
bad.write_text("{not json", encoding="utf-8")
rc = run.main(_argv(bundle, bad))
assert rc == 1
assert "refused" in capsys.readouterr().err.lower()