approval-engine/tests/test_reconciliation.py
tegwick 24a5ecd56b Add bounded source reconciliation counts and clarify receiver dependencies
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1046e-fdbf-7b93-a267-5f72b9b24526
2026-10-04 11:34:42 +02:00

86 lines
4 KiB
Python

"""Bounded source counts do not equate occurrence, acknowledgment and acceptance."""
from dataclasses import replace
from datetime import timedelta, timezone
from urllib.parse import urlencode
import pytest
from approval_engine.api import App, call
from approval_engine.auth import StaticTokenAuthenticator
from approval_engine.errors import StoreUnavailable
from approval_engine.store import EVENT_CLASSES
from tests.conftest import FROZEN, approve
def query(since=FROZEN, until=FROZEN + timedelta(seconds=1)):
return urlencode({"since": since.isoformat(), "until": until.isoformat()})
def test_occurrence_window_separates_pending_and_delayed_acknowledgments(engine, app):
engine.clock = lambda: FROZEN - timedelta(seconds=1)
approve(engine)
engine.clock = lambda: FROZEN
current = approve(engine)
engine.clock = lambda: FROZEN + timedelta(seconds=1)
engine.consume(current.id, current.binding_digest)
status, before = call(app, "GET", "/v1/cadence", query_string=query())
assert status == 200
assert before["counts"]["issuance"] == {
"committed": 1, "drained": 0, "pending": 1, "acknowledged_in_window": 0,
}
assert before["counts"]["use"]["committed"] == 0 # until is exclusive
engine.clock = lambda: FROZEN + timedelta(seconds=2)
engine.drain(lambda _: None)
_, after = call(app, "GET", "/v1/cadence", query_string=query())
assert after["counts"]["issuance"] == {
"committed": 1, "drained": 1, "pending": 0, "acknowledged_in_window": 0,
}
_, ack = call(app, "GET", "/v1/cadence", query_string=query(
FROZEN + timedelta(seconds=2), FROZEN + timedelta(seconds=3)))
assert ack["counts"]["issuance"]["acknowledged_in_window"] == 2
assert ack["counts"]["use"]["acknowledged_in_window"] == 1
assert all(c["committed"] == 0 for c in ack["counts"].values())
assert set(after["counts"]) == set(EVENT_CLASSES)
assert after["source"] == "approval-engine" and after["tenant"] == engine.tenant
assert after["window_basis"] == "created_at"
assert "acceptance-time" in after["means"]
assert "approvers" not in str(after) and current.id not in str(after)
def test_timezone_and_subsecond_bounds_are_preserved(app, engine):
approve(engine)
offset = FROZEN.astimezone(timezone(timedelta(hours=2)))
_, result = call(app, "GET", "/v1/cadence", query_string=query(offset, offset + timedelta(seconds=1)))
assert result["counts"]["issuance"]["committed"] == 1
_, result = call(app, "GET", "/v1/cadence", query_string=query(
FROZEN + timedelta(microseconds=1), FROZEN + timedelta(seconds=1)))
assert result["counts"]["issuance"]["committed"] == 0
@pytest.mark.parametrize("qs", [
"since=bad&until=bad", "since=2026-10-04&until=2026-10-05",
query(FROZEN, FROZEN), query(FROZEN + timedelta(seconds=1), FROZEN),
"since=x", "until=x", "since=&until=x", "since=x&since=y&until=z",
query() + "&tenant=other", "window=all",
])
def test_bad_or_unbounded_queries_are_refused(app, qs):
assert call(app, "GET", "/v1/cadence", query_string=qs)[0] == 422
def test_window_counts_preserve_lifetime_endpoint_and_authentication(app, engine):
approve(engine)
_, counts = call(app, "GET", "/v1/cadence")
assert counts["issuance"] == 1 and counts["form"] == "heartbeat-or-reconciliation"
assert call(app, "GET", "/v1/cadence", authorization=None, query_string=query())[0] == 401
identity = app.authenticator.identities["test-token"]
for altered in (replace(identity, scopes=frozenset({"approval:read"})),
replace(identity, tenant="tenant:other")):
other = App(engine, StaticTokenAuthenticator({"test-token": altered}))
assert call(other, "GET", "/v1/cadence", query_string=query())[0] == 403
def test_window_counts_fail_closed_on_store_failure(app, engine, monkeypatch):
def unavailable():
raise StoreUnavailable("unavailable")
monkeypatch.setattr(engine, "_conn", unavailable)
assert call(app, "GET", "/v1/cadence", query_string=query())[0] == 503