diff --git a/src/features/authFiles/AuthFilesPage.tsx b/src/features/authFiles/AuthFilesPage.tsx index 972eb40e..c7e1bd7c 100644 --- a/src/features/authFiles/AuthFilesPage.tsx +++ b/src/features/authFiles/AuthFilesPage.tsx @@ -29,6 +29,7 @@ import { OAuthModelAliasCard } from '@/features/authFiles/components/OAuthModelA import { ProviderTabs } from '@/features/authFiles/components/ProviderTabs'; import { VaultHeader } from '@/features/authFiles/components/VaultHeader'; import { VaultPulse } from '@/features/authFiles/components/VaultPulse'; +import { invalidateAuthFileDerivedCaches } from '@/features/authFiles/cacheInvalidation'; import { useAuthFilesData } from '@/features/authFiles/hooks/useAuthFilesData'; import { useAuthFilesModels } from '@/features/authFiles/hooks/useAuthFilesModels'; import { useAuthFilesOauth } from '@/features/authFiles/hooks/useAuthFilesOauth'; @@ -110,6 +111,11 @@ export function AuthFilesPage() { invalidateModels, } = useAuthFilesModels(); + const invalidateDerivedCaches = useCallback( + (names?: string[]) => invalidateAuthFileDerivedCaches(invalidateModels, names), + [invalidateModels] + ); + const { files, selectedFiles, @@ -139,7 +145,7 @@ export function AuthFilesPage() { batchDownload, batchSetStatus, batchDelete, - } = useAuthFilesData({ onFilesMutated: invalidateModels }); + } = useAuthFilesData({ onFilesMutated: invalidateDerivedCaches }); const statusBarCache = useAuthFilesStatusBarCache(files); diff --git a/src/features/authFiles/cacheInvalidation.ts b/src/features/authFiles/cacheInvalidation.ts new file mode 100644 index 00000000..56304398 --- /dev/null +++ b/src/features/authFiles/cacheInvalidation.ts @@ -0,0 +1,12 @@ +import { useQuotaStore } from '@/stores/useQuotaStore'; + +type ModelsInvalidator = (names?: string[]) => void; + +/** Invalidate every cache whose contents depend on an auth file's credentials. */ +export const invalidateAuthFileDerivedCaches = ( + invalidateModels: ModelsInvalidator, + names?: string[] +): void => { + invalidateModels(names); + useQuotaStore.getState().clearQuotaCache(); +}; diff --git a/tests/quotaSessionIsolation.test.ts b/tests/quotaSessionIsolation.test.ts index 4a56102a..64770a24 100644 --- a/tests/quotaSessionIsolation.test.ts +++ b/tests/quotaSessionIsolation.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, test } from 'bun:test'; +import { invalidateAuthFileDerivedCaches } from '../src/features/authFiles/cacheInvalidation'; import { captureQuotaCacheGeneration, commitIfQuotaCacheCurrent, @@ -35,4 +36,35 @@ describe('quota cache session isolation', () => { ).toBe(true); expect(committed).toBe(true); }); + + test('clears same-name quota and rejects an in-flight commit after auth mutation', () => { + const fileName = 'shared-codex.json'; + useQuotaStore.getState().setCodexQuota({ + [fileName]: { + status: 'success', + windows: [], + planType: 'account-a', + }, + }); + const accountARequest = captureQuotaCacheGeneration(); + let invalidatedNames: string[] | undefined; + + invalidateAuthFileDerivedCaches( + (names) => { + invalidatedNames = names; + }, + [fileName] + ); + + expect(invalidatedNames).toEqual([fileName]); + expect(useQuotaStore.getState().codexQuota[fileName]).toBeUndefined(); + + let committed = false; + expect( + commitIfQuotaCacheCurrent(accountARequest, () => { + committed = true; + }) + ).toBe(false); + expect(committed).toBe(false); + }); });