多pipeline同步支持

This commit is contained in:
strepsiades
2026-03-23 21:02:52 +08:00
parent 0812489f9d
commit b51ed0175b
49 changed files with 2110 additions and 792 deletions
@@ -0,0 +1,123 @@
from __future__ import annotations
from pathlib import Path
import pytest
import yaml
from pydantic import BaseModel
from sync_state_machine.config.multi_project_config import build_multi_project_config_from_file
from sync_state_machine.common.binding import BindingManager
from sync_state_machine.common.collection import DataCollection
from sync_state_machine.common.persistence import PersistenceBackend
from sync_state_machine.common.sync_node import SyncNode
from sync_state_machine.pipeline.multi_project_pipeline import (
_project_scope_overrides,
_resolve_project_bootstrap_node_types,
)
def test_build_multi_project_config_reads_explicit_project_bootstrap_node_types(tmp_path: Path) -> None:
config_path = tmp_path / "multi.yaml"
config_path.write_text(
yaml.safe_dump(
{
"global_config": "run_profiles/global.yaml",
"default_project_config": "run_profiles/project.yaml",
"project_bootstrap_node_types": ["company", "supplier"],
"projects": [
{
"local_project_id": "local-project",
"remote_project_id": "remote-project",
}
],
},
allow_unicode=True,
sort_keys=False,
),
encoding="utf-8",
)
config = build_multi_project_config_from_file(project_root=tmp_path, file_path=config_path)
assert config.project_bootstrap_node_types == ["company", "supplier"]
def test_project_scope_overrides_only_inject_project_handler_filter() -> None:
item = type("Item", (), {"local_project_id": "local-project", "remote_project_id": "remote-project"})()
overrides = _project_scope_overrides(item, node_types=["project", "contract", "user"])
assert overrides == {
"local_datasource": {
"handler_configs": {
"project": {"data_id_filter": ["local-project"]},
}
},
"remote_datasource": {
"handler_configs": {
"project": {"data_id_filter": ["remote-project"]},
}
},
}
def test_resolve_project_bootstrap_node_types_prefers_explicit_config() -> None:
config = type(
"Config",
(),
{"project_bootstrap_node_types": ["company", "supplier", "company"]},
)()
resolved = _resolve_project_bootstrap_node_types(
config=config,
shared_node_types=["company", "supplier", "project"],
)
assert resolved == ["company", "supplier"]
class _ProjectSchema(BaseModel):
id: str
class _ProjectNode(SyncNode[dict]):
node_type = "project"
schema = _ProjectSchema
@pytest.mark.asyncio
async def test_binding_manager_prunes_stale_bindings_after_project_scope_cleanup() -> None:
persistence = PersistenceBackend(":memory:")
await persistence.initialize()
try:
local_collection = DataCollection("local", persistence=persistence, auto_persist=False)
remote_collection = DataCollection("remote", persistence=persistence, auto_persist=False)
binding_manager = BindingManager(persistence, auto_persist=False, scope="project_id:p1")
local_node = _ProjectNode(node_id="local-project-1", data={"id": "p1"}, data_id="p1")
remote_node = _ProjectNode(node_id="remote-project-1", data={"id": "p1"}, data_id="p1")
stale_local_node = _ProjectNode(node_id="local-project-2", data={"id": "p2"}, data_id="p2")
stale_remote_node = _ProjectNode(node_id="remote-project-2", data={"id": "p2"}, data_id="p2")
await local_collection.add(local_node)
await local_collection.add(stale_local_node)
await remote_collection.add(remote_node)
await remote_collection.add(stale_remote_node)
await binding_manager.bind("project", "local-project-1", "remote-project-1")
await binding_manager.bind("project", "local-project-2", "remote-project-2")
await local_collection.filter_by_project_ids(["p1"])
await remote_collection.filter_by_project_ids(["p1"])
deleted = await binding_manager.prune_missing_nodes(
local_collection=local_collection,
remote_collection=remote_collection,
node_types=["project"],
)
assert deleted == 1
assert await binding_manager.get_remote_id("project", "local-project-1") == "remote-project-1"
assert await binding_manager.get_remote_id("project", "local-project-2") is None
finally:
await persistence.close()