diff --git a/tests/test_edge_sync_journal.py b/tests/test_edge_sync_journal.py new file mode 100644 index 00000000..a0a7cfa2 --- /dev/null +++ b/tests/test_edge_sync_journal.py @@ -0,0 +1,135 @@ +from __future__ import annotations + +from core.sync.journal import JournalDirection, JournalStatus, LocalSyncJournal + + +def test_append_upload_creates_pending_entry() -> None: + journal = LocalSyncJournal() + decision = journal.append_upload( + entry_id="u1", + artifact_id="trace-1", + artifact_type="trace", + target_uri="s3://bucket/traces/trace-1.jsonl.zst", + ) + + assert decision.accepted + assert decision.reason == "pending" + assert decision.entry is not None + assert decision.entry.direction is JournalDirection.UPLOAD + assert decision.entry.status is JournalStatus.PENDING + assert journal.pending() == (decision.entry,) + + +def test_append_download_creates_pending_entry() -> None: + journal = LocalSyncJournal() + decision = journal.append_download( + entry_id="d1", + artifact_id="pack-v1", + artifact_type="pack_release", + ) + + assert decision.accepted + assert decision.entry is not None + assert decision.entry.direction is JournalDirection.DOWNLOAD + assert decision.entry.status is JournalStatus.PENDING + + +def test_duplicate_entry_rejects_without_overwrite() -> None: + journal = LocalSyncJournal() + first = journal.append_upload(entry_id="u1", artifact_id="a1", artifact_type="trace") + second = journal.append_upload(entry_id="u1", artifact_id="a2", artifact_type="trace") + + assert first.accepted + assert not second.accepted + assert second.reason == "duplicate_entry_id" + assert journal.get("u1").artifact_id == "a1" # type: ignore[union-attr] + + +def test_mark_completed_preserves_entry_metadata() -> None: + journal = LocalSyncJournal() + journal.append_upload(entry_id="u1", artifact_id="trace-1", artifact_type="trace") + decision = journal.mark_completed("u1") + + assert decision.accepted + assert decision.reason == "completed" + assert decision.entry is not None + assert decision.entry.status is JournalStatus.COMPLETED + assert decision.entry.artifact_id == "trace-1" + assert journal.pending() == () + + +def test_mark_failed_preserves_reason_and_increments_attempts() -> None: + journal = LocalSyncJournal() + journal.append_upload(entry_id="u1", artifact_id="trace-1", artifact_type="trace") + failure = journal.mark_failed("u1", "object_store_unavailable") + + assert failure.accepted + assert failure.entry is not None + assert failure.entry.status is JournalStatus.FAILED + assert failure.entry.reason == "object_store_unavailable" + assert failure.entry.attempts == 1 + + +def test_failed_upload_does_not_clear_journal_state() -> None: + journal = LocalSyncJournal() + journal.append_upload(entry_id="u1", artifact_id="trace-1", artifact_type="trace") + journal.mark_failed("u1", "timeout") + + entry = journal.get("u1") + assert entry is not None + assert entry.artifact_id == "trace-1" + assert entry.status is JournalStatus.FAILED + + +def test_retry_failed_entry_returns_to_pending_without_incrementing_attempts() -> None: + journal = LocalSyncJournal() + journal.append_upload(entry_id="u1", artifact_id="trace-1", artifact_type="trace") + journal.mark_failed("u1", "timeout") + retry = journal.retry("u1") + + assert retry.accepted + assert retry.reason == "pending" + assert retry.entry is not None + assert retry.entry.status is JournalStatus.PENDING + assert retry.entry.attempts == 1 + assert retry.entry.reason is None + + +def test_rejected_download_can_be_retried() -> None: + journal = LocalSyncJournal() + journal.append_download(entry_id="d1", artifact_id="pack-v1", artifact_type="pack_release") + journal.mark_rejected("d1", "hash_mismatch") + retry = journal.retry("d1") + + assert retry.accepted + assert retry.entry is not None + assert retry.entry.status is JournalStatus.PENDING + assert retry.entry.reason is None + + +def test_unknown_entry_transition_rejects() -> None: + journal = LocalSyncJournal() + decision = journal.mark_completed("missing") + + assert not decision.accepted + assert decision.reason == "unknown_entry" + + +def test_journal_serializes_to_dicts() -> None: + journal = LocalSyncJournal() + journal.append_upload(entry_id="u1", artifact_id="trace-1", artifact_type="trace") + journal.mark_failed("u1", "timeout") + + payload = journal.as_dicts() + assert payload == ( + { + "entry_id": "u1", + "direction": "upload", + "artifact_id": "trace-1", + "artifact_type": "trace", + "status": "failed", + "target_uri": None, + "reason": "timeout", + "attempts": 1, + }, + )