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---
This commit is contained in:
Jon Chery
2026-07-30 02:23:55 +00:00
parent eb7634da28
commit e15eea067b
20 changed files with 106 additions and 137 deletions
+5 -5
View File
@@ -1,11 +1,11 @@
{
"phase": 4,
"phase": 5,
"stage": "complete",
"milestone": "v1.15",
"phase_role": "execution",
"phase_role": "final",
"attempts": 0,
"updated_at": "2026-07-30T00:10:00Z",
"milestone_complete": false,
"requirements": ["REQ-163"],
"updated_at": "2026-07-30T00:11:00Z",
"milestone_complete": true,
"requirements": ["REQ-155", "REQ-156", "REQ-157", "REQ-158", "REQ-159", "REQ-160", "REQ-161", "REQ-162", "REQ-163", "REQ-164"],
"tag": "v1.15.4"
}
+1 -1
View File
@@ -828,7 +828,7 @@ IDEATE-01..IDEATE-10, mapped to REQ-155..REQ-164.
| REQ-161 | P3 | complete |
| REQ-162 | P3 | complete |
| REQ-163 | P4 | complete |
| REQ-164 | P5 | pending |
| REQ-164 | P5 | complete |
### Out of Scope (v1.15)
- Renaming the real Gitea org/repo (`continuous-intelligence/acdl`) or
+2 -2
View File
@@ -1435,7 +1435,7 @@ After Phase P21: milestone COMPLETE — `v1.13.24` IS the v1.14 release.
---
## v1.15 (active — Nova Rebrand, tag `v1.15.4`)
## v1.15 (complete — Nova Rebrand, tag `v1.15.4`)
A full rebrand from **ACDL** / "Agentic Cloud Delivery Platform" →
**Nova** / "The New Dawn of DevSecOps — security as a seamless enabler
@@ -1620,7 +1620,7 @@ milestone release). (G-104 binding.)
update REQUIREMENTS.md (REQ-155..164 marked complete), ROADMAP.md
(v1.15 complete), PROJECT.md. Tag final patch `v1.14.5` (IS the
milestone release). Merge `milestone/v1.15-nova` → `main`.
- **Status:** in progress (branch `phase/05-final-review-ship`)
- **Status:** complete (v1.15.4, milestone release)
- **Depends on:** [P1-P4]
- **Requirements:** REQ-164
- **Success Criteria:**
@@ -39,12 +39,11 @@ NON_TAGGABLE_TYPES = (
"aws_internet_gateway",
)
# P3 hard mode (D-109): hard-fail when a taggable resource is missing any
# required nova:* tag, or when a resource carries only legacy acdl:* tag
# keys and no nova:* tags. P2 shipped warn mode (`_WARN_MODE = True`); P3
# flips to `False` (hard-fail) once terraform emits nova:* and the ABAC
# policy is swapped to nova:*. P5 keeps hard mode and additionally fails
# on any acdl:* tag key present at all.
# P5 hard mode (D-109, REQ-164): `_WARN_MODE = False` (set in P3) AND
# any `acdl:*` tag key present at all is a hard FAIL (P5 tightens from
# P3's "acdl:*-only fails" to "any acdl:* key fails"). The legacy tag
# keys are fully removed from terraform (P3); any remaining `acdl:*` key
# is a rebrand regression.
_WARN_MODE = False
@@ -71,21 +70,13 @@ class NovaTaggingStandard(BaseResourceCheck):
tag_keys = set(tag_block.keys())
elif isinstance(tags, dict):
tag_keys = set(tags.keys())
# P5 (REQ-164): any legacy acdl:* tag key present = hard FAIL.
legacy_present = tag_keys & set(LEGACY_TAGS)
if legacy_present:
return CheckResult.FAILED
missing = [t for t in REQUIRED_TAGS if t not in tag_keys]
if not missing:
return CheckResult.PASSED
# Warn mode (D-109, P2): if the resource carries the legacy acdl:*
# tag keys for every required tag, emit a warning rather than a
# hard fail — existing resources still carry acdl:* until P3.
if _WARN_MODE:
has_all_legacy = all(t in tag_keys for t in LEGACY_TAGS)
if has_all_legacy:
sys.stderr.write(
f"[nova_tagging] WARN: {entity_type} has legacy acdl:* tags "
f"but no nova:* tags (P2 warn mode, D-109). Migrate to "
f"nova:* tags before P5.\n"
)
return CheckResult.PASSED
return CheckResult.FAILED
check = NovaTaggingStandard()
+14 -31
View File
@@ -1,19 +1,14 @@
"""Dual-read environment helper (D-108, REQ-159, G-106).
"""Environment helper (D-108, REQ-159, REQ-164).
During the Nova rebrand transition window (P2–P4), every `NOVA_*`
environment variable is the preferred source, with the legacy `ACDL_*`
name as the fallback. This keeps deployments from breaking while the
keys are rotated across `.env`, `.env.secrets`, Gitea repo secrets, and
operator-managed process environments.
During the Nova rebrand transition window (P2–P4), `get_env` read
`NOVA_*` preferred with the legacy `ACDL_*` name as the fallback. **P5
(REQ-164) removed the fallback** — `get_env` now reads `NOVA_*` only.
`get_env(name, default=None)` resolves `NOVA_<name>` first, then falls
back to `ACDL_<name>`, then returns `default` if neither is set.
This helper is removed (NOVA-only) in P5 (REQ-164). Direct-read paths
that bypass this helper (the `.env.secrets` shell export in
`scripts/run_platform.sh` and the Python parser in
`core/regression_verify.py`) mirror this contract inline per the G-106
binding — see those sites for the dual-read shell/Python forms.
`get_env(name, default=None)` resolves `NOVA_<name>`, then returns
`default` if unset. Direct-read paths that bypass this helper (the
`.env.secrets` shell export in `scripts/run_platform.sh` and the Python
parser in `core/regression_verify.py`) were updated to NOVA-only in P5
(the G-106 dual-read contract was retired with the fallback).
"""
from __future__ import annotations
@@ -25,24 +20,12 @@ __all__ = ["get_env"]
def get_env(name: str, default: Optional[str] = None) -> Optional[str]:
"""Resolve a config value with a NOVA-preferred / ACDL-fallback read.
"""Resolve a config value from the `NOVA_*` environment.
`name` is the bare key WITHOUT the prefix (e.g. ``"AWS_ACCOUNT_ID"``).
The lookup order is:
1. ``NOVA_<name>`` (preferred)
2. ``ACDL_<name>`` (legacy fallback, removed in P5)
3. ``default``
Returns the first value that is present and non-empty, or ``default``
if neither env var is set. An explicitly-set empty string is treated
as "unset" so an operator cannot accidentally shadow the fallback
with a blank NOVA key.
Returns ``NOVA_<name>`` if set and non-empty, else ``default``.
"""
nova_val = os.environ.get(f"NOVA_{name}")
if nova_val:
return nova_val
acdl_val = os.environ.get(f"ACDL_{name}")
if acdl_val:
return acdl_val
val = os.environ.get(f"NOVA_{name}")
if val:
return val
return default
+1 -1
View File
@@ -502,6 +502,6 @@ if __name__ == "__main__":
# Set both so the dual-read in is_local_tier() finds NOVA_* (preferred);
# the ACDL_* alias stays for any unmigrated reader until P5.
os.environ["NOVA_LOCAL_TIER"] = "1"
os.environ["ACDL_LOCAL_TIER"] = "1" # legacy alias (dual-read fallback), removed in P5
# P5 (REQ-164): ACDL_LOCAL_TIER legacy alias removed (NOVA_* only)
result = run_local_e2e(contract)
print(json.dumps(result, indent=2))
+3 -6
View File
@@ -315,13 +315,10 @@ def _load_aws_env() -> Dict[str, str]:
continue
if "=" in line:
k, v = line.split("=", 1)
# G-106 binding: dual-read NOVA_* first, ACDL_* fallback.
# The .env.secrets keys are renamed to NOVA_* in P2; the
# ACDL_* fallback covers operators who haven't rotated
# their local .env.secrets yet. Removed in P5.
if k == "NOVA_AWS_ACCESS_KEY_ID" or k == "ACDL_AWS_ACCESS_KEY_ID":
# P5 (REQ-164): dual-read fallback removed — NOVA_* only.
if k == "NOVA_AWS_ACCESS_KEY_ID":
env["AWS_ACCESS_KEY_ID"] = v
elif k == "NOVA_AWS_SECRET_ACCESS_KEY" or k == "ACDL_AWS_SECRET_ACCESS_KEY":
elif k == "NOVA_AWS_SECRET_ACCESS_KEY":
env["AWS_SECRET_ACCESS_KEY"] = v
elif k == "AWS_DEFAULT_REGION":
env["AWS_DEFAULT_REGION"] = v
+1 -1
View File
@@ -74,7 +74,7 @@ def route_halt_artifact(contract_id: str, violation_reason: str,
sns.publish(
TopicArn=topic_arn,
Message=json.dumps(halt_payload),
Subject="ACDL SoD halt",
Subject="Nova SoD halt",
)
print(f"[halt-artifact] SNS published contract={contract_id} "
f"topic={topic_arn}", flush=True)
+2 -2
View File
@@ -65,8 +65,8 @@ def main():
creds = _load_env(ENV_FILE)
# Dual-read: NOVA_* preferred, ACDL_* fallback (G-106, removed in P5).
access_key = creds.get("NOVA_AWS_ACCESS_KEY_ID") or creds.get("ACDL_AWS_ACCESS_KEY_ID")
secret_key = creds.get("NOVA_AWS_SECRET_ACCESS_KEY") or creds.get("ACDL_AWS_SECRET_ACCESS_KEY")
access_key = creds.get("NOVA_AWS_ACCESS_KEY_ID")
secret_key = creds.get("NOVA_AWS_SECRET_ACCESS_KEY")
region = creds.get("AWS_DEFAULT_REGION", AWS_REGION)
if not access_key or not secret_key:
print("FAIL: NOVA_AWS_ACCESS_KEY_ID / NOVA_AWS_SECRET_ACCESS_KEY missing",
+6 -6
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# scripts/rotate_spike_key.sh - rotate the nova-spike-runner IAM access key.
#
# Uses the bootstrap root key (NOVA_BOOTSTRAP_AWS_*, ACDL_BOOTSTRAP_AWS_*
# Uses the bootstrap root key (NOVA_BOOTSTRAP_AWS_*
# fallback) from the env to:
# 1. List nova-spike-runner's access keys.
# 2. Create a new key.
@@ -24,8 +24,8 @@ ENV_FILE="$ROOT/.env.secrets"
fail() { echo "FAIL: $*" >&2; exit 1; }
# Dual-read bootstrap creds: NOVA_* preferred, ACDL_* fallback (removed in P5).
: "${NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID:-${ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID:?set NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID (or ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID) to the root key}}"
: "${NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:-${ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:?set NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY (or ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY) to the root key}}"
: "${NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID:?set NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID to the root key}"
: "${NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:?set NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY to the root key}"
REGION="${AWS_DEFAULT_REGION:-us-east-1}"
USER_NAME="nova-spike-runner"
@@ -43,8 +43,8 @@ user = "nova-spike-runner"
env_file = os.path.join(os.getcwd(), ".env.secrets")
# Dual-read bootstrap creds: NOVA_* preferred, ACDL_* fallback (G-106, removed in P5).
bootstrap_key = os.environ.get("NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID") or os.environ["ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID"]
bootstrap_secret = os.environ.get("NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY") or os.environ["ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY"]
bootstrap_key = os.environ["NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID"]
bootstrap_secret = os.environ["NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY"]
session = boto3.Session(
aws_access_key_id=bootstrap_key,
@@ -84,7 +84,7 @@ print(f"rotated key written to {env_file} (chmod 600)", file=sys.stderr)
# Optionally upload to Gitea secrets.
# Dual-read token: NOVA_GITEA_TOKEN preferred, ACDL_GITEA_TOKEN fallback (G-106).
gitea_token = os.environ.get("NOVA_GITEA_TOKEN") or os.environ.get("ACDL_GITEA_TOKEN")
gitea_token = os.environ.get("NOVA_GITEA_TOKEN")
if gitea_token:
import urllib.request
base = "https://git.cloudinit.dev/api/v1/repos/continuous-intelligence/acdl/actions/secrets"
+3 -3
View File
@@ -4,7 +4,7 @@
# Usage: run_l2_lifecycle_destroy.sh <module>
#
# Wraps run_platform.sh for L2 composition modules in the modules-lifecycle
# pipeline. Sets NOVA_REMOTE_STATE_KEY (ACDL_REMOTE_STATE_KEY fallback until
# pipeline. Sets NOVA_REMOTE_STATE_KEY (NOVA-only since P5, REQ-164) for
# P5) to point to the CI VPC state.
#
# NOTE: unlike the L1 scripts (run_lifecycle_destroy.sh), the L2 path does
@@ -26,7 +26,7 @@ MODULE="$1"
# Lifecycle mode: "plan" (default) skips destroy; "full" runs the real destroy.
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5).
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}"
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}"
if [ "$LIFECYCLE_MODE" != "full" ]; then
echo "lifecycle mode=$LIFECYCLE_MODE — nothing to destroy (plan-only run), exiting 0"
@@ -38,7 +38,7 @@ CONTRACT="modules/l2/${MODULE}/examples/complex.yml"
# Point terraform_remote_state to the CI VPC state (not the platform VPC).
# Set both NOVA_* (preferred) and ACDL_* (legacy fallback) until P5.
export NOVA_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate"
export ACDL_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" # legacy fallback, removed in P5
# Run the platform lifecycle destroy command
bash scripts/run_platform.sh --destroy "$CONTRACT"
+3 -3
View File
@@ -4,7 +4,7 @@
# Usage: run_l2_lifecycle_test.sh <module> <example>
#
# Wraps run_platform.sh for L2 composition modules in the modules-lifecycle
# pipeline. Sets NOVA_REMOTE_STATE_KEY (ACDL_REMOTE_STATE_KEY fallback until
# pipeline. Sets NOVA_REMOTE_STATE_KEY (NOVA-only since P5, REQ-164) for
# P5) to point to the CI VPC state so the microservice composition's
# terraform_remote_state data source reads from the short-lived CI VPC
# (not the long-lived platform VPC).
@@ -30,7 +30,7 @@ EXAMPLE="$2" # simple or complex
# Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS).
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5).
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}"
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}"
CONTRACT="modules/l2/${MODULE}/examples/${EXAMPLE}.yml"
@@ -38,7 +38,7 @@ CONTRACT="modules/l2/${MODULE}/examples/${EXAMPLE}.yml"
# Set both NOVA_* (preferred by the dual-read helper) and ACDL_* (legacy
# fallback) so any unmigrated reader finds the key until P5.
export NOVA_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate"
export ACDL_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" # legacy fallback, removed in P5
# Run the platform lifecycle command (plan-only by default; full = apply).
if [ "$LIFECYCLE_MODE" = "full" ]; then
+1 -1
View File
@@ -21,7 +21,7 @@ CI_VPC_OUTPUTS="${2:-}"
# Lifecycle mode: "plan" (default) skips destroy (nothing was applied);
# "full" runs the real terraform destroy.
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5).
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}"
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}"
if [ "$LIFECYCLE_MODE" != "full" ]; then
echo "lifecycle mode=$LIFECYCLE_MODE — nothing to destroy (plan-only run), exiting 0"
+1 -1
View File
@@ -27,7 +27,7 @@ CI_VPC_OUTPUTS="${3:-}"
# Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS).
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5).
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}"
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}"
CONTRACT="modules/l1/${MODULE}/examples/${EXAMPLE}.yml"
+10 -12
View File
@@ -121,9 +121,9 @@ if [ "$LOCAL_TIER" = "1" ]; then
echo "=== Nova Local Emulating Tier (D-092) ==="
echo "contract: $CONTRACT (no AWS credentials required)"
echo ""
# Dual-read: set NOVA_LOCAL_TIER (preferred); ACDL_LOCAL_TIER fallback
# P5 (REQ-164): NOVA_* only (dual-read fallback removed)
# kept for any unmigrated reader until P5 (removed in P5).
NOVA_LOCAL_TIER=1 ACDL_LOCAL_TIER=1 python3 core/local_emulators.py "$CONTRACT" \
NOVA_LOCAL_TIER=1 python3 core/local_emulators.py "$CONTRACT" \
|| fail "local E2E failed"
echo ""
echo "=== LOCAL E2E OK ==="
@@ -287,14 +287,12 @@ if [ -z "${AWS_ACCESS_KEY_ID:-}" ] || [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then
[ -f "$ENV_FILE" ] || fail ".env.secrets missing (run scripts/rotate_spike_key.sh) or set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY env vars"
set -a
. "$ENV_FILE"
set +a
# G-106 binding: dual-read NOVA_* first, ACDL_* fallback. The .env.secrets
# keys are renamed to NOVA_* in P2; the ACDL_* fallback covers operators
# who haven't rotated their local .env.secrets yet. Removed in P5.
export AWS_ACCESS_KEY_ID="${NOVA_AWS_ACCESS_KEY_ID:-$ACDL_AWS_ACCESS_KEY_ID}"
export AWS_SECRET_ACCESS_KEY="${NOVA_AWS_SECRET_ACCESS_KEY:-$ACDL_AWS_SECRET_ACCESS_KEY}"
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"
fi
set +a
# P5 (REQ-164): dual-read fallback removed — NOVA_* only.
export AWS_ACCESS_KEY_ID="$NOVA_AWS_ACCESS_KEY_ID"
export AWS_SECRET_ACCESS_KEY="$NOVA_AWS_SECRET_ACCESS_KEY"
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"
fi
echo "=== Step 4: terraform init + validate + plan -lock=false (real AWS) ==="
cd "$TF_DIR"
@@ -349,7 +347,7 @@ if ok:
else:
print(f'HITL BLOCK: {reason}', file=sys.stderr)
sys.exit(1)
" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" ACDL_HITL_CONTRACT_ID="$CONTRACT_ID" ACDL_HITL_ENV="$RESOLVED_ENV" ACDL_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the apply" >&2; exit 1; }
" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the apply" >&2; exit 1; }
else
echo "Environment is dev — autonomous (no HITL gate)."
fi
@@ -465,7 +463,7 @@ if ok:
else:
print(f'HITL BLOCK: {reason}', file=sys.stderr)
sys.exit(1)
" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" ACDL_HITL_CONTRACT_ID="$CONTRACT_ID" ACDL_HITL_ENV="$RESOLVED_ENV" ACDL_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; exit 1; }
" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; exit 1; }
else
echo "Environment is dev — autonomous (no HITL gate)."
fi
+1 -1
View File
@@ -20,7 +20,7 @@ cd "$ROOT"
echo "=== Nova Regression VERIFY (D-091) ==="
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5).
echo "milestone: ${NOVA_REGRESSION_MILESTONE:-${ACDL_REGRESSION_MILESTONE:-v1.10}} phase: ${NOVA_REGRESSION_PHASE:-${ACDL_REGRESSION_PHASE:-52}}"
echo "milestone: ${NOVA_REGRESSION_MILESTONE:-v1.10} phase: ${NOVA_REGRESSION_PHASE:-52}"
echo ""
python3 core/regression_verify.py
+25 -25
View File
@@ -1,14 +1,11 @@
"""Unit tests for the dual-read env helper (core/env.py, D-108, REQ-159).
"""Unit tests for the NOVA-only env helper (core/env.py, D-108, REQ-164).
Covers the four cases:
- both NOVA_* and ACDL_* set (NOVA wins)
- only NOVA_* set
- only ACDL_* set (fallback)
- neither set (default returned)
The ACDL_* fallback is the intentional dual-read source and is removed
in P5 (REQ-164). These fixtures deliberately keep the ACDL_* names as
the fallback source — they are the one allowed ACDL_* reference.
P5 (REQ-164) removed the ACDL_* dual-read fallback. `get_env` now reads
`NOVA_*` only. These tests verify:
- only NOVA_* set → returned
- neither set → default returned
- ACDL_* set but NOVA_* unset → default returned (fallback REMOVED)
- blank NOVA_* → default returned (not the ACDL_* value)
"""
from __future__ import annotations
@@ -27,30 +24,33 @@ def _isolate_env(monkeypatch):
yield
def test_both_set_nova_wins(monkeypatch):
monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "nova-value")
monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value")
assert env.get_env("AWS_ACCOUNT_ID") == "nova-value"
def test_only_nova_set(monkeypatch):
monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "nova-value")
assert env.get_env("AWS_ACCOUNT_ID") == "nova-value"
def test_only_acdl_set_fallback(monkeypatch):
# ACDL_* is the intentional dual-read fallback source (removed in P5).
monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value")
assert env.get_env("AWS_ACCOUNT_ID") == "acdl-value"
def test_neither_set_returns_default():
assert env.get_env("AWS_ACCOUNT_ID") is None
assert env.get_env("AWS_ACCOUNT_ID", default="581513795199") == "581513795199"
def test_blank_nova_falls_back_to_acdl(monkeypatch):
# An explicitly-empty NOVA key must not shadow the ACDL fallback.
def test_only_acdl_set_no_fallback(monkeypatch):
# P5 (REQ-164): ACDL_* fallback removed — ACDL_* alone returns default.
monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value")
assert env.get_env("AWS_ACCOUNT_ID") is None
assert env.get_env("AWS_ACCOUNT_ID", default="fallback") == "fallback"
def test_both_set_nova_wins(monkeypatch):
# NOVA_* takes precedence; ACDL_* is ignored.
monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "nova-value")
monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value")
assert env.get_env("AWS_ACCOUNT_ID") == "nova-value"
def test_blank_nova_returns_default_not_acdl(monkeypatch):
# An explicitly-empty NOVA key returns default (not an ACDL_* value).
monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "")
monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value")
assert env.get_env("AWS_ACCOUNT_ID") == "acdl-value"
assert env.get_env("AWS_ACCOUNT_ID") is None
assert env.get_env("AWS_ACCOUNT_ID", default="d") == "d"
+8 -8
View File
@@ -32,15 +32,15 @@ class TestLifecycleModeFlag:
"""REQ-134: the lifecycle scripts dispatch to plan-only by default."""
@pytest.mark.parametrize("script", SCRIPTS)
def test_script_reads_acdl_lifecycle_mode(self, script):
"""Every lifecycle script reads NOVA_LIFECYCLE_MODE (dual-read with
ACDL_LIFECYCLE_MODE fallback) with a 'plan' default."""
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)
# P2 (REQ-159): dual-read NOVA_* preferred, ACDL_* fallback. The
# ACDL_LIFECYCLE_MODE:-plan substring is still present inside the
# nested dual-read expression (removed in P5).
assert "${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" in src, \
f"{script} must dual-read NOVA_/ACDL_LIFECYCLE_MODE defaulting to 'plan'"
# 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"
+9 -9
View File
@@ -43,7 +43,7 @@ class TestPublishToSsm:
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
monkeypatch.setenv("ACDL_KMS_KEY_ID", "alias/aws/ssm")
monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm")
with mock_aws():
ssm = boto3.client("ssm", region_name="us-east-1")
@@ -67,7 +67,7 @@ class TestPublishToSsm:
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
monkeypatch.setenv("ACDL_KMS_KEY_ID", "alias/aws/ssm")
monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm")
with mock_aws():
ssm = boto3.client("ssm", region_name="us-east-1")
@@ -82,7 +82,7 @@ class TestPublishToSsm:
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
monkeypatch.setenv("ACDL_KMS_KEY_ID", "alias/aws/ssm")
monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm")
with mock_aws():
ssm = boto3.client("ssm", region_name="us-east-1")
@@ -105,7 +105,7 @@ class TestPublishToSsm:
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
monkeypatch.setenv("ACDL_KMS_KEY_ID", "alias/aws/ssm")
monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm")
with mock_aws():
ssm = boto3.client("ssm", region_name="us-east-1")
@@ -123,7 +123,7 @@ class TestPublishToSsm:
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
monkeypatch.setenv("ACDL_KMS_KEY_ID", "alias/aws/ssm")
monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm")
with mock_aws():
ssm = boto3.client("ssm", region_name="us-east-1")
@@ -377,7 +377,7 @@ class TestCli:
class TestKmsFailLoud:
"""P1-3: SSM publisher must fail loud when NOVA_KMS_KEY_ID is unset
(P2 renamed from ACDL_KMS_KEY_ID; dual-read NOVA_* preferred,
(P2 renamed from NOVA_KMS_KEY_ID; dual-read NOVA_* preferred,
ACDL_* fallback until P5)."""
def test_kms_unset_raises(self, monkeypatch):
@@ -389,9 +389,9 @@ class TestKmsFailLoud:
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
# Both NOVA_* and ACDL_* unset → helper returns default (None) → fail loud.
monkeypatch.delenv("NOVA_KMS_KEY_ID", raising=False)
monkeypatch.delenv("ACDL_KMS_KEY_ID", raising=False)
monkeypatch.delenv("NOVA_KMS_KEY_ID", raising=False)
monkeypatch.delenv("NOVA_ALLOW_DEFAULT_KMS", raising=False)
monkeypatch.delenv("NOVA_ALLOW_DEFAULT_KMS", raising=False)
monkeypatch.delenv("ACDL_ALLOW_DEFAULT_KMS", raising=False)
with mock_aws():
with pytest.raises(RuntimeError, match="NOVA_KMS_KEY_ID is not set"):
@@ -405,7 +405,7 @@ class TestKmsFailLoud:
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
monkeypatch.delenv("NOVA_KMS_KEY_ID", raising=False)
monkeypatch.delenv("ACDL_KMS_KEY_ID", raising=False)
monkeypatch.delenv("NOVA_KMS_KEY_ID", raising=False)
monkeypatch.setenv("NOVA_ALLOW_DEFAULT_KMS", "1")
with mock_aws():
+1 -1
View File
@@ -24,7 +24,7 @@ def test_route_halt_publishes_to_sns_when_arn_set(monkeypatch):
assert call.kwargs["TopicArn"] == "arn:aws:sns:us-east-1:000000000000:nova-sod-halt"
assert "contract-123" in call.kwargs["Message"]
assert "SEPARATION_OF_DUTIES_VIOLATION" in call.kwargs["Message"]
assert call.kwargs["Subject"] == "ACDL SoD halt"
assert call.kwargs["Subject"] == "Nova SoD halt"
def test_route_halt_falls_back_to_stderr_when_arn_unset(monkeypatch, capsys):