from types import SimpleNamespace from typing import cast from unittest.mock import AsyncMock, MagicMock, patch import pytest import pytest_asyncio import sync_state_machine.domain # noqa: F401 from sync_state_machine.common.binding import BindingManager from sync_state_machine.common.collection import DataCollection from sync_state_machine.common.sync_node import SyncNode from sync_state_machine.common.sqlite_backend import SQLitePersistenceBackend from sync_state_machine.common.types import BindingStatus from sync_state_machine.domain.project_detail.sync_strategy import ProjectDetailSyncStrategy from sync_state_machine.domain.project_detail.sync_node import ProjectDetailSyncNode from sync_state_machine.sync_system.config import UpdateDirection from sync_state_machine.sync_system.strategy_ops import BoundNodePair def _fake_node(*, node_id: str, key: str): data = {"id": node_id, "key": key} return cast( SyncNode, SimpleNamespace( node_id=node_id, data_id=node_id, action=None, context={}, get_data=lambda: data, ), ) def _build_project_detail_node(*, node_id: str, data_id: str, key: str) -> ProjectDetailSyncNode: return ProjectDetailSyncNode( node_id=node_id, data_id=data_id, data={ "id": data_id, "project_id": "project-1", "key": key, "value": [{"year": 2026, "capacity": 4.0}], }, ) @pytest_asyncio.fixture async def project_detail_env(tmp_path): persistence = SQLitePersistenceBackend(str(tmp_path / "project-detail.db")) await persistence.initialize() local = DataCollection("local", persistence=persistence, auto_persist=False) remote = DataCollection("remote", persistence=persistence, auto_persist=False) binding_manager = BindingManager(persistence, auto_persist=False) yield local, remote, binding_manager, persistence await persistence.close() @pytest.mark.asyncio async def test_project_detail_process_update_pairs_partitions_group_production_plans_before_generic_logic(): strategy = ProjectDetailSyncStrategy("project_detail_data", MagicMock(), MagicMock(), MagicMock()) pull_pair = BoundNodePair( local_node=_fake_node(node_id="local-pull", key="ensure_production"), remote_node=_fake_node(node_id="remote-pull", key="ensure_production"), ) generic_pair = BoundNodePair( local_node=_fake_node(node_id="local-generic", key="production"), remote_node=_fake_node(node_id="remote-generic", key="production"), ) pull_node = SimpleNamespace(node_id="pull-update") generic_node = SimpleNamespace(node_id="generic-update") with patch.object( strategy, "prepare_update_for_direction", new=AsyncMock(return_value=pull_node), ) as mock_prepare, patch.object( strategy, "update_pair", new=AsyncMock(return_value=[generic_node]), ) as mock_update_pair: updated_by_id = await strategy.process_update_pairs([pull_pair, generic_pair], {}) assert updated_by_id == { "pull-update": pull_node, "generic-update": generic_node, } mock_prepare.assert_awaited_once_with( pull_pair.local_node, pull_pair.remote_node, UpdateDirection.PULL, {}, ) mock_update_pair.assert_awaited_once_with(generic_pair.local_node, generic_pair.remote_node, {}) def test_project_detail_domain_option_is_typed_schema() -> None: strategy = ProjectDetailSyncStrategy("project_detail_data", MagicMock(), MagicMock(), MagicMock()) assert strategy.domain_option.pull_group_production_plans is True @pytest.mark.asyncio async def test_project_detail_bind_create_pulls_group_production_plan_from_remote(project_detail_env) -> None: local, remote, binding_manager, _ = project_detail_env strategy = ProjectDetailSyncStrategy("project_detail_data", local, remote, binding_manager) strategy.config.depend_fields = {} remote_node = _build_project_detail_node( node_id="remote-pull", data_id="detail-remote-pull", key="ensure_production", ) await remote.add(remote_node) await strategy.bind() created_nodes = await strategy.create() assert len(created_nodes) == 1 created_node = created_nodes[0] created_data = created_node.get_data() assert created_node.node_type == "project_detail_data" assert created_data is not None assert created_data["key"] == "ensure_production" assert remote_node.binding_status == BindingStatus.PEER_CREATING assert await binding_manager.get_local_id("project_detail_data", remote_node.node_id) == created_node.node_id @pytest.mark.asyncio async def test_project_detail_bind_does_not_push_group_production_plan_from_local(project_detail_env) -> None: local, remote, binding_manager, _ = project_detail_env strategy = ProjectDetailSyncStrategy("project_detail_data", local, remote, binding_manager) strategy.config.depend_fields = {} local_node = _build_project_detail_node( node_id="local-pull", data_id="detail-local-pull", key="ensure_production", ) await local.add(local_node) await strategy.bind() created_nodes = await strategy.create() assert created_nodes == [] assert remote.filter(node_type="project_detail_data") == [] assert await binding_manager.get_remote_id("project_detail_data", local_node.node_id) is None