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)