diff --git a/apps/web/src/components/settings/ModelSettingsSection.test.tsx b/apps/web/src/components/settings/ModelSettingsSection.test.tsx index f4ba3a985..6428ae430 100644 --- a/apps/web/src/components/settings/ModelSettingsSection.test.tsx +++ b/apps/web/src/components/settings/ModelSettingsSection.test.tsx @@ -643,6 +643,40 @@ describe('ModelSettingsSection', () => { ).toBeNull(); }); + it('stops retrying a rejected save and allows a deliberate retry', async () => { + const { toast } = await import('sonner'); + settingsData.current = buildSettingsData({ + orchestrationEffectiveModelId: 'openrouter/openai/gpt-5.4', + orchestrationPersistedModelId: 'openrouter/openai/gpt-5.4', + }); + updateMutateAsyncMock + .mockRejectedValueOnce(new Error('Network unavailable')) + // Bound a regression to one extra attempt instead of an infinite loop. + .mockImplementationOnce(() => new Promise(() => {})); + const { container } = renderModelSettingsSection(); + const orchestrationTrigger = () => + container.querySelectorAll( + '[data-slot="select-trigger"]', + )[2]!; + const originalSelection = orchestrationTrigger().textContent; + + fireEvent.click(orchestrationTrigger()); + fireEvent.click(await screen.findByRole('option', { name: 'GLM 5.2' })); + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith( + 'Failed to update model settings.', + ); + }); + expect(updateMutateAsyncMock).toHaveBeenCalledTimes(1); + expect(orchestrationTrigger().textContent).toBe(originalSelection); + + updateMutateAsyncMock.mockReset().mockResolvedValue({ success: true }); + fireEvent.click(orchestrationTrigger()); + fireEvent.click(await screen.findByRole('option', { name: 'GLM 5.2' })); + await waitFor(() => expect(updateMutateAsyncMock).toHaveBeenCalledTimes(1)); + expect(orchestrationTrigger()).toHaveTextContent('GLM 5.2'); + }); + it('renders model metadata in the available models list', () => { const data = buildSettingsData(); (data.models[0]!.metadata as TaskModelMetadata).lastRefreshedAt = diff --git a/apps/web/src/components/settings/ModelSettingsSection.tsx b/apps/web/src/components/settings/ModelSettingsSection.tsx index 88f1a827d..0aae106d3 100644 --- a/apps/web/src/components/settings/ModelSettingsSection.tsx +++ b/apps/web/src/components/settings/ModelSettingsSection.tsx @@ -1333,6 +1333,7 @@ export function ModelSettingsSection({ saveInFlightRef.current = true; setIsSaving(true); + let saveFailed = false; try { const result = await updateMutation.mutateAsync({ @@ -1361,6 +1362,7 @@ export function ModelSettingsSection({ }); if (!result.success) { + saveFailed = true; if (lastSyncedDraftRef.current) { applyDraftLocally(lastSyncedDraftRef.current); } @@ -1394,12 +1396,20 @@ export function ModelSettingsSection({ queryKey: trpc.taskModels.launchOptions.queryKey(), }), ]); + } catch { + saveFailed = true; + if (lastSyncedDraftRef.current) { + applyDraftLocally(lastSyncedDraftRef.current); + } + saveQueuedRef.current = false; + toast.error('Failed to update model settings.'); } finally { saveInFlightRef.current = false; const shouldRunAgain = - saveQueuedRef.current || - !draftsEqual(draftStateRef.current, lastSyncedDraftRef.current); + !saveFailed && + (saveQueuedRef.current || + !draftsEqual(draftStateRef.current, lastSyncedDraftRef.current)); saveQueuedRef.current = false;