Admit source evidence snapshots and harden stream completeness
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a06e89-93a2-7aa2-82b3-ce5ccd2682e6
This commit is contained in:
parent
824fb1b966
commit
31e9963933
34 changed files with 1057 additions and 122 deletions
|
|
@ -1,7 +1,12 @@
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from kings_guard.cadence import load_qonto_assistant_source_cadence
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DRAFT = ROOT / "specs" / "EmissionCadenceDeclaration.md"
|
||||
QONTO_SOURCE_DECLARATION = Path("/home/worsch/qonto-assistant/specs/audit-emission-cadence.yaml")
|
||||
|
||||
|
||||
def test_emission_cadence_draft_exists_for_taxonomy_handover() -> None:
|
||||
|
|
@ -17,3 +22,62 @@ def test_emission_cadence_draft_exists_for_taxonomy_handover() -> None:
|
|||
assert "reconciliation" in text.lower()
|
||||
assert "alongside the security genome" in text.lower() or "alongside the genome" in text.lower()
|
||||
assert "security_genome" in text
|
||||
|
||||
|
||||
def test_qonto_source_declaration_is_consumed_separately_from_the_taxonomy_draft() -> None:
|
||||
cadence = load_qonto_assistant_source_cadence()
|
||||
|
||||
assert cadence.status == "source-declared"
|
||||
assert cadence.owner == "qonto-assistant"
|
||||
assert cadence.reference_instance == "QONTO-WP-0005"
|
||||
assert cadence.forms() == {"heartbeat-or-reconciliation"}
|
||||
assert cadence.rates == ()
|
||||
assert cadence.heartbeats[0].interval.total_seconds() == 86400
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not QONTO_SOURCE_DECLARATION.is_file(),
|
||||
reason="qonto-assistant source declaration checkout is unavailable",
|
||||
)
|
||||
def test_qonto_source_cadence_fixture_matches_the_source_owned_declaration() -> None:
|
||||
yaml = pytest.importorskip("yaml")
|
||||
source = yaml.safe_load(QONTO_SOURCE_DECLARATION.read_text(encoding="utf-8"))[
|
||||
"audit_emission_cadence"
|
||||
]
|
||||
cadence = load_qonto_assistant_source_cadence()
|
||||
|
||||
assert cadence.source_system == source["source"]
|
||||
assert cadence.heartbeats[0].event_class == source["heartbeat"]["event_class"]
|
||||
assert (
|
||||
cadence.heartbeats[0].interval.total_seconds()
|
||||
== source["heartbeat"]["default_interval_seconds"]
|
||||
)
|
||||
assert source["event_classes"]["audit.deny"]["form"] in cadence.forms()
|
||||
assert source["event_classes"]["audit.allow"]["completeness_claimed"] is False
|
||||
assert cadence.rates == ()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("filename", [
|
||||
"qonto_assistant_cadence.json", "qonto_assistant_source_cadence.json",
|
||||
])
|
||||
def test_cadence_fixtures_conform_to_owner_schema(filename):
|
||||
import json
|
||||
|
||||
import jsonschema
|
||||
import yaml
|
||||
|
||||
schema_path = Path(
|
||||
"/home/worsch/info-tech-canon/infospace/schemas/emission-cadence.schema.yaml"
|
||||
)
|
||||
if not schema_path.is_file():
|
||||
pytest.skip("canonical InfoTechCanon schema checkout is unavailable")
|
||||
schema = yaml.safe_load(schema_path.read_text())
|
||||
payload = json.loads((ROOT / "src/kings_guard/fixtures" / filename).read_text())
|
||||
jsonschema.Draft202012Validator(schema).validate(payload)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("value", "seconds"), [("P1DT2H30M5S", 95405), ("P1D", 86400)])
|
||||
def test_canonical_compound_durations_are_consumed(value, seconds):
|
||||
from kings_guard.cadence import parse_interval
|
||||
|
||||
assert parse_interval(value).total_seconds() == seconds
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
from kings_guard.cadence import load_qonto_assistant_cadence
|
||||
from kings_guard.contracts import (
|
||||
PostureAssessment,
|
||||
|
|
@ -9,8 +11,6 @@ from kings_guard.contracts import (
|
|||
)
|
||||
from kings_guard.posture import PostureEvaluator
|
||||
|
||||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
|
||||
def _healthy_assessment(*, completeness: StreamCompleteness, confidence: int) -> PostureAssessment:
|
||||
return PostureAssessment(
|
||||
|
|
@ -33,7 +33,10 @@ def test_completeness_is_separated_from_record_richness() -> None:
|
|||
assert evaluation.assessment.confidence_score >= 85
|
||||
assert evaluation.assessment.stream_completeness is StreamCompleteness.UNKNOWN
|
||||
assert "record" in evaluation.assessment.completeness_reason
|
||||
assert "cannot vouch" in evaluation.assessment.rationale or "unknown" in evaluation.assessment.rationale
|
||||
assert (
|
||||
"cannot vouch" in evaluation.assessment.rationale
|
||||
or "unknown" in evaluation.assessment.rationale
|
||||
)
|
||||
|
||||
|
||||
def test_unmet_cadence_degrades_completeness_and_says_so_in_words() -> None:
|
||||
|
|
@ -47,7 +50,9 @@ def test_unmet_cadence_degrades_completeness_and_says_so_in_words() -> None:
|
|||
now="2026-07-24T10:10:00Z",
|
||||
watching_since="2026-07-22T09:10:00Z",
|
||||
heartbeats=(),
|
||||
reconciliation=ReconciliationView(source_counts={"audit.deny": 1}, evidence_counts={"audit.deny": 1}),
|
||||
reconciliation=ReconciliationView(
|
||||
source_counts={"audit.deny": 1}, evidence_counts={"audit.deny": 1}
|
||||
),
|
||||
)
|
||||
|
||||
assert evaluation.assessment.stream_completeness is StreamCompleteness.DEGRADED
|
||||
|
|
@ -56,12 +61,8 @@ def test_unmet_cadence_degrades_completeness_and_says_so_in_words() -> None:
|
|||
|
||||
|
||||
def test_incomplete_stream_is_never_more_trustworthy_than_a_complete_one() -> None:
|
||||
rich_incomplete = _healthy_assessment(
|
||||
completeness=StreamCompleteness.DEGRADED, confidence=95
|
||||
)
|
||||
sparse_complete = _healthy_assessment(
|
||||
completeness=StreamCompleteness.COMPLETE, confidence=70
|
||||
)
|
||||
rich_incomplete = _healthy_assessment(completeness=StreamCompleteness.DEGRADED, confidence=95)
|
||||
sparse_complete = _healthy_assessment(completeness=StreamCompleteness.COMPLETE, confidence=70)
|
||||
unknown = _healthy_assessment(completeness=StreamCompleteness.UNKNOWN, confidence=95)
|
||||
|
||||
assert assessment_trust_key(rich_incomplete) < assessment_trust_key(sparse_complete)
|
||||
|
|
@ -103,7 +104,9 @@ def test_complete_stream_keeps_richness_and_states_completeness() -> None:
|
|||
counts={"audit.deny": 1},
|
||||
),
|
||||
),
|
||||
reconciliation=ReconciliationView(source_counts={"audit.deny": 1}, evidence_counts={"audit.deny": 1}),
|
||||
reconciliation=ReconciliationView(
|
||||
source_counts={"audit.deny": 1}, evidence_counts={"audit.deny": 1}
|
||||
),
|
||||
)
|
||||
|
||||
assert evaluation.assessment.stream_completeness is StreamCompleteness.COMPLETE
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
from kings_guard.contracts import PostureLevel, SecurityGenome, as_jsonable
|
||||
from kings_guard.posture import PostureEvaluator
|
||||
|
||||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
|
||||
def test_qonto_fixture_loads_a_normalized_genome() -> None:
|
||||
fixture = load_pilot()
|
||||
|
|
@ -25,6 +25,18 @@ def test_qonto_audit_event_normalizes_to_immune_observation() -> None:
|
|||
assert observation.deny_reason == "credential_exfil"
|
||||
|
||||
|
||||
def test_qonto_source_identity_and_egress_override_legacy_mapping_hints() -> None:
|
||||
fixture = load_pilot()
|
||||
event = dict(fixture.audit_event)
|
||||
event["identity_binding"] = "key_cape_jwt"
|
||||
event["egress_destination"] = "qonto-proxy"
|
||||
|
||||
observation = observation_from_fixture(fixture, event)
|
||||
|
||||
assert observation.identity_binding == "key_cape_jwt"
|
||||
assert observation.egress_destination == "qonto-proxy"
|
||||
|
||||
|
||||
def test_posture_evaluation_is_jsonable() -> None:
|
||||
fixture = load_pilot()
|
||||
observation = observation_from_fixture(fixture)
|
||||
|
|
@ -34,3 +46,11 @@ def test_posture_evaluation_is_jsonable() -> None:
|
|||
assert payload["assessment"]["posture"] == PostureLevel.INFLAMED.value
|
||||
assert payload["signals"][0]["signal_kind"] == "posture_hint"
|
||||
assert payload["assessment"]["stream_completeness"] == "unknown"
|
||||
|
||||
|
||||
def test_explicitly_missing_source_context_does_not_recover_legacy_hints() -> None:
|
||||
fixture = load_pilot()
|
||||
event = dict(fixture.audit_event, identity_binding=None, egress_destination=None)
|
||||
observation = observation_from_fixture(fixture, event)
|
||||
assert observation.identity_binding is None
|
||||
assert observation.egress_destination is None
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from kings_guard.contracts import StreamCompleteness
|
||||
|
||||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
from kings_guard.contracts import StreamCompleteness
|
||||
from kings_guard.posture import PostureEvaluator
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import pytest
|
||||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
from kings_guard.adapters import observation_from_audit_event
|
||||
from kings_guard.contracts import DeclaredEvidenceSource, EvidenceClass
|
||||
|
||||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
|
||||
def test_genome_declares_both_evidence_classes() -> None:
|
||||
genome = load_pilot().genome
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Tooling-layer client. A test that only ran the checker against a clean tree
|
|||
would prove nothing — it would pass just as happily if the checker were broken.
|
||||
So the negative case is exercised too, on a synthetic tree.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
from kings_guard.cadence import load_qonto_assistant_source_cadence
|
||||
from kings_guard.contracts import StreamCompleteness
|
||||
from kings_guard.live import capture_qonto_assistant_events, qonto_assistant_available
|
||||
from kings_guard.posture import PostureEvaluator
|
||||
|
||||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not qonto_assistant_available(),
|
||||
reason="qonto-assistant checkout is required for live observation",
|
||||
|
|
@ -28,7 +29,10 @@ def test_real_emitted_qonto_events_reach_the_evaluator() -> None:
|
|||
assert "allow" in decisions
|
||||
assert "deny" in decisions
|
||||
assert capture.mapping_notes
|
||||
assert all(observation.source_system == "qonto-assistant" for observation in capture.observations)
|
||||
assert capture.stream_notes
|
||||
assert all(
|
||||
observation.source_system == "qonto-assistant" for observation in capture.observations
|
||||
)
|
||||
|
||||
deny = next(item for item in capture.observations if item.decision.value == "deny")
|
||||
evaluation = PostureEvaluator().evaluate(fixture.genome, deny)
|
||||
|
|
@ -39,8 +43,25 @@ def test_real_emitted_qonto_events_reach_the_evaluator() -> None:
|
|||
for request in signal.effector_requests:
|
||||
assert request.authority_boundary in {"advisory_only", "metadata_only"}
|
||||
|
||||
# Mapping confirmed against the real emit path; remaining gaps are source omissions,
|
||||
# not adapter drift.
|
||||
assert any("identity_binding" in item for item in capture.corrections_for_source)
|
||||
assert any("egress_destination" in item for item in capture.corrections_for_source)
|
||||
assert not any("mapping drifted" in item for item in capture.corrections_for_source)
|
||||
# The source now emits identity binding and egress directly. The adapter must
|
||||
# preserve those values instead of masking them with its legacy mapping hints.
|
||||
for event, observation in zip(capture.events, capture.observations, strict=True):
|
||||
assert observation.identity_binding == event["identity_binding"]
|
||||
assert observation.egress_destination == event["egress_destination"]
|
||||
assert capture.corrections_for_source == ()
|
||||
assert capture.stream_corrections == ()
|
||||
|
||||
cadence = load_qonto_assistant_source_cadence()
|
||||
stream_evaluation = PostureEvaluator().evaluate_with_stream(
|
||||
fixture.genome,
|
||||
deny,
|
||||
cadence,
|
||||
now=capture.heartbeats[-1].timestamp,
|
||||
watching_since=capture.heartbeats[0].timestamp,
|
||||
observations=capture.observations,
|
||||
heartbeats=capture.heartbeats,
|
||||
reconciliation=capture.reconciliation,
|
||||
)
|
||||
assert stream_evaluation.assessment.stream_completeness is StreamCompleteness.COMPLETE
|
||||
assert stream_evaluation.stream is not None
|
||||
assert stream_evaluation.stream.findings == ()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from kings_guard.posture import PostureEvaluator
|
||||
|
||||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
from kings_guard.posture import PostureEvaluator
|
||||
|
||||
|
||||
def test_qonto_pilot_produces_inflamed_posture_with_tolerance_context() -> None:
|
||||
fixture = load_pilot()
|
||||
|
|
|
|||
83
tests/test_secrets_engine_snapshot.py
Normal file
83
tests/test_secrets_engine_snapshot.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
from copy import deepcopy
|
||||
from dataclasses import asdict
|
||||
|
||||
import pytest
|
||||
|
||||
from kings_guard.adapters.secrets_engine import snapshot_from_engine
|
||||
from kings_guard.contracts import StreamCompleteness
|
||||
|
||||
NOW = "2026-09-05T12:00:00Z"
|
||||
PAYLOAD = {
|
||||
"surface": "secret-use-evidence", "as_of": NOW, "completeness_claimed": False,
|
||||
"cadence": {"form": "heartbeat", "interval": "1d"},
|
||||
"lanes": [{"as_of": NOW, "catalog_id": "lane-1", "stage": "test", "kind": "kv"}],
|
||||
}
|
||||
BINDINGS = {"lane-1": ("tenant-1", "subject-1")}
|
||||
|
||||
|
||||
def admit(payload, now=NOW, bindings=BINDINGS):
|
||||
return snapshot_from_engine(payload, lane_bindings=bindings, now=now)
|
||||
|
||||
|
||||
def test_catalog_only_snapshot_does_not_invent_evidence_or_completeness():
|
||||
result = admit(PAYLOAD)
|
||||
assert result.lanes[0].ready is None
|
||||
assert result.lanes[0].revocation_succeeded is None
|
||||
assert result.lanes[0].evidence_kind is None
|
||||
assert result.completeness is StreamCompleteness.UNKNOWN
|
||||
assert result.heartbeat_interval.total_seconds() == 86400
|
||||
|
||||
|
||||
def test_evidence_preserves_false_and_drops_authorization_and_sensitive_fields():
|
||||
payload = deepcopy(PAYLOAD)
|
||||
payload["lanes"][0].update(ready=False, revocation_succeeded=False,
|
||||
decision_id="dec-1", session_handle="session-1",
|
||||
secret="never-retained", lifecycle_operation="revoke")
|
||||
result = admit(payload)
|
||||
assert result.lanes[0].ready is False
|
||||
assert result.lanes[0].revocation_succeeded is False
|
||||
assert result.lanes[0].lifecycle_operation == "revoke"
|
||||
fields = asdict(result.lanes[0])
|
||||
assert {"decision_id", "session_handle", "secret"}.isdisjoint(fields)
|
||||
assert result.completeness is StreamCompleteness.UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("now", "findings"), [
|
||||
("2026-09-06T12:00:00Z", ()),
|
||||
("2026-09-06T12:00:01Z", ("snapshot:stale",)),
|
||||
("2026-09-05T11:59:59Z", ("snapshot:future_timestamp",)),
|
||||
])
|
||||
def test_freshness_does_not_upgrade_completeness(now, findings):
|
||||
result = admit(PAYLOAD, now)
|
||||
assert result.findings == findings
|
||||
assert result.completeness is StreamCompleteness.UNKNOWN
|
||||
|
||||
|
||||
def test_unbound_or_duplicate_lanes_cannot_cross_scope():
|
||||
with pytest.raises(ValueError, match="unbound"):
|
||||
admit(PAYLOAD, bindings={})
|
||||
payload = deepcopy(PAYLOAD)
|
||||
payload["lanes"].append(payload["lanes"][0])
|
||||
with pytest.raises(ValueError, match="duplicate"):
|
||||
admit(payload)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", [
|
||||
{"completeness_claimed": True}, {"completeness_claimed": None},
|
||||
{"cadence": {"form": "heartbeat", "interval": "0s"}},
|
||||
{"surface": "authorization"}, {"lanes": None},
|
||||
])
|
||||
def test_invalid_envelope_is_rejected(change):
|
||||
with pytest.raises(ValueError):
|
||||
admit({**PAYLOAD, **change})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", [
|
||||
{"ready": "false"}, {"revocation_attempted": 0},
|
||||
{"as_of": "2026-09-04T12:00:00Z"}, {"evidence_kind": "guessed"},
|
||||
])
|
||||
def test_invalid_lane_is_rejected(change):
|
||||
payload = deepcopy(PAYLOAD)
|
||||
payload["lanes"][0].update(change)
|
||||
with pytest.raises(ValueError):
|
||||
admit(payload)
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
from kings_guard.cadence import load_qonto_assistant_cadence
|
||||
from kings_guard.contracts import (
|
||||
ReconciliationView,
|
||||
|
|
@ -7,8 +9,6 @@ from kings_guard.contracts import (
|
|||
)
|
||||
from kings_guard.posture import PostureEvaluator, is_stream_finding
|
||||
|
||||
from helpers import load_pilot, observation_from_fixture
|
||||
|
||||
|
||||
def test_cadence_draft_covers_both_forms_against_qonto() -> None:
|
||||
cadence = load_qonto_assistant_cadence()
|
||||
|
|
@ -36,7 +36,9 @@ def test_unmet_declared_rate_is_a_stream_finding() -> None:
|
|||
counts={"audit.deny": 0},
|
||||
),
|
||||
),
|
||||
reconciliation=ReconciliationView(source_counts={"audit.deny": 0}, evidence_counts={"audit.deny": 0}),
|
||||
reconciliation=ReconciliationView(
|
||||
source_counts={"audit.deny": 0}, evidence_counts={"audit.deny": 0}
|
||||
),
|
||||
)
|
||||
|
||||
assert any(item.startswith("stream:cadence_unmet:audit.allow") for item in stream.findings)
|
||||
|
|
@ -54,7 +56,9 @@ def test_missing_heartbeat_is_a_stream_finding() -> None:
|
|||
now="2026-07-24T10:10:00Z",
|
||||
watching_since="2026-07-22T09:10:00Z",
|
||||
heartbeats=(),
|
||||
reconciliation=ReconciliationView(source_counts={"audit.deny": 1}, evidence_counts={"audit.deny": 1}),
|
||||
reconciliation=ReconciliationView(
|
||||
source_counts={"audit.deny": 1}, evidence_counts={"audit.deny": 1}
|
||||
),
|
||||
)
|
||||
|
||||
assert any(item.startswith("stream:heartbeat_missing:audit.deny") for item in stream.findings)
|
||||
|
|
@ -103,7 +107,9 @@ def test_stream_findings_are_distinguishable_from_content_findings() -> None:
|
|||
now="2026-07-24T10:10:00Z",
|
||||
watching_since="2026-07-22T09:10:00Z",
|
||||
heartbeats=(),
|
||||
reconciliation=ReconciliationView(source_counts={"audit.deny": 1}, evidence_counts={"audit.deny": 1}),
|
||||
reconciliation=ReconciliationView(
|
||||
source_counts={"audit.deny": 1}, evidence_counts={"audit.deny": 1}
|
||||
),
|
||||
)
|
||||
|
||||
content_findings = set(evaluation.assessment.findings)
|
||||
|
|
@ -127,7 +133,9 @@ def test_heartbeat_not_yet_due_does_not_false_alarm() -> None:
|
|||
now=observation.timestamp,
|
||||
watching_since=observation.timestamp,
|
||||
heartbeats=(),
|
||||
reconciliation=ReconciliationView(source_counts={"audit.deny": 1}, evidence_counts={"audit.deny": 1}),
|
||||
reconciliation=ReconciliationView(
|
||||
source_counts={"audit.deny": 1}, evidence_counts={"audit.deny": 1}
|
||||
),
|
||||
)
|
||||
|
||||
assert not any(item.startswith("stream:heartbeat_missing") for item in stream.findings)
|
||||
|
|
|
|||
88
tests/test_stream_admission.py
Normal file
88
tests/test_stream_admission.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
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]
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue