fix(ai): clean up old auth profile when rename + key change shifts profile name

When a model service is renamed and its API key changed simultaneously,
onApiKeyCreated derives a new profile name from the new service name and
writes a new auth profile, but the old profile (keyed by the old name)
was never deleted, leaving an orphaned credential on disk.

Capture the old authProfile before applying updates. If onApiKeyCreated
returns a different profile name, call onApiKeyDeleted on the old config
to trigger cleanup before persisting the new profile.

No cleanup is needed for key-only updates (profile name unchanged) or
name-only updates (onApiKeyCreated is not called).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
diguaandClaude Sonnet 4.6 committed 2026-06-23 22:58:22 +08:00
1 parent 04680fc840
commit 46066d7a5f
2 files changed
+47 -1

No files matched your search

@@ -200,6 +200,46 @@ describe('LLMConfigStore', () => {
assert.equal(storeWithHook.getAllConfigs().length, 0, 'config should be removed')
})
it('calls onApiKeyDeleted for old profile when rename + key change causes profile name to change', () => {
const deleted: string[] = []
const storeWithHook = new LLMConfigStore(storage, {
generateId: () => `id-${++idCounter}`,
onApiKeyCreated: (config, apiKey) => {
const name = config.name.toLowerCase().replace(/\s+/g, '-')
;(config as unknown as Record<string, unknown>).authProfile = name
return name
},
onApiKeyDeleted: (config) => {
const profile = (config as unknown as Record<string, unknown>).authProfile as string | undefined
if (profile) deleted.push(profile)
},
})
storeWithHook.addConfig({ name: 'My OpenAI', provider: 'openai', apiKey: 'sk-old' })
const id = storeWithHook.getAllConfigs()[0].id
storeWithHook.updateConfig(id, { name: 'Work OpenAI', apiKey: 'sk-new' })
assert.deepEqual(deleted, ['my-openai'], 'old profile should be cleaned up when profile name changes')
})
it('does not call onApiKeyDeleted when key changes but profile name stays the same', () => {
const deleted: string[] = []
const storeWithHook = new LLMConfigStore(storage, {
generateId: () => `id-${++idCounter}`,
onApiKeyCreated: (config, apiKey) => {
const name = config.name.toLowerCase().replace(/\s+/g, '-')
;(config as unknown as Record<string, unknown>).authProfile = name
return name
},
onApiKeyDeleted: (config) => {
const profile = (config as unknown as Record<string, unknown>).authProfile as string | undefined
if (profile) deleted.push(profile)
},
})
storeWithHook.addConfig({ name: 'My OpenAI', provider: 'openai', apiKey: 'sk-old' })
const id = storeWithHook.getAllConfigs()[0].id
storeWithHook.updateConfig(id, { apiKey: 'sk-new' })
assert.deepEqual(deleted, [], 'no cleanup needed when profile name is unchanged')
})
it('resolves correct key for same-provider configs with different authProfiles', () => {
const profiles = new Map<string, string>()
const storeWithAuth = new LLMConfigStore(storage, {
@@ -211,8 +211,11 @@ export class LLMConfigStore {
return { success: false, error: this.t('llm.configNotFound') }
}
const oldConfig = store.configs[index]
const oldProfileName = (oldConfig as unknown as Record<string, unknown>).authProfile as string | undefined
const updated = {
...store.configs[index],
...oldConfig,
...updates,
updatedAt: Date.now(),
}
@@ -221,6 +224,9 @@ export class LLMConfigStore {
if (updates.apiKey && this.onApiKeyCreated) {
const profileName = this.onApiKeyCreated(updated, updates.apiKey)
if (profileName) {
if (oldProfileName && oldProfileName !== profileName) {
this.onApiKeyDeleted?.(oldConfig)
}
;(store.configs[index] as unknown as Record<string, unknown>).authProfile = profileName
}
}