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
This commit is contained in:
parent
624e43f554
commit
9c9528f5b2
29 changed files with 2121 additions and 26 deletions
72
tests/test_outbox.py
Normal file
72
tests/test_outbox.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
from approval_engine.errors import StoreUnavailable
|
||||
from approval_engine.store import Engine
|
||||
from tests.conftest import approve, binding, validity
|
||||
|
||||
|
||||
def test_issuance_queued_in_same_commit(engine):
|
||||
obj = approve(engine)
|
||||
pending = engine.undrained()
|
||||
classes = [p["class"] for p in pending]
|
||||
assert classes == ["issuance"]
|
||||
assert pending[0]["approval_id"] == obj.id
|
||||
payload = pending[0]["payload"]
|
||||
assert payload["source"] == "approval-engine"
|
||||
assert payload["action"] == "approval.issuance"
|
||||
assert payload["event_id"] == pending[0]["event_id"]
|
||||
|
||||
|
||||
def test_failed_outbox_rolls_back_mutation():
|
||||
eng = Engine(":memory:", fail_outbox=True)
|
||||
obj = eng.create(binding(), validity(), required_count=1)
|
||||
try:
|
||||
eng.add_entry(obj.id, "user:alice")
|
||||
raise AssertionError("must fail")
|
||||
except StoreUnavailable:
|
||||
pass
|
||||
obj = eng.get(obj.id)
|
||||
assert obj.status == "requested"
|
||||
assert obj.entries == []
|
||||
assert eng.undrained() == []
|
||||
eng.close()
|
||||
|
||||
|
||||
def test_revoke_succeeds_when_drain_sink_is_down(engine):
|
||||
obj = approve(engine)
|
||||
engine.revoke(obj.id)
|
||||
assert engine.get(obj.id).status == "revoked"
|
||||
|
||||
def down(_payload):
|
||||
raise ConnectionError("audit-core unreachable")
|
||||
|
||||
result = engine.drain(down)
|
||||
assert result["failed"] >= 1
|
||||
assert engine.get(obj.id).status == "revoked"
|
||||
assert any(p["class"] == "revocation" for p in engine.undrained())
|
||||
|
||||
|
||||
def test_drain_marks_delivered(engine):
|
||||
approve(engine)
|
||||
sink: list[dict] = []
|
||||
result = engine.drain(sink.append)
|
||||
assert result["delivered"] == 1
|
||||
assert result["failed"] == 0
|
||||
assert engine.undrained() == []
|
||||
assert sink[0]["action"] == "approval.issuance"
|
||||
|
||||
|
||||
def test_heartbeat_is_positive_claim(engine):
|
||||
approve(engine)
|
||||
beat = engine.emit_heartbeat()
|
||||
assert beat["assertion"] == "nothing-to-report"
|
||||
assert beat["counts"]["issuance"] == 1
|
||||
assert beat["counts"]["heartbeat"] == 1
|
||||
pending = [p for p in engine.undrained() if p["class"] == "heartbeat"]
|
||||
assert len(pending) == 1
|
||||
assert pending[0]["payload"]["details"]["assertion"] == "nothing-to-report"
|
||||
|
||||
|
||||
def test_revocation_event_class(engine):
|
||||
obj = approve(engine)
|
||||
engine.revoke(obj.id)
|
||||
classes = [p["class"] for p in engine.undrained()]
|
||||
assert "revocation" in classes
|
||||
Loading…
Add table
Add a link
Reference in a new issue