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
80
tests/test_machine.py
Normal file
80
tests/test_machine.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
from datetime import datetime, timezone
|
||||
|
||||
from approval_engine.errors import Conflict, DuplicateApprover
|
||||
from approval_engine.store import Engine
|
||||
from tests.conftest import approve, binding, validity
|
||||
|
||||
|
||||
def test_requested_until_threshold(engine):
|
||||
obj = engine.create(binding(), validity(), required_count=2)
|
||||
assert obj.status == "requested"
|
||||
obj = engine.add_entry(obj.id, "user:alice")
|
||||
assert obj.status == "requested"
|
||||
claim = engine.claim(obj.id)
|
||||
assert claim["valid_now"] is False
|
||||
assert claim["reason_code"] == "requested"
|
||||
|
||||
|
||||
def test_threshold_approves_and_valid_now(engine):
|
||||
obj = approve(engine, required=2)
|
||||
assert obj.status == "approved"
|
||||
assert len(obj.entries) == 2
|
||||
claim = engine.claim(obj.id)
|
||||
assert claim["state"] == "valid"
|
||||
assert claim["valid_now"] is True
|
||||
|
||||
|
||||
def test_duplicate_approver_fails_closed(engine):
|
||||
obj = engine.create(binding(), validity(), required_count=2)
|
||||
engine.add_entry(obj.id, "user:alice")
|
||||
try:
|
||||
engine.add_entry(obj.id, "user:alice")
|
||||
raise AssertionError("duplicate must fail")
|
||||
except DuplicateApprover:
|
||||
pass
|
||||
obj = engine.get(obj.id)
|
||||
assert len(obj.entries) == 1
|
||||
assert obj.status == "requested"
|
||||
|
||||
|
||||
def test_revoke_without_holder_and_next_claim(engine):
|
||||
obj = approve(engine)
|
||||
engine.revoke(obj.id)
|
||||
claim = engine.claim(obj.id)
|
||||
assert claim["valid_now"] is False
|
||||
assert claim["state"] == "revoked"
|
||||
assert claim["reason_code"] == "revoked"
|
||||
|
||||
|
||||
def test_expiry_on_observation():
|
||||
jumping = {"t": datetime(2026, 8, 29, 12, 0, tzinfo=timezone.utc)}
|
||||
eng = Engine(":memory:", clock=lambda: jumping["t"])
|
||||
obj = eng.create(binding(), validity(), required_count=1)
|
||||
obj = eng.add_entry(obj.id, "user:alice")
|
||||
assert eng.claim(obj.id)["valid_now"] is True
|
||||
jumping["t"] = datetime(2026, 8, 29, 16, 0, tzinfo=timezone.utc)
|
||||
claim = eng.claim(obj.id)
|
||||
assert claim["state"] == "expired"
|
||||
assert claim["valid_now"] is False
|
||||
eng.close()
|
||||
|
||||
|
||||
def test_not_yet_valid():
|
||||
future = datetime(2026, 8, 29, 10, 0, tzinfo=timezone.utc)
|
||||
eng = Engine(":memory:", clock=lambda: future)
|
||||
obj = eng.create(binding(), validity(), required_count=1)
|
||||
obj = eng.add_entry(obj.id, "user:alice")
|
||||
claim = eng.claim(obj.id)
|
||||
assert claim["valid_now"] is False
|
||||
assert claim["reason_code"] == "not_yet_valid"
|
||||
eng.close()
|
||||
|
||||
|
||||
def test_cannot_revoke_twice(engine):
|
||||
obj = approve(engine)
|
||||
engine.revoke(obj.id)
|
||||
try:
|
||||
engine.revoke(obj.id)
|
||||
raise AssertionError("second revoke must conflict")
|
||||
except Conflict:
|
||||
pass
|
||||
Loading…
Add table
Add a link
Reference in a new issue