Files
acdl/tests/test_lifecycle_mode_flag.py
T
Jon Chery e15eea067b docs(milestone): complete v1.15 — Nova Rebrand (tag v1.15.4)
P5 final-review-ship complete: dual-read fallback removed (REQ-164) —
core/env.py NOVA-only, .env.secrets load paths NOVA-only (G-106 retired),
nova_tagging.py hard-fails any acdl:* tag, legacy ACDL_* Gitea secrets
deleted, ACDL_LIFECYCLE_MODE/ACDL_LOCAL_TIER/ACDL_HITL_* exports removed
from scripts, SNS subject → Nova SoD halt (P1-2), bootstrap scripts
NOVA-only. Review: 2 P0 auto-fixed (duplicate delenv), P1-1/P1-2 resolved,
doc-drift fixed. Audit: tags v1.15.0-4 exist; traceability REQ-155..164
all complete; ARCHITECTURE naming table matches codebase. 615 pytest PASS;
run_ci.sh 3-stage PASS. NOVA_MIGRATION.md marked COMPLETE.

---ci---
project: acdl
phase: 5
milestone: v1.15
status: complete
phase_role: final
requirements:
  covered: [REQ-155, REQ-156, REQ-157, REQ-158, REQ-159, REQ-160, REQ-161, REQ-162, REQ-163, REQ-164]
  partial: []
---/ci---
2026-07-30 02:23:55 +00:00

107 lines
5.1 KiB
Python

"""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)