"""LedgerOutbox + LedgerWriter direct unit tests (REQ-019).""" from __future__ import annotations from datetime import datetime, timedelta, timezone from trex_core.adapters.ledger.in_memory import InMemoryLedger from trex_core.application.ledger_outbox import ( InMemoryLedgerOutbox, LedgerWriter, OutboxOutcome, WorkItemStatus, ) from trex_core.application.ledger_port import LedgerTrade def _trade(execution_id="paper:PAPER-1:10") -> LedgerTrade: return LedgerTrade( asset_id="PF-1", order_id="PAPER-1", execution_id=execution_id, side="buy", qty=10, price=1000.0, executed_at=datetime(2026, 6, 4, tzinfo=timezone.utc), ) def test_enqueue_is_idempotent_on_correlation_id(): outbox = InMemoryLedgerOutbox() a = outbox.enqueue(_trade()) b = outbox.enqueue(_trade()) assert a.correlation_id == b.correlation_id assert len(outbox.all_items()) == 1 def test_writer_run_once_drains_pending_and_marks_completed(): outbox = InMemoryLedgerOutbox() ledger = InMemoryLedger() writer = LedgerWriter(outbox, ledger) outbox.enqueue(_trade(execution_id="e-1")) outbox.enqueue(_trade(execution_id="e-2")) stats = writer.run_once() assert stats.completed == 2 and ledger.count() == 2 assert all(it.status is WorkItemStatus.COMPLETED for it in outbox.all_items()) assert all(it.last_outcome is OutboxOutcome.COMPLETED for it in outbox.all_items()) def test_writer_marks_conflict_blocked_and_does_not_retry(): outbox = InMemoryLedgerOutbox() ledger = InMemoryLedger(conflict_executions={"e-x"}) writer = LedgerWriter(outbox, ledger) outbox.enqueue(_trade(execution_id="e-x")) writer.run_once() [item] = outbox.all_items() assert item.status is WorkItemStatus.CONFLICT_BLOCKED assert item.last_outcome is OutboxOutcome.CONFLICT_BLOCKED # No pending items remain, so a subsequent run is a no-op. assert outbox.list_pending() == [] def test_writer_retries_until_success(): # dcy-015: retryable rows now back off; share an injected clock between the # outbox (list_pending "now") and the writer (next_retry_at) to advance past it. now = {"t": datetime(2026, 6, 4, tzinfo=timezone.utc)} outbox = InMemoryLedgerOutbox(clock=lambda: now["t"]) ledger = InMemoryLedger(fail_n_then_succeed=1) writer = LedgerWriter(outbox, ledger, clock=lambda: now["t"]) outbox.enqueue(_trade(execution_id="e-r")) s1 = writer.run_once() assert s1.retryable == 1 and ledger.count() == 0 assert writer.run_once().completed == 0 # still in backoff -> skipped now["t"] += timedelta(seconds=600) # past the backoff window s2 = writer.run_once() assert s2.completed == 1 and ledger.count() == 1