"""Run-entrance seams — LOAD-BEARING (C2.0; method-spec §3 Step 7, §5, §8, §11). The seam this file keeps alive: the SHIPPABLE entrance (``run.py``) composes the §5 sequence (merge inbox → seed → fold) and drives the same orchestration the fasit run used — so the README's inbox claim is true of a deliverable path. ``run_s10.py`` stays byte-frozen and is still never imported here. Detach proofs: drop the merge call from the composition → the inbox verdict never reaches the composed context → red. Drop the budget-stop persistence branch from the entrance → the structured stop leaves no artifacts → red (R-10: the orchestration branch the fasit suite could never bind, bound here on the new path with a scripted client and a low cap). """ from __future__ import annotations import json from pathlib import Path from typing import Callable from _scripted import ScriptedClient, reply from portfolio_optimiser_claude.contracts import Contracts, load_contracts 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.loop import ModelClient from portfolio_optimiser_claude.run import compose_run_context, default_client_factory, main BUNDLE = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro" # Distinct from the bundle seed's own learning marker (realiseringsgrad=0.82), # so its presence proves the INBOX path, not the seeding path. MARKER = "INBOX-MARKER-79" RATIONALE = f"Justert: i drift realiseres ~79% ({MARKER})." ClientFactory = Callable[[Contracts, float], ModelClient] def _inbox_document() -> VerdictDocument: # The expert judges the bundle's own candidate, so retrieval ranks the # verdict at similarity 1.0 and the fold must carry it. features = CandidateFeatures.from_proposal(load_validator_input(BUNDLE)) return VerdictDocument.from_candidate( features, decision="approved_with_adjustment", rationale=RATIONALE, description="expert judgement of the bundle candidate", ) def _scripted_factory(replies: list[object]) -> tuple[ClientFactory, list[ScriptedClient]]: created: list[ScriptedClient] = [] def factory(contracts: Contracts, max_budget_usd_per_call: float) -> ModelClient: client = ScriptedClient(replies=list(replies)) # type: ignore[arg-type] created.append(client) return client return factory, created class TestComposeRunContext: """§5 sequence, composed: merge inbox → seed → fold — read-only on the inbox.""" def test_a_dropped_verdict_reaches_the_composed_context(self, tmp_path: Path) -> None: inbox = tmp_path / "inbox" write_verdict(inbox, _inbox_document()) composed = compose_run_context(BUNDLE, inbox, k=3) assert MARKER in composed.context assert composed.inbox_merged == 1 assert composed.seeded == 1 def test_empty_inbox_control_keeps_the_base_composition(self, tmp_path: Path) -> None: inbox = tmp_path / "inbox" inbox.mkdir() with_empty = compose_run_context(BUNDLE, inbox, k=3) without = compose_run_context(BUNDLE, None, k=3) assert with_empty.context == without.context assert MARKER not in without.context assert with_empty.inbox_merged == 0 def test_missing_inbox_folder_behaves_like_empty(self, tmp_path: Path) -> None: composed = compose_run_context(BUNDLE, tmp_path / "never-created", k=3) assert composed.context == compose_run_context(BUNDLE, None, k=3).context def test_composition_never_writes_to_the_inbox(self, tmp_path: Path) -> None: # Role split (§3 Step 7, unwaivable): the entrance READS the inbox. inbox = tmp_path / "inbox" write_verdict(inbox, _inbox_document()) before = {p.name: p.read_bytes() for p in inbox.iterdir()} compose_run_context(BUNDLE, inbox, k=3) after = {p.name: p.read_bytes() for p in inbox.iterdir()} assert after == before class TestEntranceHappyPath: """The thin CLI wires compose → run → persist; the inbox signal survives the chain.""" def test_exit_zero_persists_run_artifacts_and_threads_the_inbox(self, tmp_path: Path) -> None: inbox = tmp_path / "inbox" write_verdict(inbox, _inbox_document()) out = tmp_path / "out" factory, created = _scripted_factory( [ reply("debate reasoning"), reply("VERDICT: APPROVE"), reply(json.dumps(load_validator_input(BUNDLE).model_dump())), ] ) code = main( ["--bundle", str(BUNDLE), "--inbox", str(inbox), "--out", str(out)], client_factory=factory, ) assert code == 0 (client,) = created assert any(MARKER in prompt for prompt in client.prompts("proposer")) run_result = json.loads((out / "run_result.json").read_text("utf-8")) assert run_result["validator_decision"] == "validated" for artifact in ("proposal.json", "provenance.json", "usage.json"): assert (out / artifact).is_file() class TestBudgetStopBinding: """R-10: the budget-stop branch — structured exit + persisted stop artifacts.""" def test_budget_stop_persists_stop_artifacts_and_exits_structurally( self, tmp_path: Path ) -> None: out = tmp_path / "out" factory, _ = _scripted_factory([reply("debate reasoning", usage_tokens=10)]) code = main( ["--bundle", str(BUNDLE), "--out", str(out), "--max-tokens", "5"], client_factory=factory, ) assert code == 3 stop = json.loads((out / "stop.json").read_text("utf-8")) assert stop == {"kind": "tokens", "limit": 5, "observed": 10} usage = json.loads((out / "usage.json").read_text("utf-8")) assert usage["tokens_used"] == 10 assert usage["max_tokens"] == 5 # A client without cost accounting persists an honest null, never a 0.0. assert usage["cost_usd"] is None assert not (out / "proposal.json").exists() assert not (out / "run_result.json").exists() class TestDefaultClientFactory: """The CLI's default factory constructs the REAL SDK client — wired, not executed.""" def test_default_factory_returns_the_sdk_client(self) -> None: # Construction needs no API key (verified premise); no query() is made. from portfolio_optimiser_claude.sdk_client import SdkModelClient contracts = load_contracts( data_source={"docs_dir": str(BUNDLE), "top_k": 3}, termination={"max_rounds": 1, "max_tokens": 1}, feedback={"decision": "approved", "rationale": "startup shape check (§10)"}, ) client = default_client_factory(contracts, 0.25) assert isinstance(client, SdkModelClient)