2026-10-03 22:59:46 +02:00
|
|
|
import json
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
from pathlib import Path
|
|
|
|
|
|
2026-10-03 22:59:46 +02:00
|
|
|
from approval_engine.audit import audit_envelope
|
|
|
|
|
from approval_engine.store import EVENT_CLASSES, HEARTBEAT_CLASSES
|
|
|
|
|
|
Implement the engine spine: claim, outbox, machine, API
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9
2026-08-29 12:52:49 +02:00
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_layer_yaml_cadence_declared():
|
|
|
|
|
text = (ROOT / "layer.yaml").read_text()
|
|
|
|
|
assert "cadence_status: declared" in text
|
|
|
|
|
assert "cadence_form: heartbeat-or-reconciliation" in text
|
|
|
|
|
assert "cadence: cadence.yaml" in text
|
|
|
|
|
|
|
|
|
|
|
2026-10-03 22:59:46 +02:00
|
|
|
def test_cadence_inventory_matches_durable_emitter(engine):
|
|
|
|
|
declaration = json.loads((ROOT / "cadence.yaml").read_text())
|
|
|
|
|
inventory = declaration["extensions"]["approval-engine"]["inventory"]
|
|
|
|
|
assert set(inventory["load_bearing"]) == set(EVENT_CLASSES)
|
|
|
|
|
assert set(inventory["rare_load_bearing"]) == set(HEARTBEAT_CLASSES)
|
|
|
|
|
entries = {entry["event_class"]: entry for entry in declaration["sources"]}
|
|
|
|
|
assert set(entries) == set(EVENT_CLASSES)
|
|
|
|
|
engine.emit_heartbeat()
|
|
|
|
|
observed = {audit_envelope(row["payload"])["data"]["class"]:
|
|
|
|
|
audit_envelope(row["payload"]) for row in engine.undrained()}
|
|
|
|
|
assert set(observed) == set(HEARTBEAT_CLASSES)
|
|
|
|
|
for cls in HEARTBEAT_CLASSES:
|
|
|
|
|
beat = entries[cls]["heartbeat"]
|
|
|
|
|
assert beat["event_class"] == observed[cls]["type"] == "audit-core.heartbeat"
|
|
|
|
|
assert beat["assertion"] == observed[cls]["data"]["assertion"]
|
|
|
|
|
assert beat["extensions"]["approval-engine"]["asserted_class"] == cls
|
|
|
|
|
assert beat["interval_seconds"] == 86400
|
|
|
|
|
assert beat["extensions"]["approval-engine"]["requested_observer_max_gap_seconds"] == 172800
|
|
|
|
|
assert entries[cls]["extensions"]["netkingdom"]["rate_monitoring"] == "forbidden"
|
|
|
|
|
assert entries[cls]["reconciliation"]["undrained_local"] == "lag-not-divergence"
|
|
|
|
|
assert "heartbeat" not in entries["heartbeat"] # transport control has no recursive heartbeat
|