import pytest from types import SimpleNamespace from unittest.mock import AsyncMock, patch, MagicMock from sync_state_machine.domain.company.sync_strategy import CompanySyncStrategy from sync_state_machine.sync_system.strategy_ops.update_ops import BoundNodePair, build_merged_update_payload from sync_state_machine.domain.construction.sync_strategy import ConstructionTaskSyncStrategy from sync_state_machine.domain.project_detail.sync_strategy import ProjectDetailSyncStrategy from sync_state_machine.sync_system.config import UpdateDirection from schemas.project_extensions.project_detail import ProjectDetailKey @pytest.mark.asyncio async def test_default_strategy_update_orchestrates_pairs_in_strategy_layer(): local_node = SimpleNamespace(node_id="local-1") remote_node = SimpleNamespace(node_id="remote-1") strategy = CompanySyncStrategy("company", MagicMock(), MagicMock(), MagicMock()) strategy.ensure_runtime = MagicMock() updated_node = SimpleNamespace(node_id="target-1") strategy.update_pair = AsyncMock(return_value=[updated_node]) with patch( 'sync_state_machine.sync_system.strategy.build_data_id_normalization_map', new=AsyncMock(return_value={"project": "remote-project"}), ), patch( 'sync_state_machine.sync_system.strategy.collect_bound_node_pairs', new=AsyncMock(return_value=[BoundNodePair(local_node=local_node, remote_node=remote_node)]), ), patch( 'sync_state_machine.sync_system.strategy.emit_schema_diff_report', ): updated = await strategy.update() assert updated == [updated_node] strategy.update_pair.assert_awaited_once_with( local_node, remote_node, {"project": "remote-project"}, ) def test_build_merged_update_payload_supports_include_and_exclude_fields(): source_data = { "id": "remote-1", "project_name": "new-name", "special": "from-source", } target_data = { "id": "remote-1", "project_name": "old-name", "special": "from-target", } include_only = build_merged_update_payload( source_data=source_data, target_data=target_data, include_fields=["special"], ) exclude_special = build_merged_update_payload( source_data=source_data, target_data=target_data, exclude_fields=["special"], ) assert include_only == { "id": "remote-1", "project_name": "old-name", "special": "from-source", } assert exclude_special == { "id": "remote-1", "project_name": "new-name", "special": "from-target", } def test_construction_task_compare_payload_ignores_plan_node_is_audit(): strategy = ConstructionTaskSyncStrategy( "construction_task", MagicMock(), MagicMock(), MagicMock(), ) local_payload = { "plan_node": [{"name": "A", "is_audit": True}], "task_type": "demo", } remote_payload = { "plan_node": [{"name": "A", "is_audit": False}], "task_type": "demo", } assert strategy.normalize_compare_payload(local_payload) == strategy.normalize_compare_payload(remote_payload) def test_default_strategy_exposes_normalize_compare_payload(): strategy = CompanySyncStrategy( "company", MagicMock(), MagicMock(), MagicMock(), ) local_payload = { "id": "remote-company-id", "code": "COMP-001", } remote_payload = { "id": "local-company-id", "code": "COMP-001", } assert strategy.normalize_compare_payload(local_payload) == strategy.normalize_compare_payload(remote_payload) def test_project_detail_compare_payload_ignores_response_only_year_and_is_audit_noise(): strategy = ProjectDetailSyncStrategy( "project_detail_data", MagicMock(), MagicMock(), MagicMock(), ) local_payload = { "project_id": "project-1", "key": "actual_construction", "value": [{"date": "2023-08-23", "capacity": 4.0, "is_audit": False, "year": None}], } remote_payload = { "project_id": "project-1", "key": "actual_construction", "value": [{"date": "2023-08-23", "capacity": 4.0, "is_audit": True, "year": 2023}], } assert strategy.normalize_compare_payload(local_payload) == strategy.normalize_compare_payload(remote_payload) def test_project_detail_compare_payload_normalizes_enum_key_values(): strategy = ProjectDetailSyncStrategy( "project_detail_data", MagicMock(), MagicMock(), MagicMock(), ) remote_payload = { "project_id": "project-1", "key": ProjectDetailKey.PLAN_CONSTRUCTION, "value": [{"date": "2023-06-28", "capacity": 4.0, "year": 2023, "is_audit": True}], } normalized = strategy.normalize_compare_payload(remote_payload) assert normalized["key"] == "plan_construction" assert normalized["value"] == [{"date": "2023-06-28", "capacity": 4.0}] @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=SimpleNamespace(node_id="local-pull", get_data=lambda: {"key": "ensure_production"}), remote_node=SimpleNamespace(node_id="remote-pull", get_data=lambda: {"key": "ensure_production"}), ) generic_pair = BoundNodePair( local_node=SimpleNamespace(node_id="local-generic", get_data=lambda: {"key": "production"}), remote_node=SimpleNamespace(node_id="remote-generic", get_data=lambda: {"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, {})