fix(settings): stop a background refetch from wiping unsaved agent edits

keepDirtyValues covers reset(), and the create form seeds its agents with
setValue, which ignores it. So on /settings/providers/new?type=… every field
under agents.* snapped back to the type's defaults whenever a settingsProviders
result landed that differed from the rendered one — a reconnect refetch, another
session touching a provider, a changed model catalogue — while the dirty name
and type survived, making the loss look arbitrary. Skip the re-seed when the
type has not changed and the agents subtree is already dirty; a real type switch
still re-seeds, since the previous type's models and defaults are wrong for it.

The create-path twin of the existing edit-path test fails on the old code with
the edited temperature back at the default; a second case pins the type switch,
which a plain dirty-guard would have broken.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-07-25 23:50:34 +07:00
co-authored by Claude Opus 4.8
parent 0030bfb6cd
commit 18abc46162
2 changed files with 67 additions and 6 deletions
@@ -26,8 +26,6 @@ const enabled = {
qwen: true,
};
const emptyProvider = { agents: {} };
const userDefined = [
{
agents: {},
@@ -47,13 +45,23 @@ const userDefined = [
},
];
// The create form seeds agents from the type's defaults, and bails when the type has no models —
// so a fixture with an empty catalogue would make the seeding tests vacuously green.
const agentDefaults = (model: string, temperature: number) => ({
simple: { maxTokens: 1000, model, temperature },
});
const settingsProviders = {
default: {
anthropic: emptyProvider,
openai: emptyProvider,
anthropic: { agents: agentDefaults('claude-e2e', 0.2) },
openai: { agents: agentDefaults('gpt-e2e', 0.7) },
},
enabled,
models: { anthropic: [], minimax: [], openai: [] },
models: {
anthropic: [{ name: 'claude-e2e' }],
minimax: [],
openai: [{ name: 'gpt-e2e' }],
},
userDefined,
};
@@ -159,6 +167,47 @@ describe('SettingsProvider create-form type guards', () => {
expect(navigate).not.toHaveBeenCalled();
});
const expandAgent = () => {
if (!screen.queryByLabelText('Temperature')) {
fireEvent.click(screen.getByRole('button', { name: /Simple/ }));
}
};
const temperatureInput = () => screen.getByLabelText('Temperature') as HTMLInputElement;
it('preserves an in-flight agent edit on the create form across a background refetch', () => {
setSearch('type=openai');
const { rerender } = render(<SettingsProvider />);
expandAgent();
expect(temperatureInput().value).toBe('0.7');
fireEvent.change(temperatureInput(), { target: { value: '1.5' } });
// A refetch that carries no change leaves `data` referentially equal and is inert, so the
// payload has to actually differ for this to exercise the seeding effect.
queryResult.data = {
settingsProviders: { ...settingsProviders, userDefined: [...userDefined] },
};
rerender(<SettingsProvider />);
expect(temperatureInput().value).toBe('1.5');
});
it('still re-seeds the agents when the type changes mid-edit', () => {
setSearch('type=openai');
const { rerender } = render(<SettingsProvider />);
expandAgent();
fireEvent.change(temperatureInput(), { target: { value: '1.5' } });
setSearch('type=anthropic');
rerender(<SettingsProvider />);
expandAgent();
expect(temperatureInput().value).toBe('0.2');
});
it('preserves an in-flight edit across a background refetch', () => {
state.providerId = 'edit-1';
const { rerender } = render(<SettingsProvider />);
@@ -12,7 +12,7 @@ import {
Trash2,
XCircle,
} from 'lucide-react';
import { type ComponentProps, useEffect, useMemo, useState } from 'react';
import { type ComponentProps, useEffect, useMemo, useRef, useState } from 'react';
import {
type Control,
type FieldPath,
@@ -1213,6 +1213,7 @@ function SettingsProvider() {
const { control, formState, handleSubmit: handleFormSubmit, reset, setValue, trigger, watch } = form;
const { isDirty } = useFormState({ control });
const seededTypeRef = useRef<null | string>(null);
useEffect(() => {
if (submitError) {
@@ -1283,6 +1284,17 @@ function SettingsProvider() {
return;
}
// setValue is outside the form's keepDirtyValues, so a background refetch re-runs this effect
// with a fresh `data` identity and overwrites agent edits the user has not saved. Re-seed only
// when the type actually changed — that is the case where the previous type's agents are wrong.
const isSameType = seededTypeRef.current === selectedType;
seededTypeRef.current = selectedType;
if (isSameType && form.getFieldState('agents').isDirty) {
return;
}
const defaultProvider =
data.settingsProviders.default[selectedType as keyof typeof data.settingsProviders.default];