"""IMP-#85 u7 — pytest env isolation for src.config defaults. This conftest.py runs BEFORE any test module is imported by pytest. Setting ``os.environ["AI_FALLBACK_*"]`` here overrides values that the live ``.env`` file would otherwise inject through ``pydantic-settings`` (priority: init args > os.environ > env_file). The ``src.config`` module-level ``settings = Settings()`` singleton is therefore built against the test-clean environment when src.config is first imported during test collection. Scope (per Stage 2 plan u7): * Restore the default-OFF contract for ``ai_fallback_enabled`` so ``tests/test_phase_z2_ai_fallback_config.py`` and ``tests/test_imp47b_step12_ai_wiring.py`` (which lock the flag-off short-circuit) match the source-of-truth default in ``src/config.py``. * Restore the default-OFF contract for ``ai_fallback_auto_cache``. Out of scope: * Touching ``ANTHROPIC_API_KEY`` / ``KEI_API_URL`` / ``LOG_LEVEL``. * Resetting the ``src.config.settings`` singleton mid-session. Tests that need to flip ``settings.ai_fallback_enabled`` at runtime mutate the singleton directly (mirrors the production ``--auto-cache`` CLI path in ``src/phase_z2_pipeline.py``). IMP-35 baseline-red invariance carve-out ======================================== The IMP-35 baseline-red invariance gate at ``tests/phase_z2/test_imp35_baseline_red_invariance.py`` spawns a child pytest subprocess that targets ONLY the two baseline-area files: tests/test_imp47b_step12_ai_wiring.py tests/test_phase_z2_ai_fallback_config.py That gate's binding contract (Stage 2 u11 lock) is that those four registered known-red tests STAY RED until a follow-up issue deregisters them. If this conftest blindly forces ``AI_FALLBACK_ENABLED=false`` in the gate's subprocess, the ``test_ai_fallback_master_flag_default_off`` registered red flips green and the invariance gate trips — a real cross-issue contract conflict (see Codex #8 Stage 3 verification of IMP-#85 u7). The carve-out below detects that exact subprocess signature (positional ``.py`` targets are entirely baseline-area files) and skips env isolation, leaving the gate's child process in its native ``.env``-loaded state. Every other pytest invocation — full-suite ``pytest -q tests``, the IMP-#85 smoke targets, single-file dev runs on non-baseline files — still gets the default-OFF isolation. Per ``feedback_demo_env_toggle_policy``: demo activation belongs in ``.env`` only. The override below is test-scoped (lives under ``tests/``) and never propagates into ``src/`` or ``vite.config``. """ from __future__ import annotations import os import sys # File suffixes (basenames) of the IMP-35 baseline-red area files. # The IMP-35 gate spawns its subprocess with these as the sole positional # pytest targets. Suffix matching is used so the detection is robust # across Windows/POSIX path separators and absolute/relative cwd. _IMP35_BASELINE_AREA_FILE_SUFFIXES: tuple[str, ...] = ( "test_imp47b_step12_ai_wiring.py", "test_phase_z2_ai_fallback_config.py", ) def _is_imp35_baseline_subprocess() -> bool: """True iff the current pytest argv targets ONLY IMP-35 baseline-area files. The IMP-35 baseline-red invariance gate (``tests/phase_z2/test_imp35_baseline_red_invariance.py``) runs: python -m pytest -q --tb=no -p no:cacheprovider \\ tests/test_imp47b_step12_ai_wiring.py \\ tests/test_phase_z2_ai_fallback_config.py The two trailing positional ``.py`` arguments are the signature. We compare on basename suffix so the check is path-separator and cwd agnostic. Returning True here suppresses the ``AI_FALLBACK_*`` env override so the baseline-red registry contract (Stage 2 u11 lock) holds for the gate's child process while every other invocation (full-suite, IMP-#85 smokes, mixed-target dev runs) still gets the default-OFF isolation. """ file_targets = [arg for arg in sys.argv[1:] if arg.endswith(".py")] if not file_targets: return False return all( any( arg.replace("\\", "/").endswith(suffix) for suffix in _IMP35_BASELINE_AREA_FILE_SUFFIXES ) for arg in file_targets ) if _is_imp35_baseline_subprocess(): # Drop any inherited AI_FALLBACK_* values so the gate's child process # falls back to the live ``.env`` (AI_FALLBACK_ENABLED=true) — the # exact precondition under which the four registered baseline-red # tests are red. ``pop`` is no-op when the key is absent, so a # developer running the gate manually with a clean environment is # unaffected. os.environ.pop("AI_FALLBACK_ENABLED", None) os.environ.pop("AI_FALLBACK_AUTO_CACHE", None) else: os.environ["AI_FALLBACK_ENABLED"] = "false" os.environ["AI_FALLBACK_AUTO_CACHE"] = "false"