informed-decision/tests/test_lifecycle_audit.py
tegwick 5fec7de4cb Implement lifecycle workspace publication and audit delivery
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f
2026-10-04 20:17:24 +02:00

80 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)