From f1ec6fb4e965e99a5a0ec13a7bd451ae990376cd Mon Sep 17 00:00:00 2001 From: CleverThis Date: Fri, 29 May 2026 02:46:50 -0400 Subject: [PATCH] fix(sync): add missing coverage scenarios and remove dead _iso_now from sync_models - Remove unused _iso_now() from sync_models.py (dead code: called nowhere in the module; sync_service.py has its own _iso_now()) - Remove now-unused `from datetime import UTC, datetime` import - Add 5 new BDD scenarios covering previously uncovered code paths: - VectorClock.happens_before() TypeError guard (sync_models.py:134-135) - VectorClock.is_concurrent() TypeError guard (sync_models.py:159-160) - process_offline_queue() PULL direction branch (sync_service.py:468-475) - resolve_conflict() last_writer_wins when server entity is newer (sync_service.py:549 else) - SyncService.push() with CLIENT_WINS resolution (_resolve_conflict lines 663-665) - Add corresponding step implementations for the new scenarios ISSUES CLOSED: #1125 --- features/entity_sync.feature | 34 ++++++++++++++++++++ features/steps/entity_sync_steps.py | 50 +++++++++++++++++++++++++++++ src/cleveragents/a2a/sync_models.py | 6 ---- 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/features/entity_sync.feature b/features/entity_sync.feature index 283b833ea..fe406daa8 100644 --- a/features/entity_sync.feature +++ b/features/entity_sync.feature @@ -416,3 +416,37 @@ Feature: Entity sync via _cleveragents/sync/* And a local actor snapshot for offline queuing When I enqueue an offline pull from namespace "team" Then the offline queue should have 1 entry + + Scenario: Vector clock happens_before rejects non-VectorClock + Given an empty vector clock + When I try to call happens_before with a non-VectorClock value + Then a sync TypeError should be raised + + Scenario: Vector clock is_concurrent rejects non-VectorClock + Given an empty vector clock + When I try to call is_concurrent with a non-VectorClock value + Then a sync TypeError should be raised + + Scenario: Process offline queue processes pull operation + Given a sync service with node_id "test-client" + And server entities in namespace "team" with 1 actors + And a local actor snapshot for offline queuing + When I enqueue an offline pull from namespace "team" + And I process the offline queue + Then the processed entries should include the offline actor + And the offline queue should be empty + + Scenario: Resolve conflict with last_writer_wins picks server when newer + Given a sync service with node_id "test-client" + And an unresolved conflict where server is newer + When I resolve the conflict with last_writer_wins strategy + Then the conflict should be marked as resolved + And the conflict winner should be "server" + + Scenario: Push with client_wins resolution accepts local entity + Given a sync service with node_id "test-client" + And a local actor "conflict-actor" in namespace "team" with clock {"client": 2, "server": 1} + And a server actor "conflict-actor" in namespace "team" with clock {"client": 1, "server": 2} + When I push the local actor to namespace "team" with client-wins resolution + Then the push response should have 1 accepted + And the push conflict winner should be "local" diff --git a/features/steps/entity_sync_steps.py b/features/steps/entity_sync_steps.py index b55af2063..af3e79f9b 100644 --- a/features/steps/entity_sync_steps.py +++ b/features/steps/entity_sync_steps.py @@ -119,6 +119,24 @@ def step_merge_non_clock(context: Any) -> None: context.caught_exception = exc +@when("I try to call happens_before with a non-VectorClock value") +def step_happens_before_bad_type(context: Any) -> None: + context.caught_exception = None + try: + context.clock1.happens_before("not a clock") # type: ignore[arg-type] + except TypeError as exc: + context.caught_exception = exc + + +@when("I try to call is_concurrent with a non-VectorClock value") +def step_is_concurrent_bad_type(context: Any) -> None: + context.caught_exception = None + try: + context.clock1.is_concurrent("not a clock") # type: ignore[arg-type] + except TypeError as exc: + context.caught_exception = exc + + # ------------------------------------------------------------------ # Model validation steps # ------------------------------------------------------------------ @@ -445,6 +463,28 @@ def step_setup_unresolved_conflict(context: Any) -> None: context.conflict_id = svc.conflicts[0].conflict_id +@given("an unresolved conflict where server is newer") +def step_setup_conflict_server_newer(context: Any) -> None: + svc: SyncService = context.sync_service + local_entity = _make_actor_snapshot( + entity_id="ts-conflict-srv", + namespace="team", + clock_entries={"client": 2, "server": 1}, + updated_at="2025-01-01T00:00:00+00:00", + ) + server_entity = _make_actor_snapshot( + entity_id="ts-conflict-srv", + namespace="team", + clock_entries={"client": 1, "server": 2}, + updated_at="2025-07-01T00:00:00+00:00", + ) + svc.register_local_entity(local_entity) + svc.register_server_entity(server_entity) + pull_req = SyncPullRequest(namespace="team") + svc.pull(pull_req) + context.conflict_id = svc.conflicts[0].conflict_id + + @given("an unresolved conflict where local is newer") def step_setup_conflict_local_newer(context: Any) -> None: svc: SyncService = context.sync_service @@ -581,6 +621,16 @@ def step_push_server_wins(context: Any, namespace: str) -> None: context.push_response = context.sync_service.push(request) +@when('I push the local actor to namespace "{namespace}" with client-wins resolution') +def step_push_client_wins(context: Any, namespace: str) -> None: + request = SyncPushRequest( + namespace=namespace, + entities=[context.local_entity], + resolution=ConflictResolution.CLIENT_WINS, + ) + context.push_response = context.sync_service.push(request) + + @when('I push the local actor to namespace "{namespace}" with manual resolution') def step_push_manual(context: Any, namespace: str) -> None: request = SyncPushRequest( diff --git a/src/cleveragents/a2a/sync_models.py b/src/cleveragents/a2a/sync_models.py index f930a0e1b..e2a8e0569 100644 --- a/src/cleveragents/a2a/sync_models.py +++ b/src/cleveragents/a2a/sync_models.py @@ -9,7 +9,6 @@ The ``local/`` namespace is **never** synced — it exists only on the client. from __future__ import annotations -from datetime import UTC, datetime from enum import StrEnum from typing import Any @@ -379,11 +378,6 @@ class SyncQueueEntry(BaseModel): return value -def _iso_now() -> str: - """Return the current UTC time as an ISO-8601 string.""" - return datetime.now(tz=UTC).isoformat() - - __all__ = [ "ConflictResolution", "SyncConflict",