Files
ecm_sync_system/tests/unit/test_update_ops.py
T
2026-04-01 16:23:43 +08:00

195 lines
6.5 KiB
Python

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, {})