feat: add memory sync outbox worker
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from app.model.memory import MemorySyncOutbox
|
||||
from app.worker.memory_sync_outbox_worker import MemorySyncOutboxWorker
|
||||
|
||||
|
||||
class FakeSession:
|
||||
def __init__(self, event: MemorySyncOutbox | None) -> None:
|
||||
self.event = event
|
||||
self.commits = 0
|
||||
self.rollbacks = 0
|
||||
|
||||
async def __aenter__(self) -> "FakeSession":
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *args: object) -> None:
|
||||
return None
|
||||
|
||||
async def scalar(self, statement: object) -> MemorySyncOutbox | None:
|
||||
del statement
|
||||
return self.event
|
||||
|
||||
async def commit(self) -> None:
|
||||
self.commits += 1
|
||||
|
||||
async def rollback(self) -> None:
|
||||
self.rollbacks += 1
|
||||
|
||||
|
||||
def event(*, target: str = "neo4j", retry_count: int = 0) -> MemorySyncOutbox:
|
||||
return MemorySyncOutbox(
|
||||
id=1, event_uuid="event-1", aggregate_type="profile_snapshot",
|
||||
aggregate_uuid="profile-1", aggregate_version=1, target_store=target,
|
||||
operation="upsert", payload={"customer_id": 7}, status="pending",
|
||||
retry_count=retry_count, next_retry_at=None, last_error=None,
|
||||
created_at=datetime(2026, 1, 1), processed_at=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_success_marks_event_processed() -> None:
|
||||
item = event()
|
||||
session = FakeSession(item)
|
||||
seen: list[dict[str, object]] = []
|
||||
|
||||
async def handler(payload: dict[str, object]) -> None:
|
||||
seen.append(payload)
|
||||
|
||||
worker = MemorySyncOutboxWorker({"neo4j": handler}, session_factory=lambda: session)
|
||||
assert await worker.run_once() is True
|
||||
assert seen == [{"customer_id": 7}]
|
||||
assert item.status == "processed"
|
||||
assert item.processed_at is not None
|
||||
assert session.commits == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handler_failure_uses_backoff_and_keeps_event() -> None:
|
||||
item = event()
|
||||
session = FakeSession(item)
|
||||
|
||||
async def handler(payload: dict[str, object]) -> None:
|
||||
del payload
|
||||
raise TimeoutError
|
||||
|
||||
worker = MemorySyncOutboxWorker({"neo4j": handler}, session_factory=lambda: session)
|
||||
assert await worker.run_once() is True
|
||||
assert item.status == "failed"
|
||||
assert item.retry_count == 1
|
||||
assert item.next_retry_at is not None
|
||||
assert item.last_error == "TimeoutError"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fifth_failure_enters_dead_state() -> None:
|
||||
item = event(retry_count=4)
|
||||
session = FakeSession(item)
|
||||
|
||||
async def handler(payload: dict[str, object]) -> None:
|
||||
del payload
|
||||
raise RuntimeError
|
||||
|
||||
worker = MemorySyncOutboxWorker({"neo4j": handler}, session_factory=lambda: session)
|
||||
await worker.run_once()
|
||||
assert item.status == "dead"
|
||||
assert item.retry_count == 5
|
||||
assert item.next_retry_at is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_handler_enters_dead_state_without_external_call() -> None:
|
||||
item = event(target="milvus")
|
||||
session = FakeSession(item)
|
||||
worker = MemorySyncOutboxWorker({"neo4j": lambda _: None}, session_factory=lambda: session)
|
||||
|
||||
assert await worker.run_once() is True
|
||||
assert item.status == "dead"
|
||||
assert item.last_error == "target_handler_not_configured"
|
||||
Reference in New Issue
Block a user