Files
acdl/tests/test_local_emulating_adapters.py
T
Jon Chery 031887ec56 refactor(P57): contract surface redesign + rename + .yml repo-wide
Contract surface redesign:
- New top-level fields: id (3-6 char acronym → stack.name), name (full → stack.title),
  infrastructure (map keyed by module name, replaces module:)
- Drop uses: field (dead reference; version pin lives in CI workflow uses: line)
- Drop top-level module/inputs (now nested under infrastructure map)
- Per-module optional version (defaults to latest published from registry)
- Multi-module contracts: one file deploys N modules in one pipeline run,
  resource IDs namespaced with module name to avoid collisions
- stack.schema.json: add optional title field for display name

Rename:
- pipelines/deploy.yaml → pipelines/contract.yml (declarative spec, not a pipeline)
- pipelines/ci.yaml → pipelines/ci.yml
- All 44 .yaml files → .yml repo-wide (contracts, module examples, kyverno policies)
- .acdl/contract.yaml → .acdl/contract.yml

Resolver (core/contract_resolver.py):
- Rewrite resolve() to loop infrastructure map, default version to latest,
  merge module fragments into one stack with namespaced resource IDs
- _latest_version() picks highest non-deprecated from registry
- _namespace_resources() prefixes IDs + rewrites ref: expressions for multi-module
- Single-module path: unprefixed IDs (backward compatible)

Verification:
- 494 tests pass (0 contract-shape failures)
- Local E2E passes (contract → resolver → adapter → local ECS HTTP 200 → outbox)

---ci---
project: acdl
phase: 57
milestone: v1.10.2
status: execute
---/ci---
2026-07-27 21:37:40 +00:00

217 lines
8.0 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 = "acdl-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["ACDL_LOCAL_TIER"] = "1"
try:
result = le.run_local_e2e("contracts/microservice.yml")
finally:
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["ACDL_LOCAL_TIER"] = "1"
try:
result = le.run_local_e2e("contracts/static-assets.yml")
finally:
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["ACDL_LOCAL_TIER"] = "1"
try:
assert le.is_local_tier() is True
finally:
os.environ.pop("ACDL_LOCAL_TIER", None)
assert le.is_local_tier() is False