修改多个问题,优化代码结构
This commit is contained in:
@@ -225,6 +225,8 @@ async def test_full_pipeline_multinode_mock_covers_business_points(pipeline_bund
|
||||
|
||||
assert local_collection.get("lp_norm").context.get("bind_data_id") == remote_collection.get("rp_norm").data_id
|
||||
assert remote_collection.get("rp_norm").context.get("bind_data_id") == local_collection.get("lp_norm").data_id
|
||||
assert local_collection.get("lp_auto").context.get("bind_data_id") == remote_collection.get("rp_auto").data_id
|
||||
assert remote_collection.get("rp_auto").context.get("bind_data_id") == local_collection.get("lp_auto").data_id
|
||||
|
||||
assert await binding_manager.get_remote_id("test_project", "lp_auto") == "rp_auto"
|
||||
|
||||
@@ -242,6 +244,12 @@ async def test_full_pipeline_multinode_mock_covers_business_points(pipeline_bund
|
||||
assert create_fail_nodes[0].status == SyncStatus.FAILED
|
||||
assert create_async_nodes[0].status == SyncStatus.PENDING
|
||||
assert create_async_nodes[0].action == SyncAction.NONE
|
||||
assert local_collection.get("lp_create_ok").context.get("bind_data_id") == create_ok_nodes[0].data_id
|
||||
assert create_ok_nodes[0].context.get("bind_data_id") == local_collection.get("lp_create_ok").data_id
|
||||
assert local_collection.get("lp_create_async").context.get("bind_data_id") == create_async_nodes[0].data_id
|
||||
assert create_async_nodes[0].context.get("bind_data_id") == local_collection.get("lp_create_async").data_id
|
||||
assert "bind_data_id" not in local_collection.get("lp_create_fail").context
|
||||
assert "bind_data_id" not in create_fail_nodes[0].context
|
||||
|
||||
assert remote_collection.get("rc_upd_skip").status == SyncStatus.SKIPPED
|
||||
assert remote_collection.get("rc_upd_skip").action == SyncAction.NONE
|
||||
|
||||
@@ -100,9 +100,11 @@ async def test_core_binding_matrix_migrated(
|
||||
await bm.bind("test_project", f"{name}_l", f"{name}_r")
|
||||
|
||||
await phase_core_state(
|
||||
strategy,
|
||||
local.filter(node_type="test_project"),
|
||||
remote.filter(node_type="test_project"),
|
||||
node_type=strategy.node_type,
|
||||
runtime=strategy.ensure_runtime(),
|
||||
binding_manager=bm,
|
||||
local_nodes=local.filter(node_type="test_project"),
|
||||
remote_nodes=remote.filter(node_type="test_project"),
|
||||
)
|
||||
|
||||
if expected_local is not None:
|
||||
@@ -123,9 +125,11 @@ async def test_dependency_resolution_migrated(setup_env):
|
||||
await bm.bind("test_project", "proj_l", "proj_r")
|
||||
|
||||
await phase_core_state(
|
||||
project_strategy,
|
||||
local.filter(node_type="test_project"),
|
||||
remote.filter(node_type="test_project"),
|
||||
node_type=project_strategy.node_type,
|
||||
runtime=project_strategy.ensure_runtime(),
|
||||
binding_manager=bm,
|
||||
local_nodes=local.filter(node_type="test_project"),
|
||||
remote_nodes=remote.filter(node_type="test_project"),
|
||||
)
|
||||
assert local.get("proj_l").binding_status == BindingStatus.NORMAL
|
||||
|
||||
@@ -159,8 +163,22 @@ async def test_dependency_resolution_migrated(setup_env):
|
||||
|
||||
local_contracts = local.filter(node_type="test_contract")
|
||||
remote_contracts = remote.filter(node_type="test_contract")
|
||||
await phase_core_state(contract_strategy, local_contracts, remote_contracts)
|
||||
await phase_dependency_check(contract_strategy, local_contracts, remote_contracts)
|
||||
await phase_core_state(
|
||||
node_type=contract_strategy.node_type,
|
||||
runtime=contract_strategy.ensure_runtime(),
|
||||
binding_manager=bm,
|
||||
local_nodes=local_contracts,
|
||||
remote_nodes=remote_contracts,
|
||||
)
|
||||
await phase_dependency_check(
|
||||
node_type=contract_strategy.node_type,
|
||||
runtime=contract_strategy.ensure_runtime(),
|
||||
depend_fields=dict(contract_strategy.config.depend_fields),
|
||||
local_nodes=local_contracts,
|
||||
remote_nodes=remote_contracts,
|
||||
local_collection=local,
|
||||
remote_collection=remote,
|
||||
)
|
||||
|
||||
assert local.get("contract_ok").binding_status == BindingStatus.MISSING
|
||||
assert local.get("contract_ok").depend_ids == ["proj_l"]
|
||||
@@ -269,8 +287,22 @@ async def test_dependency_check_data_missing_goes_abnormal(setup_env):
|
||||
)
|
||||
|
||||
local_contracts = local.filter(node_type="test_contract")
|
||||
await phase_core_state(strategy, local_contracts, [])
|
||||
await phase_dependency_check(strategy, local_contracts, [])
|
||||
await phase_core_state(
|
||||
node_type=strategy.node_type,
|
||||
runtime=strategy.ensure_runtime(),
|
||||
binding_manager=bm,
|
||||
local_nodes=local_contracts,
|
||||
remote_nodes=[],
|
||||
)
|
||||
await phase_dependency_check(
|
||||
node_type=strategy.node_type,
|
||||
runtime=strategy.ensure_runtime(),
|
||||
depend_fields=dict(strategy.config.depend_fields),
|
||||
local_nodes=local_contracts,
|
||||
remote_nodes=[],
|
||||
local_collection=local,
|
||||
remote_collection=remote,
|
||||
)
|
||||
|
||||
assert local.get("contract_no_data").binding_status == BindingStatus.ABNORMAL
|
||||
|
||||
@@ -306,8 +338,31 @@ async def test_auto_binding_key_id_resolve_fail_migrated(setup_env):
|
||||
await local.add(build_project_node("kid_l", "KID-L", name="KID", code="KID-1"))
|
||||
|
||||
local_nodes = local.filter(node_type="test_project")
|
||||
await phase_core_state(strategy, local_nodes, [])
|
||||
await phase_auto_binding(strategy, local_nodes, [])
|
||||
await phase_core_state(
|
||||
node_type=strategy.node_type,
|
||||
runtime=strategy.ensure_runtime(),
|
||||
binding_manager=bm,
|
||||
local_nodes=local_nodes,
|
||||
remote_nodes=[],
|
||||
)
|
||||
local_flags = {
|
||||
node.node_id: strategy.config.local_orphan_action == OrphanAction.CREATE_REMOTE
|
||||
for node in local_nodes
|
||||
}
|
||||
remote_flags = {}
|
||||
await phase_auto_binding(
|
||||
node_type=strategy.node_type,
|
||||
runtime=strategy.ensure_runtime(),
|
||||
config=strategy.config,
|
||||
local_collection=local,
|
||||
remote_collection=remote,
|
||||
binding_manager=bm,
|
||||
local_nodes=local_nodes,
|
||||
remote_nodes=[],
|
||||
id_field_hints=dict(strategy.config.depend_fields),
|
||||
local_orphan_create_enabled_by_node=local_flags,
|
||||
remote_orphan_create_enabled_by_node=remote_flags,
|
||||
)
|
||||
|
||||
node = local.get("kid_l")
|
||||
assert node.binding_status == BindingStatus.DEPENDENCY_ERROR
|
||||
|
||||
@@ -110,7 +110,7 @@ async def test_api_client_logs_one_line_request_summary_in_debug_mode(caplog: py
|
||||
fake_client = _FakeAsyncClient()
|
||||
client._client = fake_client
|
||||
|
||||
with caplog.at_level(logging.DEBUG, logger=logger_name):
|
||||
with caplog.at_level(logging.INFO, logger=logger_name):
|
||||
await client.request("GET", "/ping")
|
||||
|
||||
assert "ApiClient request: method=GET endpoint=/ping elapsed=" in caplog.text
|
||||
assert "🔎 ApiClient request: method=GET endpoint=/ping elapsed=" in caplog.text
|
||||
|
||||
@@ -20,7 +20,7 @@ class _DS(BaseDataSource):
|
||||
class _FailStrategy(DefaultSyncStrategy):
|
||||
schema = object # type: ignore
|
||||
|
||||
async def bind(self, **kwargs):
|
||||
async def bind(self):
|
||||
raise RuntimeError("bind failed")
|
||||
|
||||
async def create(self):
|
||||
@@ -39,7 +39,7 @@ class _OkStrategy(DefaultSyncStrategy):
|
||||
self.create_calls = 0
|
||||
self.update_calls = 0
|
||||
|
||||
async def bind(self, **kwargs):
|
||||
async def bind(self):
|
||||
self.bound = True
|
||||
|
||||
async def create(self):
|
||||
|
||||
@@ -1,26 +1,62 @@
|
||||
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 SimpleNamespace(
|
||||
node_id=node_id,
|
||||
data_id=node_id,
|
||||
action=None,
|
||||
context={},
|
||||
get_data=lambda: data,
|
||||
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_update_partitions_group_production_plans_before_generic_logic():
|
||||
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(
|
||||
@@ -35,13 +71,6 @@ async def test_project_detail_update_partitions_group_production_plans_before_ge
|
||||
generic_node = SimpleNamespace(node_id="generic-update")
|
||||
|
||||
with patch.object(
|
||||
strategy,
|
||||
"collect_update_pairs",
|
||||
new=AsyncMock(return_value=[pull_pair, generic_pair]),
|
||||
), patch(
|
||||
"sync_state_machine.domain.project_detail.sync_strategy.build_data_id_normalization_map",
|
||||
new=AsyncMock(return_value={}),
|
||||
), patch.object(
|
||||
strategy,
|
||||
"prepare_update_for_direction",
|
||||
new=AsyncMock(return_value=pull_node),
|
||||
@@ -50,9 +79,12 @@ async def test_project_detail_update_partitions_group_production_plans_before_ge
|
||||
"update_pair",
|
||||
new=AsyncMock(return_value=[generic_node]),
|
||||
) as mock_update_pair:
|
||||
updated_nodes = await strategy.update()
|
||||
updated_by_id = await strategy.process_update_pairs([pull_pair, generic_pair], {})
|
||||
|
||||
assert updated_nodes == [pull_node, generic_node]
|
||||
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,
|
||||
@@ -65,4 +97,51 @@ async def test_project_detail_update_partitions_group_production_plans_before_ge
|
||||
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
|
||||
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
|
||||
@@ -2,34 +2,32 @@ 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, run_update
|
||||
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_run_update_delegates_to_strategy_pair_planner():
|
||||
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 = MagicMock()
|
||||
strategy.node_type = "TestNode"
|
||||
strategy.local_collection = MagicMock()
|
||||
strategy.remote_collection = MagicMock()
|
||||
strategy.binding_manager = MagicMock()
|
||||
strategy.reset_schema_diff_validator = MagicMock()
|
||||
strategy.emit_schema_diff_report = MagicMock()
|
||||
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_ops.update_ops.build_data_id_normalization_map',
|
||||
'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_ops.update_ops.collect_bound_node_pairs',
|
||||
'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 run_update(strategy)
|
||||
updated = await strategy.update()
|
||||
|
||||
assert updated == [updated_node]
|
||||
|
||||
@@ -155,3 +153,42 @@ def test_project_detail_compare_payload_normalizes_enum_key_values():
|
||||
|
||||
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, {})
|
||||
|
||||
Reference in New Issue
Block a user