portfolio-optimiser-claude/tests/test_hitl_loadbearing.py
Kjell Tore Guttormsen 82d46148e2 test(loadbearing): measure the last 8 all-negative tests instead of assuming them
Point 2 of the vacuity sweep is now MEASURED, not paired-by-reading. Every
one of the 8 got a mutation that detaches the seam it claims to guard, run
through a harness that asserts the anchor is unique before mutating, restores
in `finally`, and sha256-verifies the restore.

Six were value-proven — the negative itself went RED under its detach:
  hitl :260  load_routing invents a default        -> RED
  hitl :298  route_pending hardcodes a fallback    -> RED
  step7 :145 the is_dir() guard deleted            -> RED
  step7 :171 the §4.2 vocabulary filter deleted    -> RED
  step7 :253 the id grammar off the model          -> RED
  prov  :111 sdk_version becomes required          -> RED

Two did not, and both are fixed here.

hitl :197 — the guard it appeared to prove is DEAD. Deleting the `is_dir()`
early-out from load_outbox_proposals leaves all 711 tests green: the tolerance
comes from `Path.glob`, which yields nothing on a missing directory and never
raises. The contrast is the finding: load_inbox carries an identically-shaped
guard that IS load-bearing, because it walks with `Path.iterdir`, which DOES
raise (measured both ways). Same guard, opposite verdict, and the difference
is the stdlib call behind it — the point-3 lesson one level out, where the
default being pinned belongs to the standard library rather than the SDK.
The stdlib baseline is now anchored explicitly, so a Python that makes glob
raise turns this red instead of quietly promoting a dead line to a seam.
What the test always did prove is kept and stated: replacing the early-out
with a raise turns it red, so it does hold tolerance.

portfolio :175 — the negative asserted over an unheld population. Measured, it
is real today (6 prompts), so the test is not vacuous now; nothing in it says
so, and a run_portfolio that stopped prompting would leave it green while
proving nothing. A positive control now runs first. Value-proven: green before,
red after the same mutation (iterate no projects), and it is that assertion
which fails, not an import.

The sibling repo sent the same rule from the other stack this week, arrived at
independently via its B4 empty-negative: on a negative assert, prove FIRST that
the event happened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr6TwWrHDHeukHy3bL4hgb
2026-08-09 10:08:12 +02:00

405 lines
18 KiB
Python

