feat(fase1): hard/soft goal-stop in run_portfolio on accumulated ledger (F1)
This commit is contained in:
parent
c6f62d41db
commit
16b6d80b82
4 changed files with 270 additions and 6 deletions
|
|
@ -12,7 +12,9 @@ from __future__ import annotations
|
|||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from portfolio_optimiser.contracts import GoalConfig, GoalContract
|
||||
from portfolio_optimiser.ledger import LedgerEntry, SavingsLedger, stamp
|
||||
from portfolio_optimiser.run import run_portfolio
|
||||
|
||||
_TS = "2026-07-06T00:00:00Z"
|
||||
|
||||
|
|
@ -99,3 +101,115 @@ def test_save_is_byte_identical_regardless_of_order(tmp_path) -> None:
|
|||
# Round-trips: load() reconstructs an equivalent ledger with the same totals.
|
||||
reloaded = SavingsLedger.load(str(pa))
|
||||
assert reloaded.portfolio_total() == 7500
|
||||
|
||||
|
||||
# --- Step 8: hard/soft goal-stop in run_portfolio on the accumulated ledger (SC6/SC8) ------------
|
||||
|
||||
_PORTFOLIO_IDS = ["FV42-GSV-E1", "RV13-RAS-TP", "BRU-LAKS-REHAB"]
|
||||
|
||||
|
||||
def _prefilled(project_id: str, amount_ore: int) -> SavingsLedger:
|
||||
"""A ledger prefilled with ONE realized entry (representing an EARLIER, out-of-band HITL
|
||||
realization — the accumulated sum the goal-stop reads before this pass)."""
|
||||
led = SavingsLedger()
|
||||
led.add_realized(
|
||||
LedgerEntry(
|
||||
project_id=project_id,
|
||||
dimension="energi",
|
||||
candidate_identity="prior-hitl",
|
||||
amount_ore=amount_ore,
|
||||
verdict_id="v-prior",
|
||||
provenance=stamp(approver="ekspert", experiment="earlier", timestamp=_TS),
|
||||
)
|
||||
)
|
||||
return led
|
||||
|
||||
|
||||
async def test_portfolio_hard_goal_stops_the_whole_pass(make_portfolio_client_factory) -> None:
|
||||
ledger = _prefilled("FV42-GSV-E1", 100) # portfolio_total == 100
|
||||
goals = GoalConfig(portfolio=GoalContract(absolute_ore=100)) # met at 100 (>=)
|
||||
result = await run_portfolio(
|
||||
_PORTFOLIO_IDS,
|
||||
"local",
|
||||
ledger=ledger,
|
||||
goals=goals,
|
||||
client_factory=make_portfolio_client_factory({}),
|
||||
max_rounds=1,
|
||||
)
|
||||
assert result.stopped_early is True
|
||||
assert result.runs == () # goal already reached before pid 0 -> nothing runs
|
||||
assert result.stop_reason is not None
|
||||
assert result.stop_reason.scope == "portfolio"
|
||||
assert result.stop_reason.observed_ore == 100
|
||||
assert result.stop_reason.limit_ore == 100
|
||||
|
||||
|
||||
async def test_boundary_exact_equal_stops(make_portfolio_client_factory) -> None:
|
||||
""">= boundary: accumulated EXACTLY equal to the goal stops (reached, not strictly exceeded)."""
|
||||
ledger = _prefilled("FV42-GSV-E1", 500)
|
||||
goals = GoalConfig(portfolio=GoalContract(absolute_ore=500))
|
||||
result = await run_portfolio(
|
||||
_PORTFOLIO_IDS,
|
||||
"local",
|
||||
ledger=ledger,
|
||||
goals=goals,
|
||||
client_factory=make_portfolio_client_factory({}),
|
||||
max_rounds=1,
|
||||
)
|
||||
assert result.stopped_early is True
|
||||
|
||||
|
||||
async def test_per_project_hard_goal_skips_only_that_pid(make_portfolio_client_factory) -> None:
|
||||
ledger = _prefilled("FV42-GSV-E1", 1000)
|
||||
goals = GoalConfig(per_project={"FV42-GSV-E1": GoalContract(absolute_ore=1000)})
|
||||
result = await run_portfolio(
|
||||
["FV42-GSV-E1", "RV13-RAS-TP"],
|
||||
"local",
|
||||
ledger=ledger,
|
||||
goals=goals,
|
||||
client_factory=make_portfolio_client_factory({}),
|
||||
max_rounds=1,
|
||||
)
|
||||
ran = [r.outcome.proposal.project_id for r in result.runs]
|
||||
assert "FV42-GSV-E1" not in ran # its goal is reached -> skipped
|
||||
assert ran == ["RV13-RAS-TP"] # the rest of the pass proceeds
|
||||
assert result.stopped_early is False # a per-project skip is NOT a pass-stop
|
||||
|
||||
|
||||
async def test_soft_goal_flags_but_continues(make_portfolio_client_factory) -> None:
|
||||
ledger = _prefilled("FV42-GSV-E1", 100)
|
||||
goals = GoalConfig(portfolio=GoalContract(absolute_ore=100, mode="soft"))
|
||||
result = await run_portfolio(
|
||||
["FV42-GSV-E1", "RV13-RAS-TP"],
|
||||
"local",
|
||||
ledger=ledger,
|
||||
goals=goals,
|
||||
client_factory=make_portfolio_client_factory({}),
|
||||
max_rounds=1,
|
||||
)
|
||||
assert result.stopped_early is False # soft: does not stop
|
||||
assert len(result.runs) == 2 # all ran
|
||||
assert result.stop_reason is not None # but the goal-reached flag IS surfaced
|
||||
|
||||
|
||||
async def test_stop_decision_is_deterministic(make_portfolio_client_factory) -> None:
|
||||
"""SC8: the same ledger + goals twice -> IDENTICAL stop decision (stopped_early + stop_reason)."""
|
||||
goals = GoalConfig(portfolio=GoalContract(absolute_ore=100))
|
||||
r1 = await run_portfolio(
|
||||
_PORTFOLIO_IDS,
|
||||
"local",
|
||||
ledger=_prefilled("FV42-GSV-E1", 100),
|
||||
goals=goals,
|
||||
client_factory=make_portfolio_client_factory({}),
|
||||
max_rounds=1,
|
||||
)
|
||||
r2 = await run_portfolio(
|
||||
_PORTFOLIO_IDS,
|
||||
"local",
|
||||
ledger=_prefilled("FV42-GSV-E1", 100),
|
||||
goals=goals,
|
||||
client_factory=make_portfolio_client_factory({}),
|
||||
max_rounds=1,
|
||||
)
|
||||
assert r1.stopped_early == r2.stopped_early
|
||||
assert r1.stop_reason == r2.stop_reason # frozen-dataclass equality: identical decision
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue