Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a06e89-93a2-7aa2-82b3-ce5ccd2682e6
88 lines
3.5 KiB
Python
88 lines
3.5 KiB
Python
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]
|
|
)
|