"""Tests for the lifecycle test scripts' plan-only/full mode flag (REQ-134). The lifecycle scripts (run_lifecycle_test.sh, run_lifecycle_destroy.sh, run_l2_lifecycle_test.sh, run_l2_lifecycle_destroy.sh) wrap run_platform.sh. REQ-134 (v1.12) adds the NOVA_LIFECYCLE_MODE env var (P2 renamed from ACDL_LIFECYCLE_MODE, dual-read NOVA_* preferred / ACDL_* fallback until P5): default "plan" runs `run_platform.sh --plan-only` (fast, no AWS mutation); "full" runs the real `--apply`/`--destroy` against live AWS. These tests verify the dispatch logic offline by inspecting script content (running the scripts end-to-end requires AWS credentials in full mode). """ import re import pytest ROOT = __import__("pathlib").Path(__file__).resolve().parent.parent SCRIPTS = [ "scripts/run_lifecycle_test.sh", "scripts/run_lifecycle_destroy.sh", "scripts/run_l2_lifecycle_test.sh", "scripts/run_l2_lifecycle_destroy.sh", ] def _read(path): return (ROOT / path).read_text() class TestLifecycleModeFlag: """REQ-134: the lifecycle scripts dispatch to plan-only by default.""" @pytest.mark.parametrize("script", SCRIPTS) def test_script_reads_nova_lifecycle_mode(self, script): """Every lifecycle script reads NOVA_LIFECYCLE_MODE (NOVA-only since P5, REQ-164 — dual-read fallback removed) with a 'plan' default.""" src = _read(script) # P5 (REQ-164): NOVA_* only (dual-read fallback removed). assert "${NOVA_LIFECYCLE_MODE:-plan}" in src, \ f"{script} must read NOVA_LIFECYCLE_MODE defaulting to 'plan'" assert "ACDL_LIFECYCLE_MODE" not in src, \ f"{script} must not reference ACDL_LIFECYCLE_MODE (P5 removed the fallback)" assert "LIFECYCLE_MODE=" in src, \ f"{script} must assign LIFECYCLE_MODE from the env var" @pytest.mark.parametrize("script", [ "scripts/run_lifecycle_test.sh", "scripts/run_l2_lifecycle_test.sh", ]) def test_apply_scripts_dispatch_to_plan_only_by_default(self, script): """Apply/modify scripts run --plan-only when mode != full.""" src = _read(script) assert "--plan-only" in src, f"{script} must support --plan-only (plan mode)" assert "--apply" in src, f"{script} must support --apply (full mode)" # The dispatch must branch on LIFECYCLE_MODE assert "LIFECYCLE_MODE" in src and "full" in src, \ f"{script} must branch on LIFECYCLE_MODE == 'full'" @pytest.mark.parametrize("script", [ "scripts/run_lifecycle_destroy.sh", "scripts/run_l2_lifecycle_destroy.sh", ]) def test_destroy_scripts_noop_in_plan_mode(self, script): """Destroy scripts are a no-op (exit 0) in plan mode — nothing was applied.""" src = _read(script) # The destroy script must check the mode and exit 0 in plan mode. assert "nothing to destroy" in src, \ f"{script} must no-op (with a message) in plan mode" assert "--destroy" in src, f"{script} must still support --destroy in full mode" assert "LIFECYCLE_MODE" in src, f"{script} must read LIFECYCLE_MODE" def test_apply_script_plan_mode_uses_plan_only(self): """In plan mode, run_lifecycle_test.sh dispatches to --plan-only.""" src = _read("scripts/run_lifecycle_test.sh") # The if/else: full -> --apply, else -> --plan-only assert re.search(r'if.*LIFECYCLE_MODE.*=.*"full".*;.*then', src) assert re.search(r'else\s+bash scripts/run_platform\.sh --plan-only', src, re.DOTALL) def test_apply_script_full_mode_uses_apply(self): """In full mode, run_lifecycle_test.sh dispatches to --apply.""" src = _read("scripts/run_lifecycle_test.sh") assert re.search(r'LIFECYCLE_MODE.*=.*"full".*bash scripts/run_platform\.sh --apply', src, re.DOTALL) def test_destroy_script_plan_mode_exits_zero(self): """In plan mode, the L1 destroy script exits 0 without calling run_platform.""" src = _read("scripts/run_lifecycle_destroy.sh") assert re.search(r'LIFECYCLE_MODE.*!=.*"full".*exiting 0', src, re.DOTALL) def test_l2_apply_script_dispatches_correctly(self): """The L2 apply script dispatches to plan-only by default, apply in full.""" src = _read("scripts/run_l2_lifecycle_test.sh") assert "--plan-only" in src and "--apply" in src assert re.search(r'if.*LIFECYCLE_MODE.*=.*"full"', src) def test_l2_destroy_script_noop_in_plan_mode(self): """The L2 destroy script is a no-op in plan mode.""" src = _read("scripts/run_l2_lifecycle_destroy.sh") assert "nothing to destroy" in src assert re.search(r'LIFECYCLE_MODE.*!=.*"full".*exiting 0', src, re.DOTALL) def test_vpc_injection_gated_on_full_mode(self): """VPC output injection is gated on full mode (plan mode skips it).""" src = _read("scripts/run_lifecycle_test.sh") # The VPC injection block must be gated on LIFECYCLE_MODE == full. assert re.search(r'LIFECYCLE_MODE.*=.*"full".*&&.*echo.*VPC_DEPENDENT', src, re.DOTALL) or \ re.search(r'\[ "\$LIFECYCLE_MODE" = "full" \] && echo.*VPC_DEPENDENT', src, re.DOTALL)