更新了部分测试
This commit is contained in:
@@ -173,7 +173,12 @@ logging:
|
||||
- `local_orphan_action: none`
|
||||
- `remote_orphan_action: none`
|
||||
- `update_direction: none`
|
||||
- 当三者同时为 `none/none/none` 时,pipeline 会跳过该 node_type 的 bind/create/update。
|
||||
- `skip_sync=true` 的当前语义是:
|
||||
- 仍会执行该 node_type 的 load / bind
|
||||
- 只跳过 create / update 写入阶段
|
||||
- 适合“保留绑定与依赖可见性,但临时禁止写入”的场景。
|
||||
- `none/none/none` 是更推荐的输入风格,因为它表达的是“该类型不做创建、不做补本地、不做更新”。
|
||||
- 这组配置不会绕过前置 load / bind / 依赖检查;它只会把该类型变成无写入策略。
|
||||
|
||||
### 4.6 Handler 级参数(`handler_configs`)
|
||||
|
||||
@@ -227,7 +232,7 @@ project_detail_data:
|
||||
- 若发现差异,会在日志中打印差异样本,并在汇总表新增 `Consistent` 列展示 `Y` / `N(差异数)`。
|
||||
- 全局跳过
|
||||
- `local_orphan_action=none` + `remote_orphan_action=none` + `update_direction=none`
|
||||
可直接跳过该类型的 bind/create/update。
|
||||
可将该类型收敛为“只做 load/bind,不做 create/update 写入”的只读同步策略。
|
||||
|
||||
## 6. 覆盖配置建议
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
- 校验 datasource/handler 工具链、配置严格性、导入面稳定性。
|
||||
- 对具体 domain 的 update/create 行为做快速回归。
|
||||
|
||||
单元测试取舍原则:
|
||||
- 优先测文档里承诺的行为语义,不优先测单纯转发、setter、空包装器。
|
||||
- 如果一个测试只是在断言“函数 A 调了函数 B”,但不验证最终行为,通常应该删掉或改写成行为测试。
|
||||
- 如果需要 test double,优先让它承载真实行为断言,不保留仅为凑接口而存在的空壳类。
|
||||
|
||||
常用命令:
|
||||
- 全量单元测试:`pytest tests/unit -q`
|
||||
- 指定文件:`pytest tests/unit/test_update_ops.py -q`
|
||||
@@ -165,6 +170,7 @@ python tools/remote_env.py \
|
||||
1. 先跑 `pytest tests/unit -q`。
|
||||
2. 如果改动影响 pipeline,再跑目标集成测试。
|
||||
3. 如果改动影响真实接口流程,再使用 `tools/remote_env.py` 管理远程环境。
|
||||
4. 如果修改了配置语义或 pipeline 控制流,优先补一条直接覆盖设计语义的 pytest,而不是只补装配/转发测试。
|
||||
|
||||
### 3.2 真实接口联调
|
||||
|
||||
|
||||
@@ -65,9 +65,9 @@ class ApiDataSource(BaseDataSource):
|
||||
"""关闭数据源(关闭 HTTP 客户端)"""
|
||||
await self.client.close()
|
||||
|
||||
def register_handler(self, handler: NodeHandler) -> None:
|
||||
def add_handler(self, handler: NodeHandler) -> None:
|
||||
"""
|
||||
注册业务 Handler
|
||||
添加业务 Handler
|
||||
|
||||
Args:
|
||||
handler: API Handler 实例
|
||||
@@ -75,8 +75,8 @@ class ApiDataSource(BaseDataSource):
|
||||
handler.set_api_client(self.client)
|
||||
handler.set_poll_mode(self.poll_mode)
|
||||
|
||||
# 调用基类注册方法
|
||||
super().register_handler(handler)
|
||||
# 调用基类添加方法
|
||||
super().add_handler(handler)
|
||||
|
||||
# 如果 collection 已设置,确保新 handler 拿到引用
|
||||
if self._collection is not None:
|
||||
|
||||
@@ -57,9 +57,9 @@ class BaseDataSource(ABC):
|
||||
# 1. 初始化 DataSource
|
||||
datasource = MyDataSource()
|
||||
|
||||
# 2. 注册 Handler
|
||||
datasource.register_handler(project_handler)
|
||||
datasource.register_handler(contract_handler)
|
||||
# 2. 添加 Handler
|
||||
datasource.add_handler(project_handler)
|
||||
datasource.add_handler(contract_handler)
|
||||
|
||||
# 3. 设置 Collection
|
||||
datasource.set_collection(collection)
|
||||
@@ -360,15 +360,15 @@ class BaseDataSource(ABC):
|
||||
|
||||
# ========== Handler 管理 ==========
|
||||
|
||||
def register_handler(self, handler: "NodeHandler") -> None:
|
||||
def add_handler(self, handler: "NodeHandler") -> None:
|
||||
"""
|
||||
注册 Handler
|
||||
添加 Handler 到当前 datasource
|
||||
|
||||
Args:
|
||||
handler: 节点处理器实例
|
||||
|
||||
示例:
|
||||
datasource.register_handler(ContractNodeHandler(api_client))
|
||||
datasource.add_handler(ContractNodeHandler(api_client))
|
||||
"""
|
||||
self._handlers[handler.node_type] = handler
|
||||
|
||||
|
||||
@@ -495,59 +495,6 @@ class BaseNodeHandler(ABC, Generic[T]):
|
||||
return self.create_node(data)
|
||||
|
||||
|
||||
class CompatibleNodeHandler(BaseNodeHandler[T]):
|
||||
def __init__(self, handler: BaseNodeHandler[T]):
|
||||
super().__init__(handler.node_type, handler.node_class, handler.schema)
|
||||
self._handler = handler
|
||||
|
||||
def set_collection(self, collection: "DataCollection") -> None:
|
||||
super().set_collection(collection)
|
||||
self._handler.set_collection(collection)
|
||||
|
||||
def set_handler_config(self, config: Dict[str, Any]) -> None:
|
||||
self._handler.set_handler_config(config)
|
||||
self.handler_config = dict(self._handler.handler_config)
|
||||
|
||||
def set_api_client(self, client: "ApiClient") -> None:
|
||||
self._handler.set_api_client(client)
|
||||
|
||||
def set_poll_mode(self, mode: str) -> None:
|
||||
self._handler.set_poll_mode(mode)
|
||||
|
||||
def get_update_fields(self, *, exclude: frozenset[str] = frozenset({"id"})) -> List[str]:
|
||||
return self._handler.get_update_fields(exclude=exclude)
|
||||
|
||||
async def load(self) -> List['SyncNode']:
|
||||
return await self._handler.load()
|
||||
|
||||
async def create_all(
|
||||
self,
|
||||
nodes: List["SyncNode"]
|
||||
) -> List[TaskResult]:
|
||||
return await self._handler.create_all(nodes)
|
||||
|
||||
async def update_all(
|
||||
self,
|
||||
nodes: List["SyncNode"]
|
||||
) -> List[TaskResult]:
|
||||
return await self._handler.update_all(nodes)
|
||||
|
||||
async def delete_all(
|
||||
self,
|
||||
nodes: List["SyncNode"]
|
||||
) -> List[TaskResult]:
|
||||
return await self._handler.delete_all(nodes)
|
||||
|
||||
async def poll_tasks(self, task_ids: List[str]) -> Dict[str, TaskResult]:
|
||||
return await self._handler.poll_tasks(task_ids)
|
||||
|
||||
async def validate(self, data: Dict[str, Any]) -> ValidationResult:
|
||||
return await self._handler.validate(data)
|
||||
|
||||
def extract_id(self, data: Dict[str, Any]) -> str:
|
||||
return self._handler.extract_id(data)
|
||||
|
||||
|
||||
class NodeHandlerRegistry:
|
||||
"""节点处理器注册表"""
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class JsonlDataSource(BaseDataSource):
|
||||
|
||||
示例:
|
||||
datasource = JsonlDataSource(Path("filtered_datasource/datasource/ecm-2025-12-02"))
|
||||
datasource.register_handler(ContractJsonlHandler(datasource))
|
||||
datasource.add_handler(ContractJsonlHandler(datasource))
|
||||
|
||||
collection = DataCollection()
|
||||
datasource.set_collection(collection)
|
||||
|
||||
@@ -4,13 +4,13 @@ Domain Handler Registry - 集中管理所有领域的 API 和 JSONL Handlers
|
||||
使用方式:
|
||||
from sync_state_machine.domain.registry import API_HANDLERS, JSONL_HANDLERS
|
||||
|
||||
# 注册所有 API handlers
|
||||
# 添加所有 API handlers
|
||||
for handler_class in API_HANDLERS:
|
||||
api_ds.register_handler(handler_class())
|
||||
api_ds.add_handler(handler_class())
|
||||
|
||||
# 注册所有 JSONL handlers
|
||||
# 添加所有 JSONL handlers
|
||||
for handler_class in JSONL_HANDLERS:
|
||||
jsonl_ds.register_handler(handler_class(jsonl_ds))
|
||||
jsonl_ds.add_handler(handler_class(jsonl_ds))
|
||||
"""
|
||||
|
||||
# ============================================
|
||||
|
||||
@@ -67,7 +67,7 @@ class CopyDataPipeline:
|
||||
supported_types = []
|
||||
for handler_class in JSONL_HANDLERS:
|
||||
handler = handler_class(self.source_datasource)
|
||||
self.source_datasource.register_handler(handler)
|
||||
self.source_datasource.add_handler(handler)
|
||||
supported_types.append(handler.node_type)
|
||||
self._owns_source = True
|
||||
self._supported_types = supported_types
|
||||
@@ -82,7 +82,7 @@ class CopyDataPipeline:
|
||||
self.target_datasource = JsonlDataSource(dir_path=Path(target_dir), read_only=False)
|
||||
# 自动注册所有 JSONL handlers
|
||||
for handler_class in JSONL_HANDLERS:
|
||||
self.target_datasource.register_handler(handler_class(self.target_datasource))
|
||||
self.target_datasource.add_handler(handler_class(self.target_datasource))
|
||||
self._owns_target = True
|
||||
else:
|
||||
raise ValueError("Must provide either target_datasource or target_dir")
|
||||
@@ -228,7 +228,7 @@ class CopyDataPipeline:
|
||||
handler_class = DomainRegistry.get_jsonl_handler(node_type)
|
||||
if handler_class:
|
||||
handler = handler_class(datasource)
|
||||
datasource.register_handler(handler)
|
||||
datasource.add_handler(handler)
|
||||
|
||||
def _print_summary(self):
|
||||
"""打印执行摘要"""
|
||||
|
||||
@@ -151,14 +151,14 @@ async def _initialize_datasource(datasource, *, endpoint_name: str) -> None:
|
||||
ensure_builtin_datasource_types_registered()
|
||||
|
||||
|
||||
def _register_handlers_for_endpoint(
|
||||
def _add_handlers_for_endpoint(
|
||||
*,
|
||||
datasource,
|
||||
ds_config: DataSourceConfig,
|
||||
node_types: List[str],
|
||||
) -> None:
|
||||
for node_type in node_types:
|
||||
datasource.register_handler(
|
||||
datasource.add_handler(
|
||||
_create_handler(
|
||||
node_type=node_type,
|
||||
ds_config=ds_config,
|
||||
@@ -167,18 +167,18 @@ def _register_handlers_for_endpoint(
|
||||
)
|
||||
|
||||
|
||||
def _register_handlers(
|
||||
def _add_handlers(
|
||||
config: PipelineRunConfig,
|
||||
local_ds,
|
||||
remote_ds,
|
||||
all_types: List[str],
|
||||
) -> None:
|
||||
_register_handlers_for_endpoint(
|
||||
_add_handlers_for_endpoint(
|
||||
datasource=local_ds,
|
||||
ds_config=config.local_datasource,
|
||||
node_types=all_types,
|
||||
)
|
||||
_register_handlers_for_endpoint(
|
||||
_add_handlers_for_endpoint(
|
||||
datasource=remote_ds,
|
||||
ds_config=config.remote_datasource,
|
||||
node_types=all_types,
|
||||
@@ -320,7 +320,7 @@ async def create_pipeline_from_config(
|
||||
project_root=_project_root(),
|
||||
)
|
||||
await _initialize_datasource(remote_ds, endpoint_name="remote")
|
||||
_register_handlers(config, local_ds, remote_ds, all_types)
|
||||
_add_handlers(config, local_ds, remote_ds, all_types)
|
||||
|
||||
local_collection, remote_collection, binding_manager = _build_collections_and_bindings(
|
||||
persistence=persistence,
|
||||
|
||||
@@ -172,15 +172,6 @@ class FullSyncPipeline:
|
||||
self._logger.error(f"❌ Strategy failed but pipeline continues: {node_type} | {type(exc).__name__}: {exc}")
|
||||
continue
|
||||
|
||||
@staticmethod
|
||||
def _is_noop_strategy(strategy: BaseSyncStrategy[Any]) -> bool:
|
||||
cfg = strategy.config
|
||||
return (
|
||||
cfg.local_orphan_action == OrphanAction.NONE
|
||||
and cfg.remote_orphan_action == OrphanAction.NONE
|
||||
and cfg.update_direction == UpdateDirection.NONE
|
||||
)
|
||||
|
||||
async def phase_bootstrap(self) -> None:
|
||||
await self._load_persistence_state()
|
||||
self._logger.info("✅ Bootstrap: persistence loaded")
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
## 设计原则
|
||||
- 不绕过 schema/状态机校验;配置错误必须显式失败。
|
||||
- 单元测试聚焦纯逻辑,集成测试覆盖流水线与边界行为。
|
||||
- 优先保留行为测试,避免只验证转发器、setter 或仅供测试存在的空壳包装层。
|
||||
- 所有测试均从仓库根路径解析配置,避免目录迁移导致路径脆弱。
|
||||
- 保留高价值回归测试,移除仅重复路径或无断言价值的历史脚本型测试(脚本保留在 `scripts/`,不纳入 pytest)。
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ async def pipeline_bundle():
|
||||
local_ds = InMemoryDataSource(poll_max_retries=2, poll_interval=0)
|
||||
remote_ds = InMemoryDataSource(poll_max_retries=2, poll_interval=0)
|
||||
|
||||
local_ds.register_handler(
|
||||
local_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_project",
|
||||
node_class=TestProjectNode,
|
||||
@@ -120,7 +120,7 @@ async def pipeline_bundle():
|
||||
load_nodes=local_project_nodes,
|
||||
)
|
||||
)
|
||||
local_ds.register_handler(
|
||||
local_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_contract",
|
||||
node_class=TestContractNode,
|
||||
@@ -129,7 +129,7 @@ async def pipeline_bundle():
|
||||
)
|
||||
)
|
||||
|
||||
remote_ds.register_handler(
|
||||
remote_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_project",
|
||||
node_class=TestProjectNode,
|
||||
@@ -143,7 +143,7 @@ async def pipeline_bundle():
|
||||
update_mode_by_code={"UPD-1": "success"},
|
||||
)
|
||||
)
|
||||
remote_ds.register_handler(
|
||||
remote_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_contract",
|
||||
node_class=TestContractNode,
|
||||
@@ -453,7 +453,7 @@ async def test_pipeline_create_then_update_completes_in_single_run_with_reload()
|
||||
local_ds = InMemoryDataSource(poll_max_retries=2, poll_interval=0)
|
||||
remote_ds = InMemoryDataSource(poll_max_retries=2, poll_interval=0)
|
||||
|
||||
local_ds.register_handler(
|
||||
local_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_project",
|
||||
node_class=TestProjectNode,
|
||||
@@ -464,7 +464,7 @@ async def test_pipeline_create_then_update_completes_in_single_run_with_reload()
|
||||
)
|
||||
)
|
||||
remote_handler = _MutableProjectRemoteHandler()
|
||||
remote_ds.register_handler(remote_handler)
|
||||
remote_ds.add_handler(remote_handler)
|
||||
|
||||
project_strategy = TestProjectStrategy("test_project", local_collection, remote_collection, binding_manager)
|
||||
project_strategy.update_config(
|
||||
|
||||
@@ -73,7 +73,7 @@ async def test_full_pipeline_multinode_mock_persisted_db_snapshot() -> None:
|
||||
local_ds = InMemoryDataSource(poll_max_retries=2, poll_interval=0)
|
||||
remote_ds = InMemoryDataSource(poll_max_retries=2, poll_interval=0)
|
||||
|
||||
local_ds.register_handler(
|
||||
local_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_project",
|
||||
node_class=TestProjectNode,
|
||||
@@ -81,7 +81,7 @@ async def test_full_pipeline_multinode_mock_persisted_db_snapshot() -> None:
|
||||
load_nodes=local_project_nodes,
|
||||
)
|
||||
)
|
||||
local_ds.register_handler(
|
||||
local_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_contract",
|
||||
node_class=TestContractNode,
|
||||
@@ -90,7 +90,7 @@ async def test_full_pipeline_multinode_mock_persisted_db_snapshot() -> None:
|
||||
)
|
||||
)
|
||||
|
||||
remote_ds.register_handler(
|
||||
remote_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_project",
|
||||
node_class=TestProjectNode,
|
||||
@@ -104,7 +104,7 @@ async def test_full_pipeline_multinode_mock_persisted_db_snapshot() -> None:
|
||||
update_mode_by_code={"UPD-1": "success"},
|
||||
)
|
||||
)
|
||||
remote_ds.register_handler(
|
||||
remote_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_contract",
|
||||
node_class=TestContractNode,
|
||||
|
||||
@@ -97,8 +97,8 @@ class _DS(BaseDataSource):
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_all_continue_when_one_handler_fails(capsys) -> None:
|
||||
ds = _DS()
|
||||
ds.register_handler(_FailHandler())
|
||||
ds.register_handler(_OkHandler())
|
||||
ds.add_handler(_FailHandler())
|
||||
ds.add_handler(_OkHandler())
|
||||
ds.set_collection(DataCollection("local"))
|
||||
|
||||
await ds.load_all(order=["fail", "ok"])
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from sync_state_machine.common.registry import DomainRegistry
|
||||
@@ -17,7 +18,7 @@ from sync_state_machine.config import (
|
||||
from sync_state_machine.datasource import BaseDataSource
|
||||
from sync_state_machine.datasource.handler import BaseNodeHandler
|
||||
from sync_state_machine.datasource.jsonl import BaseJsonlHandler, JsonlDataSource
|
||||
from sync_state_machine.pipeline.factory import _register_handlers
|
||||
from sync_state_machine.pipeline.factory import create_pipeline_from_config
|
||||
from sync_state_machine.sync_system.strategy import DefaultSyncStrategy
|
||||
|
||||
|
||||
@@ -79,7 +80,8 @@ def _register_test_db_datasource_type() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_register_handlers_uses_db_handler_for_db_datasource(tmp_path: Path) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_handlers_uses_db_handler_for_db_datasource(tmp_path: Path) -> None:
|
||||
_register_test_db_datasource_type()
|
||||
|
||||
if not DomainRegistry.is_registered("test_db_handler"):
|
||||
@@ -104,10 +106,9 @@ def test_register_handlers_uses_db_handler_for_db_datasource(tmp_path: Path) ->
|
||||
logging=LoggingConfig(initialize=False),
|
||||
)
|
||||
|
||||
local_ds = BaseDataSource()
|
||||
remote_ds = JsonlDataSource(tmp_path)
|
||||
|
||||
_register_handlers(config, local_ds, remote_ds, ["test_db_handler"])
|
||||
|
||||
assert isinstance(local_ds.get_handler("test_db_handler"), TestDbHandler)
|
||||
assert isinstance(remote_ds.get_handler("test_db_handler"), TestJsonlHandler)
|
||||
pipeline = await create_pipeline_from_config(config)
|
||||
try:
|
||||
assert isinstance(pipeline.local_datasource.get_handler("test_db_handler"), TestDbHandler)
|
||||
assert isinstance(pipeline.remote_datasource.get_handler("test_db_handler"), TestJsonlHandler)
|
||||
finally:
|
||||
await pipeline.close()
|
||||
@@ -1,86 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from sync_state_machine.common.sync_node import SyncNode
|
||||
from sync_state_machine.datasource.api.handler import BaseApiHandler
|
||||
from sync_state_machine.datasource.handler import BaseNodeHandler, CompatibleNodeHandler
|
||||
from sync_state_machine.datasource.task_result import TaskResult
|
||||
|
||||
|
||||
class SharedConfigSchema(BaseModel):
|
||||
__test__ = False
|
||||
id: str
|
||||
|
||||
|
||||
class SharedConfigNode(SyncNode[SharedConfigSchema]):
|
||||
__test__ = False
|
||||
node_type = "shared_config_demo"
|
||||
schema = SharedConfigSchema
|
||||
|
||||
|
||||
class SharedConfigBaseHandler(BaseNodeHandler[SharedConfigSchema]):
|
||||
__test__ = False
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__("shared_config_demo", SharedConfigNode, SharedConfigSchema)
|
||||
|
||||
async def load(self):
|
||||
return []
|
||||
|
||||
async def create_all(self, nodes):
|
||||
return []
|
||||
|
||||
async def update_all(self, nodes):
|
||||
return []
|
||||
|
||||
def extract_id(self, data: dict[str, Any]) -> str:
|
||||
return str(data.get("id", ""))
|
||||
|
||||
|
||||
class SharedConfigApiHandler(BaseApiHandler[SharedConfigSchema]):
|
||||
__test__ = False
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._node_type = "shared_api_demo"
|
||||
self._node_class = SharedConfigNode
|
||||
self._schema = SharedConfigSchema
|
||||
|
||||
async def load(self):
|
||||
return []
|
||||
|
||||
async def create_all(self, nodes):
|
||||
return [TaskResult.success(node_id=node.node_id) for node in nodes]
|
||||
|
||||
async def update_all(self, nodes):
|
||||
return [TaskResult.success(node_id=node.node_id) for node in nodes]
|
||||
|
||||
async def delete_all(self, nodes):
|
||||
return [TaskResult.success(node_id=node.node_id) for node in nodes]
|
||||
|
||||
|
||||
def test_base_handler_supports_shared_handler_config() -> None:
|
||||
handler = SharedConfigBaseHandler()
|
||||
|
||||
handler.set_handler_config({"mode": "local", "batch_size": 10})
|
||||
|
||||
assert handler.handler_config == {"mode": "local", "batch_size": 10}
|
||||
|
||||
|
||||
def test_compatible_handler_forwards_shared_handler_config() -> None:
|
||||
handler = CompatibleNodeHandler(SharedConfigBaseHandler())
|
||||
|
||||
handler.set_handler_config({"mode": "remote"})
|
||||
|
||||
assert handler.handler_config == {"mode": "remote"}
|
||||
|
||||
|
||||
def test_api_handler_supports_shared_handler_config() -> None:
|
||||
handler = SharedConfigApiHandler()
|
||||
|
||||
handler.set_handler_config({"filter_project_id": ["p1", "p2"]})
|
||||
|
||||
assert handler.handler_config == {"filter_project_id": ["p1", "p2"]}
|
||||
@@ -35,14 +35,18 @@ class _OkStrategy(BaseSyncStrategy):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.bound = False
|
||||
self.create_calls = 0
|
||||
self.update_calls = 0
|
||||
|
||||
async def bind(self, **kwargs):
|
||||
self.bound = True
|
||||
|
||||
async def create(self):
|
||||
self.create_calls += 1
|
||||
return []
|
||||
|
||||
async def update(self):
|
||||
self.update_calls += 1
|
||||
return []
|
||||
|
||||
|
||||
@@ -87,6 +91,52 @@ async def test_phase_sync_by_node_type_continues_after_strategy_error(tmp_path:
|
||||
await pipeline.phase_sync_by_node_type()
|
||||
|
||||
assert ok_strategy.bound is True
|
||||
assert ok_strategy.create_calls == 0
|
||||
assert ok_strategy.update_calls == 0
|
||||
assert pipeline.stats["failed"] >= 1
|
||||
|
||||
await pipeline.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_phase_sync_by_node_type_skip_sync_keeps_bind_but_skips_writes(tmp_path: Path) -> None:
|
||||
db_path = tmp_path / "state.db"
|
||||
persistence = PersistenceBackend(str(db_path))
|
||||
await persistence.initialize()
|
||||
|
||||
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)
|
||||
|
||||
strategy = _OkStrategy("ok", local_collection, remote_collection, binding_manager)
|
||||
strategy.skip_sync = True
|
||||
|
||||
pipeline = FullSyncPipeline(
|
||||
local_datasource=_DS(),
|
||||
remote_datasource=_DS(),
|
||||
strategies=[strategy],
|
||||
persistence=persistence,
|
||||
local_collection=local_collection,
|
||||
remote_collection=remote_collection,
|
||||
binding_manager=binding_manager,
|
||||
node_types=["ok"],
|
||||
load_order=["ok"],
|
||||
sync_order=["ok"],
|
||||
enable_persistence=False,
|
||||
)
|
||||
|
||||
load_calls: list[tuple[str, str]] = []
|
||||
|
||||
async def _record_load(node_type: str, reason: str = "") -> None:
|
||||
load_calls.append((node_type, reason))
|
||||
|
||||
pipeline._load_node_type = _record_load # type: ignore[method-assign]
|
||||
|
||||
await pipeline.phase_sync_by_node_type()
|
||||
|
||||
assert load_calls == [("ok", "pre-strategy refresh")]
|
||||
assert strategy.bound is True
|
||||
assert strategy.create_calls == 0
|
||||
assert strategy.update_calls == 0
|
||||
|
||||
await pipeline.close()
|
||||
|
||||
@@ -45,7 +45,7 @@ async def test_jsonl_project_load_applies_target_project_filter(tmp_path):
|
||||
datasource = JsonlDataSource(tmp_path)
|
||||
handler = BaseJsonlHandler(datasource, "project", _ProjectNode, _ProjectSchema)
|
||||
handler.set_handler_config({"data_id_filter": ["p1"]})
|
||||
datasource.register_handler(handler)
|
||||
datasource.add_handler(handler)
|
||||
datasource.set_collection(DataCollection("local"))
|
||||
|
||||
await datasource.load_all(order=["project"])
|
||||
@@ -70,7 +70,7 @@ async def test_jsonl_non_project_load_filters_rows_by_project_id(tmp_path):
|
||||
datasource = JsonlDataSource(tmp_path)
|
||||
handler = BaseJsonlHandler(datasource, "contract", _BizNode, _BizSchema)
|
||||
handler.set_handler_config({"data_id_filter": ["p1"]})
|
||||
datasource.register_handler(handler)
|
||||
datasource.add_handler(handler)
|
||||
datasource.set_collection(DataCollection("local"))
|
||||
|
||||
await datasource.load_all(order=["contract"])
|
||||
@@ -104,8 +104,8 @@ async def test_jsonl_non_project_load_inherits_project_filter_from_loaded_projec
|
||||
project_handler = BaseJsonlHandler(datasource, "project", _ProjectNode, _ProjectSchema)
|
||||
project_handler.set_handler_config({"data_id_filter": ["p1"]})
|
||||
contract_handler = BaseJsonlHandler(datasource, "contract", _BizNode, _BizSchema)
|
||||
datasource.register_handler(project_handler)
|
||||
datasource.register_handler(contract_handler)
|
||||
datasource.add_handler(project_handler)
|
||||
datasource.add_handler(contract_handler)
|
||||
datasource.set_collection(DataCollection("local"))
|
||||
|
||||
await datasource.load_all(order=["project", "contract"])
|
||||
|
||||
@@ -27,7 +27,7 @@ async def test_units_jsonl_handler_loads_units_filename(tmp_path):
|
||||
)
|
||||
|
||||
datasource = JsonlDataSource(tmp_path)
|
||||
datasource.register_handler(UnitsJsonlHandler(datasource))
|
||||
datasource.add_handler(UnitsJsonlHandler(datasource))
|
||||
datasource.set_collection(DataCollection("local"))
|
||||
|
||||
await datasource.load_all(order=["units"])
|
||||
|
||||
Reference in New Issue
Block a user