learning_ai_common_plat/packages/sync/src/sync.test.ts
saravanakumardb1 359d6e18a5 feat: Platform Acceleration + A/B Testing Framework
Platform Acceleration Phase 1:
- @bytelyst/sync package: Offline-first sync engine with conflict resolution
  - Storage adapters: LocalStorage, InMemory, MMKV
  - Deduplication, retry with backoff, auto-flush on reconnect
  - 12 comprehensive tests
- @bytelyst/dashboard-components package: Shared React components
  - ErrorPage, NotFoundPage, LoadingSpinner, LoadingSkeleton, EmptyState, PageHeader
  - Theme-aware with CSS custom properties

A/B Testing Framework (Complete):
- Admin UI at /ops/ab-testing with experiments list, variant performance, AI suggestions
- Sidebar navigation with Beaker icon
- 40 tests passing in ab-testing module

All 909 platform-service tests pass.
2026-03-03 19:47:47 -08:00

276 lines
8.3 KiB
TypeScript

/**
* Sync Engine Tests
*
* @module @bytelyst/sync/engine.test
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { createSyncEngine, InMemoryAdapter } from './index.js';
import type { ApiClient, ApiResult } from '@bytelyst/api-client';
// ─────────────────────────────────────────────────────────────────────────────
// Mock API Client
// ─────────────────────────────────────────────────────────────────────────────
function createMockApiClient(): ApiClient & {
getRequests: () => { path: string; options?: RequestInit }[];
} {
const requests: { path: string; options?: RequestInit }[] = [];
return {
fetch: async <T>(path: string, options?: RequestInit): Promise<T> => {
requests.push({ path, options });
return { items: [] } as unknown as T;
},
safeFetch: async <T>(path: string, options?: RequestInit): Promise<ApiResult<T>> => {
requests.push({ path, options });
return { data: { items: [] } as unknown as T, error: null };
},
getRequests: () => requests,
};
}
// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────
describe('Sync Engine', () => {
let storage: InMemoryAdapter;
let apiClient: ReturnType<typeof createMockApiClient>;
beforeEach(() => {
storage = new InMemoryAdapter();
apiClient = createMockApiClient();
});
describe('createSyncEngine', () => {
it('creates a sync engine with default config', () => {
const engine = createSyncEngine({
productId: 'test',
entities: {
tasks: { endpoint: '/tasks', partitionKey: 'userId', conflictStrategy: 'server-wins' },
},
storage,
apiClient,
});
expect(engine).toBeDefined();
expect(engine.push).toBeDefined();
expect(engine.pull).toBeDefined();
expect(engine.fullSync).toBeDefined();
});
});
describe('push', () => {
it('adds item to queue', async () => {
const engine = createSyncEngine({
productId: 'test',
entities: {
tasks: { endpoint: '/tasks', partitionKey: 'userId', conflictStrategy: 'server-wins' },
},
storage,
apiClient,
});
await engine.push('tasks', { title: 'Test Task' });
const status = engine.getStatus();
expect(status.status).toBe('idle');
});
it('deduplicates items for same entity', async () => {
const engine = createSyncEngine({
productId: 'test',
entities: {
tasks: { endpoint: '/tasks', partitionKey: 'userId', conflictStrategy: 'server-wins' },
},
storage,
apiClient,
});
await engine.push('tasks', { id: '1', title: 'Task 1' });
await engine.push('tasks', { id: '1', title: 'Task 1 Updated' });
// Queue should have 1 item (deduplicated)
const result = await engine.fullSync();
expect(result.pushed).toBe(1);
});
});
describe('delete', () => {
it('creates delete operation', async () => {
const engine = createSyncEngine({
productId: 'test',
entities: {
tasks: { endpoint: '/tasks', partitionKey: 'userId', conflictStrategy: 'server-wins' },
},
storage,
apiClient,
});
await engine.delete('tasks', 'task-123');
const requests = apiClient.getRequests();
// Delete is queued but not flushed until sync
expect(requests.length).toBe(0);
});
});
describe('fullSync', () => {
it('pushes queued items', async () => {
const engine = createSyncEngine({
productId: 'test',
entities: {
tasks: { endpoint: '/tasks', partitionKey: 'userId', conflictStrategy: 'server-wins' },
},
storage,
apiClient,
});
await engine.push('tasks', { title: 'Test Task' });
const result = await engine.fullSync();
expect(result.pushed).toBe(1);
const requests = apiClient.getRequests();
expect(requests).toHaveLength(2); // Pull + Push
});
it('pulls remote changes', async () => {
const engine = createSyncEngine({
productId: 'test',
entities: {
tasks: { endpoint: '/tasks', partitionKey: 'userId', conflictStrategy: 'server-wins' },
},
storage,
apiClient,
});
const result = await engine.fullSync();
expect(result.pulled).toBe(0); // Mock returns empty
});
});
describe('status and monitoring', () => {
it('returns initial status', () => {
const engine = createSyncEngine({
productId: 'test',
entities: {
tasks: { endpoint: '/tasks', partitionKey: 'userId', conflictStrategy: 'server-wins' },
},
storage,
apiClient,
});
const status = engine.getStatus();
expect(status.status).toBe('idle');
});
it('notifies status changes', async () => {
const engine = createSyncEngine({
productId: 'test',
entities: {
tasks: { endpoint: '/tasks', partitionKey: 'userId', conflictStrategy: 'server-wins' },
},
storage,
apiClient,
});
const statuses: string[] = [];
engine.onStatusChange(status => {
statuses.push(status.status);
});
await engine.push('tasks', { title: 'Test' });
// Status changes during push
expect(statuses.length).toBeGreaterThanOrEqual(0);
});
});
describe('clearQueue', () => {
it('removes all queued items', async () => {
const engine = createSyncEngine({
productId: 'test',
entities: {
tasks: { endpoint: '/tasks', partitionKey: 'userId', conflictStrategy: 'server-wins' },
},
storage,
apiClient,
});
await engine.push('tasks', { title: 'Task 1' });
await engine.push('tasks', { title: 'Task 2' });
await engine.clearQueue();
const result = await engine.fullSync();
expect(result.pushed).toBe(0);
});
});
describe('reprocessFailed', () => {
it('resets retry count on failed items', async () => {
const engine = createSyncEngine({
productId: 'test',
entities: {
tasks: { endpoint: '/tasks', partitionKey: 'userId', conflictStrategy: 'server-wins' },
},
storage,
apiClient,
});
await engine.push('tasks', { title: 'Test' });
await engine.reprocessFailed();
// Items should be reprocessed
const result = await engine.fullSync();
expect(result.pushed).toBe(1);
});
});
});
describe('Storage Adapters', () => {
describe('InMemoryAdapter', () => {
it('stores and retrieves items', () => {
const storage = new InMemoryAdapter();
storage.setItem('key1', { value: 123 });
const retrieved = storage.getItem<{ value: number }>('key1');
expect(retrieved).toEqual({ value: 123 });
});
it('returns null for missing keys', () => {
const storage = new InMemoryAdapter();
const retrieved = storage.getItem('missing');
expect(retrieved).toBeNull();
});
it('lists all keys', () => {
const storage = new InMemoryAdapter();
storage.setItem('key1', 'value1');
storage.setItem('key2', 'value2');
const keys = storage.keys();
expect(keys).toContain('key1');
expect(keys).toContain('key2');
});
it('removes items', () => {
const storage = new InMemoryAdapter();
storage.setItem('key1', 'value1');
storage.removeItem('key1');
expect(storage.getItem('key1')).toBeNull();
});
it('clears all items', () => {
const storage = new InMemoryAdapter();
storage.setItem('key1', 'value1');
storage.setItem('key2', 'value2');
storage.clear();
expect(storage.keys()).toHaveLength(0);
});
});
});