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

View file

@ -167,6 +167,20 @@ describe('extensionsSlice', () => {
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 () => {
let resolveFetch!: (plugins: EnrichedPlugin[]) => void;
const inFlight = new Promise<EnrichedPlugin[]>((resolve) => {
@ -243,6 +257,15 @@ describe('extensionsSlice', () => {
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', () => {