/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { render, cleanup } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { ModelDialog } from './ModelDialog.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js'; import { ConfigContext } from '../contexts/ConfigContext.js'; import { SettingsContext } from '../contexts/SettingsContext.js'; import type { Config } from '@qwen-code/qwen-code-core'; import { AuthType } from '@qwen-code/qwen-code-core'; import type { LoadedSettings } from '../../config/settings.js'; import { SettingScope } from '../../config/settings.js'; import { AVAILABLE_MODELS_QWEN, MAINLINE_CODER, MAINLINE_VLM, } from '../models/availableModels.js'; vi.mock('../hooks/useKeypress.js', () => ({ useKeypress: vi.fn(), })); const mockedUseKeypress = vi.mocked(useKeypress); vi.mock('./shared/DescriptiveRadioButtonSelect.js', () => ({ DescriptiveRadioButtonSelect: vi.fn(() => null), })); const mockedSelect = vi.mocked(DescriptiveRadioButtonSelect); const renderComponent = ( props: Partial> = {}, contextValue: Partial | undefined = undefined, ) => { const defaultProps = { onClose: vi.fn(), }; const combinedProps = { ...defaultProps, ...props }; const mockSettings = { isTrusted: true, user: { settings: {} }, workspace: { settings: {} }, setValue: vi.fn(), } as unknown as LoadedSettings; const mockConfig = contextValue ? ({ // --- Functions used by ModelDialog --- getModel: vi.fn(() => MAINLINE_CODER), setModel: vi.fn().mockResolvedValue(undefined), switchModel: vi.fn().mockResolvedValue(undefined), getAuthType: vi.fn(() => 'qwen-oauth'), // --- Functions used by ClearcutLogger --- getUsageStatisticsEnabled: vi.fn(() => true), getSessionId: vi.fn(() => 'mock-session-id'), getDebugMode: vi.fn(() => false), getContentGeneratorConfig: vi.fn(() => ({ authType: AuthType.QWEN_OAUTH, model: MAINLINE_CODER, })), getUseSmartEdit: vi.fn(() => false), getUseModelRouter: vi.fn(() => false), getProxy: vi.fn(() => undefined), // --- Spread test-specific overrides --- ...contextValue, } as unknown as Config) : undefined; const renderResult = render( , ); return { ...renderResult, props: combinedProps, mockConfig, mockSettings, }; }; describe('', () => { beforeEach(() => { vi.clearAllMocks(); // Ensure env-based fallback models don't leak into this suite from the developer environment. delete process.env['OPENAI_MODEL']; delete process.env['ANTHROPIC_MODEL']; }); afterEach(() => { cleanup(); }); it('renders the title and help text', () => { const { getByText } = renderComponent(); expect(getByText('Select Model')).toBeDefined(); expect(getByText('(Press Esc to close)')).toBeDefined(); }); it('passes all model options to DescriptiveRadioButtonSelect', () => { renderComponent(); expect(mockedSelect).toHaveBeenCalledTimes(1); const props = mockedSelect.mock.calls[0][0]; expect(props.items).toHaveLength(AVAILABLE_MODELS_QWEN.length); expect(props.items[0].value).toBe( `${AuthType.QWEN_OAUTH}::${MAINLINE_CODER}`, ); expect(props.items[1].value).toBe( `${AuthType.QWEN_OAUTH}::${MAINLINE_VLM}`, ); expect(props.showNumbers).toBe(true); }); it('initializes with the model from ConfigContext', () => { const mockGetModel = vi.fn(() => MAINLINE_VLM); renderComponent({}, { getModel: mockGetModel }); expect(mockGetModel).toHaveBeenCalled(); expect(mockedSelect).toHaveBeenCalledWith( expect.objectContaining({ initialIndex: 1, }), undefined, ); }); it('initializes with default coder model if context is not provided', () => { renderComponent({}, undefined); expect(mockedSelect).toHaveBeenCalledWith( expect.objectContaining({ initialIndex: 0, }), undefined, ); }); it('initializes with default coder model if getModel returns undefined', () => { const mockGetModel = vi.fn(() => undefined); // @ts-expect-error This test validates component robustness when getModel // returns an unexpected undefined value. renderComponent({}, { getModel: mockGetModel }); expect(mockGetModel).toHaveBeenCalled(); // When getModel returns undefined, preferredModel falls back to MAINLINE_CODER // which has index 0, so initialIndex should be 0 expect(mockedSelect).toHaveBeenCalledWith( expect.objectContaining({ initialIndex: 0, }), undefined, ); expect(mockedSelect).toHaveBeenCalledTimes(1); }); it('calls config.switchModel and onClose when DescriptiveRadioButtonSelect.onSelect is triggered', async () => { const { props, mockConfig, mockSettings } = renderComponent({}, {}); // Pass empty object for contextValue const childOnSelect = mockedSelect.mock.calls[0][0].onSelect; expect(childOnSelect).toBeDefined(); await childOnSelect(`${AuthType.QWEN_OAUTH}::${MAINLINE_CODER}`); expect(mockConfig?.switchModel).toHaveBeenCalledWith( AuthType.QWEN_OAUTH, MAINLINE_CODER, undefined, { reason: 'user_manual', context: 'Model switched via /model dialog', }, ); expect(mockSettings.setValue).toHaveBeenCalledWith( SettingScope.User, 'model.name', MAINLINE_CODER, ); expect(mockSettings.setValue).toHaveBeenCalledWith( SettingScope.User, 'security.auth.selectedType', AuthType.QWEN_OAUTH, ); expect(props.onClose).toHaveBeenCalledTimes(1); }); it('calls config.switchModel and persists authType+model when selecting a different authType', async () => { const switchModel = vi.fn().mockResolvedValue(undefined); const getAuthType = vi.fn(() => AuthType.USE_OPENAI); const getAvailableModelsForAuthType = vi.fn((t: AuthType) => { if (t === AuthType.USE_OPENAI) { return [{ id: 'gpt-4', label: 'GPT-4', authType: t }]; } if (t === AuthType.QWEN_OAUTH) { return AVAILABLE_MODELS_QWEN.map((m) => ({ id: m.id, label: m.label, authType: AuthType.QWEN_OAUTH, })); } return []; }); const mockConfigWithSwitchAuthType = { getAuthType, getModel: vi.fn(() => 'gpt-4'), getContentGeneratorConfig: vi.fn(() => ({ authType: AuthType.QWEN_OAUTH, model: MAINLINE_CODER, })), // Add switchModel to the mock object (not the type) switchModel, getAvailableModelsForAuthType, }; const { props, mockSettings } = renderComponent( {}, // Cast to Config to bypass type checking, matching the runtime behavior mockConfigWithSwitchAuthType as unknown as Partial, ); const childOnSelect = mockedSelect.mock.calls[0][0].onSelect; await childOnSelect(`${AuthType.QWEN_OAUTH}::${MAINLINE_CODER}`); expect(switchModel).toHaveBeenCalledWith( AuthType.QWEN_OAUTH, MAINLINE_CODER, { requireCachedCredentials: true }, { reason: 'user_manual', context: 'AuthType+model switched via /model dialog', }, ); expect(mockSettings.setValue).toHaveBeenCalledWith( SettingScope.User, 'model.name', MAINLINE_CODER, ); expect(mockSettings.setValue).toHaveBeenCalledWith( SettingScope.User, 'security.auth.selectedType', AuthType.QWEN_OAUTH, ); expect(props.onClose).toHaveBeenCalledTimes(1); }); it('does not pass onHighlight to DescriptiveRadioButtonSelect', () => { renderComponent(); const childOnHighlight = mockedSelect.mock.calls[0][0].onHighlight; expect(childOnHighlight).toBeUndefined(); }); it('calls onClose prop when "escape" key is pressed', () => { const { props } = renderComponent(); expect(mockedUseKeypress).toHaveBeenCalled(); const keyPressHandler = mockedUseKeypress.mock.calls[0][0]; const options = mockedUseKeypress.mock.calls[0][1]; expect(options).toEqual({ isActive: true }); keyPressHandler({ name: 'escape', ctrl: false, meta: false, shift: false, paste: false, sequence: '', }); expect(props.onClose).toHaveBeenCalledTimes(1); keyPressHandler({ name: 'a', ctrl: false, meta: false, shift: false, paste: false, sequence: '', }); expect(props.onClose).toHaveBeenCalledTimes(1); }); it('updates initialIndex when config context changes', () => { const mockGetModel = vi.fn(() => MAINLINE_CODER); const mockGetAuthType = vi.fn(() => 'qwen-oauth'); const mockSettings = { isTrusted: true, user: { settings: {} }, workspace: { settings: {} }, setValue: vi.fn(), } as unknown as LoadedSettings; const { rerender } = render( , ); expect(mockedSelect.mock.calls[0][0].initialIndex).toBe(0); mockGetModel.mockReturnValue(MAINLINE_VLM); const newMockConfig = { getModel: mockGetModel, getAuthType: mockGetAuthType, } as unknown as Config; rerender( , ); // Should be called at least twice: initial render + re-render after context change expect(mockedSelect).toHaveBeenCalledTimes(2); expect(mockedSelect.mock.calls[1][0].initialIndex).toBe(1); }); });