from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch import pytest from sync_state_machine.common.binding import BindingManager from sync_state_machine.common.collection import DataCollection from sync_state_machine.common.types import SyncAction from sync_state_machine.domain.project.sync_node import ProjectSyncNode from sync_state_machine.domain.project.sync_strategy import ProjectSyncStrategy from sync_state_machine.sync_system.config import UpdateDirection def _fake_node(*, node_id: str, data_id: str, data: dict, context=None): return SimpleNamespace( node_id=node_id, data_id=data_id, data=data, context=context or {}, action=None, get_data=lambda: data, ) def test_project_get_node_update_payload_enriches_group_plan_dates_from_all_info(): strategy = ProjectSyncStrategy("project", MagicMock(), MagicMock(), MagicMock()) node = _fake_node( node_id="node-1", data_id="project-1", data={"id": "project-1", "project_name": "demo"}, context={ "all_info": { "wind_extension": { "ic_plan_first_production_date": "2026-01-01", "ic_plan_full_production_date": "2026-06-01", } } }, ) payload = strategy.get_node_update_payload(node) assert payload["ic_plan_first_production_date"] == "2026-01-01" assert payload["ic_plan_full_production_date"] == "2026-06-01" @pytest.mark.asyncio async def test_project_update_pair_runs_special_pull_then_generic_update(): strategy = ProjectSyncStrategy("project", MagicMock(), MagicMock(), MagicMock()) strategy.config.domain_option = {"pull_group_plan_production_date": True} local_node = _fake_node(node_id="local", data_id="local-1", data={"id": "local-1"}) remote_node = _fake_node(node_id="remote", data_id="remote-1", data={"id": "remote-1"}) pull_node = SimpleNamespace(node_id="pull-node") push_node = SimpleNamespace(node_id="push-node") with patch.object( strategy, "prepare_update_for_direction", new=AsyncMock(side_effect=[pull_node, push_node]), ) as mock_prepare: updated_nodes = await strategy.update_pair(local_node, remote_node, {"project": "remote-project"}) assert updated_nodes == [pull_node, push_node] pull_call = mock_prepare.await_args_list[0] assert pull_call.args == (local_node, remote_node, UpdateDirection.PULL, {"project": "remote-project"}) assert pull_call.kwargs["include_fields"] == strategy._PULL_ONLY_FIELDS generic_call = mock_prepare.await_args_list[1] assert generic_call.args[1:] == (remote_node, UpdateDirection.PUSH, {"project": "remote-project"}) assert generic_call.args[0] is not local_node assert generic_call.kwargs["exclude_fields"] == strategy._PULL_ONLY_FIELDS def _project_payload( *, project_id: str, company_id: str, company_name: str, name: str, plan_first: str | None, plan_full: str | None, ): return { "id": project_id, "project_type": "photovoltaic", "region_name": ["中国", "云南省"], "region_path": ["100000", "530000"], "province": "530000", "is_domestic": True, "company_id": company_id, "company_name": company_name, "syxs": None, "sub_type": None, "scgc_type": "self", "address": "测试地址", "code": "", "name": name, "short_name": "", "manager_name": "测试负责人", "manager_phone": "13800000000", "authorization_date": "2024-01-01", "authorized_capacity": 7.0, "investment_decision_capacity": 7.0, "investment_decision_date": "2024-01-02", "plan_construction_capacity": 7.0, "plan_construction_date": "2024-01-03", "actual_construction_capacity": 0.0, "actual_construction_date": None, "accumulated_construction_capacity": 0.0, "constructing_capacity": 0.0, "ensure_production_capacity": 2.0, "challenge_production_capacity": 2.0, "climb_production_capacity": 2.0, "plan_production_date": None, "plan_full_production_date": None, "actual_production_date": None, "actual_full_production_date": None, "production_capacity": 0.0, "production_date": None, "total_production_capacity": 0.0, "dynamic_investment": 0.0, "complete_investment": 0.0, "progress_type": None, "key_constraints": "NONE", "key_constraints_remark": "", "apply_file_ids": [], "dbtc_score_list": [], "ic_plan_first_production_date": plan_first, "ic_plan_full_production_date": plan_full, } @pytest.mark.asyncio async def test_project_update_pair_can_prepare_pull_and_push_on_same_pair(): local_collection = DataCollection("local") remote_collection = DataCollection("remote") binding_manager = BindingManager(None) local_node = ProjectSyncNode( node_id="local-project-node", data_id="local-project-id", data=_project_payload( project_id="local-project-id", company_id="local-company-id", company_name="云南公司", name="本地项目", plan_first=None, plan_full=None, ), ) remote_node = ProjectSyncNode( node_id="remote-project-node", data_id="remote-project-id", data=_project_payload( project_id="remote-project-id", company_id="remote-company-id", company_name="清洁能源", name="远端项目", plan_first="2026-01-01", plan_full="2026-06-01", ), ) await local_collection.add(local_node) await remote_collection.add(remote_node) await binding_manager.bind("project", local_node.node_id, remote_node.node_id) strategy = ProjectSyncStrategy("project", local_collection, remote_collection, binding_manager) strategy.config.update_direction = UpdateDirection.PUSH strategy.config.domain_option = {"pull_group_plan_production_date": True} strategy.config.depend_fields = {} await strategy.bind() updated_nodes = await strategy.update_pair(local_node, remote_node, data_id_map={}) assert [node.node_id for node in updated_nodes] == [local_node.node_id, remote_node.node_id] assert local_node.action == SyncAction.UPDATE assert remote_node.action == SyncAction.UPDATE assert local_node.get_data()["ic_plan_first_production_date"] == "2026-01-01" assert local_node.get_data()["ic_plan_full_production_date"] == "2026-06-01" assert remote_node.get_data()["name"] == "本地项目" assert remote_node.get_data()["company_name"] == "云南公司"