from __future__ import annotations from pathlib import Path import pytest from sync_state_machine.common.types import BindingStatus, SyncAction, SyncStatus from sync_state_machine.engine import StateMachineConfig, StateMachineRuntime from sync_state_machine.engine.events import e01_bootstrap from sync_state_machine.sync_system.strategy_ops.reset_ops import _is_create_zombie from tests.mocks.mock_datasource import MockSyncNode CFG_PATH = Path(__file__).resolve().parents[2] / "sync_state_machine" / "config" / "node_state_machine.yaml" def _new_node(*, node_id: str, action: SyncAction, status: SyncStatus, data_id: str) -> MockSyncNode: return MockSyncNode( node_id=node_id, data_id=data_id, data={"id": data_id or "", "name": "mock"}, action=action, status=status, binding_status=BindingStatus.WARNING, error="seed-error", ) _CASES = [ (action, status, data_id) for action in SyncAction for status in SyncStatus for data_id in ("", "id-1") ] @pytest.mark.parametrize("action,status,data_id", _CASES) def test_is_create_zombie_matrix(action: SyncAction, status: SyncStatus, data_id: str) -> None: node = _new_node(node_id=f"z-{action.value}-{status.value}-{data_id or 'empty'}", action=action, status=status, data_id=data_id) expected = action == SyncAction.CREATE and (status == SyncStatus.FAILED or not bool(data_id)) assert _is_create_zombie(node) is expected @pytest.mark.parametrize("action,status,data_id", _CASES) def test_e01_bootstrap_accepts_all_combinations(action: SyncAction, status: SyncStatus, data_id: str) -> None: runtime = StateMachineRuntime(StateMachineConfig.load(CFG_PATH)) node = _new_node(node_id=f"e01-{action.value}-{status.value}-{data_id or 'empty'}", action=action, status=status, data_id=data_id) original_action = node.action original_status = node.status original_binding = node.binding_status original_error = node.error is_zombie = _is_create_zombie(node) decision = e01_bootstrap( runtime, node=node, is_create_zombie=is_zombie, bootstrap_source_present=True, has_binding_record=False, ) if is_zombie: assert decision.to_state == "S15" assert node.action == original_action assert node.status == original_status assert node.binding_status == original_binding assert node.error == original_error else: assert decision.to_state == "S00" assert node.binding_status == BindingStatus.UNCHECKED assert node.action == SyncAction.NONE assert node.status == SyncStatus.PENDING assert node.error is None def test_e01_bootstrap_cleans_unbound_persistence_only_node_on_source_miss() -> None: runtime = StateMachineRuntime(StateMachineConfig.load(CFG_PATH)) node = _new_node( node_id="e01-stale-unbound", action=SyncAction.NONE, status=SyncStatus.PENDING, data_id="id-1", ) decision = e01_bootstrap( runtime, node=node, is_create_zombie=False, bootstrap_source_present=False, has_binding_record=False, ) assert decision.to_state == "S15" def test_e01_bootstrap_isolates_bound_node_on_source_miss() -> None: runtime = StateMachineRuntime(StateMachineConfig.load(CFG_PATH)) node = _new_node( node_id="e01-stale-bound", action=SyncAction.NONE, status=SyncStatus.PENDING, data_id="id-1", ) decision = e01_bootstrap( runtime, node=node, is_create_zombie=False, bootstrap_source_present=False, has_binding_record=True, ) assert decision.to_state == "S17" assert node.binding_status == BindingStatus.ABNORMAL assert node.action == SyncAction.NONE assert node.status == SyncStatus.FAILED assert node.context["bootstrap_blocked"] is True assert "BOOTSTRAP_SOURCE_MISSING" in (node.error or "")