diff --git a/apps/desktop/src/app/settings/env-credentials.tsx b/apps/desktop/src/app/settings/env-credentials.tsx index 92ba8903c2..9b5310df4d 100644 --- a/apps/desktop/src/app/settings/env-credentials.tsx +++ b/apps/desktop/src/app/settings/env-credentials.tsx @@ -98,8 +98,8 @@ export function useEnvCredentials(profile?: string): UseEnvCredentials { setVars(c => (c ? { ...c, [key]: { ...c[key], ...patch } } : c)) } - function clearLocalState(key: string) { - setEdits(c => withoutKey(c, key)) + function clearLocalState(key: string, editKey = key) { + setEdits(c => withoutKey(c, editKey)) setRevealed(c => withoutKey(c, key)) } @@ -115,7 +115,7 @@ export function useEnvCredentials(profile?: string): UseEnvCredentials { try { await setEnvVar(key, value, profile) patchVar(key, { is_set: true, redacted_value: redactedValue(value) }) - clearLocalState(editKey) + clearLocalState(key, editKey) void queryClient.invalidateQueries({ queryKey: ['model-options'] }) notify({ kind: 'success', title: toolsets.savedTitle, message: toolsets.savedMessage(key) }) } catch (err) { @@ -164,7 +164,7 @@ export function useEnvCredentials(profile?: string): UseEnvCredentials { try { await deleteEnvVar(key, profile) patchVar(key, { is_set: false, redacted_value: null }) - clearLocalState(editKey) + clearLocalState(key, editKey) void queryClient.invalidateQueries({ queryKey: ['model-options'] }) notify({ kind: 'success', title: toolsets.removedTitle, message: toolsets.removedMessage(key) }) } catch (err) { diff --git a/apps/desktop/src/app/settings/providers-settings.test.tsx b/apps/desktop/src/app/settings/providers-settings.test.tsx index 9773ceab4d..29e84c7892 100644 --- a/apps/desktop/src/app/settings/providers-settings.test.tsx +++ b/apps/desktop/src/app/settings/providers-settings.test.tsx @@ -9,6 +9,7 @@ import type { EnvVarInfo, OAuthProvider } from '@/types/hermes' const listOAuthProviders = vi.fn() const disconnectOAuthProvider = vi.fn() const getEnvVars = vi.fn() +const revealEnvVar = vi.fn() const setEnvVar = vi.fn() const startManualProviderOAuth = vi.fn() const startManualLocalEndpoint = vi.fn() @@ -25,10 +26,11 @@ vi.mock('@/store/profile', () => ({ vi.mock('@/hermes', () => ({ setApiRequestProfile: vi.fn(), getProfiles: async () => ({ profiles: (await import('@/store/profile')).$profiles.get() }), - setEnvVar: (key: string, value: string, profile?: string) => setEnvVar(key, value, profile), disconnectOAuthProvider: (...args: unknown[]) => disconnectOAuthProvider(...args), getEnvVars: (...args: unknown[]) => getEnvVars(...args), - listOAuthProviders: (...args: unknown[]) => listOAuthProviders(...args) + listOAuthProviders: (...args: unknown[]) => listOAuthProviders(...args), + revealEnvVar: (key: string, profile?: string) => revealEnvVar(key, profile), + setEnvVar: (key: string, value: string, profile?: string) => setEnvVar(key, value, profile) })) vi.mock('@/store/onboarding', () => ({ @@ -81,6 +83,8 @@ beforeEach(() => { onboarding.set({ manual: false }) getEnvVars.mockResolvedValue({}) disconnectOAuthProvider.mockResolvedValue({ ok: true, provider: 'nous' }) + revealEnvVar.mockResolvedValue({ value: 'old-secret' }) + setEnvVar.mockResolvedValue({ ok: true }) listOAuthProviders.mockResolvedValue({ providers: [provider('nous', true), provider('minimax-oauth', false)] }) @@ -279,6 +283,44 @@ describe('ProvidersSettings', () => { expect((inputs[1] as HTMLInputElement).value).toBe('') }) + it('clears the shared reveal when a namespaced provider-card draft is saved', async () => { + const varKey = 'DASHSCOPE_API_KEY' + const editKey = `Qwen Cloud:${varKey}` + getEnvVars.mockResolvedValue({ + [varKey]: keyVar({ is_set: true, redacted_value: '••••••••' }) + }) + + const { useEnvCredentials } = await import('./env-credentials') + const state = { current: null as null | ReturnType } + + function Harness() { + state.current = useEnvCredentials() + + return null + } + + render() + await waitFor(() => expect(state.current?.vars).not.toBeNull()) + + await act(async () => { + await Promise.resolve(state.current!.rowProps.onReveal(varKey)) + }) + expect(state.current!.rowProps.revealed[varKey]).toBe('old-secret') + + act(() => { + state.current!.rowProps.setEdits(current => ({ ...current, [editKey]: 'new-secret' })) + }) + await waitFor(() => expect(state.current!.rowProps.edits[editKey]).toBe('new-secret')) + + await act(async () => { + await Promise.resolve(state.current!.rowProps.onSave(varKey, editKey)) + }) + + expect(setEnvVar).toHaveBeenCalledWith(varKey, 'new-secret', undefined) + expect(state.current!.rowProps.edits[editKey]).toBeUndefined() + expect(state.current!.rowProps.revealed[varKey]).toBeUndefined() + }) + it('orders API-key providers by priority then name, and filters them via search', async () => { // These three providers have no curated PROVIDER_GROUPS priority, so they // share the default priority and fall back to alphabetical among themselves