120 lines
3.9 KiB
Python
120 lines
3.9 KiB
Python
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 "")
|