Add syncFromFlaskBulk merge tests

This commit is contained in:
2026-01-18 18:29:08 +00:00
parent cd9e65d4d2
commit aea8676a33
2 changed files with 129 additions and 8 deletions

View File

@@ -0,0 +1,98 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockFetchAllFromFlask,
mockDbPut,
mockDbGetAll,
mockDbDelete
} = vi.hoisted(() => {
return {
mockFetchAllFromFlask: vi.fn<[], Promise<Record<string, any>>>(),
mockDbPut: vi.fn<[string, any], Promise<void>>(),
mockDbGetAll: vi.fn<[string], Promise<any[]>>(),
mockDbDelete: vi.fn<[string, string], Promise<void>>()
}
})
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')
})
})

View File

@@ -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<string>>()
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<string>()
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<string>()
for (const item of localItems) {
if (!item?.id) continue
if (!remoteIds.has(item.id)) {
await db.delete(storeName, item.id)
}
}
}