"""S4.2 CLI surface for ``python -m portfolio_optimiser.run --live-dry-run``. The success arm proves the CLI wiring + rc + summary (the zero-chat-call guarantee itself is proven at ``run_project`` level in ``test_live_dry_run_loadbearing.py`` — ``main()`` exposes no ``client_factory`` seam). The refusal arm proves a misconfigured profile (the AZURE placeholder map) yields a STRUCTURED refusal + ``rc 1`` pointing at preflight, never a raw traceback. """ from __future__ import annotations from pathlib import Path import pytest from portfolio_optimiser import run BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro" @pytest.fixture(autouse=True) def _isolate_model_env(monkeypatch: pytest.MonkeyPatch) -> None: """Hermetic env: clear the S4.1 out-of-tree overrides so these CLI assertions read the BUNDLED map/config, not the operator's Foundry-configured environment. Without this, ``PORTFOLIO_MODEL_MAP`` (an azure-only override wins over the bundled placeholder, ``backends.py:48-56``) and ``PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT`` (``backends.py:103``) invert both arms' outcomes — the LOCAL success arm can't resolve, the azure refusal arm resolves a real id. Mirrors the sibling isolation in ``test_backends.py``/``test_preflight.py`` (both ``delenv PORTFOLIO_MODEL_MAP``).""" monkeypatch.delenv("PORTFOLIO_MODEL_MAP", raising=False) monkeypatch.delenv("PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT", raising=False) def test_cli_live_dry_run_ok(capsys) -> None: """Success arm: LOCAL profile (offline client construction) + ``--live-dry-run`` → rc 0 and a summary naming the profile + a no-model-call note. No socket (stops before ``debate.run``).""" rc = run.main( [ "BYGG-KONTOR-NORD", "--docs-dir", str(BUNDLE_DIR), "--bundle-dir", str(BUNDLE_DIR), "--live-dry-run", ] ) assert rc == 0 out = capsys.readouterr().out assert "LIVE-DRY-RUN OK" in out assert "local" in out assert "ingen modellkall" in out.lower() def test_cli_live_dry_run_azure_placeholder_refuses(capsys) -> None: """Refusal arm: AZURE profile with the default bundled placeholder map → ``resolve_model`` raises inside the eager factory build; ``main()`` catches it and prints a structured refusal to stderr + returns rc 1 (pointing at preflight), never a traceback. Offline (pure map read).""" rc = run.main( [ "BYGG-KONTOR-NORD", "--docs-dir", str(BUNDLE_DIR), "--bundle-dir", str(BUNDLE_DIR), "--profile", "azure", "--live-dry-run", ] ) assert rc == 1 err = capsys.readouterr().err assert "refused" in err.lower() assert "preflight" in err.lower() def test_cli_live_dry_run_local_unknown_project_no_azure_hint(capsys) -> None: """A LOCAL-profile refusal from a non-config ``ValueError`` (here: an unknown project_id) must still be a STRUCTURED refusal + ``rc 1``, but must NOT carry the azure-preflight remediation — that hint is only meaningful for the AZURE config gate (S4.1). Guards against the over-broad ``except`` re-attaching an azure-specific hint to every offline-path ``ValueError``.""" rc = run.main( [ "NOPE-UNKNOWN-PROJ", "--docs-dir", str(BUNDLE_DIR), "--bundle-dir", str(BUNDLE_DIR), "--live-dry-run", ] ) assert rc == 1 err = capsys.readouterr().err assert "refused" in err.lower() assert "preflight" not in err.lower()