81 lines
3.5 KiB
Python
81 lines
3.5 KiB
Python
|
|
from dataclasses import replace
|
||
|
|
import hashlib
|
||
|
|
import json
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from informed_decision.audit import OutboxWorker
|
||
|
|
from informed_decision.evidence import EventClass
|
||
|
|
from informed_decision.lifecycle import LifecycleRepository
|
||
|
|
from informed_decision.runtime import AuditPump
|
||
|
|
from informed_decision.store import Conflict, Store
|
||
|
|
from test_durable_component import receiver
|
||
|
|
from test_lifecycle import AGENT, workspace
|
||
|
|
|
||
|
|
|
||
|
|
def register_platform(workspace):
|
||
|
|
repo, memo = workspace
|
||
|
|
actor = replace(AGENT, tenant="tenant:platform")
|
||
|
|
event_id = repo.register(actor, memo, key="register")
|
||
|
|
return repo.store, memo, event_id
|
||
|
|
|
||
|
|
|
||
|
|
def test_lifecycle_commitment_is_content_free_stable_and_verifiable(workspace):
|
||
|
|
store, memo, event_id = register_platform(workspace)
|
||
|
|
assert store.stage_lifecycle_audit() == 1
|
||
|
|
assert store.stage_lifecycle_audit() == 0
|
||
|
|
evidence = store.evidence()[0]
|
||
|
|
envelope = json.loads(evidence["envelope"])
|
||
|
|
assert envelope["id"] == event_id and envelope["type"] == EventClass.LIFECYCLE.value
|
||
|
|
assert memo.brief not in evidence["envelope"] and memo.question not in evidence["envelope"]
|
||
|
|
assert envelope["data"]["content_sha256"] == "sha256:" + hashlib.sha256(evidence["content"].encode()).hexdigest()
|
||
|
|
assert store.settle_lifecycle_audit() == 0
|
||
|
|
assert store.lifecycle_counts()["pending"] == 1
|
||
|
|
assert Store(store.path).evidence() == store.evidence()
|
||
|
|
|
||
|
|
|
||
|
|
def test_lifecycle_delivery_uses_actual_receiver_and_recovers_lost_reply(workspace, receiver):
|
||
|
|
store, _, event_id = register_platform(workspace)
|
||
|
|
sink, transport, _ = receiver
|
||
|
|
store.stage_lifecycle_audit()
|
||
|
|
transport.lose_reply = True
|
||
|
|
worker = OutboxWorker(store, sink)
|
||
|
|
assert worker.run_once()["retrying"] == 1
|
||
|
|
assert store.settle_lifecycle_audit() == 0
|
||
|
|
with store._connection() as db:
|
||
|
|
db.execute("UPDATE outbox SET next_attempt=0 WHERE id=?", (event_id,))
|
||
|
|
reopened = Store(store.path)
|
||
|
|
assert OutboxWorker(reopened, sink).run_once()["delivered"] == 1
|
||
|
|
assert reopened.settle_lifecycle_audit() == 1
|
||
|
|
assert reopened.lifecycle_counts() == {"records": 1, "events": 1, "pending": 0}
|
||
|
|
assert worker.reconcile("2020-01-01T00:00:00Z", "2100-01-01T00:00:00Z")["counts"][EventClass.LIFECYCLE.value] == {"source": 1, "receiver": 1}
|
||
|
|
|
||
|
|
|
||
|
|
def test_runtime_delivery_is_explicit_and_restores_readiness_only_after_receipts(workspace, receiver):
|
||
|
|
store, _, _ = register_platform(workspace)
|
||
|
|
sink, _, _ = receiver
|
||
|
|
disabled = AuditPump(store, sink)
|
||
|
|
disabled.tick()
|
||
|
|
assert not disabled.ready() and store.lifecycle_counts()["pending"] == 1
|
||
|
|
enabled = AuditPump(store, sink, lifecycle_delivery=True)
|
||
|
|
enabled.tick()
|
||
|
|
assert enabled.ready() and store.lifecycle_counts()["pending"] == 0
|
||
|
|
assert store.evidence()[0]["class"] != EventClass.LIFECYCLE.value # Legacy heartbeats predate opt-in.
|
||
|
|
|
||
|
|
|
||
|
|
def test_unadmitted_tenant_remains_pending_without_audit_relabelling(workspace):
|
||
|
|
repo, memo = workspace
|
||
|
|
repo.register(AGENT, memo, key="register")
|
||
|
|
with pytest.raises(Conflict, match="tenant not admitted"):
|
||
|
|
repo.store.stage_lifecycle_audit()
|
||
|
|
assert repo.store.evidence() == []
|
||
|
|
assert repo.store.lifecycle_counts()["pending"] == 1
|
||
|
|
|
||
|
|
|
||
|
|
def test_pending_events_do_not_get_a_nothing_to_report_heartbeat(workspace):
|
||
|
|
store, _, _ = register_platform(workspace)
|
||
|
|
store.stage_lifecycle_audit()
|
||
|
|
store.queue_heartbeats(include_lifecycle=True)
|
||
|
|
assert not any(json.loads(row["envelope"])["data"].get("class") == EventClass.LIFECYCLE.value
|
||
|
|
for row in store.evidence() if row["class"] == EventClass.HEARTBEAT.value)
|