fix(plugin): serialize project refresh mutations
This commit is contained in:
@@ -118,6 +118,162 @@ describe('coding plugins store', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not let an older same-project load overwrite a completed enable mutation', async () => {
|
||||
const staleLoad = deferred<ReturnType<typeof projection>>();
|
||||
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<ReturnType<typeof projection>>();
|
||||
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<DataServiceHostResult<DataServiceInstanceState>>();
|
||||
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<DataServiceHostResult<DataServiceInstanceState>>();
|
||||
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<DataServiceHostResult<DataServiceCollectionRemoval>>();
|
||||
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<DataServiceHostResult<DataServiceInstanceRemoval>>();
|
||||
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<ReturnType<typeof projection>>();
|
||||
const inspectDataService = vi.fn();
|
||||
|
||||
Reference in New Issue
Block a user