from __future__ import annotations from pathlib import Path import pytest from sync_state_machine.common.binding import BindingManager from sync_state_machine.common.collection import DataCollection from sync_state_machine.common.persistence import PersistenceBackend from sync_state_machine.common.types import BindingStatus, SyncAction, SyncStatus from sync_state_machine.datasource.datasource import BaseDataSource from sync_state_machine.engine.state_apply import apply_state_to_node from sync_state_machine.pipeline.full_sync_pipeline import FullSyncPipeline from tests.mocks.mock_datasource import MockSyncNode class _DS(BaseDataSource): pass def _set_state(pipeline: FullSyncPipeline, node: MockSyncNode, state_id: str, *, data_id: str) -> None: apply_state_to_node( pipeline.sm_runtime, node, state_id=state_id, reason="test setup", fields={"binding_status", "action", "status"}, ) apply_state_to_node( pipeline.sm_runtime, node, state_id=state_id, reason="test setup", fields={"data_id"}, data_id=data_id, ) async def _new_pipeline(tmp_path: Path) -> tuple[FullSyncPipeline, DataCollection, DataCollection, BindingManager, PersistenceBackend]: db_path = tmp_path / "state.db" persistence = PersistenceBackend(str(db_path)) await persistence.initialize() local_collection = DataCollection("local", persistence=persistence, auto_persist=False) remote_collection = DataCollection("remote", persistence=persistence, auto_persist=False) binding_manager = BindingManager(persistence, auto_persist=False) pipeline = FullSyncPipeline( local_datasource=_DS(), remote_datasource=_DS(), strategies=[], persistence=persistence, local_collection=local_collection, remote_collection=remote_collection, binding_manager=binding_manager, node_types=["mock"], load_order=["mock"], sync_order=["mock"], enable_persistence=False, ) return pipeline, local_collection, remote_collection, binding_manager, persistence @pytest.mark.asyncio async def test_peer_creating_source_moves_to_s01_when_target_create_success(tmp_path: Path) -> None: pipeline, local, remote, bm, persistence = await _new_pipeline(tmp_path) src = MockSyncNode( node_id="src_local", data_id="SRC-ID", data={"id": "SRC-ID", "name": "src"}, action=SyncAction.NONE, status=SyncStatus.PENDING, binding_status=BindingStatus.UNCHECKED, ) target = MockSyncNode( node_id="tgt_remote", data_id="TGT-ID", data={"id": "TGT-ID", "name": "tgt"}, action=SyncAction.CREATE, status=SyncStatus.SUCCESS, binding_status=BindingStatus.NORMAL, ) await local.add(src) await remote.add(target) _set_state(pipeline, src, "S13", data_id="SRC-ID") _set_state(pipeline, target, "S11C", data_id="TGT-ID") await bm.bind("mock", "src_local", "tgt_remote") await pipeline._finalize_peer_creating_sources("mock") assert src.binding_status == BindingStatus.NORMAL assert src.action == SyncAction.NONE assert src.status == SyncStatus.PENDING await pipeline.close() await persistence.close() @pytest.mark.asyncio async def test_peer_creating_source_stays_s13_when_target_create_failed(tmp_path: Path) -> None: pipeline, local, remote, bm, persistence = await _new_pipeline(tmp_path) src = MockSyncNode( node_id="src_local_fail", data_id="SRC-ID-FAIL", data={"id": "SRC-ID-FAIL", "name": "src-fail"}, action=SyncAction.NONE, status=SyncStatus.PENDING, binding_status=BindingStatus.UNCHECKED, ) target = MockSyncNode( node_id="tgt_remote_fail", data_id="", data={"id": "", "name": "tgt-fail"}, action=SyncAction.CREATE, status=SyncStatus.FAILED, binding_status=BindingStatus.NORMAL, ) await local.add(src) await remote.add(target) _set_state(pipeline, src, "S13", data_id="SRC-ID-FAIL") _set_state(pipeline, target, "S12C", data_id="") target.error = "remote create failed" await bm.bind("mock", "src_local_fail", "tgt_remote_fail") await pipeline._finalize_peer_creating_sources("mock") assert src.binding_status == BindingStatus.PEER_CREATING assert src.action == SyncAction.NONE assert src.status == SyncStatus.PENDING assert "peer_create_failed" in (src.error or "") await pipeline.close() await persistence.close()