From aea8676a331106a5f0704f6510abea0b44c1ee9c Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 18:29:08 +0000 Subject: [PATCH] Add syncFromFlaskBulk merge tests --- src/store/slices/syncSlice.test.ts | 98 ++++++++++++++++++++++++++++++ src/store/slices/syncSlice.ts | 39 +++++++++--- 2 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 src/store/slices/syncSlice.test.ts diff --git a/src/store/slices/syncSlice.test.ts b/src/store/slices/syncSlice.test.ts new file mode 100644 index 0000000..314401a --- /dev/null +++ b/src/store/slices/syncSlice.test.ts @@ -0,0 +1,98 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockFetchAllFromFlask, + mockDbPut, + mockDbGetAll, + mockDbDelete +} = vi.hoisted(() => { + return { + mockFetchAllFromFlask: vi.fn<[], Promise>>(), + mockDbPut: vi.fn<[string, any], Promise>(), + mockDbGetAll: vi.fn<[string], Promise>(), + mockDbDelete: vi.fn<[string, string], Promise>() + } +}) + +vi.mock('@/store/middleware/flaskSync', () => ({ + fetchAllFromFlask: mockFetchAllFromFlask +})) + +vi.mock('@/lib/db', () => ({ + db: { + put: mockDbPut, + getAll: mockDbGetAll, + delete: mockDbDelete + } +})) + +import { syncFromFlaskBulk } from './syncSlice' + +describe('syncFromFlaskBulk', () => { + const dispatch = vi.fn() + const getState = vi.fn() + + beforeEach(() => { + mockFetchAllFromFlask.mockReset() + mockDbPut.mockReset() + mockDbGetAll.mockReset() + mockDbDelete.mockReset() + dispatch.mockReset() + getState.mockReset() + }) + + it('ignores invalid keys from Flask', async () => { + mockFetchAllFromFlask.mockResolvedValue({ + 'unknown:1': { id: '1' }, + 'files': { id: 'missing-colon' }, + 'models:': { id: 'empty-id' }, + 'components:abc:extra': { id: 'abc' } + }) + mockDbGetAll.mockResolvedValue([]) + + const action = await syncFromFlaskBulk()(dispatch, getState, undefined) + + expect(action.type).toBe('sync/syncFromFlaskBulk/fulfilled') + expect(mockDbPut).not.toHaveBeenCalled() + expect(mockDbDelete).not.toHaveBeenCalled() + }) + + it('updates local DB for valid keys', async () => { + const file = { id: 'file-1', name: 'File 1' } + const model = { id: 'model-1', name: 'Model 1' } + + mockFetchAllFromFlask.mockResolvedValue({ + 'files:file-1': file, + 'models:model-1': model + }) + mockDbGetAll.mockResolvedValue([]) + + const action = await syncFromFlaskBulk()(dispatch, getState, undefined) + + expect(action.type).toBe('sync/syncFromFlaskBulk/fulfilled') + expect(mockDbPut).toHaveBeenCalledWith('files', file) + expect(mockDbPut).toHaveBeenCalledWith('models', model) + }) + + it('deletes local entries missing from Flask data', async () => { + const file = { id: 'keep', name: 'Keep' } + + mockFetchAllFromFlask.mockResolvedValue({ + 'files:keep': file + }) + mockDbGetAll.mockImplementation((storeName) => { + if (storeName === 'files') { + return Promise.resolve([file, { id: 'stale', name: 'Stale' }]) + } + return Promise.resolve([]) + }) + + const action = await syncFromFlaskBulk()(dispatch, getState, undefined) + + expect(action.type).toBe('sync/syncFromFlaskBulk/fulfilled') + expect(mockDbPut).toHaveBeenCalledWith('files', file) + expect(mockDbDelete).toHaveBeenCalledTimes(1) + expect(mockDbDelete).toHaveBeenCalledWith('files', 'stale') + expect(mockDbDelete).not.toHaveBeenCalledWith('files', 'keep') + }) +}) diff --git a/src/store/slices/syncSlice.ts b/src/store/slices/syncSlice.ts index 046bef8..85a9b99 100644 --- a/src/store/slices/syncSlice.ts +++ b/src/store/slices/syncSlice.ts @@ -68,15 +68,38 @@ export const syncFromFlaskBulk = createAsyncThunk( async (_, { rejectWithValue }) => { try { const data = await fetchAllFromFlask() - + + const validStoreNames = ['files', 'models', 'components', 'workflows'] as const + const remoteIdsByStore = new Map<(typeof validStoreNames)[number], Set>() + for (const [key, value] of Object.entries(data)) { - const [storeName, id] = key.split(':') - - if (storeName === 'files' || - storeName === 'models' || - storeName === 'components' || - storeName === 'workflows') { - await db.put(storeName as any, value) + const keyParts = key.split(':') + if (keyParts.length !== 2) continue + + const [storeName, id] = keyParts + + if (!validStoreNames.includes(storeName as (typeof validStoreNames)[number])) { + continue + } + + if (!id) continue + + await db.put(storeName as any, value) + + const ids = remoteIdsByStore.get(storeName as any) ?? new Set() + ids.add(id) + remoteIdsByStore.set(storeName as any, ids) + } + + for (const storeName of validStoreNames) { + const localItems = await db.getAll(storeName) + const remoteIds = remoteIdsByStore.get(storeName) ?? new Set() + + for (const item of localItems) { + if (!item?.id) continue + if (!remoteIds.has(item.id)) { + await db.delete(storeName, item.id) + } } }