From dd4a6ef4af9ba2d10383cee618313bc9ab0f64bd Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 26 Jun 2026 12:10:04 +0200 Subject: [PATCH] test(fase3): SC7 both profiles offline + gated Azure portfolio arm --- tests/test_portfolio.py | 28 ++++++++++++++++++++++++++++ tests/test_portfolio_live.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 tests/test_portfolio_live.py diff --git a/tests/test_portfolio.py b/tests/test_portfolio.py index 8517325..89b4d54 100644 --- a/tests/test_portfolio.py +++ b/tests/test_portfolio.py @@ -150,3 +150,31 @@ async def test_c_execution_state_isolation_is_load_bearing( meter_factory=lambda: shared, ) assert sh.runs[1].provenance.token_usage > sh.runs[0].provenance.token_usage + + +@pytest.mark.parametrize("profile", ["local", "azure"]) +async def test_d_both_profiles_run_offline( + make_portfolio_client_factory, fresh_store, profile +) -> None: + """SC7: both profiles drive the portfolio contract path OFFLINE under the synthetic client + (the path is actually executed, not just the backend instantiated). Teeth: ``resolve_model`` + 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.""" + from portfolio_optimiser.backends import resolve_model + + result = await run_portfolio( + _PORTFOLIO_IDS, + profile, + store=fresh_store, + client_factory=make_portfolio_client_factory(REPLIES), + ) + assert isinstance(result, PortfolioResult) + assert len(result.runs) == 3 + assert all(isinstance(r, RunResult) for r in result.runs) + # Profile-dependent teeth (offline, no egress): each profile resolves its own model. + assert resolve_model(profile, "proposer") + assert resolve_model("local", "proposer") != resolve_model("azure", "proposer") diff --git a/tests/test_portfolio_live.py b/tests/test_portfolio_live.py new file mode 100644 index 0000000..aa4e1cc --- /dev/null +++ b/tests/test_portfolio_live.py @@ -0,0 +1,28 @@ +"""Fase 3 — GATED live Azure portfolio run (SC7 / SC9). + +NOT default CI: skips cleanly without a configured Foundry endpoint + deployment. When set, +``run_portfolio`` fans out over the reference portfolio on the REAL ``azure`` profile (one +project, hard token-capped per D6). Mirrors ``test_foundry_profile_live.py:14-24``. SC9 is +unchanged — this arm stays skipped offline. +""" + +import os + +import pytest + +from portfolio_optimiser.run import PortfolioResult, run_portfolio + +_ENDPOINT = os.environ.get("PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT") +_DEPLOYMENT = os.environ.get("PORTFOLIO_FOUNDRY_DEPLOYMENT") +_NO_FOUNDRY = not (_ENDPOINT and _DEPLOYMENT) + + +@pytest.mark.skipif( + _NO_FOUNDRY, + reason="Foundry not configured (set PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT + PORTFOLIO_FOUNDRY_DEPLOYMENT)", +) +async def test_portfolio_live_azure_fanout() -> None: + # No client_factory -> the real AZURE backend is used; hard-capped per D6. + result = await run_portfolio(["FV42-GSV-E1"], "azure", max_tokens=2000) + assert isinstance(result, PortfolioResult) + assert len(result.runs) == 1