Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
112 lines
5.6 KiB
Python
112 lines
5.6 KiB
Python
from datetime import datetime, timezone
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from informed_decision.audit import AuditCoreSink, AuditDeliveryError, OutboxWorker
|
|
from informed_decision.http_transport import TransportError
|
|
from informed_decision.store import Store
|
|
from test_durable_store import storage, present
|
|
|
|
|
|
class Replies:
|
|
def __init__(self, *replies):
|
|
self.replies = list(replies)
|
|
self.calls = []
|
|
|
|
def request(self, method, url, **kwargs):
|
|
self.calls.append((method, url, kwargs))
|
|
reply = self.replies.pop(0)
|
|
if isinstance(reply, Exception): raise reply
|
|
return reply
|
|
|
|
|
|
@pytest.mark.parametrize("status,name", [(202, "accepted"), (200, "duplicate")])
|
|
def test_only_confirmed_receiver_receipt_marks_delivered(storage, status, name):
|
|
store, memo = storage
|
|
present(store, memo)
|
|
event = store.evidence()[0]
|
|
transport = Replies((status, {"status": name, "reference": "audit:" + event["id"]}))
|
|
sink = AuditCoreSink("https://audit.test", lambda: "synthetic-token", transport=transport)
|
|
assert OutboxWorker(store, sink).run_once()["delivered"] == 1
|
|
row = Store(store.path).outbox()[0]
|
|
assert row["state"] == "delivered" and row["receiver_reference"] == "audit:" + event["id"]
|
|
method, url, params = transport.calls[0]
|
|
assert (method, url) == ("POST", "https://audit.test/v1/events")
|
|
assert params["headers"]["Idempotency-Key"] == event["id"]
|
|
assert params["body"] == event["envelope"].encode()
|
|
assert "synthetic-token" not in event["envelope"]
|
|
|
|
|
|
@pytest.mark.parametrize("reply,code,blocked", [
|
|
((400, {"error": "secret-sentinel"}), "rejected", True),
|
|
((401, {}), "unauthorized", True), ((403, {}), "unauthorized", True),
|
|
((409, {}), "conflict", True), ((503, {}), "unavailable", False),
|
|
((500, {}), "unavailable", False), (TransportError("secret-sentinel"), "unavailable", False),
|
|
((202, {"status": "accepted", "reference": "audit:other-event"}), "invalid_receipt", False),
|
|
((200, {"status": "ok", "reference": "audit:fixture"}), "invalid_receipt", False),
|
|
])
|
|
def test_refusals_preserve_pending_evidence_with_bounded_error(storage, reply, code, blocked):
|
|
store, memo = storage
|
|
present(store, memo)
|
|
before = store.evidence()
|
|
transport = Replies(reply)
|
|
worker = OutboxWorker(store, AuditCoreSink("https://audit.test", lambda: "synthetic-token", transport=transport))
|
|
result = worker.run_once()
|
|
assert result["blocked" if blocked else "retrying"] == 1
|
|
row = Store(store.path).outbox()[0]
|
|
assert row["state"] == ("blocked" if blocked else "pending")
|
|
assert row["last_error"] == code and "secret-sentinel" not in str(row)
|
|
assert store.evidence() == before
|
|
assert worker.run_once() == {"delivered": 0, "retrying": 0, "blocked": 0}
|
|
|
|
|
|
def test_repaired_credential_requeues_original_bytes(storage):
|
|
store, memo = storage
|
|
present(store, memo)
|
|
event = store.evidence()[0]
|
|
transport = Replies((401, {}), (202, {"status": "accepted", "reference": "audit:" + event["id"]}))
|
|
tokens = iter(["old-synthetic", "new-synthetic"])
|
|
worker = OutboxWorker(store, AuditCoreSink("https://audit.test", lambda: next(tokens), transport=transport))
|
|
assert worker.run_once()["blocked"] == 1
|
|
store.requeue_blocked(event["id"])
|
|
assert worker.run_once()["delivered"] == 1
|
|
assert transport.calls[0][2]["body"] == transport.calls[1][2]["body"]
|
|
assert transport.calls[1][2]["headers"]["Authorization"] == "Bearer new-synthetic"
|
|
|
|
|
|
def test_unavailable_policy_record_is_never_a_human_decline(storage):
|
|
store, memo = storage
|
|
store.record_unreachable(memo.id, "access-engine")
|
|
envelope = json.loads(store.evidence()[0]["envelope"])
|
|
assert envelope["type"] == "informed-decision.stance_application"
|
|
assert envelope["data"]["stance_applied"] == "fail_closed"
|
|
assert envelope["data"]["decision_attributable"] is False
|
|
assert "verb" not in envelope["data"]
|
|
|
|
|
|
def test_reconciliation_reports_gap_without_claiming_completeness(storage):
|
|
store, memo = storage
|
|
present(store, memo)
|
|
since, until = "2020-01-01T00:00:00Z", "2100-01-01T00:00:00Z"
|
|
remote = {"source": "informed-decision", "tenant": "tenant:platform", "since": since, "until": until, "counts": []}
|
|
transport = Replies((200, remote))
|
|
worker = OutboxWorker(store, AuditCoreSink("https://audit.test", lambda: "synthetic-token", transport=transport))
|
|
report = worker.reconcile(since, until)
|
|
assert report["count_values_match"] is False
|
|
assert report["counts"]["informed-decision.presentation"] == {"source": 1, "receiver": 0}
|
|
assert report["completeness_proven"] is report["reconstructability_proven"] is False
|
|
assert report["source_time_basis"] == "occurred_at" and report["receiver_time_basis"] == "accepted_at"
|
|
assert report["automatic_loss_finding"] is False
|
|
with pytest.raises(ValueError): worker.reconcile(None, until)
|
|
|
|
|
|
@pytest.mark.parametrize("change", [{"source": "other-source"}, {"tenant": "other-tenant"},
|
|
{"since": "2019-01-01T00:00:00Z"}, {"counts": [{"class": "presentation", "count": True}]},
|
|
{"counts": [{"class": "presentation", "count": -1}]},
|
|
{"counts": [{"class": "presentation", "count": 1}, {"class": "presentation", "count": 1}]}])
|
|
def test_reconciliation_rejects_wrong_scope_window_or_shape(change):
|
|
body = {"source": "informed-decision", "tenant": "tenant:platform", "since": "2020-01-01T00:00:00Z",
|
|
"until": "2100-01-01T00:00:00Z", "counts": [], **change}
|
|
sink = AuditCoreSink("https://audit.test", lambda: "synthetic-token", transport=Replies((200, body)))
|
|
with pytest.raises(AuditDeliveryError): sink.counts("2020-01-01T00:00:00Z", "2100-01-01T00:00:00Z")
|