import { describe, expect, it, vi } from 'vitest'; import { createCodingPluginsStore } from '@/stores/coding-plugins'; import type { DataServiceCollectionRemoval, DataServiceHostResult, DataServiceInstanceRemoval, DataServiceInstanceState, } from '../../shared/data-service'; function projection(enabled = false, projectId = 'local-project') { return { schemaVersion: 1 as const, project: { localProjectId: projectId, durableProjectId: null }, policyStatus: 'current' as const, unknownPluginIds: [], items: [{ id: 'makelore.data-service', version: '1.0.0', displayName: '开发数据服务', description: '项目数据', enabled, state: enabled ? 'configuration_required' as const : 'disabled' as const, backend: { status: 'unconfigured' as const }, skills: [{ id: 'data-service', assignedAgentIds: [] }], capabilities: [], settingsSurface: 'data-service', }], }; } function deferred() { let resolve!: (value: T) => void; const promise = new Promise((done) => { resolve = done; }); return { promise, resolve }; } function dataServiceInstance(instanceId: string): DataServiceInstanceState { return { instance_id: instanceId, project_id: 'cloud-project', collections: [], usage: { document_count: 0, total_bytes: 0 }, limits: { max_collections: 20, max_documents: 1000, max_total_bytes: 20_971_520, max_document_bytes: 65_536, list_default_limit: 50, list_max_limit: 100, list_max_data_bytes: 1_048_576, mutations_per_minute: 120, }, created_at: '2026-08-27T00:00:00Z', updated_at: '2026-08-27T00:00:00Z', }; } function success(data: T): DataServiceHostResult { return { success: true, status: 200, code: null, error: null, retryable: false, data, }; } describe('coding plugins store', () => { it('coalesces duplicate loads and mutations while retaining the last projection on failure', async () => { let resolveLoad!: (value: ReturnType) => void; const list = vi.fn(() => new Promise>((resolve) => { resolveLoad = resolve; })); const setEnabled = vi.fn().mockResolvedValue(projection(true)); const store = createCodingPluginsStore({ list, setEnabled }); const first = store.getState().load('local-project'); const second = store.getState().load('local-project'); resolveLoad(projection()); await Promise.all([first, second]); expect(list).toHaveBeenCalledOnce(); await Promise.all([ store.getState().setEnabled('local-project', 'makelore.data-service', true), store.getState().setEnabled('local-project', 'makelore.data-service', true), ]); expect(setEnabled).toHaveBeenCalledOnce(); expect(store.getState().projection?.items[0].enabled).toBe(true); list.mockRejectedValueOnce(new Error('offline')); await expect(store.getState().load('other-project')).rejects.toThrow('offline'); expect(store.getState().projection?.project.localProjectId).toBe('local-project'); expect(store.getState().loadState).toBe('error'); }); it('clears project-scoped Data Service data when another project loads', async () => { const store = createCodingPluginsStore({ list: vi.fn().mockResolvedValue(projection(false, 'other-project')) }); store.setState({ projectId: 'local-project', dataService: { instance_id: 'instance-1', project_id: 'cloud-project', collections: [], usage: { document_count: 0, total_bytes: 0 }, limits: { max_collections: 20, max_documents: 1000, max_total_bytes: 20971520, max_document_bytes: 65536, list_default_limit: 50, list_max_limit: 100, list_max_data_bytes: 1048576, mutations_per_minute: 120 }, created_at: '2026-08-27T00:00:00Z', updated_at: '2026-08-27T00:00:00Z', }, }); await store.getState().load('other-project'); expect(store.getState().dataService).toBeNull(); }); it('commits only the latest project load success or error', async () => { const pending = new Map) => void; reject: (reason: Error) => void; }>(); const list = vi.fn((projectId: string) => new Promise>((resolve, reject) => { pending.set(projectId, { resolve, reject }); })); const store = createCodingPluginsStore({ list }); const first = store.getState().load('project-a'); const second = store.getState().load('project-b'); pending.get('project-b')?.resolve(projection(false, 'project-b')); await second; pending.get('project-a')?.resolve(projection(false, 'project-a')); await first; expect(store.getState()).toMatchObject({ projectId: 'project-b', loadState: 'ready', error: null, }); const third = store.getState().load('project-a'); const fourth = store.getState().load('project-b'); pending.get('project-b')?.resolve(projection(false, 'project-b')); await fourth; pending.get('project-a')?.reject(new Error('late project-a failure')); await expect(third).rejects.toThrow('late project-a failure'); expect(store.getState()).toMatchObject({ projectId: 'project-b', loadState: 'ready', error: null, }); }); it.each(['project-b first', 'project-a first'] as const)( 'renews a coalesced project load as the latest intent when %s completes', async (completionOrder) => { const pending = new Map>>>(); const list = vi.fn((projectId: string) => { const request = deferred>(); pending.set(projectId, request); return request.promise; }); const store = createCodingPluginsStore({ list }); store.setState({ projectId: 'project-a', projection: projection(false, 'project-a') }); const firstProjectA = store.getState().load('project-a'); const projectB = store.getState().load('project-b'); const renewedProjectA = store.getState().load('project-a'); expect(renewedProjectA).toBe(firstProjectA); expect(list).toHaveBeenCalledTimes(2); expect(Object.keys(store.getState().pending).sort()).toEqual([ 'load:project-a', 'load:project-b', ]); if (completionOrder === 'project-b first') { pending.get('project-b')?.resolve(projection(false, 'project-b')); await projectB; expect(store.getState().projectId).toBe('project-a'); pending.get('project-a')?.resolve(projection(true, 'project-a')); } else { pending.get('project-a')?.resolve(projection(true, 'project-a')); await firstProjectA; pending.get('project-b')?.resolve(projection(false, 'project-b')); } await Promise.all([firstProjectA, projectB, renewedProjectA]); expect(store.getState()).toMatchObject({ projectId: 'project-a', loadState: 'ready', error: null, }); expect(store.getState().projection?.items[0].enabled).toBe(true); expect(store.getState().pending).toEqual({}); }, ); it('does not let an older same-project load overwrite a completed enable mutation', async () => { const staleLoad = deferred>(); const store = createCodingPluginsStore({ list: vi.fn(() => staleLoad.promise), setEnabled: vi.fn().mockResolvedValue(projection(true)), }); store.setState({ projectId: 'local-project', projection: projection(false) }); const load = store.getState().load('local-project'); await store.getState().setEnabled('local-project', 'makelore.data-service', true); staleLoad.resolve(projection(false)); await load; expect(store.getState().projection?.items[0].enabled).toBe(true); }); it('commits a delayed enable after a same-project refresh observes old state', async () => { const enabled = deferred>(); const store = createCodingPluginsStore({ list: vi.fn().mockResolvedValue(projection(false)), setEnabled: vi.fn(() => enabled.promise), }); store.setState({ projectId: 'local-project', projection: projection(false) }); const mutation = store.getState().setEnabled('local-project', 'makelore.data-service', true); await store.getState().load('local-project'); expect(store.getState().projection?.items[0].enabled).toBe(false); enabled.resolve(projection(true)); await mutation; expect(store.getState().projection?.items[0].enabled).toBe(true); }); const adjacentMutationCases = [ { name: 'configure', verify: async () => { const configured = deferred>(); let loadCount = 0; const list = vi.fn(async () => projection(++loadCount > 1)); const store = createCodingPluginsStore({ list, configureDataService: vi.fn(() => configured.promise), }); store.setState({ projectId: 'local-project', projection: projection(false) }); const mutation = store.getState().configure(['todos']); await store.getState().load('local-project'); configured.resolve(success(dataServiceInstance('configured'))); await mutation; expect(list).toHaveBeenCalledTimes(2); expect(store.getState()).toMatchObject({ projection: { items: [{ enabled: true }] }, dataService: { instance_id: 'configured' }, }); }, }, { name: 'reset', verify: async () => { const reset = deferred>(); const store = createCodingPluginsStore({ list: vi.fn().mockResolvedValue(projection(false)), resetDataService: vi.fn(() => reset.promise), }); store.setState({ projectId: 'local-project', projection: projection(false), dataService: dataServiceInstance('before-reset'), }); const mutation = store.getState().reset(); await store.getState().load('local-project'); reset.resolve(success(dataServiceInstance('after-reset'))); await mutation; expect(store.getState().dataService?.instance_id).toBe('after-reset'); }, }, { name: 'removeCollection', verify: async () => { const removed = deferred>(); const inspectDataService = vi.fn().mockResolvedValue(success(dataServiceInstance('after-remove'))); const store = createCodingPluginsStore({ list: vi.fn().mockResolvedValue(projection(false)), removeCollection: vi.fn(() => removed.promise), inspectDataService, }); store.setState({ projectId: 'local-project', projection: projection(false), dataService: dataServiceInstance('before-remove'), }); const mutation = store.getState().removeCollection('todos'); await store.getState().load('local-project'); removed.resolve(success({ removed: true, usage: { document_count: 0, total_bytes: 0 } })); await mutation; expect(inspectDataService).toHaveBeenCalledOnce(); expect(store.getState().dataService?.instance_id).toBe('after-remove'); }, }, { name: 'removeProject', verify: async () => { const removed = deferred>(); const list = vi.fn().mockResolvedValue(projection(false)); const store = createCodingPluginsStore({ list, removeProject: vi.fn(() => removed.promise), }); store.setState({ projectId: 'local-project', projection: projection(false), dataService: dataServiceInstance('before-remove-project'), }); const mutation = store.getState().removeProject(); await store.getState().load('local-project'); removed.resolve(success({ removed: true })); await mutation; expect(list).toHaveBeenCalledTimes(2); expect(store.getState().dataService).toBeNull(); }, }, ]; it.each(adjacentMutationCases)( 'uses the same same-project ordering guard for $name', async ({ verify }) => await verify(), ); it('reloads the newest projection when collection removal succeeds but inspection fails', async () => { const readyProjection = projection(true); readyProjection.items[0] = { ...readyProjection.items[0], state: 'ready', backend: { status: 'ready' }, }; const afterRemoval = dataServiceInstance('after-remove-reload'); const inspectDataService = vi.fn() .mockResolvedValueOnce({ success: false, status: 503, code: 'offline', error: 'offline', retryable: true, data: null, }) .mockResolvedValueOnce(success(afterRemoval)); const list = vi.fn().mockResolvedValue(readyProjection); const store = createCodingPluginsStore({ list, inspectDataService, removeCollection: vi.fn().mockResolvedValue(success({ removed: true, usage: { document_count: 0, total_bytes: 0 }, })), }); store.setState({ projectId: 'local-project', projection: readyProjection, dataService: dataServiceInstance('before-remove-reload'), }); await store.getState().removeCollection('todos'); expect(list).toHaveBeenCalledOnce(); expect(inspectDataService).toHaveBeenCalledTimes(2); expect(store.getState().dataService).toEqual(afterRemoval); }); it('does not let a late project A enable mutation overwrite loaded project B', async () => { const enabledA = deferred>(); const inspectDataService = vi.fn(); const store = createCodingPluginsStore({ list: vi.fn().mockResolvedValue(projection(false, 'project-b')), setEnabled: vi.fn(() => enabledA.promise), inspectDataService, }); store.setState({ projectId: 'project-a', projection: projection(false, 'project-a') }); const mutation = store.getState().setEnabled('project-a', 'makelore.data-service', true); await store.getState().load('project-b'); enabledA.resolve(projection(true, 'project-a')); await mutation; expect(store.getState()).toMatchObject({ projectId: 'project-b', projection: { project: { localProjectId: 'project-b' } }, }); expect(inspectDataService).not.toHaveBeenCalled(); }); it('does not let adjacent late Data Service mutations commit or reload over project B', async () => { const configure = deferred>(); const reset = deferred>(); const removeCollection = deferred>(); const removeProject = deferred>(); const inspectDataService = vi.fn(); const list = vi.fn().mockResolvedValue(projection(false, 'project-b')); const store = createCodingPluginsStore({ list, inspectDataService, configureDataService: vi.fn(() => configure.promise), resetDataService: vi.fn(() => reset.promise), removeCollection: vi.fn(() => removeCollection.promise), removeProject: vi.fn(() => removeProject.promise), }); store.setState({ projectId: 'project-a', projection: projection(false, 'project-a') }); const mutations = [ store.getState().configure(['todos']), store.getState().reset(), store.getState().removeCollection('todos'), store.getState().removeProject(), ]; await store.getState().load('project-b'); configure.resolve(success(dataServiceInstance('late-configure'))); reset.resolve(success(dataServiceInstance('late-reset'))); removeCollection.resolve(success({ removed: true, usage: { document_count: 0, total_bytes: 0 } })); removeProject.resolve(success({ removed: true })); await Promise.all(mutations); expect(store.getState()).toMatchObject({ projectId: 'project-b', projection: { project: { localProjectId: 'project-b' } }, dataService: null, }); expect(inspectDataService).not.toHaveBeenCalled(); expect(list).toHaveBeenCalledOnce(); }); it('retains ready Data Service usage when enabling an existing instance', async () => { const readyProjection = projection(true); readyProjection.items[0] = { ...readyProjection.items[0], state: 'ready', backend: { status: 'ready' }, }; const existing = { instance_id: 'instance-1', project_id: 'cloud-project', collections: ['todos'], usage: { document_count: 3, total_bytes: 128 }, limits: { max_collections: 20, max_documents: 1000, max_total_bytes: 20971520, max_document_bytes: 65536, list_default_limit: 50, list_max_limit: 100, list_max_data_bytes: 1048576, mutations_per_minute: 120 }, created_at: '2026-08-27T00:00:00Z', updated_at: '2026-08-27T00:00:00Z', }; const inspectDataService = vi.fn().mockResolvedValue({ success: true, status: 200, code: null, error: null, retryable: false, data: existing, }); const store = createCodingPluginsStore({ setEnabled: vi.fn().mockResolvedValue(readyProjection), inspectDataService, }); store.setState({ projectId: 'local-project' }); await store.getState().setEnabled('local-project', 'makelore.data-service', true); expect(inspectDataService).toHaveBeenCalledOnce(); expect(store.getState().dataService).toEqual(existing); expect(store.getState().projection).toEqual(readyProjection); }); });