feat(s51): hitl CLI — python -m portfolio_optimiser.hitl pending|route

Gate: pytest tests/test_hitl.py -k cli → 4 passed.
This commit is contained in:
Kjell Tore Guttormsen 2026-07-15 19:39:49 +02:00
commit c4c55fd417
2 changed files with 133 additions and 1 deletions

View file

@ -36,6 +36,15 @@ _PROVENANCE = ProvenanceStamp(
)
@pytest.fixture(autouse=True)
def _isolate_model_env(monkeypatch: pytest.MonkeyPatch) -> None:
"""Defensive hermetic env (copy of ``test_live_dry_run.py:20-29``): clear the S4.1 out-of-tree
overrides. hitl's CLI never reads these, but importing the package eagerly loads ``run`` — keep
the assertions insensitive to the operator's Foundry-configured environment."""
monkeypatch.delenv("PORTFOLIO_MODEL_MAP", raising=False)
monkeypatch.delenv("PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT", raising=False)
def _make_validated(measure: str, codes: list[str], claimed: float = 200.0) -> ValidatedProposal:
items = [AffectedItem(code=c, quantity=1000.0, unit_cost=1.0) for c in codes]
proposal = SavingsProposal(
@ -299,3 +308,72 @@ def test_route_measure_constrained_entry_filters(tmp_path: Path) -> None:
hit = tmp_path / "hit"
_write_proposal(hit, "run-1", verdict_id="v1", measure="scope_reduction", codes=["05.2"])
assert hitl.route(str(hit), str(inbox), config)[0].expert == "Ola"
# --- CLI: python -m portfolio_optimiser.hitl pending|route ----------------------------------------
def test_cli_pending_lists_unjudged(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
"""``pending`` subcommand → rc 0 and one greppable ``run_id verdict_id outcome_type`` line per
un-judged proposal."""
outbox = tmp_path / "outbox"
inbox = tmp_path / "inbox"
_write_proposal(outbox, "run-1", verdict_id="v1")
rc = hitl.main(["pending", "--outbox-dir", str(outbox), "--verdict-dir", str(inbox)])
assert rc == 0
out = capsys.readouterr().out
assert "run-1 v1 validated" in out
def test_cli_route_lists_expert_and_unroutable(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""``route`` subcommand → rc 0; a routed proposal carries its expert + ``dim:``, an unmatched one
carries ``UNROUTABLE`` (and no ``dim:``)."""
outbox = tmp_path / "outbox"
inbox = tmp_path / "inbox"
_write_proposal(outbox, "run-1", verdict_id="v1", codes=["05.2"])
_write_proposal(outbox, "run-2", verdict_id="v2", codes=["99.9"])
cfg = _write_config(
tmp_path, {"entries": [{"id": "energi", "allowed_code_prefixes": ["05"], "expert": "Ola"}]}
)
rc = hitl.main(
["route", "--outbox-dir", str(outbox), "--verdict-dir", str(inbox), "--routing-config", str(cfg)]
)
assert rc == 0
out = capsys.readouterr().out
assert "run-1 v1" in out and "Ola" in out and "dim:energi" in out
assert "run-2 v2" in out and "UNROUTABLE" in out
def test_cli_route_malformed_config_returns_rc1(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""A malformed routing config → rc 1, a structured ``hitl:`` message to stderr, NO traceback."""
outbox = tmp_path / "outbox"
inbox = tmp_path / "inbox"
_write_proposal(outbox, "run-1", verdict_id="v1")
bad_cfg = _write_config(tmp_path, {"entries": [{"id": "energi"}]}) # missing required expert
rc = hitl.main(
["route", "--outbox-dir", str(outbox), "--verdict-dir", str(inbox), "--routing-config", str(bad_cfg)]
)
assert rc == 1
captured = capsys.readouterr()
assert "hitl:" in captured.err
assert "Traceback" not in captured.err
def test_cli_pending_output_is_deterministic(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Two identical ``pending`` invocations produce byte-identical stdout."""
outbox = tmp_path / "outbox"
inbox = tmp_path / "inbox"
_write_proposal(outbox, "run-b", verdict_id="v2")
_write_proposal(outbox, "run-a", verdict_id="v1")
hitl.main(["pending", "--outbox-dir", str(outbox), "--verdict-dir", str(inbox)])
first = capsys.readouterr().out
hitl.main(["pending", "--outbox-dir", str(outbox), "--verdict-dir", str(inbox)])
second = capsys.readouterr().out
assert first == second
assert first.index("run-a") < first.index("run-b") # sorted