feat(fase1): fail-closed expert realize gate (F1)
This commit is contained in:
parent
e0778d2230
commit
9720acb18c
2 changed files with 167 additions and 0 deletions
|
|
@ -15,12 +15,17 @@ Pattern: ``tests/test_step8_promotion_loadbearing.py:77`` (fail-closed gate trio
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from portfolio_optimiser.ledger import (
|
||||
LedgerEntry,
|
||||
RealizationRefused,
|
||||
SavingsLedger,
|
||||
_candidate_identity,
|
||||
realize,
|
||||
stamp,
|
||||
)
|
||||
from portfolio_optimiser.verdicts import ProposalFeatures, Verdict
|
||||
|
||||
_TS = "2026-07-06T00:00:00Z"
|
||||
_CODES = frozenset({"ENERGI-TOTAL-EL"})
|
||||
|
|
@ -76,3 +81,102 @@ def test_exact_full_key_duplicate_is_not_stored_twice() -> None:
|
|||
assert ledger.add_realized(_entry("energi", amount_ore=5000)) is True
|
||||
assert ledger.add_realized(_entry("energi", amount_ore=5000)) is False # exact duplicate
|
||||
assert ledger.portfolio_total() == 5000
|
||||
|
||||
|
||||
# --- Step 6: fail-closed realize gate + deterministic øre conversion (SC5/SC8) --------------------
|
||||
|
||||
|
||||
def _features(nok: float = 5000.0) -> ProposalFeatures:
|
||||
return ProposalFeatures(affected_codes=_CODES, measure_type=_MEASURE, claimed_saving_nok=nok)
|
||||
|
||||
|
||||
def _verdict(decision: str) -> Verdict:
|
||||
return Verdict(
|
||||
id="v-abc", proposal_features=_features(), decision=decision, rationale="expert prose"
|
||||
)
|
||||
|
||||
|
||||
def test_realize_fail_closed_refuses_non_approved() -> None:
|
||||
"""LOAD-BEARING (SC5): a non-approved verdict — OTHERWISE fully valid (correct amount, valid
|
||||
provenance, so detaching the gate WOULD add it) — raises ``RealizationRefused`` and the ledger is
|
||||
unchanged. RED the moment the ``raise`` detaches: the raw entry then appears (self-contamination)."""
|
||||
ledger = SavingsLedger()
|
||||
with pytest.raises(RealizationRefused):
|
||||
realize(
|
||||
ledger,
|
||||
_features(),
|
||||
_verdict("rejected"),
|
||||
project_id="P1",
|
||||
dimension="energi",
|
||||
approver="ekspert",
|
||||
experiment="sim",
|
||||
timestamp=_TS,
|
||||
)
|
||||
assert ledger.entries == [] # nothing written
|
||||
assert ledger.portfolio_total() == 0
|
||||
|
||||
|
||||
def test_realize_approved_reaches_ledger() -> None:
|
||||
"""CAUSALITY: a HITL-approved verdict reaches the ledger (5000 NOK -> 500000 øre)."""
|
||||
ledger = SavingsLedger()
|
||||
entry = realize(
|
||||
ledger,
|
||||
_features(),
|
||||
_verdict("approved"),
|
||||
project_id="P1",
|
||||
dimension="energi",
|
||||
approver="ekspert",
|
||||
experiment="sim",
|
||||
timestamp=_TS,
|
||||
)
|
||||
assert entry in ledger.entries
|
||||
assert entry.verdict_id == "v-abc"
|
||||
assert ledger.portfolio_total() == 500000
|
||||
|
||||
|
||||
def test_realize_accepts_approved_with_adjustment() -> None:
|
||||
"""The gate uses the PROMOTION set: ``approved_with_adjustment`` is admitted (not the binary
|
||||
run-path FeedbackContract — H6)."""
|
||||
ledger = SavingsLedger()
|
||||
realize(
|
||||
ledger,
|
||||
_features(),
|
||||
_verdict("approved_with_adjustment"),
|
||||
project_id="P1",
|
||||
dimension="energi",
|
||||
approver="ekspert",
|
||||
experiment="sim",
|
||||
timestamp=_TS,
|
||||
)
|
||||
assert ledger.portfolio_total() == 500000
|
||||
|
||||
|
||||
def test_realize_requires_timestamp_keyword() -> None:
|
||||
"""SC8: ``timestamp`` is a required keyword — no wall-clock default. Omitting it raises TypeError."""
|
||||
ledger = SavingsLedger()
|
||||
with pytest.raises(TypeError):
|
||||
realize(
|
||||
ledger,
|
||||
_features(),
|
||||
_verdict("approved"),
|
||||
project_id="P1",
|
||||
dimension="energi",
|
||||
approver="ekspert",
|
||||
experiment="sim",
|
||||
) # no timestamp
|
||||
|
||||
|
||||
def test_realize_ore_conversion_is_exact() -> None:
|
||||
"""øre boundary: 12345.67 NOK -> 1234567 øre exactly, no binary-float ...66.9999 drift."""
|
||||
ledger = SavingsLedger()
|
||||
entry = realize(
|
||||
ledger,
|
||||
_features(12345.67),
|
||||
_verdict("approved"),
|
||||
project_id="P1",
|
||||
dimension="energi",
|
||||
approver="ekspert",
|
||||
experiment="sim",
|
||||
timestamp=_TS,
|
||||
)
|
||||
assert entry.amount_ore == 1234567
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue