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
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:
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user