| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112 |
- from __future__ import annotations
- from datetime import datetime, timedelta, timezone
- from pathlib import Path
- ROOT = Path(__file__).resolve().parents[1]
- def test_retry_delay_is_exponential_and_capped():
- from app.core.events.consumer import retry_delay
- assert retry_delay(1, base_seconds=2, maximum_seconds=30) == timedelta(seconds=2)
- assert retry_delay(2, base_seconds=2, maximum_seconds=30) == timedelta(seconds=4)
- assert retry_delay(10, base_seconds=2, maximum_seconds=30) == timedelta(seconds=30)
- def test_successful_dispatch_records_idempotent_completion():
- from app.core.events.consumer import ClaimedEvent, dispatch_event
- event = ClaimedEvent(
- event_id="01900000-0000-7000-8000-000000000001",
- event_type="governance.updated",
- payload={"uid": "domain-1"},
- attempts=0,
- )
- calls = []
- result = dispatch_event(event, lambda payload: calls.append(payload))
- duplicate = dispatch_event(
- event,
- lambda payload: calls.append(payload),
- already_processed=True,
- )
- assert result.status == "published"
- assert result.record_consumption is True
- assert duplicate.status == "published"
- assert duplicate.record_consumption is False
- assert calls == [{"uid": "domain-1"}]
- def test_failed_dispatch_retries_then_enters_dead_letter():
- from app.core.events.consumer import ClaimedEvent, dispatch_event
- now = datetime(2026, 7, 16, tzinfo=timezone.utc)
- def fail(_payload):
- raise RuntimeError("downstream unavailable with secret-token")
- retry = dispatch_event(
- ClaimedEvent("event-1", "test", {}, attempts=0),
- fail,
- max_attempts=2,
- now=now,
- )
- dead = dispatch_event(
- ClaimedEvent("event-1", "test", {}, attempts=1),
- fail,
- max_attempts=2,
- now=now,
- )
- assert retry.status == "pending"
- assert retry.attempts == 1
- assert retry.available_at > now
- assert retry.last_error == "downstream unavailable with [redacted]"
- assert dead.status == "dead_letter"
- assert dead.attempts == 2
- def test_outbox_migration_and_claim_query_define_delivery_contract():
- migration = (
- ROOT / "migrations" / "versions" / "20260716_03_outbox.py"
- ).read_text(encoding="utf-8")
- repository = (ROOT / "app/core/events/outbox.py").read_text(encoding="utf-8")
- for column in (
- "event_id",
- "aggregate_type",
- "aggregate_id",
- "event_type",
- "payload",
- "status",
- "attempts",
- "available_at",
- "published_at",
- "last_error",
- ):
- assert column in migration
- assert "outbox_consumptions" in migration
- assert "FOR UPDATE SKIP LOCKED" in repository
- assert "outbox_health" in repository
- def test_system_health_exposes_outbox_counts_without_payloads():
- health = (ROOT / "app/core/system/health.py").read_text(encoding="utf-8")
- assert "outbox_health" in health
- assert 'health_status["outbox"]' in health
- assert "payload" not in health
- def test_consumer_without_registered_handlers_does_not_claim_events(monkeypatch):
- from app.commands import process_outbox
- def unexpected_claim(*_args, **_kwargs):
- raise AssertionError("events must not be claimed without a registered handler")
- monkeypatch.setattr(process_outbox, "claim_outbox", unexpected_claim)
- assert process_outbox.process_available_events(object(), handlers={}) == 0
|