From 1b1c27e9c6ea0eb965ce41ac2559a037ced34d77 Mon Sep 17 00:00:00 2001 From: 777genius Date: Fri, 17 Apr 2026 15:03:34 +0300 Subject: [PATCH] fix(extensions): preserve api key writes on refresh failures --- src/renderer/store/slices/extensionsSlice.ts | 40 +++++++-- test/renderer/store/extensionsSlice.test.ts | 91 ++++++++++++++++++++ 2 files changed, 126 insertions(+), 5 deletions(-) diff --git a/src/renderer/store/slices/extensionsSlice.ts b/src/renderer/store/slices/extensionsSlice.ts index a0301d00..ca766aaf 100644 --- a/src/renderer/store/slices/extensionsSlice.ts +++ b/src/renderer/store/slices/extensionsSlice.ts @@ -336,6 +336,11 @@ function getSkillsCatalogKey(projectPath?: string): string { return projectPath ?? USER_SKILLS_CATALOG_KEY; } +function upsertApiKeyEntry(entries: ApiKeyEntry[], entry: ApiKeyEntry): ApiKeyEntry[] { + const nextEntries = entries.filter((candidate) => candidate.id !== entry.id); + return [entry, ...nextEntries]; +} + /** Duration to show "success" state before returning to idle */ const SUCCESS_DISPLAY_MS = 2_000; const PROJECT_SCOPE_REQUIRED_MESSAGE = @@ -1286,10 +1291,29 @@ export const createExtensionsSlice: StateCreator ({ + apiKeys: upsertApiKeyEntry(prev.apiKeys, savedKey), + })); + } + + await get().fetchCliStatus(); + const refreshError = get().cliStatusError; + if (refreshError) { + warnings.push(`API key saved, but failed to refresh provider status. ${refreshError}`); + } + set({ apiKeySaving: false, apiKeysError: warnings.length > 0 ? warnings.join(' ') : null }); } catch (err) { set({ apiKeySaving: false, @@ -1305,10 +1329,16 @@ export const createExtensionsSlice: StateCreator ({ apiKeys: prev.apiKeys.filter((k) => k.id !== id), })); + await get().fetchCliStatus(); + const refreshError = get().cliStatusError; + set({ + apiKeysError: refreshError + ? `API key deleted, but failed to refresh provider status. ${refreshError}` + : null, + }); } catch (err) { set({ apiKeysError: err instanceof Error ? err.message : 'Failed to delete API key', diff --git a/test/renderer/store/extensionsSlice.test.ts b/test/renderer/store/extensionsSlice.test.ts index 27790d32..e994b3b9 100644 --- a/test/renderer/store/extensionsSlice.test.ts +++ b/test/renderer/store/extensionsSlice.test.ts @@ -1025,6 +1025,71 @@ describe('extensionsSlice', () => { expect(store.getState().apiKeys).toHaveLength(1); }); + it('keeps saved API keys updated when provider status refresh fails', async () => { + (api.apiKeys!.save as ReturnType).mockResolvedValue({ + id: 'k1', + name: 'Codex key', + envVarName: 'OPENAI_API_KEY', + maskedValue: '***', + scope: 'user', + createdAt: '2026-04-17T10:00:00.000Z', + }); + (api.apiKeys!.list as ReturnType).mockResolvedValue([ + { + id: 'k1', + name: 'Codex key', + envVarName: 'OPENAI_API_KEY', + maskedValue: '***', + scope: 'user', + createdAt: '2026-04-17T10:00:00.000Z', + }, + ]); + (api.cliInstaller!.getStatus as ReturnType).mockRejectedValue( + new Error('refresh boom') + ); + + await store.getState().saveApiKey({ + name: 'Codex key', + envVarName: 'OPENAI_API_KEY', + value: 'secret', + scope: 'user', + }); + + expect(store.getState().apiKeys).toHaveLength(1); + expect(store.getState().apiKeysError).toContain('API key saved, but failed to refresh provider status.'); + }); + + it('keeps local API key state updated when key list refresh fails after save', async () => { + (api.apiKeys!.save as ReturnType).mockResolvedValue({ + id: 'k1', + name: 'Codex key', + envVarName: 'OPENAI_API_KEY', + maskedValue: '***', + scope: 'user', + createdAt: '2026-04-17T10:00:00.000Z', + }); + (api.apiKeys!.list as ReturnType).mockRejectedValue(new Error('list boom')); + + await store.getState().saveApiKey({ + name: 'Codex key', + envVarName: 'OPENAI_API_KEY', + value: 'secret', + scope: 'user', + }); + + expect(store.getState().apiKeys).toEqual([ + { + id: 'k1', + name: 'Codex key', + envVarName: 'OPENAI_API_KEY', + maskedValue: '***', + scope: 'user', + createdAt: '2026-04-17T10:00:00.000Z', + }, + ]); + expect(store.getState().apiKeysError).toContain('API key saved, but failed to refresh key list.'); + }); + it('refreshes CLI status after deleting an API key', async () => { store.setState({ apiKeys: [ @@ -1046,6 +1111,32 @@ describe('extensionsSlice', () => { expect(store.getState().apiKeys).toEqual([]); }); + it('keeps local API key state updated when provider status refresh fails after delete', async () => { + store.setState({ + apiKeys: [ + { + id: 'k1', + name: 'Codex key', + envVarName: 'OPENAI_API_KEY', + maskedValue: '***', + scope: 'user', + createdAt: '2026-04-17T10:00:00.000Z', + }, + ], + }); + (api.apiKeys!.delete as ReturnType).mockResolvedValue(undefined); + (api.cliInstaller!.getStatus as ReturnType).mockRejectedValue( + new Error('refresh boom') + ); + + await store.getState().deleteApiKey('k1'); + + expect(store.getState().apiKeys).toEqual([]); + expect(store.getState().apiKeysError).toContain( + 'API key deleted, but failed to refresh provider status.' + ); + }); + it('keys MCP diagnostics by scope when the same server exists in multiple scopes', async () => { (api.mcpRegistry!.diagnose as ReturnType).mockResolvedValue([ {