Endepunktet løses som første ikke-tomme av vårt eget PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT og Foundrys injiserte FOUNDRY_PROJECT_ENDPOINT — vårt vinner, fallbacken lar samme image kjøre hostet uten ekstra wiring. Presedensen gjelder verdier, ikke deklarasjoner. Credential velges av samme miljø: AzureCliCredential lokalt, ManagedIdentityCredential når FOUNDRY_HOSTING_ENVIRONMENT er satt, fordi containeren ikke har noen Azure CLI. Ikke DefaultAzureCredential — Learns MAF-veiledning navngir den spesifikke credentialen for å unngå probing. Load-bearing målt mot hele suiten, fire mutasjoner alle røde + grønn kontroll: detach credential-valget · presence i stedet for truthiness · detach fallbacken · snu presedensen. Fail-fast-testen var vakuøs først — vårt variabelnavn inneholder det injiserte som delstreng. De fire åpne azure.yaml-valgene lukket mot de to JSON-skjemaene og ført i docs/2026-08-13-fase4-azure-yaml-valg.md. Ingen azure.yaml skrevet (4d). 821 passed / 4 skipped. Ruff + format + mypy rene. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jk8tauXXAojNKC7Tzq7ziF
96 lines
4 KiB
Python
96 lines
4 KiB
Python
"""S4.2 CLI surface for ``python -m portfolio_optimiser.run <proj> --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)
|
|
# Fase 4b: the endpoint now also resolves from the platform-injected ``FOUNDRY_PROJECT_ENDPOINT``
|
|
# (backends.py ``_ENDPOINT_ENVS``), so hermetic isolation must clear that name too — otherwise
|
|
# this fixture no longer achieves what its own docstring above promises.
|
|
monkeypatch.delenv("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()
|