@mock_only Feature: Entity sync via _cleveragents/sync/* As a CleverAgents user with multiple devices I want to sync entities between my client and the server So that I can access the same actors, skills, actions, plans, and sessions everywhere # ------------------------------------------------------------------ # Sync models # ------------------------------------------------------------------ Scenario: Vector clock increment produces new clock Given a vector clock with entries {"client1": 1, "client2": 3} When I increment the clock for node "client1" Then the clock entry for "client1" should be 2 And the clock entry for "client2" should be 3 Scenario: Vector clock merge takes element-wise max Given a vector clock with entries {"a": 2, "b": 1} And another vector clock with entries {"a": 1, "b": 3, "c": 1} When I merge the two clocks Then the merged clock should have entries {"a": 2, "b": 3, "c": 1} Scenario: Vector clock happens-before detection Given a vector clock with entries {"a": 1, "b": 1} And another vector clock with entries {"a": 2, "b": 2} Then the first clock should happen before the second Scenario: Vector clock concurrency detection Given a vector clock with entries {"a": 2, "b": 1} And another vector clock with entries {"a": 1, "b": 2} Then the clocks should be concurrent Scenario: Vector clock increment rejects empty node_id Given an empty vector clock When I try to increment the clock with an empty node_id Then a sync ValueError should be raised Scenario: Vector clock merge rejects non-VectorClock Given an empty vector clock When I try to merge with a non-VectorClock value Then a sync TypeError should be raised Scenario: SyncEntitySnapshot rejects empty entity_id When I try to create a snapshot with an empty entity_id Then a sync validation error should be raised Scenario: SyncPullRequest rejects local namespace When I try to create a pull request with namespace "local" Then a sync validation error should be raised Scenario: SyncPushRequest rejects local namespace When I try to create a push request with namespace "local" Then a sync validation error should be raised Scenario: SyncStatusRequest rejects local namespace When I try to create a status request with namespace "local" Then a sync validation error should be raised Scenario: SyncConflict requires non-empty conflict_id When I try to create a conflict with an empty conflict_id Then a sync validation error should be raised Scenario: SyncQueueEntry requires non-empty queue_id When I try to create a queue entry with an empty queue_id Then a sync validation error should be raised Scenario: SyncState requires non-empty namespace When I try to create a sync state with an empty namespace Then a sync validation error should be raised # ------------------------------------------------------------------ # SyncService — pull # ------------------------------------------------------------------ Scenario: Pull entities from server to local cache Given a sync service with node_id "test-client" And server entities in namespace "team" with 3 actors When I pull all entities from namespace "team" Then the pull response should contain 3 entities And the pull response should have 0 conflicts Scenario: Pull detects concurrent modifications as conflicts Given a sync service with node_id "test-client" And a local actor "my-actor" in namespace "team" with clock {"client": 2, "server": 1} And a server actor "my-actor" in namespace "team" with clock {"client": 1, "server": 2} When I pull all entities from namespace "team" Then the pull response should have 1 conflicts Scenario: Pull with incremental sync uses since parameter Given a sync service with node_id "test-client" And server entities in namespace "team" with timestamps When I pull entities from namespace "team" since "2025-01-01T00:00:00+00:00" Then only entities updated after the since timestamp are pulled Scenario: Pull with force overwrites local entities Given a sync service with node_id "test-client" And a local actor "my-actor" in namespace "team" with clock {"client": 2} And a server actor "my-actor" in namespace "team" with clock {"server": 1} When I force pull all entities from namespace "team" Then the pull response should contain 1 entities And the pull response should have 0 conflicts Scenario: Pull rejects local namespace Given a sync service with node_id "test-client" When I try to pull from namespace "local" Then a sync ValidationError should be raised with message containing "local" Scenario: Pull type-checks the request Given a sync service with node_id "test-client" When I try to pull with a non-SyncPullRequest argument Then a sync TypeError should be raised # ------------------------------------------------------------------ # SyncService — push # ------------------------------------------------------------------ Scenario: Push entities from local to server Given a sync service with node_id "test-client" And 2 local actors in namespace "shared" When I push all local entities to namespace "shared" Then the push response should have 2 accepted And the push response should have 0 rejected Scenario: Push detects and resolves conflicts with last-writer-wins Given a sync service with node_id "test-client" And a local actor "conflict-actor" in namespace "team" clocked {"client": 2, "server": 1} at "2025-06-01T12:00:00+00:00" And a server actor "conflict-actor" in namespace "team" clocked {"client": 1, "server": 2} at "2025-05-01T12:00:00+00:00" When I push the local actor to namespace "team" with last-writer-wins resolution Then the push response should have 1 accepted And the push response should have 1 conflicts Scenario: Push with force bypasses conflict detection Given a sync service with node_id "test-client" And a server actor "existing" in namespace "team" with clock {"server": 5} When I force push a local actor "existing" to namespace "team" Then the push response should have 1 accepted And the push response should have 0 conflicts Scenario: Push rejects local namespace Given a sync service with node_id "test-client" When I try to push to namespace "local" Then a sync ValidationError should be raised with message containing "local" Scenario: Push type-checks the request Given a sync service with node_id "test-client" When I try to push with a non-SyncPushRequest argument Then a sync TypeError should be raised # ------------------------------------------------------------------ # SyncService — status # ------------------------------------------------------------------ Scenario: Status detects local-ahead drift Given a sync service with node_id "test-client" And a local actor "new-actor" in namespace "team" not on server When I request sync status for namespace "team" Then the status should show drift detected And the status should have 1 local-ahead entities Scenario: Status detects server-ahead drift Given a sync service with node_id "test-client" And a server actor "server-only" in namespace "team" not on local When I request sync status for namespace "team" Then the status should show drift detected And the status should have 1 server-ahead entities Scenario: Status detects no drift when in sync Given a sync service with node_id "test-client" And a synced actor "synced-actor" in namespace "team" on both sides When I request sync status for namespace "team" Then the status should show no drift Scenario: Status rejects local namespace Given a sync service with node_id "test-client" When I try requesting sync status for namespace "local" Then a sync ValidationError should be raised with message containing "local" Scenario: Status type-checks the request Given a sync service with node_id "test-client" When I try to check status with a non-SyncStatusRequest argument Then a sync TypeError should be raised Scenario: Status filters by entity type Given a sync service with node_id "test-client" And a local actor "my-actor" in namespace "team" not on server And a local skill "my-skill" in namespace "team" not on server When I request filtered sync status for namespace "team" entity type "actor" Then the status should have 1 local-ahead entities # ------------------------------------------------------------------ # Conflict resolution # ------------------------------------------------------------------ Scenario: Resolve conflict manually with local winner Given a sync service with node_id "test-client" And an unresolved conflict between local and server versions When I resolve the conflict manually choosing "local" Then the conflict should be marked as resolved And the conflict winner should be "local" Scenario: Resolve conflict manually with server winner Given a sync service with node_id "test-client" And an unresolved conflict between local and server versions When I resolve the conflict manually choosing "server" Then the conflict should be marked as resolved And the conflict winner should be "server" Scenario: Manual resolution without winner raises BusinessRuleViolation Given a sync service with node_id "test-client" And an unresolved conflict between local and server versions When I try to resolve manually without specifying a winner Then a sync BusinessRuleViolation should be raised Scenario: Resolve non-existent conflict raises ValueError Given a sync service with node_id "test-client" When I try to resolve a conflict with id "nonexistent" Then a sync ValueError should be raised Scenario: Resolve conflict with empty conflict_id raises ValueError Given a sync service with node_id "test-client" When I try to resolve a conflict with an empty id Then a sync ValueError should be raised # ------------------------------------------------------------------ # Offline queue # ------------------------------------------------------------------ Scenario: Enqueue offline push operation Given a sync service with node_id "test-client" And a local actor snapshot for offline queuing When I enqueue an offline push to namespace "team" Then the offline queue should have 1 entry And the queued entry should have status "queued" Scenario: Process offline queue retries push operations Given a sync service with node_id "test-client" And a queued offline push for actor "offline-actor" in namespace "team" When I process the offline queue Then the processed entries should include the offline actor And the offline queue should be empty Scenario: Offline queue respects max retries Given a sync service with node_id "test-client" And a queued offline push at max retries When I process the offline queue Then the failed entry should have status "failed" Scenario: Enqueue rejects local namespace Given a sync service with node_id "test-client" And a local actor snapshot for offline queuing When I try to enqueue to namespace "local" Then a sync ValueError should be raised Scenario: Enqueue rejects empty namespace Given a sync service with node_id "test-client" And a local actor snapshot for offline queuing When I try to enqueue to an empty namespace Then a sync ValueError should be raised # ------------------------------------------------------------------ # Facade integration # ------------------------------------------------------------------ Scenario: Facade sync/pull without service returns stub Given a facade without sync service When I dispatch "_cleveragents/sync/pull" with sync params {"namespace": "team"} Then the facade response should be ok And the facade response data should have "stub" set to true Scenario: Facade sync/push without service returns stub Given a facade without sync service When I dispatch "_cleveragents/sync/push" with sync params {"namespace": "team", "entities": []} Then the facade response should be ok And the facade response data should have "stub" set to true Scenario: Facade sync/status without service returns stub Given a facade without sync service When I dispatch "_cleveragents/sync/status" with sync params {"namespace": "team"} Then the facade response should be ok And the facade response data should have "stub" set to true Scenario: Facade sync/pull with service returns entities Given a facade with sync service containing server entities When I dispatch "_cleveragents/sync/pull" with sync params {"namespace": "team"} Then the facade response should be ok And the facade response data should contain key "entities" And the facade response data should contain key "total_pulled" Scenario: Facade sync/push with service accepts entities Given a facade with sync service When I dispatch "_cleveragents/sync/push" with sync push params Then the facade response should be ok And the facade response data should contain key "accepted" Scenario: Facade sync/status with service returns drift info Given a facade with sync service containing local-only entities When I dispatch "_cleveragents/sync/status" with sync params {"namespace": "team"} Then the facade response should be ok And the facade response data should contain key "drift_detected" # ------------------------------------------------------------------ # SyncService constructor validation # ------------------------------------------------------------------ Scenario: SyncService rejects non-string node_id When I try to create a SyncService with a non-string node_id Then a sync TypeError should be raised Scenario: SyncService with None node_id generates ULID When I create a SyncService with None node_id Then the service should have a non-empty node_id Scenario: SyncService register_local_entity rejects non-snapshot Given a sync service with node_id "test-client" When I try to register a non-snapshot as local entity Then a sync TypeError should be raised Scenario: SyncService register_server_entity rejects non-snapshot Given a sync service with node_id "test-client" When I try to register a non-snapshot as server entity Then a sync TypeError should be raised Scenario: Enqueue offline rejects invalid direction type Given a sync service with node_id "test-client" And a local actor snapshot for offline queuing When I try to enqueue with an invalid direction type Then a sync TypeError should be raised Scenario: Enqueue offline rejects invalid entity type Given a sync service with node_id "test-client" When I try to enqueue with an invalid entity type Then a sync TypeError should be raised Scenario: Resolve conflict rejects invalid resolution type Given a sync service with node_id "test-client" And an unresolved conflict between local and server versions When I try to resolve with an invalid resolution type Then a sync TypeError should be raised # ------------------------------------------------------------------ # Edge cases # ------------------------------------------------------------------ Scenario: Pull from empty server returns empty response Given a sync service with node_id "test-client" When I pull all entities from namespace "empty-ns" Then the pull response should contain 0 entities Scenario: Push empty entity list returns empty response Given a sync service with node_id "test-client" When I push an empty entity list to namespace "team" Then the push response should have 0 accepted Scenario: Vector clock equal clocks are not concurrent Given a vector clock with entries {"a": 1} And another vector clock with entries {"a": 1} Then the clocks should not be concurrent Scenario: Status with entity type filter on skills Given a sync service with node_id "test-client" And a local skill "my-skill" in namespace "team" not on server When I request filtered sync status for namespace "team" entity type "skill" Then the status should have 1 local-ahead entities Scenario: Push with server_wins resolution resolves to server 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 server-wins resolution Then the push response should have 1 accepted And the push conflict winner should be "server" Scenario: Push with manual resolution rejects conflicting entities 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 manual resolution Then the push response should have 0 accepted And the push response should have 1 rejected Scenario: Resolve conflict with client_wins Given a sync service with node_id "test-client" And an unresolved conflict between local and server versions When I resolve the conflict with client_wins strategy Then the conflict should be marked as resolved And the conflict winner should be "local" Scenario: Resolve conflict with server_wins Given a sync service with node_id "test-client" And an unresolved conflict between local and server versions When I resolve the conflict with server_wins strategy Then the conflict should be marked as resolved And the conflict winner should be "server" Scenario: Resolve conflict with last_writer_wins picks newer timestamp Given a sync service with node_id "test-client" And an unresolved conflict where local 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 "local" Scenario: Pull updates sync state Given a sync service with node_id "test-client" And server entities in namespace "team" with 1 actors When I pull all entities from namespace "team" Then the pull response should have a sync state with last_pull_at set Scenario: Push updates sync state Given a sync service with node_id "test-client" And 1 local actors in namespace "shared" When I push all local entities to namespace "shared" Then the push response should have a sync state with last_push_at set Scenario: Offline queue enqueue pull operation Given a sync service with node_id "test-client" 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