test(fase3): portfolio rejection arm + provenance-seam assertions (F2 + F1 follow-up)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019any9zfGNNwWJPX5Zq2QRz
This commit is contained in:
parent
e0f93dfa7b
commit
a7c69dbd55
1 changed files with 30 additions and 3 deletions
|
|
@ -13,6 +13,7 @@ scanning the prompt for its id, so one production-shaped factory serves the whol
|
|||
import pytest
|
||||
|
||||
from portfolio_optimiser.run import PortfolioResult, RunResult, run_portfolio, run_project
|
||||
from portfolio_optimiser.validator import Rejection
|
||||
|
||||
# The synthetic reply IS the proposal: generate._parse_ir builds affected_items (each with its
|
||||
# own quantity/unit_cost) straight from this JSON, and the validator's P90 = 0.30 x Σ(qty·unit_cost)
|
||||
|
|
@ -84,6 +85,32 @@ async def test_a2_unknown_project_id_raises(make_portfolio_client_factory, fresh
|
|||
)
|
||||
|
||||
|
||||
async def test_a3_rejected_proposal_excluded_from_aggregate(
|
||||
make_portfolio_client_factory, fresh_store
|
||||
) -> None:
|
||||
"""F2 (rejection arm, previously unexercised): one project's claim exceeds its P90, so the
|
||||
validator REJECTS it (FV42 800000 > P90 444750, <= Σ 1,482,500), while RV13+BRU validate.
|
||||
rejected_count counts it and the validated-only sum (run.py:241) EXCLUDES its claim. Also the
|
||||
F1 portfolio seam: provenance.model mirrors the injected client across all N records."""
|
||||
replies = {**REPLIES, "FV42-GSV-E1": REPLIES["FV42-GSV-E1"].replace("200000", "800000")}
|
||||
result = await run_portfolio(
|
||||
_PORTFOLIO_IDS,
|
||||
"local",
|
||||
store=fresh_store,
|
||||
client_factory=make_portfolio_client_factory(replies),
|
||||
)
|
||||
assert result.rejected_count == 1
|
||||
assert result.validated_count == 2
|
||||
# Validated-only sum EXCLUDES the rejected 800000 (run.py:241): 130000 + 210000.
|
||||
assert result.sum_claimed_saving_nok == 340000
|
||||
rejected = [r for r in result.runs if isinstance(r.outcome, Rejection)]
|
||||
assert len(rejected) == 1
|
||||
assert rejected[0].outcome.proposal.claimed_saving_nok == 800000 # carried, not summed
|
||||
assert rejected[0].provenance.validator_decision == "rejected"
|
||||
# F1 portfolio seam: the injected client's model is stamped across ALL N records.
|
||||
assert all(r.provenance.model == "synthetic" for r in result.runs)
|
||||
|
||||
|
||||
async def test_b_shared_store_accumulates_and_surfaces_prior_verdict(
|
||||
make_portfolio_client_factory, fresh_store
|
||||
) -> None:
|
||||
|
|
@ -161,9 +188,9 @@ async def test_d_both_profiles_run_offline(
|
|||
is a profile-dependent seam reachable WITHOUT egress — each profile resolves its own
|
||||
configured model from the bundled model_map.json, and local != azure (deleting the azure
|
||||
entry reddens this). Honest limitation: under an injected ``client_factory``, ``profile`` is
|
||||
otherwise inert inside ``run_project`` (run.py:152,173 ``model="fake-model"``, the F9 standing
|
||||
item); the end-to-end run proves the path executes under both, and ``resolve_model`` is the
|
||||
only profile-dependent seam provable offline."""
|
||||
otherwise inert inside ``run_project`` (run.py:197 stamps the injected client's own model, not
|
||||
the profile's ``resolve_model``); the end-to-end run proves the path executes under both, and
|
||||
``resolve_model`` is the only profile-dependent seam provable offline."""
|
||||
from portfolio_optimiser.backends import resolve_model
|
||||
|
||||
result = await run_portfolio(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue