From 83ce412d7e85aa92dfb1a51f0cb429e24879cc65 Mon Sep 17 00:00:00 2001 From: Alona Date: Mon, 29 Jun 2026 14:08:47 -0500 Subject: [PATCH] fix: don't switch LLM profile before the conversation UUID exists (avoids 422) (#14900) --- .../features/chat/switch-profile-button.test.tsx | 11 ++++++++++- .../mutation/use-switch-llm-profile-and-log.test.ts | 11 +++++++++++ .../features/chat/switch-profile-button.tsx | 6 +++++- .../hooks/mutation/use-switch-llm-profile-and-log.ts | 5 +++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/frontend/__tests__/components/features/chat/switch-profile-button.test.tsx b/frontend/__tests__/components/features/chat/switch-profile-button.test.tsx index dab76885b4..04be9d2d04 100644 --- a/frontend/__tests__/components/features/chat/switch-profile-button.test.tsx +++ b/frontend/__tests__/components/features/chat/switch-profile-button.test.tsx @@ -13,6 +13,7 @@ import type { LlmProfileSummary } from "#/api/settings-service/profiles-service. const mockUseLlmProfiles = vi.hoisted(() => vi.fn()); const mockUseActiveConversation = vi.hoisted(() => vi.fn()); const mockSwitchAndLog = vi.hoisted(() => vi.fn()); +const mockConversationId = vi.hoisted(() => ({ value: "conv-1" })); const mockModelStore = vi.hoisted(() => ({ activeProfileByConversation: {} as Record, })); @@ -34,7 +35,7 @@ vi.mock("#/hooks/mutation/use-switch-llm-profile-and-log", () => ({ })); vi.mock("#/hooks/use-conversation-id", () => ({ - useConversationId: () => ({ conversationId: "conv-1" }), + useConversationId: () => ({ conversationId: mockConversationId.value }), })); vi.mock("#/stores/model-store", () => ({ @@ -102,6 +103,7 @@ const setupHooks = ( describe("SwitchProfileButton", () => { beforeEach(() => { mockSwitchAndLog.mockReset(); + mockConversationId.value = "conv-1"; }); afterEach(() => { @@ -205,6 +207,13 @@ describe("SwitchProfileButton", () => { expect(mockSwitchAndLog).toHaveBeenCalledWith("conv-1", "default"); }); + it("is disabled while the conversation id is still the `task-` placeholder", () => { + mockConversationId.value = "task-abc-123"; + setupHooks({ conversationModel: "openai/gpt-5" }); + renderButton(); + expect(screen.getByTestId("switch-profile-button")).toBeDisabled(); + }); + it("does not call switchAndLog when the already-active profile is selected", async () => { const user = userEvent.setup(); setupHooks({ conversationModel: "openai/gpt-5" }); // gpt-5 is active diff --git a/frontend/__tests__/hooks/mutation/use-switch-llm-profile-and-log.test.ts b/frontend/__tests__/hooks/mutation/use-switch-llm-profile-and-log.test.ts index 217343c00d..5491ef3c48 100644 --- a/frontend/__tests__/hooks/mutation/use-switch-llm-profile-and-log.test.ts +++ b/frontend/__tests__/hooks/mutation/use-switch-llm-profile-and-log.test.ts @@ -52,6 +52,17 @@ describe("useSwitchLlmProfileAndLog", () => { ); }); + it("no-ops for a `task-` placeholder id (no call, no error toast) during startup", async () => { + const { result } = renderTestHook(); + act(() => result.current.switchAndLog("task-abc-123", "gpt-5")); + // Give any pending mutation a tick to fire (it shouldn't). + await new Promise((r) => { + setTimeout(r, 0); + }); + expect(mockSwitchProfile).not.toHaveBeenCalled(); + expect(mockDisplayErrorToast).not.toHaveBeenCalled(); + }); + it("records a switch entry on success, anchored to the latest rendered v1 event", async () => { useEventStore.setState({ events: [], diff --git a/frontend/src/components/features/chat/switch-profile-button.tsx b/frontend/src/components/features/chat/switch-profile-button.tsx index e501b9ff34..690635e119 100644 --- a/frontend/src/components/features/chat/switch-profile-button.tsx +++ b/frontend/src/components/features/chat/switch-profile-button.tsx @@ -25,6 +25,10 @@ export function SwitchProfileButton() { const profiles = data?.profiles ?? []; const conversationModel = conversation?.llm_model ?? null; + // Still starting: the id is the placeholder `task-`, not the real + // conversation UUID yet, so a switch can't be persisted (would 422). + const isStarting = conversationId.startsWith("task-"); + // Resolve the active profile, most-authoritative source first: // 1. A switch the user made this session (recorded by name, so it's exact // even when several profiles share a model string, e.g. SaaS managed @@ -77,7 +81,7 @@ export function SwitchProfileButton() {