test(portfolio): cover the CLI→run_portfolio wire for --verdict-dir (§11 gap)
The gap, found by the mutation sweep of 2026-07-25: verdict_dir=args.verdict_dir → None in run.py's execute_portfolio call left the suite 603/603 GREEN. The flag was wired but not guarded — the inner merge (test_portfolio_learning_ loadbearing.py), the argparse refusal (--verdict-dir without --portfolio) and the README↔--help sync all stay green under that mutation, so none of them covered the forwarding itself. One load-bearing test, no production code. It authors an expert verdict into a tmp portfolio inbox — keyed on the bundle's own codes + measure type so it ranks into the fold, with a distinct saving so its id cannot collide with the bundle's seed — drives main(["--portfolio", …, "--verdict-dir", X]) with the scripted client, and asserts the verdict's id AND a marker token (present nowhere in the bundle) reach the proposer prompt. Detach proof (mutation restored from a COPY, never git checkout): the wire mutated to None → 1 failed, 603 passed, and the failure is this test alone. Restored → 604 passed, ruff format left 71 files unchanged, ruff check + mypy --strict clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
This commit is contained in:
parent
8d5554ba3f
commit
25d9bc6fe8
1 changed files with 61 additions and 1 deletions
|
|
@ -8,7 +8,10 @@ makes them REACHABLE. A hard goal already met by the ledger stops the run
|
||||||
BEFORE any model call — the goal bounds achievement where the §8 budget bounds
|
BEFORE any model call — the goal bounds achievement where the §8 budget bounds
|
||||||
spend, and a stop is structured output, never a silent one. Detach the goal
|
spend, and a stop is structured output, never a silent one. Detach the goal
|
||||||
check from the entrance → the run proceeds and spends → red. Detach the
|
check from the entrance → the run proceeds and spends → red. Detach the
|
||||||
portfolio branch → the config's projects never run → red.
|
portfolio branch → the config's projects never run → red. Stop forwarding
|
||||||
|
``--verdict-dir`` → the expert inbox the operator named reaches no fold → red:
|
||||||
|
the flag's refusal path and the README↔``--help`` sync leave that wire uncovered,
|
||||||
|
so it is asserted here on the fold itself.
|
||||||
|
|
||||||
**The README claims exactly what the CLI delivers (§1).** Every ``--flag`` the
|
**The README claims exactly what the CLI delivers (§1).** Every ``--flag`` the
|
||||||
README documents must exist in the help of a project CLI the README names.
|
README documents must exist in the help of a project CLI the README names.
|
||||||
|
|
@ -32,6 +35,8 @@ import pytest
|
||||||
from _scripted import ScriptedClient, reply
|
from _scripted import ScriptedClient, reply
|
||||||
|
|
||||||
from portfolio_optimiser_claude.contracts import Contracts, FeedbackContract
|
from portfolio_optimiser_claude.contracts import Contracts, FeedbackContract
|
||||||
|
from portfolio_optimiser_claude.experience import CandidateFeatures
|
||||||
|
from portfolio_optimiser_claude.inbox import VerdictDocument, write_verdict
|
||||||
from portfolio_optimiser_claude.ir import load_validator_input
|
from portfolio_optimiser_claude.ir import load_validator_input
|
||||||
from portfolio_optimiser_claude.ledger import SavingsLedger
|
from portfolio_optimiser_claude.ledger import SavingsLedger
|
||||||
from portfolio_optimiser_claude.loop import ModelClient, ModelReply
|
from portfolio_optimiser_claude.loop import ModelClient, ModelReply
|
||||||
|
|
@ -97,6 +102,35 @@ def _ledger_file(tmp_path: Path, *, amount_nok: float) -> Path:
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
# A token that lives NOWHERE in the bundle — its presence in a prompt can only
|
||||||
|
# have come through the inbox the operator named with --verdict-dir, never from
|
||||||
|
# the bundle context or from seeding.
|
||||||
|
_VERDICT_DIR_MARKER = "K12-CLI-VERDICT-DIR-MARKER"
|
||||||
|
|
||||||
|
|
||||||
|
def _portfolio_inbox(tmp_path: Path) -> tuple[Path, VerdictDocument]:
|
||||||
|
"""An EXPERT verdict in a portfolio inbox, keyed near the bundle's candidate.
|
||||||
|
|
||||||
|
Same codes + measure type as the bundle's proposal (so it ranks into the
|
||||||
|
fold) with a distinct saving (so its id cannot collide with the bundle's own
|
||||||
|
seed). The marker rides in the rationale — the fold's learning signal.
|
||||||
|
"""
|
||||||
|
candidate = CandidateFeatures.from_proposal(load_validator_input(BUNDLE))
|
||||||
|
verdict = VerdictDocument.from_candidate(
|
||||||
|
CandidateFeatures(
|
||||||
|
affected_codes=candidate.affected_codes,
|
||||||
|
measure_type=candidate.measure_type,
|
||||||
|
claimed_saving_nok=candidate.claimed_saving_nok + 3000.0,
|
||||||
|
),
|
||||||
|
decision="approved",
|
||||||
|
rationale=f"prior portfolio verdict — realiseringskorreksjon [{_VERDICT_DIR_MARKER}]",
|
||||||
|
description="K12 CLI portfolio-inbox fixture (surface text, excluded from ranking)",
|
||||||
|
)
|
||||||
|
verdict_dir = tmp_path / "portfolio-inbox"
|
||||||
|
write_verdict(verdict_dir, verdict)
|
||||||
|
return verdict_dir, verdict
|
||||||
|
|
||||||
|
|
||||||
def _portfolio_file(tmp_path: Path, project_ids: list[str]) -> Path:
|
def _portfolio_file(tmp_path: Path, project_ids: list[str]) -> Path:
|
||||||
path = tmp_path / "portfolio.json"
|
path = tmp_path / "portfolio.json"
|
||||||
path.write_text(
|
path.write_text(
|
||||||
|
|
@ -263,6 +297,32 @@ class TestPortfolioOnTheEntrance:
|
||||||
main([], client_factory=factory)
|
main([], client_factory=factory)
|
||||||
assert created == []
|
assert created == []
|
||||||
|
|
||||||
|
def test_verdict_dir_reaches_every_projects_fold(self, tmp_path: Path) -> None:
|
||||||
|
# Detach point: stop forwarding args.verdict_dir out of main() (pass None
|
||||||
|
# to execute_portfolio) → the inbox the operator named never reaches a
|
||||||
|
# fold → RED. The merge itself is proven inside run_portfolio
|
||||||
|
# (test_portfolio_learning_loadbearing.py); what this keeps alive is the
|
||||||
|
# CLI→run_portfolio WIRE — the refusal test below and the README↔--help
|
||||||
|
# sync both stay green under that mutation, so neither covers it.
|
||||||
|
verdict_dir, inbox_verdict = _portfolio_inbox(tmp_path)
|
||||||
|
factory, created = _scripted_factory(runs=1)
|
||||||
|
code = main(
|
||||||
|
[
|
||||||
|
"--portfolio",
|
||||||
|
str(_portfolio_file(tmp_path, ["prosjekt-a"])),
|
||||||
|
"--verdict-dir",
|
||||||
|
str(verdict_dir),
|
||||||
|
],
|
||||||
|
client_factory=factory,
|
||||||
|
)
|
||||||
|
assert code == 0
|
||||||
|
assert created
|
||||||
|
prompts = created[0].prompts("proposer")
|
||||||
|
assert prompts, "the project must have driven the loop at all"
|
||||||
|
assert any(_VERDICT_DIR_MARKER in p and inbox_verdict.id in p for p in prompts), (
|
||||||
|
"the verdict from --verdict-dir (id + marker) must reach the project's fold"
|
||||||
|
)
|
||||||
|
|
||||||
def test_verdict_dir_without_portfolio_is_refused(self, tmp_path: Path) -> None:
|
def test_verdict_dir_without_portfolio_is_refused(self, tmp_path: Path) -> None:
|
||||||
# --verdict-dir is the PORTFOLIO-level expert inbox; on a single run the
|
# --verdict-dir is the PORTFOLIO-level expert inbox; on a single run the
|
||||||
# per-run inbox is --inbox. Accepting it silently would claim a wiring
|
# per-run inbox is --inbox. Accepting it silently would claim a wiring
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue