"""Helper script for entity_sync.robot integration tests. Each subcommand is a self-contained check that prints a sentinel on success. """ from __future__ import annotations import sys from pathlib import Path # Ensure local source tree is importable _SRC = str(Path(__file__).resolve().parents[1] / "src") if _SRC not in sys.path: sys.path.insert(0, _SRC) from cleveragents.a2a.facade import A2aLocalFacade # noqa: E402 from cleveragents.a2a.models import A2aRequest # noqa: E402 from cleveragents.a2a.sync_models import ( # noqa: E402 ConflictResolution, SyncDirection, SyncEntitySnapshot, SyncEntityType, SyncOperationStatus, SyncPullRequest, VectorClock, ) from cleveragents.application.services.sync_service import SyncService # noqa: E402 # --------------------------------------------------------------------------- # Subcommands # --------------------------------------------------------------------------- def sync_pull() -> None: """Test sync pull via facade.""" svc = SyncService(node_id="robot-client") entity = SyncEntitySnapshot( entity_id="robot-actor", entity_type=SyncEntityType.ACTOR, namespace="team", name="robot-actor", vector_clock=VectorClock(entries={"server": 1}), ) svc.register_server_entity(entity) facade = A2aLocalFacade(services={"sync_service": svc}) request = A2aRequest( method="_cleveragents/sync/pull", params={"namespace": "team"}, ) response = facade.dispatch(request) result = response.result or {} if response.error is None and result.get("total_pulled", 0) >= 1: print("sync-pull-ok") else: print(f"FAIL: {response.error or result}", file=sys.stderr) sys.exit(1) def sync_push() -> None: """Test sync push via facade.""" svc = SyncService(node_id="robot-client") facade = A2aLocalFacade(services={"sync_service": svc}) params = { "namespace": "team", "entities": [ { "entity_id": "push-actor", "entity_type": "actor", "namespace": "team", "name": "push-test", }, ], } request = A2aRequest( method="_cleveragents/sync/push", params=params, ) response = facade.dispatch(request) result = response.result or {} if response.error is None and result.get("total_accepted", 0) >= 1: print("sync-push-ok") else: print(f"FAIL: {response.error or result}", file=sys.stderr) sys.exit(1) def sync_status() -> None: """Test sync status via facade.""" svc = SyncService(node_id="robot-client") entity = SyncEntitySnapshot( entity_id="status-actor", entity_type=SyncEntityType.ACTOR, namespace="team", name="status-actor", vector_clock=VectorClock(entries={"client": 1}), ) svc.register_local_entity(entity) facade = A2aLocalFacade(services={"sync_service": svc}) request = A2aRequest( method="_cleveragents/sync/status", params={"namespace": "team"}, ) response = facade.dispatch(request) result = response.result or {} if response.error is None and result.get("drift_detected") is True: print("sync-status-ok") else: print(f"FAIL: {response.error or result}", file=sys.stderr) sys.exit(1) def sync_conflict_resolution() -> None: """Test conflict detection and resolution.""" svc = SyncService(node_id="robot-client") local_ent = SyncEntitySnapshot( entity_id="conflict-actor", entity_type=SyncEntityType.ACTOR, namespace="team", name="conflict-actor", vector_clock=VectorClock(entries={"client": 2, "server": 1}), updated_at="2025-06-01T12:00:00+00:00", ) server_ent = SyncEntitySnapshot( entity_id="conflict-actor", entity_type=SyncEntityType.ACTOR, namespace="team", name="conflict-actor", vector_clock=VectorClock(entries={"client": 1, "server": 2}), updated_at="2025-05-01T12:00:00+00:00", ) svc.register_local_entity(local_ent) svc.register_server_entity(server_ent) pull_req = SyncPullRequest(namespace="team") pull_resp = svc.pull(pull_req) if len(pull_resp.conflicts) != 1: print( f"FAIL: expected 1 conflict, got {len(pull_resp.conflicts)}", file=sys.stderr, ) sys.exit(1) conflict_id = svc.conflicts[0].conflict_id resolved = svc.resolve_conflict( conflict_id=conflict_id, resolution=ConflictResolution.LAST_WRITER_WINS, ) if resolved.resolved and resolved.winner == "local": print("sync-conflict-resolution-ok") else: print( f"FAIL: resolved={resolved.resolved}, winner={resolved.winner}", file=sys.stderr, ) sys.exit(1) def sync_offline_queue() -> None: """Test offline queue operations.""" svc = SyncService(node_id="robot-client") entity = SyncEntitySnapshot( entity_id="offline-actor", entity_type=SyncEntityType.ACTOR, namespace="team", name="offline-actor", vector_clock=VectorClock(entries={"client": 1}), ) entry = svc.enqueue_offline( direction=SyncDirection.PUSH, namespace="team", entity=entity, ) if entry.status != SyncOperationStatus.QUEUED: print(f"FAIL: expected QUEUED, got {entry.status}", file=sys.stderr) sys.exit(1) processed = svc.process_offline_queue() if len(processed) >= 1 and len(svc.offline_queue) == 0: print("sync-offline-queue-ok") else: print( f"FAIL: processed={len(processed)}, remaining={len(svc.offline_queue)}", file=sys.stderr, ) sys.exit(1) def sync_vector_clock() -> None: """Test vector clock operations.""" c1 = VectorClock(entries={"a": 1, "b": 2}) c2 = VectorClock(entries={"a": 2, "b": 3}) if not c1.happens_before(c2): print("FAIL: c1 should happen before c2", file=sys.stderr) sys.exit(1) c3 = VectorClock(entries={"a": 2, "b": 1}) if not c1.is_concurrent(c3): print("FAIL: c1 and c3 should be concurrent", file=sys.stderr) sys.exit(1) merged = c1.merge(c2) if merged.entries != {"a": 2, "b": 3}: print(f"FAIL: unexpected merge result {merged.entries}", file=sys.stderr) sys.exit(1) incremented = c1.increment("a") if incremented.entries["a"] != 2: print(f"FAIL: expected a=2, got {incremented.entries['a']}", file=sys.stderr) sys.exit(1) print("sync-vector-clock-ok") def sync_facade_no_service() -> None: """Test facade sync stubs when no service is registered.""" facade = A2aLocalFacade() for op in ( "_cleveragents/sync/pull", "_cleveragents/sync/push", "_cleveragents/sync/status", ): request = A2aRequest(method=op, params={"namespace": "test"}) response = facade.dispatch(request) result = response.result or {} if response.error is not None or not result.get("stub"): print( f"FAIL: {op} should return stub, got {response.error or result}", file=sys.stderr, ) sys.exit(1) print("sync-facade-no-service-ok") def sync_incremental() -> None: """Test incremental sync with since parameter.""" svc = SyncService(node_id="robot-client") old_entity = SyncEntitySnapshot( entity_id="old-actor", entity_type=SyncEntityType.ACTOR, namespace="team", name="old-actor", vector_clock=VectorClock(entries={"server": 1}), updated_at="2024-01-01T00:00:00+00:00", ) new_entity = SyncEntitySnapshot( entity_id="new-actor", entity_type=SyncEntityType.ACTOR, namespace="team", name="new-actor", vector_clock=VectorClock(entries={"server": 2}), updated_at="2025-06-01T00:00:00+00:00", ) svc.register_server_entity(old_entity) svc.register_server_entity(new_entity) pull_req = SyncPullRequest( namespace="team", since="2025-01-01T00:00:00+00:00", ) resp = svc.pull(pull_req) if resp.total_pulled == 1 and resp.entities[0].entity_id == "new-actor": print("sync-incremental-ok") else: print(f"FAIL: expected 1 entity, got {resp.total_pulled}", file=sys.stderr) sys.exit(1) # --------------------------------------------------------------------------- # CLI dispatch # --------------------------------------------------------------------------- _COMMANDS: dict[str, object] = { "sync-pull": sync_pull, "sync-push": sync_push, "sync-status": sync_status, "sync-conflict-resolution": sync_conflict_resolution, "sync-offline-queue": sync_offline_queue, "sync-vector-clock": sync_vector_clock, "sync-facade-no-service": sync_facade_no_service, "sync-incremental": sync_incremental, } if __name__ == "__main__": if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS: print(f"Usage: {sys.argv[0]} <{'|'.join(sorted(_COMMANDS))}>>", file=sys.stderr) sys.exit(2) cmd = _COMMANDS[sys.argv[1]] if callable(cmd): cmd()