mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 13:18:19 +08:00
fix: don't switch LLM profile before the conversation UUID exists (avoids 422) (#14900)
This commit is contained in:
@@ -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<string, string>,
|
||||
}));
|
||||
@@ -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
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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-<uuid>`, 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() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
disabled={isPending}
|
||||
disabled={isPending || isStarting}
|
||||
data-testid="switch-profile-button"
|
||||
title={activeProfileModel ?? undefined}
|
||||
aria-haspopup="menu"
|
||||
|
||||
@@ -17,6 +17,11 @@ export function useSwitchLlmProfileAndLog() {
|
||||
// Stable identity so the /model interceptor's outer useCallback doesn't bust each render.
|
||||
const switchAndLog = useCallback(
|
||||
(conversationId: string, profileName: string) => {
|
||||
// During chat startup the id is the placeholder `task-<uuid>` (the real
|
||||
// conversation UUID doesn't exist yet); the switch endpoint types its
|
||||
// path param as a UUID and would 422. No-op until the UUID is live.
|
||||
if (conversationId.startsWith("task-")) return;
|
||||
|
||||
const last = getRenderedV1Events(useEventStore.getState().uiEvents).at(
|
||||
-1,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user