fix(extensions): tighten plugin catalog fallback state

This commit is contained in:
777genius 2026-04-16 22:04:02 +03:00
parent afad52f506
commit 09b5b4626f
2 changed files with 36 additions and 3 deletions

View file

@ -237,15 +237,19 @@ export const createExtensionsSlice: StateCreator<AppState, [], [], ExtensionsSli
}; };
}); });
} catch (err) { } catch (err) {
set(() => { set((prev) => {
if (requestSeq !== pluginCatalogRequestSeq) { if (requestSeq !== pluginCatalogRequestSeq) {
return {}; return {};
} }
const nextProjectPath = projectPath ?? null;
const isSameProjectContext = prev.pluginCatalogProjectPath === nextProjectPath;
return { return {
pluginCatalog: isSameProjectContext ? prev.pluginCatalog : [],
pluginCatalogLoading: false, pluginCatalogLoading: false,
pluginCatalogError: err instanceof Error ? err.message : 'Failed to load plugins', pluginCatalogError: err instanceof Error ? err.message : 'Failed to load plugins',
pluginCatalogProjectPath: projectPath ?? null, pluginCatalogProjectPath: nextProjectPath,
}; };
}); });
} finally { } finally {
@ -263,7 +267,13 @@ export const createExtensionsSlice: StateCreator<AppState, [], [], ExtensionsSli
fetchPluginReadme: (pluginId: string) => { fetchPluginReadme: (pluginId: string) => {
if (!api.plugins) return; if (!api.plugins) return;
const state = get(); const state = get();
if (pluginId in state.pluginReadmes || state.pluginReadmeLoading[pluginId]) return; const cachedReadme = state.pluginReadmes[pluginId];
if (
(cachedReadme !== undefined && cachedReadme !== null) ||
state.pluginReadmeLoading[pluginId]
) {
return;
}
set((prev) => ({ set((prev) => ({
pluginReadmeLoading: { ...prev.pluginReadmeLoading, [pluginId]: true }, pluginReadmeLoading: { ...prev.pluginReadmeLoading, [pluginId]: true },

View file

@ -167,6 +167,20 @@ describe('extensionsSlice', () => {
expect(store.getState().pluginCatalogLoading).toBe(false); expect(store.getState().pluginCatalogLoading).toBe(false);
}); });
it('clears stale catalog when a different project fetch fails', async () => {
store.setState({
pluginCatalog: [makePlugin({ pluginId: 'project-a@m' })],
pluginCatalogProjectPath: '/tmp/project-a',
});
(api.plugins!.getAll as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('boom'));
await store.getState().fetchPluginCatalog('/tmp/project-b');
expect(store.getState().pluginCatalog).toEqual([]);
expect(store.getState().pluginCatalogProjectPath).toBe('/tmp/project-b');
expect(store.getState().pluginCatalogError).toBe('boom');
});
it('dedups concurrent requests for the same project key', async () => { it('dedups concurrent requests for the same project key', async () => {
let resolveFetch!: (plugins: EnrichedPlugin[]) => void; let resolveFetch!: (plugins: EnrichedPlugin[]) => void;
const inFlight = new Promise<EnrichedPlugin[]>((resolve) => { const inFlight = new Promise<EnrichedPlugin[]>((resolve) => {
@ -243,6 +257,15 @@ describe('extensionsSlice', () => {
expect(api.plugins!.getReadme).not.toHaveBeenCalled(); expect(api.plugins!.getReadme).not.toHaveBeenCalled();
}); });
it('retries README fetch when the cached value is null', () => {
store.setState({ pluginReadmes: { 'test@m': null } });
(api.plugins!.getReadme as ReturnType<typeof vi.fn>).mockResolvedValue(null);
store.getState().fetchPluginReadme('test@m');
expect(api.plugins!.getReadme).toHaveBeenCalledWith('test@m');
});
}); });
describe('mcpBrowse', () => { describe('mcpBrowse', () => {