"""Offline simulation — the end-to-end method proof (replaces målbilde §11.8's real-model run). Operator decision: MAF is NOT run against a real model (Azure/Foundry or local Ollama) — API for both repos is too costly privately. Instead this offline simulation, driven by SCRIPTED synthetic agent replies, is the primary proof that the agentic loop's dataflow closes end to end: context -> hypothesis -> maker/checker debate -> validator -> verdict -> PROMOTION -> next run's hypothesis. It proves the plumbing + the deterministic spine + that the learning loop closes; it does NOT prove a live LLM would produce the proposal (that is scripted) — honesty per målbilde §1. This load-bearing test runs the actual two-run cycle: Run A (fresh wiki) produces a validated, persona-approved verdict carrying a realization marker absent from the bundle; `promote_verdict` lifts it into the OKF wiki; a re-seed picks it up; Run B's hypothesis prompt then carries the marker. The empty-store control (Run A carries no marker) proves causality — the signal reaches Run B only via promotion. RED the moment promotion is detached. """ from __future__ import annotations from pathlib import Path from portfolio_optimiser.simulation import simulate_learning_loop from portfolio_optimiser.validator import ValidatedProposal BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro" async def test_simulation_closes_the_learning_loop(tmp_path) -> None: """LOAD-BEARING: a persona verdict approved in Run A reaches Run B's hypothesis prompt purely via the file-backed OKF wiki (promote -> re-seed -> ExpeL fold). Goes RED if ``promote_verdict`` is detached from ``simulate_learning_loop`` (Run B's store then lacks the marker).""" result = await simulate_learning_loop(str(BUNDLE_DIR), str(tmp_path)) # The loop ran end to end on both runs (scripted proposal validates: P90=90000 >= 30000). assert isinstance(result.run_a.outcome, ValidatedProposal) assert isinstance(result.run_b.outcome, ValidatedProposal) # A1: the synthetic client is layered, so the always-attached BudgetMiddleware metered real-shaped # token usage (a bare BaseChatClient would silently no-op). assert result.run_a.provenance.token_usage > 0 # The promoted verdict landed in the wiki. assert result.promoted_path.exists() assert result.promoted_path.name.startswith("promoted-verdict-") # Causality control: Run A (empty wiki) carries no marker into its hypothesis prompt. assert not result.marker_in_run_a_prompt, ( "Run A carried the marker with an empty wiki — the positive result would not be caused by " "promotion" ) # The learning loop closed: the promoted persona knowledge reached Run B's hypothesis. assert result.marker_in_run_b_prompt, ( "the persona verdict approved in Run A did not reach Run B's hypothesis prompt — the " "promote -> re-seed -> fold learning loop is not closed" )