Cross-spec interop validation between ietf-act and ietf-ect: - new packages/interop/ sibling package (ietf-act-ect-interop) - 32 tests pass: shared claims, algorithm matrix, DAG structure, divergence handling, anti-goals - documents ES256 raw signature wire-compatibility - documents airtight typ separation (act+jwt vs exec+jwt) Hazards surfaced: - ACTLedger.append() silently accepts ECT Payload via duck-typing (both have .jti) — documented in interop README as a production hazard requiring external isinstance checks Session handoff: - SESSION-2026-04-12.md — snapshot of decisions, artifacts, open actions, and next-session starting points Also: session-end commit of hash-format fix propagation to packages/ect/ (the fix was applied to the old refimpl location but did not propagate through the parallel package-move agent).
95 lines
3.5 KiB
Python
95 lines
3.5 KiB
Python
"""Cross-spec DAG structural compatibility.
|
|
|
|
The refimpls keep separate stores (ACTStore for ACT records, ECTStore
|
|
for ECT payloads). These tests verify that the DAG topology expressed
|
|
via `pred` arrays has the same interpretation in both — a 3-node
|
|
graph looks the same whether it is made of ACT records or ECT payloads.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
|
|
from ect.dag import ECTStore
|
|
|
|
|
|
class TestPredArraySemantics:
|
|
def test_identical_pred_values_in_both_refimpls(
|
|
self, act_record_builder, ect_payload_builder
|
|
):
|
|
"""Same pred array produces same on-wire interpretation."""
|
|
parent1 = str(uuid.uuid4())
|
|
parent2 = str(uuid.uuid4())
|
|
|
|
_, act_rec = act_record_builder(pred=[parent1, parent2])
|
|
_, ect_pl = ect_payload_builder(pred=[parent1, parent2])
|
|
|
|
assert act_rec.pred == ect_pl.pred == [parent1, parent2]
|
|
|
|
def test_empty_pred_is_root_node_on_both(
|
|
self, act_record_builder, ect_payload_builder
|
|
):
|
|
_, act_rec = act_record_builder(pred=[])
|
|
_, ect_pl = ect_payload_builder(pred=[])
|
|
assert act_rec.pred == [] == ect_pl.pred
|
|
|
|
|
|
class TestThreeNodeDAG:
|
|
"""A diamond: root -> {child1, child2} -> join.
|
|
|
|
Built twice: once in ACT's ledger and once in ECT's store. The
|
|
shape must be recognised identically by each refimpl's DAG
|
|
machinery.
|
|
"""
|
|
|
|
def test_diamond_topology_consistent(self, act_record_builder, ect_payload_builder):
|
|
# Shared jti identifiers so the topologies are isomorphic.
|
|
root_jti = str(uuid.uuid4())
|
|
c1_jti = str(uuid.uuid4())
|
|
c2_jti = str(uuid.uuid4())
|
|
join_jti = str(uuid.uuid4())
|
|
|
|
# --- Build ACT records ---
|
|
_, root_act = act_record_builder(jti=root_jti, pred=[])
|
|
_, c1_act = act_record_builder(jti=c1_jti, pred=[root_jti])
|
|
_, c2_act = act_record_builder(jti=c2_jti, pred=[root_jti])
|
|
_, join_act = act_record_builder(jti=join_jti, pred=[c1_jti, c2_jti])
|
|
|
|
# --- Build ECT payloads ---
|
|
_, root_ect = ect_payload_builder(jti=root_jti, pred=[])
|
|
_, c1_ect = ect_payload_builder(jti=c1_jti, pred=[root_jti])
|
|
_, c2_ect = ect_payload_builder(jti=c2_jti, pred=[root_jti])
|
|
_, join_ect = ect_payload_builder(jti=join_jti, pred=[c1_jti, c2_jti])
|
|
|
|
# Topological equivalence check: same predecessor sets by jti.
|
|
act_pred_map = {
|
|
r.jti: set(r.pred) for r in (root_act, c1_act, c2_act, join_act)
|
|
}
|
|
ect_pred_map = {
|
|
p.jti: set(p.pred) for p in (root_ect, c1_ect, c2_ect, join_ect)
|
|
}
|
|
assert act_pred_map == ect_pred_map
|
|
|
|
|
|
class TestStoresAreSeparate:
|
|
"""Document that refimpls do not cross-resolve predecessor jtis."""
|
|
|
|
def test_ect_store_is_abstract_and_payload_typed(self):
|
|
"""ECTStore is an ABC whose API only accepts ECT Payload objects.
|
|
|
|
No method on ECTStore accepts ACTRecord — cross-type
|
|
predecessor resolution would have to be implemented outside
|
|
both refimpls. Documented here so no one copy-pastes a
|
|
bridging store into either codebase.
|
|
"""
|
|
# ECTStore is abstract, so we can only inspect its interface.
|
|
from ect.dag import ECTStore as _ECTStore
|
|
|
|
abstract_methods = getattr(_ECTStore, "__abstractmethods__", frozenset())
|
|
assert "get_by_tid" in abstract_methods
|
|
assert "contains" in abstract_methods
|
|
# No cross-type API surface.
|
|
assert not hasattr(_ECTStore, "add_act_record")
|