diff --git a/apps/web/src/features/settings/integrations/components/ai-section.test.tsx b/apps/web/src/features/settings/integrations/components/ai-section.test.tsx
new file mode 100644
index 000000000..f668cb376
--- /dev/null
+++ b/apps/web/src/features/settings/integrations/components/ai-section.test.tsx
@@ -0,0 +1,169 @@
+// @vitest-environment happy-dom
+
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { i18n } from "@lingui/core";
+import { I18nProvider } from "@lingui/react";
+
+type MutationName = "create" | "test" | "update" | "delete";
+
+type MockProvider = {
+ id: string;
+ label: string;
+ provider: "openai";
+ model: string;
+ baseURL: string;
+ enabled: boolean;
+ testStatus: string;
+ testError: string | null;
+ apiKeyPreview: string;
+ apiKeyFingerprint: string;
+ lastTestedAt: Date | null;
+ lastUsedAt: Date | null;
+ createdAt: Date;
+ updatedAt: Date;
+};
+
+type MutationOptions = {
+ name: MutationName;
+ meta?: { noInvalidate?: boolean };
+};
+
+type ComboboxProps = {
+ id: string;
+ value: string;
+ options: { value: string; label: string }[];
+ onValueChange: (value: string) => void;
+};
+
+const queryClient = vi.hoisted(() => ({
+ invalidateQueries: vi.fn(),
+ setQueryData: vi.fn(),
+}));
+
+const mutations = vi.hoisted(() => ({
+ create: vi.fn(),
+ test: vi.fn(),
+ update: vi.fn(),
+ delete: vi.fn(),
+}));
+
+const mutationOptions = vi.hoisted(() => ({
+ create: vi.fn((options?: MutationOptions) => ({ ...options, name: "create" })),
+ test: vi.fn((options?: MutationOptions) => ({ ...options, name: "test" })),
+ update: vi.fn((options?: MutationOptions) => ({ ...options, name: "update" })),
+ delete: vi.fn((options?: MutationOptions) => ({ ...options, name: "delete" })),
+}));
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({ data: [], isLoading: false, error: null }),
+ useQueryClient: () => queryClient,
+ useMutation: (options: MutationOptions) => ({
+ isPending: false,
+ mutate: (
+ input: unknown,
+ handlers?: { onSuccess?: (data: unknown) => void; onError?: (error: unknown) => void },
+ ) => {
+ mutations[options.name](input).then(handlers?.onSuccess).catch(handlers?.onError);
+ },
+ mutateAsync: mutations[options.name],
+ }),
+}));
+
+vi.mock("@/libs/orpc/client", () => ({
+ orpc: {
+ aiProviders: {
+ list: {
+ queryOptions: () => ({ queryKey: ["aiProviders", "list"] }),
+ queryKey: () => ["aiProviders", "list"],
+ },
+ create: { mutationOptions: mutationOptions.create },
+ test: { mutationOptions: mutationOptions.test },
+ update: { mutationOptions: mutationOptions.update },
+ delete: { mutationOptions: mutationOptions.delete },
+ },
+ },
+}));
+
+vi.mock("@/components/ui/combobox", () => ({
+ Combobox: ({ id, value, options, onValueChange }: ComboboxProps) => (
+
+ ),
+}));
+
+vi.mock("sonner", () => ({ toast: { error: vi.fn(), success: vi.fn() } }));
+
+i18n.loadAndActivate({ locale: "en", messages: {} });
+
+const { AISettingsSection } = await import("./ai-section");
+
+const provider = (overrides: Partial): MockProvider => ({
+ id: "provider-1",
+ label: "OpenAI",
+ provider: "openai",
+ model: "gpt-4.1",
+ baseURL: "https://api.openai.com/v1",
+ enabled: false,
+ testStatus: "untested",
+ testError: null,
+ apiKeyPreview: "sk-p...test",
+ apiKeyFingerprint: "fingerprint",
+ lastTestedAt: null,
+ lastUsedAt: null,
+ createdAt: new Date("2026-07-04T00:00:00Z"),
+ updatedAt: new Date("2026-07-04T00:00:00Z"),
+ ...overrides,
+});
+
+const renderSection = () =>
+ render(
+
+
+ ,
+ );
+
+describe("AISettingsSection", () => {
+ beforeEach(() => {
+ queryClient.invalidateQueries.mockReset();
+ queryClient.setQueryData.mockReset();
+ mutationOptions.create.mockClear();
+ mutationOptions.test.mockClear();
+ mutationOptions.update.mockClear();
+ mutationOptions.delete.mockClear();
+ mutations.create.mockReset();
+ mutations.test.mockReset();
+ mutations.update.mockReset();
+ mutations.delete.mockReset();
+ });
+
+ it("uses the save-and-test result as the connected provider row", async () => {
+ const created = provider({});
+ const tested = provider({ enabled: true, testStatus: "success", lastTestedAt: new Date("2026-07-04T01:00:00Z") });
+
+ mutations.create.mockResolvedValue(created);
+ mutations.test.mockResolvedValue(tested);
+
+ renderSection();
+
+ fireEvent.change(screen.getByLabelText("API Key"), { target: { value: "sk-test" } });
+ fireEvent.click(screen.getByRole("button", { name: /save & test provider/i }));
+
+ await waitFor(() => expect(queryClient.setQueryData).toHaveBeenCalled());
+
+ expect(mutationOptions.create).toHaveBeenCalledWith({ meta: { noInvalidate: true } });
+ expect(mutationOptions.test).toHaveBeenCalledWith({ meta: { noInvalidate: true } });
+ expect(mutations.update).not.toHaveBeenCalled();
+
+ const [, updater] = queryClient.setQueryData.mock.calls.at(-1) as [
+ unknown,
+ (providers: MockProvider[]) => MockProvider[],
+ ];
+ expect(updater([created])).toEqual([tested]);
+ });
+});
diff --git a/apps/web/src/features/settings/integrations/components/ai-section.tsx b/apps/web/src/features/settings/integrations/components/ai-section.tsx
index 12627ebc1..9326a9939 100644
--- a/apps/web/src/features/settings/integrations/components/ai-section.tsx
+++ b/apps/web/src/features/settings/integrations/components/ai-section.tsx
@@ -122,6 +122,13 @@ function providerLabel(provider: AIProvider) {
return providerOptions.find((option) => option.value === provider)?.label ?? provider;
}
+function upsertProvider(providers: SavedProvider[] | undefined, provider: SavedProvider) {
+ if (!providers) return [provider];
+ if (!providers.some((entry) => entry.id === provider.id)) return [...providers, provider];
+
+ return providers.map((entry) => (entry.id === provider.id ? provider : entry));
+}
+
function isAiProviderConfigError(error: unknown) {
if (error instanceof ORPCError && error.code === "PRECONDITION_FAILED") return true;
@@ -247,11 +254,18 @@ function CreateProviderForm() {
[form.provider],
);
const invalidate = () => queryClient.invalidateQueries({ queryKey: orpc.aiProviders.list.queryKey() });
+ const setProviderInCache = (provider: SavedProvider) =>
+ queryClient.setQueryData(orpc.aiProviders.list.queryKey(), (providers: SavedProvider[] | undefined) =>
+ upsertProvider(providers, provider),
+ );
- const { mutateAsync: createProvider, isPending: isCreating } = useMutation(orpc.aiProviders.create.mutationOptions());
- const { mutateAsync: testProvider, isPending: isTesting } = useMutation(orpc.aiProviders.test.mutationOptions());
- const { mutateAsync: enableProvider, isPending: isEnabling } = useMutation(orpc.aiProviders.update.mutationOptions());
- const isSaving = isCreating || isTesting || isEnabling;
+ const { mutateAsync: createProvider, isPending: isCreating } = useMutation(
+ orpc.aiProviders.create.mutationOptions({ meta: { noInvalidate: true } }),
+ );
+ const { mutateAsync: testProvider, isPending: isTesting } = useMutation(
+ orpc.aiProviders.test.mutationOptions({ meta: { noInvalidate: true } }),
+ );
+ const isSaving = isCreating || isTesting;
// Model/label are prefilled from provider defaults, so step 1 (Provider + API Key) is enough to save.
const model = form.model.trim();
@@ -271,8 +285,9 @@ function CreateProviderForm() {
// Test on save: verify the connection immediately instead of leaving it to a manual step.
const tested = await testProvider({ id: created.id });
+ setProviderInCache(tested);
+
if (tested.testStatus === "success") {
- await enableProvider({ id: created.id, enabled: true });
setForm(emptyForm);
setResult({ ok: true, message: t`Connection verified — provider is ready to use.` });
} else {