"""Tests for engine.SyncEngine — cursor semantics and bidirectional sync.""" from engine import SyncEngine from engine.adapter import InMemoryAdapter from engine.types.operation import Operation # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def make_op(entity_id: str = "e1", device_id: str = "dev-1") -> Operation: return Operation( type="insert", device_id=device_id, timestamp="1000000.0", entity_id=entity_id, entity_type="obj", payload={}, ) # --------------------------------------------------------------------------- # Initialisation # --------------------------------------------------------------------------- class TestSyncEngineInit: def test_default_adapter_is_in_memory(self) -> None: engine = SyncEngine() assert isinstance(engine.adapter, InMemoryAdapter) def test_custom_adapter_is_stored(self) -> None: adapter = InMemoryAdapter() engine = SyncEngine(adapter=adapter) assert engine.adapter is adapter def test_none_adapter_falls_back_to_in_memory(self) -> None: engine = SyncEngine(adapter=None) assert isinstance(engine.adapter, InMemoryAdapter) # --------------------------------------------------------------------------- # Cursor semantics # --------------------------------------------------------------------------- class TestCursorSemantics: def test_fresh_sync_with_no_ops_returns_zero_cursor(self) -> None: engine = SyncEngine() cursor, ops = engine.sync(cursor=0, operations=[]) assert cursor == 0 assert ops == [] def test_cursor_advances_by_number_of_new_ops(self) -> None: engine = SyncEngine() cursor, ops = engine.sync(cursor=0, operations=[make_op(), make_op()]) assert cursor == 2 assert len(ops) == 2 def test_second_sync_at_new_cursor_returns_nothing_new(self) -> None: engine = SyncEngine() cursor, _ = engine.sync(cursor=0, operations=[make_op()]) cursor2, ops2 = engine.sync(cursor=cursor, operations=[]) assert cursor2 == cursor assert ops2 == [] def test_cursor_is_stable_across_empty_syncs(self) -> None: engine = SyncEngine() engine.sync(cursor=0, operations=[make_op(), make_op()]) c1, _ = engine.sync(cursor=2, operations=[]) c2, _ = engine.sync(cursor=2, operations=[]) assert c1 == c2 == 2 # --------------------------------------------------------------------------- # Bidirectional sync # --------------------------------------------------------------------------- class TestBidirectionalSync: def test_client_ops_are_visible_to_next_client(self) -> None: """Op sent by client-1 must appear in client-2's next sync.""" engine = SyncEngine() # client-1 sends one op engine.sync(cursor=0, operations=[make_op(device_id="dev-1")]) # client-2 syncs from 0 — should see client-1's op _, ops = engine.sync(cursor=0, operations=[]) assert len(ops) == 1 assert ops[0].device_id == "dev-1" def test_client_receives_own_ops_back_on_first_sync(self) -> None: """Ops are appended then fetched, so the sender gets them back too.""" engine = SyncEngine() _cursor, ops = engine.sync(cursor=0, operations=[make_op(), make_op()]) assert len(ops) == 2 def test_client_does_not_receive_already_seen_ops(self) -> None: engine = SyncEngine() # client-1 first sync: sends 2 ops, receives them back at cursor=2 cursor, _ = engine.sync(cursor=0, operations=[make_op(), make_op()]) # client-1 second sync: sends nothing, cursor already at 2 → empty _, ops = engine.sync(cursor=cursor, operations=[]) assert ops == [] def test_two_clients_interleaved(self) -> None: engine = SyncEngine() # client-1 syncs first, sends 1 op (cursor 0→1) c1, _ = engine.sync(cursor=0, operations=[make_op(device_id="dev-1")]) assert c1 == 1 # client-2 syncs from 0, sends 1 op (cursor 0→2, sees both) c2, ops2 = engine.sync(cursor=0, operations=[make_op(device_id="dev-2")]) assert c2 == 2 assert len(ops2) == 2 # client-1 syncs again from 1, receives only the new op from dev-2 c1b, ops1b = engine.sync(cursor=c1, operations=[]) assert c1b == 2 assert len(ops1b) == 1 assert ops1b[0].device_id == "dev-2" def test_server_assigned_ids_are_sequential(self) -> None: engine = SyncEngine() _, ops = engine.sync(cursor=0, operations=[make_op(), make_op(), make_op()]) assert [op.id for op in ops] == ["1", "2", "3"] # --------------------------------------------------------------------------- # Append-only (no conflict resolution) # --------------------------------------------------------------------------- class TestAppendOnly: def test_conflicting_ops_on_same_entity_both_stored(self) -> None: """Both ops survive; no merging or rejection.""" engine = SyncEngine() op1 = Operation( type="update", device_id="dev-1", timestamp="1.0", entity_id="shared", entity_type="obj", payload={"v": 1}, ) op2 = Operation( type="update", device_id="dev-2", timestamp="2.0", entity_id="shared", entity_type="obj", payload={"v": 2}, ) engine.sync(cursor=0, operations=[op1]) engine.sync(cursor=0, operations=[op2]) _, all_ops = engine.sync(cursor=0, operations=[]) assert len(all_ops) == 2 payloads = [op.payload for op in all_ops] assert {"v": 1} in payloads assert {"v": 2} in payloads