Files
acdl/tests/test_local_emulating_adapters.py
T
Jon Chery 0e6ecae26d feat(P4): Nova rebrand — AWS resource migration (REQ-163)
Rename all acdl-* AWS resources → nova-* across terraform (DynamoDB,
Secrets Manager, Lambda, SNS, SG, KMS alias, ECS, ECR, IAM user/policy,
state bucket, ALB, VPC/subnet names). Lambda default table names → nova-*
(D-111). State bucket backend → nova-tfstate (-migrate-state documented).
New docs/NOVA_AWS_MIGRATION.md runbook (staged migration + rollback).
New scripts/migrate_dynamodb_data.py (scan+copy, dry-run default).
acdl-deploy- → nova-deploy- role ARN in deploy workflows. Test fixtures
updated; terraform validate + pytest + run_ci.sh PASS.

---ci---
project: acdl
phase: 4
milestone: v1.15
status: execute
---/ci---
2026-07-30 01:54:26 +00:00

217 lines
8.1 KiB
Python

"""Tests for the local emulating adapters (D-092, REQ-113).
Verifies the four local adapters and the headline E2E run against the
local tier with no cloud credentials:
1. FlatFileOutbox - flat-file DynamoDB outbox emulator
2. LocalEcsEmulator - local ECS Fargate HTTP 200 emulator
3. LocalS3StateBackend - terraform S3 -> local backend rewrite
4. LocalLambdaStub - in-process contract_ingestor invocation
5. run_local_e2e - the full headline E2E against the local tier
"""
import json
import os
import sys
import tempfile
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
import core.local_emulators as le # noqa: E402
# ---------------------------------------------------------------------------
# 1. FlatFileOutbox
# ---------------------------------------------------------------------------
def test_flat_file_outbox_writes_hash_chained_event(tmp_path):
outbox = le.FlatFileOutbox.create(dir=tmp_path)
event = {
"contractId": "c1", "eventType": "CONFIDENCE_COMPUTED",
"ts": "2026-07-27T00:00:00Z", "environment": "dev",
"stack": "s1", "score": 0.9, "band": "pass",
"prev_event_hash": "GENESIS",
}
item = outbox.write_event(event)
assert item["hash"]
assert len(item["hash"]) == 64 # SHA-256 hex
assert item["prev_event_hash"] == "GENESIS"
events = outbox.read_all()
assert len(events) == 1
assert events[0]["hash"] == item["hash"]
def test_flat_file_outbox_chain_links_prior_hash(tmp_path):
outbox = le.FlatFileOutbox.create(dir=tmp_path)
e1 = {"contractId": "c1", "eventType": "E1", "ts": "t1",
"environment": "dev", "stack": "s", "score": 0.9, "band": "pass",
"prev_event_hash": "GENESIS"}
item1 = outbox.write_event(e1)
e2 = {"contractId": "c1", "eventType": "E2", "ts": "t2",
"environment": "dev", "stack": "s", "score": 0.9, "band": "pass",
"prev_event_hash": item1["hash"]}
item2 = outbox.write_event(e2)
assert item2["prev_event_hash"] == item1["hash"]
assert outbox.verify_chain()
def test_flat_file_outbox_detects_broken_chain(tmp_path):
outbox = le.FlatFileOutbox.create(dir=tmp_path)
e1 = {"contractId": "c1", "eventType": "E1", "ts": "t1",
"environment": "dev", "stack": "s", "score": 0.9, "band": "pass",
"prev_event_hash": "GENESIS"}
item1 = outbox.write_event(e1)
# Tamper: write a second event claiming the wrong prev hash.
e2 = {"contractId": "c1", "eventType": "E2", "ts": "t2",
"environment": "dev", "stack": "s", "score": 0.9, "band": "pass",
"prev_event_hash": "WRONG"}
outbox.write_event(e2)
assert outbox.verify_chain() is False
def test_flat_file_outbox_resumes_chain_across_instances(tmp_path):
outbox1 = le.FlatFileOutbox.create(dir=tmp_path)
e1 = {"contractId": "c1", "eventType": "E1", "ts": "t1",
"environment": "dev", "stack": "s", "score": 0.9, "band": "pass",
"prev_event_hash": "GENESIS"}
item1 = outbox1.write_event(e1)
# New instance pointing at the same dir must resume from item1's hash.
outbox2 = le.FlatFileOutbox.create(dir=tmp_path)
assert outbox2._chain_tail_hash == item1["hash"]
# ---------------------------------------------------------------------------
# 2. LocalEcsEmulator
# ---------------------------------------------------------------------------
def test_local_ecs_emulator_returns_http_200():
ecs = le.LocalEcsEmulator(
service_name="test-svc",
service_definition={"desired_count": 1},
)
try:
meta = ecs.deploy()
assert meta["status"] == "RUNNING"
assert meta["endpoint"].startswith("http://127.0.0.1:")
ok, status = ecs.health_check(meta["endpoint"])
assert ok is True
assert status == 200
finally:
ecs.destroy()
def test_local_ecs_emulator_destroy_stops_server():
ecs = le.LocalEcsEmulator("svc", {"desired_count": 1})
meta = ecs.deploy()
ecs.destroy()
# After destroy, the health check must fail (server stopped).
ok, status = ecs.health_check(meta["endpoint"], timeout_s=1.0)
assert ok is False
# ---------------------------------------------------------------------------
# 3. LocalS3StateBackend
# ---------------------------------------------------------------------------
def test_local_s3_backend_rewrites_s3_to_local(tmp_path):
backend = le.LocalS3StateBackend.create(dir=tmp_path / "state")
tf = tmp_path / "terraform.tf"
tf.write_text(
'terraform {\n required_version = ">= 1.9"\n backend "s3" {\n'
' bucket = "nova-tfstate-x"\n key = "spike/s.tfstate"\n'
' region = "us-east-1"\n }\n}\n'
)
backend.rewrite_terraform_tf(tf, "test-stack")
content = tf.read_text()
assert 'backend "local"' in content
assert 'backend "s3"' not in content
assert "test-stack.tfstate" in content
def test_local_s3_backend_state_path_is_unique_per_stack(tmp_path):
backend = le.LocalS3StateBackend.create(dir=tmp_path / "state")
p1 = backend.state_path("stack-a")
p2 = backend.state_path("stack-b")
assert p1 != p2
assert p1.name == "stack-a.tfstate"
# ---------------------------------------------------------------------------
# 4. LocalLambdaStub
# ---------------------------------------------------------------------------
def test_local_lambda_stub_invokes_contract_ingestor(tmp_path):
outbox = le.FlatFileOutbox.create(dir=tmp_path / "outbox")
stub = le.LocalLambdaStub(outbox=outbox)
result = stub.invoke({
"action": "submit_contract",
"consumerRepo": "local-test/consumer",
"contractId": "lambda-test",
"contract": {"module": "microservice", "environment": "dev"},
"environment": "dev",
})
assert result["statusCode"] == 200
body = json.loads(result["body"])
assert "consumerRepo" in body or "contractId" in body
def test_local_lambda_stub_rejects_missing_field(tmp_path):
outbox = le.FlatFileOutbox.create(dir=tmp_path / "outbox")
stub = le.LocalLambdaStub(outbox=outbox)
result = stub.invoke({
"action": "submit_contract",
"consumerRepo": "local-test/consumer",
# contractId intentionally missing
"contract": {"module": "microservice", "environment": "dev"},
"environment": "dev",
})
assert result["statusCode"] == 400
# ---------------------------------------------------------------------------
# 5. run_local_e2e (the headline E2E against the local tier)
# ---------------------------------------------------------------------------
@pytest.mark.slow
def test_run_local_e2e_microservice():
"""Headline E2E: contract -> resolver -> adapter -> local S3 backend
-> local ECS (HTTP 200) -> flat-file outbox -> local Lambda. No AWS."""
os.environ["NOVA_LOCAL_TIER"] = "1"
try:
result = le.run_local_e2e("contracts/microservice.yml")
finally:
os.environ.pop("NOVA_LOCAL_TIER", None); os.environ.pop("ACDL_LOCAL_TIER", None)
assert result["tier"] == "local-emulator"
assert result["backend"] == "local"
assert result["ecs"] is not None
assert result["ecs"]["status"] == "RUNNING"
assert result["outbox_chain_verified"] is True
assert result["lambda_status"] == 200
@pytest.mark.slow
def test_run_local_e2e_static_assets():
"""Static-assets stack has no ECS service; the local E2E must still
complete (ecs=None) and the outbox chain + Lambda stub must pass."""
os.environ["NOVA_LOCAL_TIER"] = "1"
try:
result = le.run_local_e2e("contracts/static-assets.yml")
finally:
os.environ.pop("NOVA_LOCAL_TIER", None); os.environ.pop("ACDL_LOCAL_TIER", None)
assert result["tier"] == "local-emulator"
assert result["ecs"] is None # no ECS service in this stack
assert result["outbox_chain_verified"] is True
assert result["lambda_status"] == 200
def test_is_local_tier_flag():
assert le.is_local_tier() is False
os.environ["NOVA_LOCAL_TIER"] = "1"
try:
assert le.is_local_tier() is True
finally:
os.environ.pop("NOVA_LOCAL_TIER", None); os.environ.pop("ACDL_LOCAL_TIER", None)
assert le.is_local_tier() is False