/** * RED PHASE Tests for Optimistic Updates Store * * Tests for optimistic mutations with rollback capability. * These tests should FAIL until implementation is complete. */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { createOptimisticStore, OptimisticStore, applyOptimisticUpdate, rollbackOptimisticUpdate, } from './optimistic-store' import type { BaseRecord } from '../types' interface Product extends BaseRecord { id: number name: string price: number stock: number } describe('OptimisticStore', () => { const mockProducts: Product[] = [ { id: 1, name: 'Widget', price: 10.99, stock: 100 }, { id: 2, name: 'Gadget', price: 24.99, stock: 50 }, { id: 3, name: 'Gizmo', price: 14.99, stock: 75 }, ] beforeEach(() => { // Setup for tests }) describe('createOptimisticStore()', () => { it('should return an OptimisticStore instance', () => { const store = createOptimisticStore({ collectionId: 'products' }) expect(store).toBeInstanceOf(OptimisticStore) }) it('should initialize with empty pending array', () => { const store = createOptimisticStore({ collectionId: 'products' }) expect(store.getState().pending).toEqual([]) }) it('should initialize with empty applied array', () => { const store = createOptimisticStore({ collectionId: 'products' }) expect(store.getState().applied).toEqual([]) }) }) describe('applyOptimistic()', () => { it('should add mutation to applied list', () => { const store = createOptimisticStore({ collectionId: 'products' }) store.applyOptimistic('update', 1, { price: 12.99 }, mockProducts[0]) expect(store.getState().applied).toHaveLength(1) }) it('should return mutation id', () => { const store = createOptimisticStore({ collectionId: 'products' }) const mutationId = store.applyOptimistic('update', 1, { price: 12.99 }) expect(typeof mutationId).toBe('string') }) it('should store mutation type', () => { const store = createOptimisticStore({ collectionId: 'products' }) store.applyOptimistic('insert', 4, { id: 4, name: 'New', price: 5.99, stock: 10 }) expect(store.getState().applied[0]!.type).toBe('insert') }) it('should store previous data for rollback', () => { const store = createOptimisticStore({ collectionId: 'products' }) store.applyOptimistic('update', 1, { price: 12.99 }, mockProducts[0]) expect(store.getState().applied[0]!.previousData).toEqual(mockProducts[0]) }) it('should track timestamp', () => { const store = createOptimisticStore({ collectionId: 'products' }) const before = Date.now() store.applyOptimistic('update', 1, { price: 12.99 }) expect(store.getState().applied[0]!.timestamp).toBeGreaterThanOrEqual(before) }) }) describe('confirmMutation()', () => { it('should remove mutation from applied list', () => { const store = createOptimisticStore({ collectionId: 'products' }) const mutationId = store.applyOptimistic('update', 1, { price: 12.99 }) store.confirmMutation(mutationId) expect(store.getState().applied).toHaveLength(0) }) it('should not affect other mutations', () => { const store = createOptimisticStore({ collectionId: 'products' }) const id1 = store.applyOptimistic('update', 1, { price: 12.99 }) store.applyOptimistic('update', 2, { price: 29.99 }) store.confirmMutation(id1) expect(store.getState().applied).toHaveLength(1) expect(store.getState().applied[0]!.data).toEqual({ price: 29.99 }) }) it('should be idempotent', () => { const store = createOptimisticStore({ collectionId: 'products' }) const mutationId = store.applyOptimistic('update', 1, { price: 12.99 }) store.confirmMutation(mutationId) store.confirmMutation(mutationId) // Should not throw expect(store.getState().applied).toHaveLength(0) }) }) describe('rollbackMutation()', () => { it('should remove mutation from applied list', () => { const store = createOptimisticStore({ collectionId: 'products' }) const mutationId = store.applyOptimistic('update', 1, { price: 12.99 }, mockProducts[0]) store.rollbackMutation(mutationId) expect(store.getState().applied).toHaveLength(0) }) it('should return previous data for update', () => { const store = createOptimisticStore({ collectionId: 'products' }) const mutationId = store.applyOptimistic('update', 1, { price: 12.99 }, mockProducts[0]) const rollback = store.rollbackMutation(mutationId) expect(rollback?.previousData).toEqual(mockProducts[0]) }) it('should return mutation info for insert rollback', () => { const store = createOptimisticStore({ collectionId: 'products' }) const newProduct = { id: 4, name: 'New', price: 5.99, stock: 10 } const mutationId = store.applyOptimistic('insert', 4, newProduct) const rollback = store.rollbackMutation(mutationId) expect(rollback?.type).toBe('insert') expect((rollback?.data as Product).id).toBe(4) }) it('should return mutation info for delete rollback', () => { const store = createOptimisticStore({ collectionId: 'products' }) const mutationId = store.applyOptimistic('delete', 1, { id: 1 }, mockProducts[0]) const rollback = store.rollbackMutation(mutationId) expect(rollback?.type).toBe('delete') expect(rollback?.previousData).toEqual(mockProducts[0]) }) it('should return undefined for non-existent mutation', () => { const store = createOptimisticStore({ collectionId: 'products' }) const rollback = store.rollbackMutation('non-existent') expect(rollback).toBeUndefined() }) }) describe('rollbackAll()', () => { it('should return all mutations in reverse order', () => { const store = createOptimisticStore({ collectionId: 'products' }) store.applyOptimistic('update', 1, { price: 12.99 }, mockProducts[0]) store.applyOptimistic('update', 2, { price: 29.99 }, mockProducts[1]) store.applyOptimistic('delete', 3, { id: 3 }, mockProducts[2]) const rollbacks = store.rollbackAll() expect(rollbacks).toHaveLength(3) expect(rollbacks[0]!.type).toBe('delete') // Last applied, first rolled back expect(rollbacks[2]!.type).toBe('update') }) it('should clear all applied mutations', () => { const store = createOptimisticStore({ collectionId: 'products' }) store.applyOptimistic('update', 1, { price: 12.99 }) store.applyOptimistic('update', 2, { price: 29.99 }) store.rollbackAll() expect(store.getState().applied).toHaveLength(0) }) }) describe('getState()', () => { it('should return immutable state copy', () => { const store = createOptimisticStore({ collectionId: 'products' }) const state1 = store.getState() store.applyOptimistic('update', 1, { price: 12.99 }) const state2 = store.getState() expect(state1.applied).toHaveLength(0) expect(state2.applied).toHaveLength(1) }) }) describe('hasPendingMutations()', () => { it('should return false when no mutations', () => { const store = createOptimisticStore({ collectionId: 'products' }) expect(store.hasPendingMutations()).toBe(false) }) it('should return true when mutations exist', () => { const store = createOptimisticStore({ collectionId: 'products' }) store.applyOptimistic('update', 1, { price: 12.99 }) expect(store.hasPendingMutations()).toBe(true) }) it('should return false after all confirmed', () => { const store = createOptimisticStore({ collectionId: 'products' }) const id = store.applyOptimistic('update', 1, { price: 12.99 }) store.confirmMutation(id) expect(store.hasPendingMutations()).toBe(false) }) }) describe('getMutationsForRecord()', () => { it('should return mutations for specific record', () => { const store = createOptimisticStore({ collectionId: 'products' }) store.applyOptimistic('update', 1, { price: 12.99 }) store.applyOptimistic('update', 2, { price: 29.99 }) store.applyOptimistic('update', 1, { stock: 90 }) const mutations = store.getMutationsForRecord(1) expect(mutations).toHaveLength(2) }) it('should return empty array for record with no mutations', () => { const store = createOptimisticStore({ collectionId: 'products' }) store.applyOptimistic('update', 1, { price: 12.99 }) const mutations = store.getMutationsForRecord(999) expect(mutations).toHaveLength(0) }) }) describe('subscribe()', () => { it('should call callback on mutation apply', () => { const store = createOptimisticStore({ collectionId: 'products' }) const callback = vi.fn() store.subscribe(callback) store.applyOptimistic('update', 1, { price: 12.99 }) expect(callback).toHaveBeenCalled() }) it('should call callback on mutation confirm', () => { const store = createOptimisticStore({ collectionId: 'products' }) const id = store.applyOptimistic('update', 1, { price: 12.99 }) const callback = vi.fn() store.subscribe(callback) store.confirmMutation(id) expect(callback).toHaveBeenCalled() }) it('should call callback on rollback', () => { const store = createOptimisticStore({ collectionId: 'products' }) const id = store.applyOptimistic('update', 1, { price: 12.99 }) const callback = vi.fn() store.subscribe(callback) store.rollbackMutation(id) expect(callback).toHaveBeenCalled() }) it('should return unsubscribe function', () => { const store = createOptimisticStore({ collectionId: 'products' }) const callback = vi.fn() const unsubscribe = store.subscribe(callback) unsubscribe() store.applyOptimistic('update', 1, { price: 12.99 }) expect(callback).not.toHaveBeenCalled() }) }) }) describe('applyOptimisticUpdate()', () => { it('should apply update to data map', () => { const data = new Map([ [1, { id: 1, value: 10 }], ]) const result = applyOptimisticUpdate(data, 'update', 1, { value: 20 }) expect(result.get(1)?.value).toBe(20) }) it('should apply insert to data map', () => { const data = new Map() const result = applyOptimisticUpdate(data, 'insert', 1, { id: 1, value: 10 }) expect(result.has(1)).toBe(true) expect(result.get(1)?.value).toBe(10) }) it('should apply delete to data map', () => { const data = new Map([ [1, { id: 1, value: 10 }], ]) const result = applyOptimisticUpdate(data, 'delete', 1, {}) expect(result.has(1)).toBe(false) }) it('should return new map (immutable)', () => { const data = new Map([ [1, { id: 1, value: 10 }], ]) const result = applyOptimisticUpdate(data, 'update', 1, { value: 20 }) expect(result).not.toBe(data) expect(data.get(1)?.value).toBe(10) // Original unchanged }) }) describe('rollbackOptimisticUpdate()', () => { it('should restore previous value for update', () => { const data = new Map([ [1, { id: 1, value: 20 }], ]) const previousData = { id: 1, value: 10 } const result = rollbackOptimisticUpdate(data, 'update', 1, previousData) expect(result.get(1)?.value).toBe(10) }) it('should remove inserted item', () => { const data = new Map([ [1, { id: 1, value: 10 }], ]) const result = rollbackOptimisticUpdate(data, 'insert', 1, undefined) expect(result.has(1)).toBe(false) }) it('should restore deleted item', () => { const data = new Map() const previousData = { id: 1, value: 10 } const result = rollbackOptimisticUpdate(data, 'delete', 1, previousData) expect(result.has(1)).toBe(true) expect(result.get(1)?.value).toBe(10) }) it('should return new map (immutable)', () => { const data = new Map([ [1, { id: 1, value: 20 }], ]) const previousData = { id: 1, value: 10 } const result = rollbackOptimisticUpdate(data, 'update', 1, previousData) expect(result).not.toBe(data) expect(data.get(1)?.value).toBe(20) // Original unchanged }) it('should handle update without previous data (no-op)', () => { const data = new Map([ [1, { id: 1, value: 20 }], ]) const result = rollbackOptimisticUpdate(data, 'update', 1, undefined) expect(result.get(1)?.value).toBe(20) // Unchanged without previous data }) it('should handle delete without previous data (no-op)', () => { const data = new Map() const result = rollbackOptimisticUpdate(data, 'delete', 1, undefined) expect(result.has(1)).toBe(false) // Still empty without previous data }) }) describe('OptimisticStore - Edge Cases', () => { interface Item extends BaseRecord { id: string name: string count: number } it('should handle string IDs', () => { const store = createOptimisticStore({ collectionId: 'items' }) const mutationId = store.applyOptimistic('insert', 'abc-123', { id: 'abc-123', name: 'Test', count: 1 }) expect(typeof mutationId).toBe('string') expect(store.getState().applied[0]!.recordId).toBe('abc-123') }) it('should handle mutations without previous data', () => { const store = createOptimisticStore({ collectionId: 'items' }) store.applyOptimistic('insert', 'new-1', { id: 'new-1', name: 'New Item', count: 0 }) expect(store.getState().applied[0]!.previousData).toBeUndefined() }) it('should track collectionId correctly', () => { const store = createOptimisticStore({ collectionId: 'my-items' }) store.applyOptimistic('update', 'item-1', { count: 10 }) expect(store.getState().applied[0]!.collectionId).toBe('my-items') }) it('should handle empty rollbackAll', () => { const store = createOptimisticStore({ collectionId: 'items' }) const rollbacks = store.rollbackAll() expect(rollbacks).toEqual([]) }) it('should notify subscribers on rollbackAll', () => { const store = createOptimisticStore({ collectionId: 'items' }) store.applyOptimistic('update', 'item-1', { count: 10 }) store.applyOptimistic('update', 'item-2', { count: 20 }) const callback = vi.fn() store.subscribe(callback) store.rollbackAll() expect(callback).toHaveBeenCalled() }) it('should support multiple subscribers', () => { const store = createOptimisticStore({ collectionId: 'items' }) const callback1 = vi.fn() const callback2 = vi.fn() const callback3 = vi.fn() store.subscribe(callback1) store.subscribe(callback2) store.subscribe(callback3) store.applyOptimistic('update', 'item-1', { count: 10 }) expect(callback1).toHaveBeenCalled() expect(callback2).toHaveBeenCalled() expect(callback3).toHaveBeenCalled() }) it('should pass state snapshot to subscribers', () => { const store = createOptimisticStore({ collectionId: 'items' }) let receivedState: typeof store extends OptimisticStore ? ReturnType : never store.subscribe((state) => { receivedState = state }) store.applyOptimistic('update', 'item-1', { count: 10 }) expect(receivedState!).toBeDefined() expect(receivedState!.applied).toHaveLength(1) }) it('should generate unique mutation IDs', () => { const store = createOptimisticStore({ collectionId: 'items' }) const ids = new Set() for (let i = 0; i < 100; i++) { const id = store.applyOptimistic('update', `item-${i}`, { count: i }) ids.add(id) } // All IDs should be unique expect(ids.size).toBe(100) }) it('should track mutation data correctly for partial updates', () => { interface ComplexItem extends BaseRecord { id: number field1: string field2: string field3: number } const store = createOptimisticStore({ collectionId: 'complex' }) store.applyOptimistic('update', 1, { field1: 'updated' }) const applied = store.getState().applied[0]! expect(applied.data).toEqual({ field1: 'updated' }) // Only the updated field should be stored }) }) describe('applyOptimisticUpdate - Edge Cases', () => { it('should handle update on non-existent item (no-op)', () => { const data = new Map() const result = applyOptimisticUpdate(data, 'update', 1, { value: 20 }) expect(result.has(1)).toBe(false) // Item not added }) it('should handle delete on non-existent item (no-op)', () => { const data = new Map() const result = applyOptimisticUpdate(data, 'delete', 1, {}) expect(result.has(1)).toBe(false) }) it('should preserve other items on insert', () => { const data = new Map([ [1, { id: 1, value: 10 }], [2, { id: 2, value: 20 }], ]) const result = applyOptimisticUpdate(data, 'insert', 3, { id: 3, value: 30 }) expect(result.get(1)?.value).toBe(10) expect(result.get(2)?.value).toBe(20) expect(result.get(3)?.value).toBe(30) }) it('should preserve other items on update', () => { const data = new Map([ [1, { id: 1, value: 10 }], [2, { id: 2, value: 20 }], ]) const result = applyOptimisticUpdate(data, 'update', 1, { value: 15 }) expect(result.get(1)?.value).toBe(15) expect(result.get(2)?.value).toBe(20) // Unchanged }) it('should preserve other items on delete', () => { const data = new Map([ [1, { id: 1, value: 10 }], [2, { id: 2, value: 20 }], ]) const result = applyOptimisticUpdate(data, 'delete', 1, {}) expect(result.has(1)).toBe(false) expect(result.get(2)?.value).toBe(20) // Unchanged }) it('should merge update data with existing item', () => { const data = new Map([ [1, { id: 1, name: 'Original', value: 10 }], ]) const result = applyOptimisticUpdate(data, 'update', 1, { value: 20 }) expect(result.get(1)).toEqual({ id: 1, name: 'Original', value: 20 }) }) it('should work with string keys', () => { const data = new Map([ ['abc', { id: 'abc', value: 10 }], ]) const result = applyOptimisticUpdate(data, 'update', 'abc', { value: 20 }) expect(result.get('abc')?.value).toBe(20) }) }) describe('rollbackOptimisticUpdate - Edge Cases', () => { it('should preserve other items on rollback', () => { const data = new Map([ [1, { id: 1, value: 20 }], [2, { id: 2, value: 30 }], ]) const previousData = { id: 1, value: 10 } const result = rollbackOptimisticUpdate(data, 'update', 1, previousData) expect(result.get(1)?.value).toBe(10) expect(result.get(2)?.value).toBe(30) // Unchanged }) it('should work with string keys', () => { const data = new Map([ ['abc', { id: 'abc', value: 20 }], ]) const previousData = { id: 'abc', value: 10 } const result = rollbackOptimisticUpdate(data, 'update', 'abc', previousData) expect(result.get('abc')?.value).toBe(10) }) it('should handle rollback of insert when item already removed', () => { const data = new Map() const result = rollbackOptimisticUpdate(data, 'insert', 1, undefined) expect(result.has(1)).toBe(false) }) }) describe('OptimisticStore - Concurrent Operations', () => { interface Task extends BaseRecord { id: number status: string priority: number } it('should handle multiple operations on same record', () => { const store = createOptimisticStore({ collectionId: 'tasks' }) const original: Task = { id: 1, status: 'pending', priority: 1 } const id1 = store.applyOptimistic('update', 1, { status: 'in_progress' }, original) const id2 = store.applyOptimistic('update', 1, { priority: 2 }) const id3 = store.applyOptimistic('update', 1, { status: 'done' }) const mutations = store.getMutationsForRecord(1) expect(mutations).toHaveLength(3) // Rollback in order store.rollbackMutation(id3) expect(store.getMutationsForRecord(1)).toHaveLength(2) store.rollbackMutation(id2) expect(store.getMutationsForRecord(1)).toHaveLength(1) store.rollbackMutation(id1) expect(store.getMutationsForRecord(1)).toHaveLength(0) }) it('should correctly track all mutation types for a record', () => { const store = createOptimisticStore({ collectionId: 'tasks' }) store.applyOptimistic('insert', 1, { id: 1, status: 'new', priority: 1 }) store.applyOptimistic('update', 1, { status: 'active' }) store.applyOptimistic('delete', 1, { id: 1 }, { id: 1, status: 'active', priority: 1 }) const mutations = store.getMutationsForRecord(1) expect(mutations).toHaveLength(3) expect(mutations.map(m => m.type)).toEqual(['insert', 'update', 'delete']) }) it('should handle confirmMutation in any order', () => { const store = createOptimisticStore({ collectionId: 'tasks' }) const id1 = store.applyOptimistic('update', 1, { status: 'a' }) const id2 = store.applyOptimistic('update', 2, { status: 'b' }) const id3 = store.applyOptimistic('update', 3, { status: 'c' }) // Confirm out of order store.confirmMutation(id2) expect(store.getState().applied).toHaveLength(2) store.confirmMutation(id1) expect(store.getState().applied).toHaveLength(1) store.confirmMutation(id3) expect(store.getState().applied).toHaveLength(0) }) })