Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/cli/src/ui/auth/AuthDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ import { UIActionsContext } from '../contexts/UIActionsContext.js';
import type { UIState } from '../contexts/UIStateContext.js';
import type { UIActions } from '../contexts/UIActionsContext.js';

const discoverProviderModelsMock = vi.hoisted(() =>
vi.fn().mockResolvedValue(null),
);

vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => ({
...(await importOriginal<typeof import('@qwen-code/qwen-code-core')>()),
discoverProviderModels: discoverProviderModelsMock,
}));

type UIStateOverrides = Partial<UIState> & Partial<UIState['auth']>;

type UIActionsOverrides = Partial<UIActions> & Partial<UIActions['auth']>;
Expand Down Expand Up @@ -144,6 +153,18 @@ const waitForSelectedOption = async (
);
};

const waitForText = async (
lastFrame: () => string | undefined,
expectedText: string,
) => {
await vi.waitFor(
() => {
expect(lastFrame()).toContain(expectedText);
},
{ timeout: WAIT_FOR_TIMEOUT },
);
};

const pressEnterAndWaitFor = async (
stdin: { write: (s: string) => void },
lastFrame: () => string | undefined,
Expand Down Expand Up @@ -1249,6 +1270,7 @@ describe('AuthDialog', { timeout: 15000 }, () => {
lastFrame,
'Alibaba ModelStudio · Step 3/3 · Model IDs',
);
await waitForText(lastFrame, 'Enter model IDs directly');
stdin.write('\r');
await vi.waitFor(
() => {
Expand Down Expand Up @@ -1329,6 +1351,7 @@ describe('AuthDialog', { timeout: 15000 }, () => {
lastFrame,
'Alibaba ModelStudio · Step 3/3 · Model IDs',
);
await waitForText(lastFrame, 'Enter model IDs directly');

// The Model IDs input is pre-filled with the saved custom model id
// (which only exists in settings, never among the built-in defaults).
Expand Down
208 changes: 208 additions & 0 deletions packages/cli/src/ui/auth/ProviderSetupSteps.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,28 @@ import { renderWithProviders } from '../../test-utils/render.js';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { act } from 'react';
import { AuthType } from '@qwen-code/qwen-code-core';
import type { ModelSpec } from '@qwen-code/qwen-code-core';
import type { KeypressHandler, Key } from '../contexts/KeypressContext.js';
import { useKeypress } from '../hooks/useKeypress.js';
import { ProviderSetupSteps } from './ProviderSetupSteps.js';
import type { ProviderSetupFlow } from './useProviderSetupFlow.js';

type UseKeypressMockOptions = { isActive: boolean };

const discoverProviderModelsMock = vi.hoisted(() => vi.fn());

vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => ({
...(await importOriginal<typeof import('@qwen-code/qwen-code-core')>()),
discoverProviderModels: discoverProviderModelsMock,
}));
vi.mock('../hooks/useKeypress.js');

let activeKeypressHandlers: KeypressHandler[] = [];

describe('ProviderSetupSteps', () => {
beforeEach(() => {
activeKeypressHandlers = [];
discoverProviderModelsMock.mockReset();
vi.mocked(useKeypress).mockImplementation(
(handler: KeypressHandler, options?: UseKeypressMockOptions) => {
if (options?.isActive) {
Expand Down Expand Up @@ -259,6 +267,18 @@ describe('ProviderSetupSteps', () => {
} as unknown as ProviderSetupFlow;
};

const enableDiscovery = (flow: ProviderSetupFlow) => {
if (!flow.state.provider) {
throw new Error('Expected a provider');
}
flow.state.provider = {
...flow.state.provider,
supportsModelDiscovery: true,
};
flow.state.baseUrl = 'https://example.com/v1';
flow.state.apiKey = 'secret-key';
};

it('maps Ctrl+P/N to advanced-config focus navigation', () => {
const flow = createAdvancedConfigFlow();

Expand All @@ -282,6 +302,7 @@ describe('ProviderSetupSteps', () => {
const frame = lastFrame() ?? '';
expect(frame).toContain('Enter model IDs directly');
expect(frame).toContain('Recommended models');
expect(frame).not.toContain('Other models from the provider');
expect(frame).toContain(
'Checked recommended models are applied on submit but not copied into the input.',
);
Expand Down Expand Up @@ -387,4 +408,191 @@ describe('ProviderSetupSteps', () => {
expect(submitModelIds).toHaveBeenCalledTimes(1);
unmount();
});

it('does not mount the model editor before discovery settles', () => {
discoverProviderModelsMock.mockReturnValue(new Promise(() => {}));
const flow = createModelIdsFlow();
enableDiscovery(flow);

const { lastFrame, unmount } = renderWithProviders(
<ProviderSetupSteps flow={flow} />,
);

const frame = lastFrame() ?? '';
expect(frame).toContain('Loading models from provider…');
expect(frame).toContain('Esc to go back');
expect(frame).not.toContain('Enter model IDs directly');
expect(discoverProviderModelsMock).toHaveBeenCalledWith({
baseUrl: 'https://example.com/v1',
apiKey: 'secret-key',
staticModels: flow.state.provider?.models,
signal: expect.any(AbortSignal),
});
unmount();
});

it('mounts one provider snapshot without promoting new models or dropping unserved selections', async () => {
let resolveDiscovery!: (models: ModelSpec[]) => void;
discoverProviderModelsMock.mockReturnValue(
new Promise<ModelSpec[]>((resolve) => {
resolveDiscovery = resolve;
}),
);
const submitModelIds = vi.fn();
const flow = createModelIdsFlow({
modelIds: 'custom-model, MiniMax-M3, MiniMax-M2.7',
submitModelIds,
});
enableDiscovery(flow);
const { lastFrame, unmount } = renderWithProviders(
<ProviderSetupSteps flow={flow} />,
);

await act(async () => {
resolveDiscovery([
{
id: 'MiniMax-M3',
contextWindowSize: 1000000,
modalities: { image: true, video: true },
},
{ id: 'MiniMax-M4' },
{ id: 'custom-model' },
]);
});

const frame = lastFrame() ?? '';
expect(frame).toContain('Recommended models · from the provider');
expect(frame).toContain('custom-model');
expect(frame).toContain('MiniMax-M3');
expect(frame).toContain('MiniMax-M4');
// The snapshot has no row for the previously selected MiniMax-M2.7, so it
// stays visible and selectable through the free-form input instead.
expect(frame).not.toMatch(/[◉○]\uFE0E\s+MiniMax-M2\.7/);
const inputLine = frame
.split('\n')
.find((line) => line.includes('custom-model'));
expect(inputLine).toContain('custom-model, MiniMax-M2.7');
expect(frame).toMatch(/◉\uFE0E\s+MiniMax-M3/);
expect(frame).toMatch(/○\uFE0E\s+MiniMax-M4/);
expect(frame).toMatch(/○\uFE0E\s+custom-model/);
const lines = frame.split('\n');
const otherHeadingLine = lines.findIndex((line) =>
line.includes('Other models from the provider'),
);
expect(otherHeadingLine).toBeGreaterThan(
lines.findIndex((line) => /◉\uFE0E\s+MiniMax-M3/.test(line)),
);
expect(otherHeadingLine).toBeLessThan(
lines.findIndex((line) => /○\uFE0E\s+MiniMax-M4/.test(line)),
);
await act(async () => {
pressLatestKey('x', 'x');
});
expect(lastFrame()).toContain('xcustom-model, MiniMax-M2.7');
expect(flow.changeModelIds).not.toHaveBeenCalled();
pressKey('return', '\r');
expect(submitModelIds).toHaveBeenCalledWith({
modelIds: ['xcustom-model', 'MiniMax-M2.7', 'MiniMax-M3'],
});
unmount();
});

it('lists provider-only models without the recommended endorsement', async () => {
discoverProviderModelsMock.mockResolvedValue([
{ id: 'served-unknown-a' },
{ id: 'served-unknown-b' },
]);
const flow = createModelIdsFlow();
enableDiscovery(flow);

const { lastFrame, unmount } = renderWithProviders(
<ProviderSetupSteps flow={flow} />,
);
await act(async () => {});

const frame = lastFrame() ?? '';
expect(frame).not.toContain('Recommended models · from the provider');
expect(frame).toContain('Other models from the provider');
expect(frame).toMatch(/○\uFE0E\s+served-unknown-a/);
expect(frame).toMatch(/○\uFE0E\s+served-unknown-b/);
unmount();
});

it('toggles and submits an unendorsed provider model like a recommended one', async () => {
let resolveDiscovery!: (models: ModelSpec[]) => void;
discoverProviderModelsMock.mockReturnValue(
new Promise<ModelSpec[]>((resolve) => {
resolveDiscovery = resolve;
}),
);
const submitModelIds = vi.fn();
const flow = createModelIdsFlow({
modelIds: 'custom-model, MiniMax-M3',
submitModelIds,
});
enableDiscovery(flow);
const { lastFrame, unmount } = renderWithProviders(
<ProviderSetupSteps flow={flow} />,
);

await act(async () => {
resolveDiscovery([
{ id: 'MiniMax-M3', contextWindowSize: 1000000 },
{ id: 'MiniMax-M4' },
]);
});

await act(async () => {
pressLatestKey('down');
});
await act(async () => {
pressLatestKey('down');
});
await act(async () => {
pressLatestKey('down');
});
await act(async () => {
pressLatestKey('space', ' ');
});

expect(lastFrame()).toMatch(/◉\uFE0E\s+MiniMax-M4/);

pressKey('return', '\r');
expect(submitModelIds).toHaveBeenCalledWith({
modelIds: ['custom-model', 'MiniMax-M3', 'MiniMax-M4'],
});
unmount();
});

it('falls back to built-ins after an unavailable catalog', async () => {
discoverProviderModelsMock.mockResolvedValue(null);
const flow = createModelIdsFlow();
enableDiscovery(flow);

const { lastFrame, unmount } = renderWithProviders(
<ProviderSetupSteps flow={flow} />,
);
await act(async () => {});

const frame = lastFrame() ?? '';
expect(frame).toContain(
'Recommended models · provider list unavailable, showing built-ins',
);
expect(frame).not.toContain('Other models from the provider');
expect(frame).toContain('MiniMax-M2.7');
unmount();
});

it('cancels a pending discovery when the step unmounts', () => {
discoverProviderModelsMock.mockReturnValue(new Promise(() => {}));
const flow = createModelIdsFlow();
enableDiscovery(flow);
const { unmount } = renderWithProviders(<ProviderSetupSteps flow={flow} />);
const signal = discoverProviderModelsMock.mock.calls[0]?.[0]
.signal as AbortSignal;

unmount();

expect(signal.aborted).toBe(true);
});
});
Loading
Loading