fix(sync): add missing coverage scenarios and remove dead _iso_now from sync_models
CI / push-validation (pull_request) Successful in 36s
CI / lint (pull_request) Successful in 39s
CI / helm (pull_request) Successful in 40s
CI / build (pull_request) Successful in 54s
CI / quality (pull_request) Successful in 1m7s
CI / typecheck (pull_request) Successful in 1m13s
CI / security (pull_request) Successful in 1m24s
CI / integration_tests (pull_request) Successful in 3m8s
CI / unit_tests (pull_request) Successful in 5m11s
CI / docker (pull_request) Successful in 1m31s
CI / coverage (pull_request) Failing after 11m5s
CI / status-check (pull_request) Failing after 3s

- 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
This commit is contained in:
2026-05-29 02:46:50 -04:00
parent 24c1d56816
commit f1ec6fb4e9
3 changed files with 84 additions and 6 deletions
+34
View File
@@ -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"
+50
View File
@@ -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(
-6
View File
@@ -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",