"""HITL verdict routing + pending tracking — LOAD-BEARING (S5.1-analog; §5, §11; K9).
The seam this file keeps alive: the operator sees which proposals still AWAIT a
verdict, and who should judge each — a pure file-based id-join across the three
layers hitl READS (never writes). A proposal is *pending* when its persisted
``verdict_id`` (minted by K5 into ``{run_id}-outcome.json``) has NO matching
verdict in the inbox and NO matching promoted verdict in a bundle. Routing maps
the proposal's measure (a config-string key NOW; K13 formalizes the dimension
catalog) to an expert.
Role split (§3 Step 7, unwaivable): hitl READS both settled layers and the
outbox; writing the inbox is the authoring primitive's job (K10 does
notification, never this). RED if hitl ever writes.
Two detached seams proven RED here:
* Detach proof (the id-join seam): drop the ``not in settled`` filter in
``pending_proposals`` so it returns every outbox proposal → a judged proposal
is still listed as pending → ``test_inbox_verdict_settles_the_proposal`` red.
* Detach proof (the read-only seam): make any read path write a byte → the
before/after snapshot of outbox+inbox diverges →
``test_hitl_never_writes`` red.
Key assumption (pinned in K5's ``test_outbox_loadbearing`` and reused here): the
outcome's ``verdict_id`` is minted the SAME way the inbox mints a verdict id
(``mint_verdict_id`` over the candidate features), so an inbox verdict about the
same candidate joins by id.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from pydantic import ValidationError
from portfolio_optimiser_claude.experience import CandidateFeatures, mint_verdict_id
from portfolio_optimiser_claude.hitl import (
PendingProposal,
RoutingContract,
load_routing,
main,
pending_proposals,
route_pending,
settled_verdict_ids,
)
from portfolio_optimiser_claude.inbox import VerdictDocument, write_verdict
from portfolio_optimiser_claude.ir import AffectedItem, SavingsProposal
from portfolio_optimiser_claude.loop import RunResult
from portfolio_optimiser_claude.outbox import persist_outbox
from portfolio_optimiser_claude.promotion import promote
from portfolio_optimiser_claude.provenance import Citation, Provenance
from portfolio_optimiser_claude.validator import ValidatedProposal
# --- fixtures: outbox pairs, inbox verdicts, promoted verdicts -------------------------------
def _proposal(measure: str = "LED-retrofit", code: str = "EL-01") -> SavingsProposal:
return SavingsProposal(
project_id="bygg-kontor-nord",
measure=measure,
affected_items=[AffectedItem(code=code, quantity=100, unit_cost=250.0)],
claimed_saving_nok=20000.0,
)
def _run(proposal: SavingsProposal) -> RunResult:
return RunResult(
outcome=ValidatedProposal(
validates=True,
claimed_saving_nok=20000.0,
nominal_feasible=25000.0,
p10=18000.0,
p50=22000.0,
p90=27000.0,
),
validator_decision="validated",
checker_decision="approve",
attempts=1,
proposal=proposal,
)
def _provenance() -> Provenance:
return Provenance(
citations=[Citation(file="index.md", span="chars 0-5", snippet="Bygg-")],
model="claude-haiku-4-5-20251001",
role="proposer",
validator_decision="validated",
tokens_used=1234,
)
def _persist(outbox: Path, proposal: SavingsProposal, run_id: str) -> str:
"""Persist an outbox pair for ``proposal`` and return its join key (verdict_id)."""
persist_outbox(outbox, run=_run(proposal), provenance=_provenance(), run_id=run_id)
return mint_verdict_id(CandidateFeatures.from_proposal(proposal))
def _drop_inbox_verdict(
inbox: Path, proposal: SavingsProposal, *, decision: str = "approved"
) -> str:
"""Author an inbox verdict for ``proposal`` (id minted the join way) and write it."""
document = VerdictDocument.from_candidate(
CandidateFeatures.from_proposal(proposal),
decision=decision,
rationale="expert judged this candidate",
description="LED retrofit for the north office",
)
write_verdict(inbox, document)
return document.id
def _snapshot(root: Path) -> dict[str, bytes]:
"""Every file byte under ``root`` keyed by relative path (missing dir → empty)."""
if not root.exists():
return {}
return {
str(path.relative_to(root)): path.read_bytes()
for path in sorted(root.rglob("*"))
if path.is_file()
}
# --- the pending registry (the id-join) ------------------------------------------------------
class TestPendingRegistry:
"""pending_proposals: outbox proposals minus settled, joined on verdict_id."""
def test_undecided_proposal_is_listed_pending(self, tmp_path: Path) -> None:
# Forslag uten dom → listes utestående (no inbox verdict, no promotion).
outbox, inbox = tmp_path / "outbox", tmp_path / "inbox"
vid = _persist(outbox, _proposal(), run_id="r-001")
pending = pending_proposals(outbox, inbox)
assert [p.verdict_id for p in pending] == [vid]
assert pending[0].run_id == "r-001"
assert pending[0].measure == "LED-retrofit"
assert pending[0].project_id == "bygg-kontor-nord"
def test_inbox_verdict_settles_the_proposal(self, tmp_path: Path) -> None:
# LOAD-BEARING (the id-join seam). Dom i inbox → forsvinner fra pending.
# Detach point: drop the ``not in settled`` filter in pending_proposals
# so it returns every outbox proposal → this judged proposal is STILL
# listed → RED. (Restore from the implemented copy, never git checkout.)
outbox, inbox = tmp_path / "outbox", tmp_path / "inbox"
proposal = _proposal()
vid = _persist(outbox, proposal, run_id="r-001")
inbox_id = _drop_inbox_verdict(inbox, proposal)
assert inbox_id == vid # the join key is shared (K5 assumption)
assert pending_proposals(outbox, inbox) == []
def test_join_is_exact_a_different_candidate_stays_pending(self, tmp_path: Path) -> None:
# A verdict about a DIFFERENT candidate must not settle this one — the
# join is by exact id, never a coincidental match.
outbox, inbox = tmp_path / "outbox", tmp_path / "inbox"
vid = _persist(outbox, _proposal(), run_id="r-001")
_drop_inbox_verdict(inbox, _proposal(measure="HVAC-upgrade", code="EL-99"))
pending = pending_proposals(outbox, inbox)
assert [p.verdict_id for p in pending] == [vid]
def test_skipped_inbox_decision_does_not_settle(self, tmp_path: Path) -> None:
# §4.2 vocabulary: an inbox verdict with an unknown decision is SKIPPED by
# load_inbox → never reaches the store → must NOT settle the proposal.
outbox, inbox = tmp_path / "outbox", tmp_path / "inbox"
proposal = _proposal()
vid = _persist(outbox, proposal, run_id="r-001")
inbox.mkdir(parents=True)
bogus_id = mint_verdict_id(CandidateFeatures.from_proposal(proposal))
(inbox / f"{bogus_id}.json").write_text(
json.dumps(
{
"id": bogus_id,
"decision": "maybe-later", # outside §4.2 → skipped
"rationale": "not a real decision",
"proposal_features": {
"affected_codes": ["EL-01"],
"measure_type": "LED-retrofit",
"claimed_saving_nok": 20000.0,
"description": "x",
},
}
),
encoding="utf-8",
)
assert [p.verdict_id for p in pending_proposals(outbox, inbox)] == [vid]
def test_multiple_proposals_deterministic_order(self, tmp_path: Path) -> None:
outbox, inbox = tmp_path / "outbox", tmp_path / "inbox"
_persist(outbox, _proposal(measure="m-b", code="EL-02"), run_id="r-002")
_persist(outbox, _proposal(measure="m-a", code="EL-01"), run_id="r-001")
pending = pending_proposals(outbox, inbox)
assert [p.run_id for p in pending] == ["r-001", "r-002"] # sorted by run_id
def test_missing_outbox_dir_is_empty(self, tmp_path: Path) -> None:
# HONEST LIMIT (measured this session): this does NOT prove the
# ``is_dir()`` early-out in load_outbox_proposals is load-bearing.
# Deleting that guard leaves all 711 tests green — the tolerance comes
# from ``Path.glob``, not from us: glob over a missing directory yields
# nothing and never raises.
#
# The contrast is what makes the shape untrustworthy on sight:
# load_inbox carries an identically-shaped guard that IS load-bearing,
# because it walks the folder with ``Path.iterdir``, which DOES raise.
# Same guard, opposite verdict, and the only difference is the stdlib
# call standing behind it.
#
# So pin the stdlib baseline the tolerance actually rests on. Should a
# future Python make glob raise on a missing directory, the guard
# becomes the seam — and this says so instead of going quiet.
assert list((tmp_path / "nope").glob("*.json")) == [] # the stdlib baseline
# What the assertion below DOES prove: the read boundary TOLERATES a
# missing outbox rather than raising. An intolerant implementation
# (measured: replace the early-out with a raise) turns it red.
assert pending_proposals(tmp_path / "nope", tmp_path / "inbox") == []
class TestPromotedSettles:
"""A PROMOTED verdict (§6) settles a proposal too — inbox-/promotert dom."""
def test_promoted_verdict_settles_the_proposal(self, tmp_path: Path) -> None:
outbox, inbox, bundle = tmp_path / "outbox", tmp_path / "inbox", tmp_path / "bundle"
proposal = _proposal()
_persist(outbox, proposal, run_id="r-001")
# A promoted verdict lives in the bundle (context layer), carrying the
# same verdict_id in frontmatter. Promote requires an accepted decision.
bundle.mkdir(parents=True)
(bundle / "index.md").write_text("# Bundle\n", encoding="utf-8")
document = VerdictDocument.from_candidate(
CandidateFeatures.from_proposal(proposal),
decision="approved",
rationale="promoted after approval",
description="d",
)
promote(
document,
bundle,
approved_by="expert",
experiment="exp-1",
timestamp="2026-07-24",
)
# Without the bundle it is pending; WITH the bundle the promotion settles it.
assert len(pending_proposals(outbox, inbox)) == 1
assert pending_proposals(outbox, inbox, bundle_dirs=[bundle]) == []
def test_settled_ids_union_inbox_and_promoted(self, tmp_path: Path) -> None:
inbox, bundle = tmp_path / "inbox", tmp_path / "bundle"
a = _drop_inbox_verdict(inbox, _proposal())
bundle.mkdir(parents=True)
(bundle / "index.md").write_text("# Bundle\n", encoding="utf-8")
other = _proposal(measure="HVAC-upgrade", code="EL-99")
document = VerdictDocument.from_candidate(
CandidateFeatures.from_proposal(other),
decision="approved",
rationale="promoted",
description="d",
)
promote(document, bundle, approved_by="e", experiment="x", timestamp="2026-07-24")
ids = settled_verdict_ids(inbox, bundle_dirs=[bundle])
assert a in ids
assert document.id in ids
# --- the routing config (nøkkel→ekspert, schema-validated fail-fast) --------------------------
class TestRoutingContract:
"""load_routing: fail-fast on a malformed routing config (§10)."""
def test_valid_config_loads(self) -> None:
routing = load_routing(
{"routes": {"LED-retrofit": "energy-expert"}, "default_expert": "triage"}
)
assert routing.routes["LED-retrofit"] == "energy-expert"
assert routing.default_expert == "triage"
def test_default_expert_optional(self) -> None:
routing = load_routing({"routes": {"LED-retrofit": "energy-expert"}})
assert routing.default_expert is None
@pytest.mark.parametrize(
"bad",
[
{"routes": {}}, # empty table — nobody can ever be routed
{"routes": {"LED-retrofit": ""}}, # empty expert id
{"routes": {"": "energy-expert"}}, # empty routing key
{"default_expert": "triage"}, # routes missing
{"routes": {"LED-retrofit": "energy-expert"}, "default_expert": ""}, # empty default
],
)
def test_malformed_config_fails_fast(self, bad: dict[str, object]) -> None:
with pytest.raises(ValidationError):
load_routing(bad)
class TestRoutePending:
"""route_pending: assign an expert per proposal by measure (config-string key)."""
def test_routes_by_measure(self, tmp_path: Path) -> None:
outbox, inbox = tmp_path / "outbox", tmp_path / "inbox"
_persist(outbox, _proposal(measure="LED-retrofit"), run_id="r-001")
pending = pending_proposals(outbox, inbox)
routing = RoutingContract(routes={"LED-retrofit": "energy-expert"})
routed = route_pending(pending, routing)
assert [(r.proposal.run_id, r.expert) for r in routed] == [("r-001", "energy-expert")]
def test_unmatched_measure_uses_default(self, tmp_path: Path) -> None:
outbox, inbox = tmp_path / "outbox", tmp_path / "inbox"
_persist(outbox, _proposal(measure="HVAC-upgrade"), run_id="r-001")
pending = pending_proposals(outbox, inbox)
routing = RoutingContract(routes={"LED-retrofit": "energy-expert"}, default_expert="triage")
routed = route_pending(pending, routing)
assert routed[0].expert == "triage"
def test_unmatched_measure_no_default_is_unrouted(self, tmp_path: Path) -> None:
outbox, inbox = tmp_path / "outbox", tmp_path / "inbox"
_persist(outbox, _proposal(measure="HVAC-upgrade"), run_id="r-001")
pending = pending_proposals(outbox, inbox)
routing = RoutingContract(routes={"LED-retrofit": "energy-expert"})
routed = route_pending(pending, routing)
assert routed[0].expert is None # UNROUTED — nobody configured to judge it
# --- the read-only invariant (LOAD-BEARING: hitl never writes) -------------------------------
class TestReadOnly:
"""LOAD-BEARING (§3 Step 7): hitl READS three layers, writes NONE of them."""
def test_hitl_never_writes(self, tmp_path: Path) -> None:
# Byte-snapshot outbox + inbox before and after every read path. Detach
# point: make any read (pending/settled/route) write a byte → a snapshot
# diverges → RED. The role split forbids hitl writing the inbox (K10 does
# notification, never this).
outbox, inbox = tmp_path / "outbox", tmp_path / "inbox"
proposal = _proposal()
_persist(outbox, proposal, run_id="r-001")
_persist(outbox, _proposal(measure="HVAC-upgrade", code="EL-99"), run_id="r-002")
_drop_inbox_verdict(inbox, proposal)
before_out, before_in = _snapshot(outbox), _snapshot(inbox)
pending = pending_proposals(outbox, inbox)
route_pending(pending, RoutingContract(routes={"LED-retrofit": "energy-expert"}))
settled_verdict_ids(inbox)
assert _snapshot(outbox) == before_out
assert _snapshot(inbox) == before_in
# --- the CLI (python -m ...hitl pending|route) -----------------------------------------------
def _write_routing(path: Path) -> Path:
path.write_text(
json.dumps({"routes": {"LED-retrofit": "energy-expert"}, "default_expert": "triage"}),
encoding="utf-8",
)
return path
class TestCli:
"""The thin CLI: pending|route subcommands, fail-fast on a bad routing file."""
def test_pending_lists_and_exits_zero(
self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
outbox, inbox = tmp_path / "outbox", tmp_path / "inbox"
_persist(outbox, _proposal(), run_id="r-001")
code = main(["pending", "--outbox", str(outbox), "--inbox", str(inbox)])
assert code == 0
assert "r-001" in capsys.readouterr().out
def test_route_lists_expert_and_exits_zero(
self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
outbox, inbox = tmp_path / "outbox", tmp_path / "inbox"
_persist(outbox, _proposal(), run_id="r-001")
routing = _write_routing(tmp_path / "routing.json")
code = main(
["route", "--outbox", str(outbox), "--inbox", str(inbox), "--routing", str(routing)]
)
assert code == 0
assert "energy-expert" in capsys.readouterr().out
def test_route_with_malformed_config_fails_fast(self, tmp_path: Path) -> None:
outbox, inbox = tmp_path / "outbox", tmp_path / "inbox"
_persist(outbox, _proposal(), run_id="r-001")
bad = tmp_path / "bad.json"
bad.write_text(json.dumps({"routes": {}}), encoding="utf-8")
code = main(
["route", "--outbox", str(outbox), "--inbox", str(inbox), "--routing", str(bad)]
)
assert code != 0
def test_pending_type_is_frozen(self) -> None:
# PendingProposal is an immutable value (no accidental mutation on the
# read path — reinforces the read-only invariant at the type level).
p = PendingProposal(
run_id="r", verdict_id="v", project_id="p", measure="m", claimed_saving_nok=1.0
)
with pytest.raises((AttributeError, TypeError)):
p.run_id = "other" # type: ignore[misc]