f68f85c9fd
---ci---
project: acdl
phase: 20
milestone: v1.5
status: review
verdict: READY TO SHIP
p0: 1 (fixed — contract path resolution in deploy workflow)
p1: 6 (flagged post-hoc)
---/ci---
Multi-persona review of v1.5 phase 20 (docs + reusable deploy workflow).
P0 (blocking) — AUTO-FIXED:
- C1: scripts/run_platform.sh contract path resolution broken in deploy
workflow. The reusable workflow invokes run_platform.sh from the consumer
workspace root with a relative contract path (.acdl/contract.yaml), but
run_platform.sh does `cd "$ROOT"` (platform repo) early, so the relative
path resolved against the platform repo and the pipeline could never run.
Fix (commit 75c2274): capture CALLER_CWD before cd "$ROOT"; resolve
caller-supplied relative paths against CALLER_CWD; default no-arg contract
stays relative to ROOT (preserves platform-local CI). Reproduced pre-fix;
verified post-fix.
P1 (important) — FLAGGED FOR POST-HOC REVIEW (do not block ship):
- C2: ref: v1.4 in the deploy workflow platform checkout — no v1.4 tag exists
(only v1.4.0 / v1.4.1). Operator must create a floating v1.4 tag or change
the ref to v1.4.1.
- C3: modules/l2/{static-asset,microservice}/README.md still use @v1 in their
Usage examples; missed by the v1.4 bump.
- S1: static-key override is not wired. ACDL_AWS_* env vars on the OIDC step
are not read by aws-actions/configure-aws-credentials@v4 (it reads AWS_*
or its own access-key/secret-key inputs). The README/CONSUMER_GUIDE claim
a working override that doesn't function as written. Needs a conditional
step or renamed env vars + input wiring.
- S2: README overstates ABAC repo:org/repo:ref:... scoping. The workflow
constructs a numeric role name (github.repository_id); the actual claim
enforcement lives in the IAM trust policy, not in this workflow.
- T1: no deploy-workflow triggers conformance test (CI workflow has one;
deploy doesn't). Minor — reusable workflows use workflow_call, not push
triggers, but the contract's triggers field is then unenforced.
- A1: terraform/spike/terraform.tf uploaded as artifact leaks the AWS account
ID via the state-backend bucket name. Recommend excluding terraform.tf or
gating artifact upload to non-public repos.
P2 (nits) — listed for awareness: floating-tag terminology imprecision (M1),
header comment "Gitea Actions" in the GitHub copy (M2, intentional byte-
identical), pip install split (P1-perf), comment drift in pipelines/deploy.yaml
header (C4), module README internal inconsistency (C5).
Verdict: READY TO SHIP. The one P0 is fixed. The 6 P1s are post-hoc items —
the deploy workflow is a scaffold whose first real consumer run requires
operator setup (tag, IAM role, secrets) that gates go-live. The P1s should
be addressed before any consumer invokes uses: acdl/.gitea/workflows/
deploy.yml@v1.4 in earnest.
Tests: 154 pass (19 new). run_ci.sh green.
135 lines
4.5 KiB
Python
135 lines
4.5 KiB
Python
import datetime
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from acdl_platform.outbox_writer import _canonical_hash, write_event
|
|
|
|
|
|
class TestCanonicalHash:
|
|
def test_deterministic(self):
|
|
event = {"b": 2, "a": 1}
|
|
h1 = _canonical_hash(event)
|
|
h2 = _canonical_hash(event)
|
|
assert h1 == h2
|
|
|
|
def test_order_independent(self):
|
|
h1 = _canonical_hash({"a": 1, "b": 2})
|
|
h2 = _canonical_hash({"b": 2, "a": 1})
|
|
assert h1 == h2
|
|
|
|
def test_is_sha256_hex(self):
|
|
h = _canonical_hash({"key": "val"})
|
|
assert len(h) == 64
|
|
assert all(c in "0123456789abcdef" for c in h)
|
|
|
|
def test_different_events_different_hash(self):
|
|
h1 = _canonical_hash({"a": 1})
|
|
h2 = _canonical_hash({"a": 2})
|
|
assert h1 != h2
|
|
|
|
|
|
class TestWriteEvent:
|
|
def _sample_event(self):
|
|
return {
|
|
"contractId": "test-contract-001",
|
|
"eventType": "CONFIDENCE_COMPUTED",
|
|
"ts": "2026-07-22T00:00:00Z",
|
|
"environment": "dev",
|
|
"stack": "s3",
|
|
"score": 0.85,
|
|
"band": "pass",
|
|
"prev_event_hash": "GENESIS",
|
|
}
|
|
|
|
def test_write_event_with_mock_dynamodb(self):
|
|
from moto import mock_aws
|
|
|
|
import boto3
|
|
|
|
with mock_aws():
|
|
dyn = boto3.client("dynamodb", region_name="us-east-1")
|
|
dyn.create_table(
|
|
TableName="acdl-outbox",
|
|
KeySchema=[
|
|
{"AttributeName": "contractId", "KeyType": "HASH"},
|
|
{"AttributeName": "eventType#eventTs", "KeyType": "RANGE"},
|
|
],
|
|
AttributeDefinitions=[
|
|
{"AttributeName": "contractId", "AttributeType": "S"},
|
|
{"AttributeName": "eventType#eventTs", "AttributeType": "S"},
|
|
],
|
|
BillingMode="PAY_PER_REQUEST",
|
|
)
|
|
|
|
event = self._sample_event()
|
|
item = write_event(event, outbox_table="acdl-outbox", region="us-east-1")
|
|
|
|
assert item["contractId"]["S"] == "test-contract-001"
|
|
assert item["prev_event_hash"]["S"] == "GENESIS"
|
|
assert "hash" in item
|
|
assert len(item["hash"]["S"]) == 64
|
|
assert "expire_at" in item
|
|
|
|
def test_write_event_hash_matches_canonical(self):
|
|
from moto import mock_aws
|
|
|
|
import boto3
|
|
|
|
with mock_aws():
|
|
dyn = boto3.client("dynamodb", region_name="us-east-1")
|
|
dyn.create_table(
|
|
TableName="acdl-outbox",
|
|
KeySchema=[
|
|
{"AttributeName": "contractId", "KeyType": "HASH"},
|
|
{"AttributeName": "eventType#eventTs", "KeyType": "RANGE"},
|
|
],
|
|
AttributeDefinitions=[
|
|
{"AttributeName": "contractId", "AttributeType": "S"},
|
|
{"AttributeName": "eventType#eventTs", "AttributeType": "S"},
|
|
],
|
|
BillingMode="PAY_PER_REQUEST",
|
|
)
|
|
|
|
event = self._sample_event()
|
|
item = write_event(event, outbox_table="acdl-outbox", region="us-east-1")
|
|
expected_hash = _canonical_hash(event)
|
|
assert item["hash"]["S"] == expected_hash
|
|
|
|
def test_write_event_persists_to_dynamodb(self):
|
|
from moto import mock_aws
|
|
|
|
import boto3
|
|
|
|
with mock_aws():
|
|
dyn = boto3.client("dynamodb", region_name="us-east-1")
|
|
dyn.create_table(
|
|
TableName="acdl-outbox",
|
|
KeySchema=[
|
|
{"AttributeName": "contractId", "KeyType": "HASH"},
|
|
{"AttributeName": "eventType#eventTs", "KeyType": "RANGE"},
|
|
],
|
|
AttributeDefinitions=[
|
|
{"AttributeName": "contractId", "AttributeType": "S"},
|
|
{"AttributeName": "eventType#eventTs", "AttributeType": "S"},
|
|
],
|
|
BillingMode="PAY_PER_REQUEST",
|
|
)
|
|
|
|
event = self._sample_event()
|
|
write_event(event, outbox_table="acdl-outbox", region="us-east-1")
|
|
|
|
resp = dyn.get_item(
|
|
TableName="acdl-outbox",
|
|
Key={
|
|
"contractId": {"S": "test-contract-001"},
|
|
"eventType#eventTs": {"S": "CONFIDENCE_COMPUTED#2026-07-22T00:00:00Z"},
|
|
},
|
|
)
|
|
assert "Item" in resp
|
|
assert resp["Item"]["band"]["S"] == "pass" |