Files
Christian Nennemann 37859beef6 feat: interop test package + session handoff doc
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).
2026-04-12 07:39:41 +02:00

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")