portfolio-optimiser-claude/tests/test_step8_promotion_loadbearing.py
Kjell Tore Guttormsen 80a2fa1a77 feat(inbox): C2.5 — inbox hardening + SDK version guard (closes C-F7, C-N3, R-6)
- File-layer decision vocabulary (§4.2 set) with SKIP semantics — an unknown
  decision never reaches the store (C-F7, the review's run proof is the fixture)
- Fail-fast caps (max_files / max_rationale_chars) via InboxLimitError raised
  OUTSIDE the tolerant try — a cap breach is never swallowed as a skip
- R-6 id grammar (mirrors ingest _ID_RE) as a pydantic pattern on
  VerdictDocument.id AND re-checked in write_verdict, since model_copy(update=)
  bypasses model validation — traversal ids can no longer write outside the inbox
- promotion._filename_token: any sanitised id maps to a content hash — 'e/vil'
  can no longer clobber the distinct id 'evil' (restarbeid-funn 2)
- SDK pinned >=0.2.111,<0.3 + version guard test naming the sdk_client.py
  attribute premises; resolved 0.2.120, all premises re-verified against it
- sdk_client read loop bound offline with REAL SDK message types (R-4/R-5):
  text aggregation, error fail-paths, usage/cost extraction, _total_tokens
  fail-closed, non-positive budget guard
- test_sdk_isolation comment no longer claims the --system-prompt ""
  serialization the test body does not bind (honesty rule §1)

Guard-G2 assessment (guard-plan §4): the allowlist + caps + id grammar landed
here are G2's necessary part; an optional scan_output depth pass over
rationale (still a verbatim prose channel into the fold prompt, R-9) remains
relevant as a later additive session — the trigger picture is unchanged.

4 detach proofs red → restored green. Full gate: 389 passed (365→389),
ruff+format+mypy clean; golden + shared/ + runs/s10/ byte-untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 20:26:41 +02:00

223 lines
9.6 KiB
Python

"""Step-8 promotion gate — LOAD-BEARING (method-spec §3 Step 8, §6, §11).
The seam this file keeps alive: only APPROVED knowledge enters the wiki
(fail-closed), the promoted file is NAVIGABLE (index-linked — an unlinked file
is unreachable), and the index label is NEUTRAL (the index body flows verbatim
into the rendered read-context, so a descriptive label would leak the learning
signal around the gated fold). RED when a non-approved verdict reaches the
wiki, when an approved one is not navigable, or when the label leaks.
"""
from __future__ import annotations
import shutil
from pathlib import Path
import pytest
from portfolio_optimiser_claude.experience import (
CandidateFeatures,
VerdictStore,
fold_experience,
seed_store_from_bundle,
)
from portfolio_optimiser_claude.inbox import VerdictDocument
from portfolio_optimiser_claude.ir import load_validator_input
from portfolio_optimiser_claude.okf import bundle_context, navigate_bundle, parse_concept_file
from portfolio_optimiser_claude.promotion import PromotionError, promote
SHARED_BUNDLE = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
MARKER = "realiseringsgrad=0.79"
RATIONALE = f"Godkjent med korreksjon: i drift realiseres ~79% ({MARKER})."
BASE_PROMPT = "Propose exactly one cost-saving measure for this project."
# Structured learning fields a promoted file MUST NOT reproduce (§6) — the raw
# verdict model carries the signal only as rationale prose.
_SEED_ONLY_FIELDS = (
"realization_rate",
"expected_actual_saving_nok",
"modelled_saving_nok",
"gap_source",
"context_key",
)
def _document(
decision: str = "approved",
rationale: str = RATIONALE,
codes: frozenset[str] = frozenset({"E01"}),
verdict_id: str | None = None,
) -> VerdictDocument:
doc = VerdictDocument.from_candidate(
CandidateFeatures(
affected_codes=codes, measure_type="led-retrofit", claimed_saving_nok=25000.0
),
decision=decision,
rationale=rationale,
description="LED retrofit",
)
if verdict_id is not None:
doc = doc.model_copy(update={"id": verdict_id})
return doc
def _promote(doc: VerdictDocument, bundle: Path) -> Path:
return promote(
doc,
bundle,
approved_by="persona:expert-reviewer",
experiment="S9-offline-sim",
timestamp="2026-07-03T12:00:00Z",
)
@pytest.fixture()
def bundle(tmp_path: Path) -> Path:
target = tmp_path / "bundle"
shutil.copytree(SHARED_BUNDLE, target)
return target
class TestFailClosed:
"""§6: a non-approved verdict is refused — writing and linking NOTHING."""
@pytest.mark.parametrize("decision", ["rejected", "maybe"])
def test_non_approved_decisions_are_refused_writing_nothing(
self, bundle: Path, decision: str
) -> None:
index_before = (bundle / "index.md").read_bytes()
files_before = sorted(p.name for p in bundle.iterdir())
with pytest.raises(PromotionError):
_promote(_document(decision=decision), bundle)
assert (bundle / "index.md").read_bytes() == index_before
assert sorted(p.name for p in bundle.iterdir()) == files_before
@pytest.mark.parametrize("decision", ["approved", "approved_with_adjustment"])
def test_the_accepted_set_is_exactly_the_two_approval_forms(
self, bundle: Path, decision: str
) -> None:
assert _promote(_document(decision=decision), bundle).is_file()
class TestPromotedFile:
"""§6: minimal promoted file — signal as rationale prose, provenance-stamped."""
def test_frontmatter_carries_the_contract_fields(self, bundle: Path) -> None:
doc = _document()
concept = parse_concept_file(_promote(doc, bundle))
assert concept.type == "verdict"
assert concept.frontmatter["decision"] == "approved"
assert concept.frontmatter["description"] == RATIONALE
assert concept.frontmatter["verdict_id"] == doc.id # verbatim
provenance = concept.frontmatter["provenance"]
for part in ("persona:expert-reviewer", "S9-offline-sim", "2026-07-03T12:00:00Z"):
assert part in provenance
def test_no_structured_learning_fields_are_reproduced(self, bundle: Path) -> None:
concept = parse_concept_file(_promote(_document(), bundle))
for field in _SEED_ONLY_FIELDS:
assert field not in concept.frontmatter
def test_timestamp_is_an_explicit_required_argument(self, bundle: Path) -> None:
# No wall-clock default — promotion is deterministic and reproducible.
with pytest.raises(TypeError):
promote( # type: ignore[call-arg]
_document(), bundle, approved_by="x", experiment="y"
)
def test_same_candidate_id_is_last_write_wins_per_file(self, bundle: Path) -> None:
first = _promote(_document(rationale=f"first ({MARKER})"), bundle)
second = _promote(_document(rationale=f"second ({MARKER})"), bundle)
assert first == second
assert parse_concept_file(second).frontmatter["description"] == f"second ({MARKER})"
class TestNavigability:
"""LOAD-BEARING (§11): an approved verdict must be navigable — and fold back."""
def test_promoted_file_is_reachable_via_index_navigation(self, bundle: Path) -> None:
path = _promote(_document(), bundle)
assert path.name in {c.path.name for c in navigate_bundle(bundle)}
def test_promoted_verdict_closes_the_loop_through_seeding(self, bundle: Path) -> None:
# Promotion → navigation → seeding → fold: the expert's rationale (the
# learning signal as prose) reaches the next run's hypothesis prompt.
_promote(_document(), bundle)
store = VerdictStore()
seed_store_from_bundle(store, bundle)
features = CandidateFeatures.from_proposal(load_validator_input(bundle))
prompt = fold_experience(store, features, BASE_PROMPT, k=3)
assert MARKER in prompt
def test_two_promoted_candidates_both_seed_verbatim_ids(self, bundle: Path) -> None:
# §4.2 read-VERBATIM at the seeding layer: distinct candidates get
# distinct store entries — re-minting from bundle features would
# collide them (first-write-wins) and silently drop one rationale.
_promote(_document(codes=frozenset({"E01"}), rationale=f"one ({MARKER})"), bundle)
_promote(_document(codes=frozenset({"E02"}), rationale=f"two ({MARKER})"), bundle)
store = VerdictStore()
seed_store_from_bundle(store, bundle)
features = CandidateFeatures.from_proposal(load_validator_input(bundle))
rationales = {r.rationale for r in store.retrieve(features, k=10)}
assert f"one ({MARKER})" in rationales
assert f"two ({MARKER})" in rationales
def test_linking_is_idempotent(self, bundle: Path) -> None:
path = _promote(_document(), bundle)
_promote(_document(), bundle)
index_text = (bundle / "index.md").read_text(encoding="utf-8")
assert index_text.count(f"]({path.name})") == 1
class TestNeutralLabel:
"""LOAD-BEARING (§11): the index label carries NO verdict signal."""
def test_index_never_carries_the_rationale_or_marker(self, bundle: Path) -> None:
_promote(_document(), bundle)
index_text = (bundle / "index.md").read_text(encoding="utf-8")
assert MARKER not in index_text
assert RATIONALE not in index_text
def test_label_is_fixed_across_verdicts(self, bundle: Path) -> None:
a = _promote(_document(codes=frozenset({"E01"})), bundle)
b = _promote(_document(codes=frozenset({"E02"})), bundle)
index_lines = (bundle / "index.md").read_text(encoding="utf-8").splitlines()
line_a = next(line for line in index_lines if a.name in line)
line_b = next(line for line in index_lines if b.name in line)
assert line_a.replace(a.name, "") == line_b.replace(b.name, "")
def test_rendered_read_context_never_carries_the_marker(self, bundle: Path) -> None:
# Belt and braces: the index body flows verbatim into the rendered
# read-context — after promotion it must still exclude the signal.
_promote(_document(), bundle)
assert MARKER not in bundle_context(bundle)
class TestPathSafety:
"""§6: path-safe, fail-closed against escaping names."""
def test_escaping_id_is_sanitised_into_the_bundle(self, bundle: Path, tmp_path: Path) -> None:
path = _promote(_document(verdict_id="../../evil"), bundle)
assert path.parent == bundle
assert not (tmp_path / "evil").exists()
assert "/" not in path.name and "\\" not in path.name
def test_sanitised_ids_never_collide_with_a_distinct_id_owning_the_name(
self, bundle: Path
) -> None:
# C2.5 restarbeid-funn 2: 'e/vil' used to sanitise to 'evil' — the
# SAME promoted filename as the distinct id 'evil', so the second
# promotion silently overwrote the first curated verdict.
first = _promote(_document(verdict_id="e/vil", rationale=f"one ({MARKER})"), bundle)
second = _promote(_document(verdict_id="evil", rationale=f"two ({MARKER})"), bundle)
assert first != second
assert parse_concept_file(first).frontmatter["verdict_id"] == "e/vil"
assert parse_concept_file(second).frontmatter["verdict_id"] == "evil"
def test_degenerate_token_falls_back_to_a_content_hash(self, bundle: Path) -> None:
path = _promote(_document(verdict_id="///"), bundle)
assert path.parent == bundle
token = path.name.removeprefix("promoted-verdict-").removesuffix(".md")
assert token, "degenerate token must fall back to a non-empty content hash"
assert set(token) <= set("0123456789abcdef")