diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index 754105c46b..f97dc4886a 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -1545,6 +1545,7 @@ func (h *AccountHandler) Refresh(c *gin.Context) { if warning == "missing_project_id_temporary" { response.Success(c, gin.H{ + "account": h.buildAccountResponseWithRuntime(c.Request.Context(), updatedAccount), "message": "Token refreshed successfully, but project_id could not be retrieved (will retry automatically)", "warning": "missing_project_id_temporary", }) diff --git a/frontend/src/api/admin/accounts.ts b/frontend/src/api/admin/accounts.ts index 9f4506182c..6bd533214e 100644 --- a/frontend/src/api/admin/accounts.ts +++ b/frontend/src/api/admin/accounts.ts @@ -311,9 +311,13 @@ export async function testAccount(id: number): Promise<{ * @param id - Account ID * @returns Updated account */ -export async function refreshCredentials(id: number): Promise { - const { data } = await apiClient.post(`/admin/accounts/${id}/refresh`) - return data +export type RefreshCredentialsResult = + | { account: Account; message: string; warning: 'missing_project_id_temporary' } + | { account: Account; message?: never; warning?: never } + +export async function refreshCredentials(id: number): Promise { + const { data } = await apiClient.post(`/admin/accounts/${id}/refresh`) + return 'account' in data ? data : { account: data } } /** diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index 233fef9791..5df2c8afaa 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -2352,9 +2352,10 @@ const handleDuplicateAccount = async (a: Account) => { } const handleRefresh = async (a: Account) => { try { - const updated = await adminAPI.accounts.refreshCredentials(a.id) - patchAccountInList(updated) + const result = await adminAPI.accounts.refreshCredentials(a.id) + patchAccountInList(result.account) enterAutoRefreshSilentWindow() + if (result.warning) appStore.showWarning(result.message) } catch (error) { console.error('Failed to refresh credentials:', error) } diff --git a/frontend/src/views/admin/__tests__/AccountsView.lite.spec.ts b/frontend/src/views/admin/__tests__/AccountsView.lite.spec.ts index 86d29caad1..828f20d6e8 100644 --- a/frontend/src/views/admin/__tests__/AccountsView.lite.spec.ts +++ b/frontend/src/views/admin/__tests__/AccountsView.lite.spec.ts @@ -13,7 +13,9 @@ const { getUpstreamBillingProbeSettings, getAllProxies, getAllGroups, - showError + refreshCredentials, + showError, + showWarning } = vi.hoisted(() => ({ listAccounts: vi.fn(), listWithEtag: vi.fn(), @@ -22,7 +24,9 @@ const { getUpstreamBillingProbeSettings: vi.fn(), getAllProxies: vi.fn(), getAllGroups: vi.fn(), - showError: vi.fn() + refreshCredentials: vi.fn(), + showError: vi.fn(), + showWarning: vi.fn() })) vi.mock('@/api/admin', () => ({ @@ -36,7 +40,8 @@ vi.mock('@/api/admin', () => ({ delete: vi.fn(), batchClearError: vi.fn(), batchRefresh: vi.fn(), - toggleSchedulable: vi.fn() + toggleSchedulable: vi.fn(), + refreshCredentials }, proxies: { getAll: getAllProxies }, groups: { getAll: getAllGroups } @@ -44,7 +49,7 @@ vi.mock('@/api/admin', () => ({ })) vi.mock('@/stores/app', () => ({ - useAppStore: () => ({ showError, showSuccess: vi.fn(), showInfo: vi.fn() }) + useAppStore: () => ({ showError, showWarning, showSuccess: vi.fn(), showInfo: vi.fn() }) })) vi.mock('@/stores/auth', () => ({ @@ -60,7 +65,7 @@ const DataTableStub = defineComponent({ props: { data: { type: Array, default: () => [] } }, template: `
-
+
@@ -161,7 +166,9 @@ describe('admin AccountsView lite account list', () => { getUpstreamBillingProbeSettings.mockReset().mockResolvedValue({ enabled: true }) getAllProxies.mockReset().mockResolvedValue([]) getAllGroups.mockReset().mockResolvedValue([{ id: 7, name: 'codex', platform: 'openai' }]) + refreshCredentials.mockReset() showError.mockReset() + showWarning.mockReset() }) afterEach(() => { @@ -254,6 +261,24 @@ describe('admin AccountsView lite account list', () => { wrapper.unmount() }) + it('shows the warning and patches the account after a partial Antigravity refresh', async () => { + refreshCredentials.mockResolvedValue({ + account: { ...fullAccount, name: 'refreshed account' }, + message: 'Token refreshed, but project_id is temporarily unavailable', + warning: 'missing_project_id_temporary' + }) + const wrapper = mountView(false) + await flushPromises() + + wrapper.findComponent(AccountActionMenu).vm.$emit('refresh-token', listRow) + await flushPromises() + + expect(refreshCredentials).toHaveBeenCalledWith(42) + expect(wrapper.get('[data-account-name]').attributes('data-account-name')).toBe('refreshed account') + expect(showWarning).toHaveBeenCalledWith('Token refreshed, but project_id is temporarily unavailable') + wrapper.unmount() + }) + it('shows an error and keeps the modal closed when detail loading fails', async () => { const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) getById.mockRejectedValueOnce(new Error('detail failed'))