diff --git a/__tests__/components/features/chat/components/chat-input-model-profiles.test.tsx b/__tests__/components/features/chat/components/chat-input-model-profiles.test.tsx new file mode 100644 index 000000000..aa473d2fd --- /dev/null +++ b/__tests__/components/features/chat/components/chat-input-model-profiles.test.tsx @@ -0,0 +1,186 @@ +import { screen } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderWithProviders } from "test-utils"; +import type { ChatInputModelState } from "#/hooks/use-chat-input-model-state"; +import type { ProfileWithPlan } from "#/hooks/use-profile-runtime-plans"; + +const useProfileRuntimePlansMock = vi.fn(); +const switchAcpModelMutate = vi.fn(); +const switchAndLog = vi.fn(); + +vi.mock("#/hooks/use-profile-runtime-plans", () => ({ + useProfileRuntimePlans: () => useProfileRuntimePlansMock(), +})); + +vi.mock("#/hooks/mutation/use-switch-acp-model", () => ({ + useSwitchAcpModel: () => ({ mutate: switchAcpModelMutate }), +})); + +vi.mock("#/hooks/mutation/use-switch-llm-profile-and-log", () => ({ + useSwitchLlmProfileAndLog: () => ({ switchAndLog, isPending: false }), +})); + +import { ChatInputModelMenuContent } from "#/components/features/chat/components/chat-input-model"; + +const acpModelState: ChatInputModelState = { + isAcpContext: true, + displayModel: "Claude Opus 4.7", + currentModelId: "claude-opus-4-7", + availableAcpModels: [{ id: "claude-opus-4-7", label: "Claude Opus 4.7" }], + showAcpPicker: true, + switchConversationId: "conv-1", + destinationPath: "/settings/agent", + destinationLabel: "Agent", +}; + +const disabledProfile: ProfileWithPlan = { + profile: { + name: "Cheap GPT daily driver", + model: "openai/gpt-4o", + base_url: null, + api_key_set: true, + }, + plan: { action: "disabled", reason: "different-agent-kind" }, +}; + +describe("ChatInputModelMenuContent disabled-profile section", () => { + beforeEach(() => { + useProfileRuntimePlansMock.mockReset(); + switchAcpModelMutate.mockReset(); + switchAndLog.mockReset(); + }); + + it("shows incompatible profiles visible-but-disabled with a reason in an ACP conversation", () => { + useProfileRuntimePlansMock.mockReturnValue({ + profiles: [disabledProfile], + activeProfileName: null, + isAcpContext: true, + }); + + renderWithProviders( + {}} />, + ); + + const row = screen.getByTestId( + "chat-input-profile-option-Cheap GPT daily driver", + ); + expect(row).toBeDisabled(); + expect(row).toHaveTextContent("Cheap GPT daily driver"); + // The reason is surfaced inline (not silently swallowed). + expect( + screen.getByTestId( + "chat-input-profile-reason-Cheap GPT daily driver", + ), + ).toBeInTheDocument(); + }); + + it("clicking a disabled profile never triggers a model switch", () => { + useProfileRuntimePlansMock.mockReturnValue({ + profiles: [disabledProfile], + activeProfileName: null, + isAcpContext: true, + }); + + renderWithProviders( + {}} />, + ); + + // disabled