from copy import deepcopy import pytest from kings_guard.cadence import load_qonto_assistant_source_cadence from kings_guard.contracts import ReconciliationView, StreamCompleteness, StreamHeartbeat from kings_guard.live import _confirm_stream from kings_guard.stream import evaluate_stream NOW = "2026-09-05T12:00:00Z" COUNTS = {"audit.allow": 0, "audit.deny": 0} EVENT = { "stream_id": "qonto.audit", "stream_instance_id": "instance-1", "stream_sequence": 1, "event_class": "audit.heartbeat", "timestamp": NOW, "assertion": "nothing-to-report", "source_transition_counts": COUNTS, } SNAPSHOT = { "stream_id": "qonto.audit", "stream_instance_id": "instance-1", "last_stream_sequence": 1, "source_transition_counts": COUNTS, } @pytest.mark.parametrize("change", [ {"stream_instance_id": "instance-2"}, {"stream_id": "other.audit"}, {"last_stream_sequence": 2}, {"last_stream_sequence": True}, ]) def test_reconciliation_must_belong_to_captured_stream(change): assert _confirm_stream([EVENT], {**SNAPSHOT, **change})[-1] @pytest.mark.parametrize("sequence", [None, True, "1", -1]) def test_malformed_sequences_are_rejected(sequence): with pytest.raises(ValueError, match="stream_sequence"): _confirm_stream([{**EVENT, "stream_sequence": sequence}], SNAPSHOT) def test_empty_capture_is_rejected_without_index_error(): with pytest.raises(ValueError, match="empty"): _confirm_stream([], SNAPSHOT) def test_missing_counts_cannot_be_normalized_to_zero(): snapshot = deepcopy(SNAPSHOT) snapshot["source_transition_counts"] = {"audit.allow": 0} with pytest.raises(ValueError, match="counts"): _confirm_stream([EVENT], snapshot) @pytest.mark.parametrize("counts", [{}, {"audit.deny": -1}, {"audit.deny": True}]) def test_absent_or_invalid_reconciliation_counts_leave_completeness_unknown(counts): result = evaluate_stream( (), load_qonto_assistant_source_cadence(), now=NOW, heartbeats=[StreamHeartbeat("qonto-assistant", NOW, "audit.heartbeat", "nothing-to-report")], reconciliation=ReconciliationView(counts, counts), ) assert result.completeness is StreamCompleteness.UNKNOWN @pytest.mark.parametrize(("source", "timestamp"), [ ("other-source", NOW), ("qonto-assistant", "2026-09-07T12:00:00Z"), ]) def test_unrelated_or_future_heartbeat_cannot_satisfy_cadence(source, timestamp): result = evaluate_stream( (), load_qonto_assistant_source_cadence(), now=NOW, watching_since="2026-09-03T12:00:00Z", heartbeats=[StreamHeartbeat(source, timestamp, "audit.heartbeat", "nothing-to-report")], reconciliation=ReconciliationView(COUNTS, COUNTS), ) assert result.completeness is StreamCompleteness.DEGRADED assert "stream:heartbeat_missing:audit.deny" in result.findings def test_excess_evidence_is_also_reconciliation_divergence(): result = evaluate_stream( (), load_qonto_assistant_source_cadence(), now=NOW, heartbeats=[StreamHeartbeat("qonto-assistant", NOW, "audit.heartbeat", "nothing-to-report")], reconciliation=ReconciliationView({"audit.deny": 0}, {"audit.deny": 1}), ) assert "stream:reconciliation_divergence:audit.deny" in result.findings def test_heartbeat_counts_must_match_events_already_captured(): event = {**EVENT, "source_transition_counts": {"audit.allow": 0, "audit.deny": 1}} assert "heartbeat counts do not match preceding request events" in ( _confirm_stream([event], SNAPSHOT)[-1] )