mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
* fix(model): disambiguate providers sharing a model id by baseUrl (#5173) When multiple modelProviders.openai entries share the same model id but have different baseUrl values, selecting one in the model picker was not remembered across restarts — startup always resolved to the first id match. The gap was at the persistence -> startup-resolution boundary: - persistModelSelection only saved model.name, dropping the baseUrl that distinguishes the providers. - resolveCliGenerationConfig matched providers by id alone (first hit). Fix (CLI only; runtime switching and ModelRegistry composite keys were already correct): - Add optional model.baseUrl setting paired with model.name. - ModelDialog persists the selected provider's baseUrl, and clears it when the selection has none (prevents a stale disambiguator). - /model <id> is an id-only switch, so it clears model.baseUrl too. - resolveCliGenerationConfig disambiguates by the persisted baseUrl when the model comes from settings, falling back to the first id match when the paired provider was edited/removed or no baseUrl was saved. This keeps the persistence layer consistent with ModelRegistry's existing id+baseUrl composite key and syncAfterAuthRefresh's exact-match path. * fix(model): clear model.baseUrl on ACP setModel and provider install Round-1 review follow-up: extend the "any writer of model.name must set or clear model.baseUrl" invariant to the remaining id-only persistence paths so a stale disambiguator from a prior model-picker selection can't restore the wrong provider on next launch. - ACP Session.setModel clears model.baseUrl alongside model.name. - Provider install plan (core) clears model.baseUrl when selecting a model id. * fix(auth): disambiguate provider by baseUrl in pre-flight auth validation Round-2 review follow-up: validateAuthMethod / hasApiKeyForAuth looked up the provider by model id only, so for duplicate-id configs the auth and Anthropic baseUrl checks could validate the first provider's credentials instead of the selected one. - findModelConfig now prefers the exact id+baseUrl match and falls back to the first id match (mirrors resolveCliGenerationConfig). - New resolveSelectedModel helper pairs the resolved/persisted model id with its baseUrl (runtime generationConfig first, then settings.model.{name,baseUrl}). - Thread the baseUrl into both the API-key and Anthropic-baseUrl lookups. * fix(auth): pair model with its own baseUrl; document cross-scope assumption Round-3 review follow-up: - resolveSelectedModel no longer falls back to settings.model.baseUrl when a live Config is present, so a runtime-selected model is never paired with a stale persisted baseUrl from a previous selection. - Document the merged-settings assumption at the resolver disambiguation site: every writer pairs model.name + model.baseUrl in the same scope, and the id-only fallback bounds the blast radius if a hand-edited config desyncs them across scopes. * fix(model): use empty-string tombstone to clear model.baseUrl across scopes Round-4 review follow-up: clearing model.baseUrl by writing undefined drops the key from JSON, so a higher-priority scope's clear could not override a stale model.baseUrl left in a lower-priority scope — the stale disambiguator would resurface on merge and re-select the wrong duplicate provider after restart (e.g. pick a provider in user scope, then /model <id> in a workspace that has its own modelProviders). All clear sites (model picker, /model, ACP setModel, provider install) now write an empty-string tombstone, which is a present value that overrides lower scopes on merge. The resolver and auth lookup already treat '' as "no disambiguator". * fix(acp): clear model.baseUrl when setCoreValue sets model.name Round-5 review follow-up: the generic qwen/settings/setCoreValue RPC (ACP/serve) can change model.name directly without touching model.baseUrl, leaving a stale disambiguator from a prior model-picker selection that would re-select the wrong duplicate provider on next launch. Clear it with the same empty-string tombstone used by the other id-only model.name writers. * fix(model): warn on silent provider fallback; pair baseUrl with resolved config Address review comments: - Emit a warning when the persisted model.baseUrl no longer matches any provider and resolution falls back to the first id match (thread 1 [Critical]). - Source effectiveBaseUrl from the resolved config (after?.baseUrl) rather than the picker entry alone, so the persisted (model.name, model.baseUrl) pair stays consistent even if switchModel transforms the id (thread 2 [Critical]). * test(model): cover effectiveBaseUrl fallback to picker entry baseUrl Add a ModelDialog test where switchModel succeeds but getContentGeneratorConfig returns a config without baseUrl, exercising the 'after?.baseUrl ?? selectedEntry?.model.baseUrl' fallback. Without coverage, a regression there would silently write an empty-string tombstone and resolve the wrong same-id provider on next launch.
647 lines
19 KiB
TypeScript
647 lines
19 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Qwen
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { modelCommand } from './modelCommand.js';
|
|
import { type CommandContext } from './types.js';
|
|
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
|
import {
|
|
AuthType,
|
|
type ContentGeneratorConfig,
|
|
type Config,
|
|
} from '@qwen-code/qwen-code-core';
|
|
import type { LoadedSettings } from '../../config/settings.js';
|
|
|
|
// Helper function to create a mock config
|
|
function createMockConfig(
|
|
contentGeneratorConfig: ContentGeneratorConfig | null,
|
|
): Partial<Config> {
|
|
return {
|
|
getContentGeneratorConfig: vi.fn().mockReturnValue(contentGeneratorConfig),
|
|
};
|
|
}
|
|
|
|
function createMockSettings(setValue = vi.fn()): Partial<LoadedSettings> {
|
|
return {
|
|
merged: {},
|
|
user: { settings: {} },
|
|
workspace: { settings: {} },
|
|
isTrusted: false,
|
|
setValue,
|
|
} as unknown as Partial<LoadedSettings>;
|
|
}
|
|
|
|
describe('modelCommand', () => {
|
|
let mockContext: CommandContext;
|
|
|
|
beforeEach(() => {
|
|
mockContext = createMockCommandContext();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should have the correct name and description', () => {
|
|
expect(modelCommand.name).toBe('model');
|
|
expect(modelCommand.description).toBe(
|
|
'Switch the model for this session (--fast for suggestion model, [model-id] to switch immediately).',
|
|
);
|
|
});
|
|
|
|
it('should return error when config is not available', async () => {
|
|
mockContext.services.config = null;
|
|
|
|
const result = await modelCommand.action!(mockContext, '');
|
|
|
|
expect(result).toEqual({
|
|
type: 'message',
|
|
messageType: 'error',
|
|
content: 'Configuration not available.',
|
|
});
|
|
});
|
|
|
|
it('should return error when content generator config is not available', async () => {
|
|
const mockConfig = createMockConfig(null);
|
|
mockContext.services.config = mockConfig as Config;
|
|
|
|
const result = await modelCommand.action!(mockContext, '');
|
|
|
|
expect(result).toEqual({
|
|
type: 'message',
|
|
messageType: 'error',
|
|
content: 'Content generator configuration not available.',
|
|
});
|
|
});
|
|
|
|
it('should return error when auth type is not available', async () => {
|
|
const mockConfig = createMockConfig({
|
|
model: 'test-model',
|
|
authType: undefined,
|
|
});
|
|
mockContext.services.config = mockConfig as Config;
|
|
|
|
const result = await modelCommand.action!(mockContext, '');
|
|
|
|
expect(result).toEqual({
|
|
type: 'message',
|
|
messageType: 'error',
|
|
content: 'Authentication type not available.',
|
|
});
|
|
});
|
|
|
|
it('should return dialog action for QWEN_OAUTH auth type', async () => {
|
|
const mockConfig = createMockConfig({
|
|
model: 'test-model',
|
|
authType: AuthType.QWEN_OAUTH,
|
|
});
|
|
mockContext.services.config = mockConfig as Config;
|
|
|
|
const result = await modelCommand.action!(mockContext, '');
|
|
|
|
expect(result).toEqual({
|
|
type: 'dialog',
|
|
dialog: 'model',
|
|
});
|
|
});
|
|
|
|
it('should return dialog action for USE_OPENAI auth type', async () => {
|
|
const mockConfig = createMockConfig({
|
|
model: 'test-model',
|
|
authType: AuthType.USE_OPENAI,
|
|
});
|
|
mockContext.services.config = mockConfig as Config;
|
|
|
|
const result = await modelCommand.action!(mockContext, '');
|
|
|
|
expect(result).toEqual({
|
|
type: 'dialog',
|
|
dialog: 'model',
|
|
});
|
|
});
|
|
|
|
it('should return dialog action for unsupported auth types', async () => {
|
|
const mockConfig = createMockConfig({
|
|
model: 'test-model',
|
|
authType: 'UNSUPPORTED_AUTH_TYPE' as AuthType,
|
|
});
|
|
mockContext.services.config = mockConfig as Config;
|
|
|
|
const result = await modelCommand.action!(mockContext, '');
|
|
|
|
expect(result).toEqual({
|
|
type: 'dialog',
|
|
dialog: 'model',
|
|
});
|
|
});
|
|
|
|
it('should handle undefined auth type', async () => {
|
|
const mockConfig = createMockConfig({
|
|
model: 'test-model',
|
|
authType: undefined,
|
|
});
|
|
mockContext.services.config = mockConfig as Config;
|
|
|
|
const result = await modelCommand.action!(mockContext, '');
|
|
|
|
expect(result).toEqual({
|
|
type: 'message',
|
|
messageType: 'error',
|
|
content: 'Authentication type not available.',
|
|
});
|
|
});
|
|
|
|
it('should switch the main model directly in interactive mode when args are provided', async () => {
|
|
const setValue = vi.fn();
|
|
const switchModel = vi.fn().mockResolvedValue(undefined);
|
|
mockContext = createMockCommandContext({
|
|
invocation: { raw: '/model qwen-max', name: 'model', args: 'qwen-max' },
|
|
services: {
|
|
config: {
|
|
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
|
model: 'qwen-plus',
|
|
authType: AuthType.QWEN_OAUTH,
|
|
}),
|
|
getAvailableModelsForAuthType: vi
|
|
.fn()
|
|
.mockReturnValue([{ id: 'qwen-max', label: 'Qwen Max' }]),
|
|
switchModel,
|
|
},
|
|
settings: createMockSettings(setValue),
|
|
},
|
|
});
|
|
|
|
const result = await modelCommand.action!(mockContext, 'qwen-max');
|
|
|
|
expect(switchModel).toHaveBeenCalledWith(
|
|
AuthType.QWEN_OAUTH,
|
|
'qwen-max',
|
|
undefined,
|
|
);
|
|
expect(setValue).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
'model.name',
|
|
'qwen-max',
|
|
);
|
|
// `/model <id>` is an id-only switch, so any baseUrl disambiguator left by
|
|
// a previous model-picker selection must be cleared (empty-string tombstone)
|
|
// to avoid resolving to a different provider on next launch.
|
|
expect(setValue).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
'model.baseUrl',
|
|
'',
|
|
);
|
|
expect(result).toEqual({
|
|
type: 'message',
|
|
messageType: 'info',
|
|
content: 'Model: qwen-max',
|
|
});
|
|
});
|
|
|
|
it('should not persist the model when direct model validation fails', async () => {
|
|
const setValue = vi.fn();
|
|
const switchModel = vi.fn();
|
|
mockContext = createMockCommandContext({
|
|
invocation: {
|
|
raw: '/model missing-model',
|
|
name: 'model',
|
|
args: 'missing-model',
|
|
},
|
|
services: {
|
|
config: {
|
|
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
|
model: 'qwen-plus',
|
|
authType: AuthType.QWEN_OAUTH,
|
|
}),
|
|
switchModel,
|
|
getAvailableModelsForAuthType: vi.fn().mockReturnValue([]),
|
|
},
|
|
settings: createMockSettings(setValue),
|
|
},
|
|
});
|
|
|
|
const result = await modelCommand.action!(mockContext, 'missing-model');
|
|
|
|
expect(switchModel).not.toHaveBeenCalled();
|
|
expect(setValue).not.toHaveBeenCalled();
|
|
expect(result).toEqual({
|
|
type: 'message',
|
|
messageType: 'error',
|
|
content:
|
|
"Model 'missing-model' is not available for auth type 'qwen-oauth'.\n" +
|
|
"No models are configured for auth type 'qwen-oauth'.\n" +
|
|
'Configure models in settings.modelProviders or run /model to select an available model.',
|
|
});
|
|
});
|
|
|
|
it('should not persist the model when direct model switching fails after validation', async () => {
|
|
const setValue = vi.fn();
|
|
const switchError = new Error('Refresh failed');
|
|
const switchModel = vi.fn().mockRejectedValue(switchError);
|
|
mockContext = createMockCommandContext({
|
|
invocation: {
|
|
raw: '/model qwen-max',
|
|
name: 'model',
|
|
args: 'qwen-max',
|
|
},
|
|
services: {
|
|
config: {
|
|
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
|
model: 'qwen-plus',
|
|
authType: AuthType.QWEN_OAUTH,
|
|
}),
|
|
switchModel,
|
|
getAvailableModelsForAuthType: vi
|
|
.fn()
|
|
.mockReturnValue([{ id: 'qwen-max', label: 'Qwen Max' }]),
|
|
},
|
|
settings: createMockSettings(setValue),
|
|
},
|
|
});
|
|
|
|
await expect(modelCommand.action!(mockContext, 'qwen-max')).rejects.toThrow(
|
|
'Refresh failed',
|
|
);
|
|
|
|
expect(switchModel).toHaveBeenCalledWith(
|
|
AuthType.QWEN_OAUTH,
|
|
'qwen-max',
|
|
undefined,
|
|
);
|
|
expect(setValue).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should explain how to configure models when direct switching fails', async () => {
|
|
const setValue = vi.fn();
|
|
const switchModel = vi.fn();
|
|
mockContext = createMockCommandContext({
|
|
invocation: {
|
|
raw: '/model definitely-not-a-model',
|
|
name: 'model',
|
|
args: 'definitely-not-a-model',
|
|
},
|
|
services: {
|
|
config: {
|
|
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
|
model: 'qwen-plus',
|
|
authType: AuthType.USE_OPENAI,
|
|
}),
|
|
getAvailableModelsForAuthType: vi
|
|
.fn()
|
|
.mockReturnValue([{ id: 'gpt-4', label: 'GPT-4' }]),
|
|
switchModel,
|
|
},
|
|
settings: createMockSettings(setValue),
|
|
},
|
|
});
|
|
|
|
const result = await modelCommand.action!(
|
|
mockContext,
|
|
'definitely-not-a-model',
|
|
);
|
|
|
|
expect(switchModel).not.toHaveBeenCalled();
|
|
expect(setValue).not.toHaveBeenCalled();
|
|
expect(result).toEqual({
|
|
type: 'message',
|
|
messageType: 'error',
|
|
content:
|
|
"Model 'definitely-not-a-model' is not available for auth type 'openai'.\n" +
|
|
"Available models for 'openai': gpt-4.\n" +
|
|
'Configure models in settings.modelProviders or run /model to select an available model.',
|
|
});
|
|
});
|
|
|
|
it('should explain when no models are configured for direct switching', async () => {
|
|
const setValue = vi.fn();
|
|
const switchModel = vi
|
|
.fn()
|
|
.mockRejectedValue(
|
|
new Error("Model 'gpt-4o' not found for authType 'openai'"),
|
|
);
|
|
mockContext = createMockCommandContext({
|
|
invocation: {
|
|
raw: '/model gpt-4o',
|
|
name: 'model',
|
|
args: 'gpt-4o',
|
|
},
|
|
services: {
|
|
config: {
|
|
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
|
model: 'qwen-plus',
|
|
authType: AuthType.USE_OPENAI,
|
|
}),
|
|
getAvailableModelsForAuthType: vi.fn().mockReturnValue([]),
|
|
switchModel,
|
|
},
|
|
settings: createMockSettings(setValue),
|
|
},
|
|
});
|
|
|
|
const result = await modelCommand.action!(mockContext, 'gpt-4o');
|
|
|
|
expect(setValue).not.toHaveBeenCalled();
|
|
expect(result).toEqual({
|
|
type: 'message',
|
|
messageType: 'error',
|
|
content:
|
|
"Model 'gpt-4o' is not available for auth type 'openai'.\n" +
|
|
"No models are configured for auth type 'openai'.\n" +
|
|
'Configure models in settings.modelProviders or run /model to select an available model.',
|
|
});
|
|
});
|
|
|
|
it('should switch provider-qualified models through switchModel', async () => {
|
|
const setValue = vi.fn();
|
|
const switchModel = vi.fn().mockResolvedValue(undefined);
|
|
mockContext = createMockCommandContext({
|
|
invocation: {
|
|
raw: `/model gpt-4(${AuthType.USE_OPENAI})`,
|
|
name: 'model',
|
|
args: `gpt-4(${AuthType.USE_OPENAI})`,
|
|
},
|
|
services: {
|
|
config: {
|
|
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
|
model: 'qwen-plus',
|
|
authType: AuthType.QWEN_OAUTH,
|
|
}),
|
|
getAuthType: vi.fn().mockReturnValue(AuthType.QWEN_OAUTH),
|
|
getAvailableModelsForAuthType: vi
|
|
.fn()
|
|
.mockReturnValue([{ id: 'gpt-4', label: 'GPT-4' }]),
|
|
switchModel,
|
|
},
|
|
settings: createMockSettings(setValue),
|
|
},
|
|
});
|
|
|
|
const result = await modelCommand.action!(
|
|
mockContext,
|
|
`gpt-4(${AuthType.USE_OPENAI})`,
|
|
);
|
|
|
|
expect(switchModel).toHaveBeenCalledWith(
|
|
AuthType.USE_OPENAI,
|
|
'gpt-4',
|
|
undefined,
|
|
);
|
|
expect(setValue).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
'security.auth.selectedType',
|
|
AuthType.USE_OPENAI,
|
|
);
|
|
expect(setValue).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
'model.name',
|
|
'gpt-4',
|
|
);
|
|
expect(result).toEqual({
|
|
type: 'message',
|
|
messageType: 'info',
|
|
content: 'Model: gpt-4',
|
|
});
|
|
});
|
|
|
|
it('should set fast models configured under another auth type', async () => {
|
|
const setValue = vi.fn();
|
|
const setFastModel = vi.fn();
|
|
mockContext = createMockCommandContext({
|
|
invocation: {
|
|
raw: '/model --fast deepseek-v4-flash',
|
|
name: 'model',
|
|
args: '--fast deepseek-v4-flash',
|
|
},
|
|
services: {
|
|
config: {
|
|
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
|
model: 'claude-opus-4-7',
|
|
authType: AuthType.USE_ANTHROPIC,
|
|
}),
|
|
getAllConfiguredModels: vi.fn().mockReturnValue([
|
|
{
|
|
id: 'deepseek-v4-flash',
|
|
label: 'deepseek-v4-flash',
|
|
authType: AuthType.USE_OPENAI,
|
|
},
|
|
{
|
|
id: 'claude-opus-4-7',
|
|
label: 'claude-opus-4-7',
|
|
authType: AuthType.USE_ANTHROPIC,
|
|
},
|
|
]),
|
|
setFastModel,
|
|
},
|
|
settings: createMockSettings(setValue),
|
|
},
|
|
});
|
|
|
|
const result = await modelCommand.action!(
|
|
mockContext,
|
|
'--fast deepseek-v4-flash',
|
|
);
|
|
|
|
expect(setValue).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
'fastModel',
|
|
'deepseek-v4-flash',
|
|
);
|
|
expect(setFastModel).toHaveBeenCalledWith('deepseek-v4-flash');
|
|
expect(result).toEqual({
|
|
type: 'message',
|
|
messageType: 'info',
|
|
content: 'Fast Model: deepseek-v4-flash',
|
|
});
|
|
});
|
|
|
|
it('should set authType-qualified fast model selectors', async () => {
|
|
const setValue = vi.fn();
|
|
const setFastModel = vi.fn();
|
|
mockContext = createMockCommandContext({
|
|
invocation: {
|
|
raw: '/model --fast openai:deepseek-v4-flash',
|
|
name: 'model',
|
|
args: '--fast openai:deepseek-v4-flash',
|
|
},
|
|
services: {
|
|
config: {
|
|
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
|
model: 'claude-opus-4-7',
|
|
authType: AuthType.USE_ANTHROPIC,
|
|
}),
|
|
getAvailableModelsForAuthType: vi.fn((authType: AuthType) =>
|
|
authType === AuthType.USE_OPENAI
|
|
? [
|
|
{
|
|
id: 'deepseek-v4-flash',
|
|
label: 'deepseek-v4-flash',
|
|
authType: AuthType.USE_OPENAI,
|
|
},
|
|
]
|
|
: [],
|
|
),
|
|
setFastModel,
|
|
},
|
|
settings: createMockSettings(setValue),
|
|
},
|
|
});
|
|
|
|
const result = await modelCommand.action!(
|
|
mockContext,
|
|
'--fast openai:deepseek-v4-flash',
|
|
);
|
|
|
|
expect(setValue).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
'fastModel',
|
|
'openai:deepseek-v4-flash',
|
|
);
|
|
expect(setFastModel).toHaveBeenCalledWith('openai:deepseek-v4-flash');
|
|
expect(result).toEqual({
|
|
type: 'message',
|
|
messageType: 'info',
|
|
content: 'Fast Model: openai:deepseek-v4-flash',
|
|
});
|
|
});
|
|
|
|
it('should reject unavailable fast models across all auth types', async () => {
|
|
const setValue = vi.fn();
|
|
const setFastModel = vi.fn();
|
|
mockContext = createMockCommandContext({
|
|
invocation: {
|
|
raw: '/model --fast missing-model',
|
|
name: 'model',
|
|
args: '--fast missing-model',
|
|
},
|
|
services: {
|
|
config: {
|
|
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
|
model: 'qwen-plus',
|
|
authType: AuthType.USE_OPENAI,
|
|
}),
|
|
getAllConfiguredModels: vi.fn().mockReturnValue([
|
|
{
|
|
id: 'qwen-turbo',
|
|
label: 'Qwen Turbo',
|
|
authType: AuthType.USE_OPENAI,
|
|
},
|
|
]),
|
|
setFastModel,
|
|
},
|
|
settings: createMockSettings(setValue),
|
|
},
|
|
});
|
|
|
|
const result = await modelCommand.action!(
|
|
mockContext,
|
|
'--fast missing-model',
|
|
);
|
|
|
|
expect(setValue).not.toHaveBeenCalled();
|
|
expect(setFastModel).not.toHaveBeenCalled();
|
|
expect(result).toEqual({
|
|
type: 'message',
|
|
messageType: 'error',
|
|
content:
|
|
"Fast model 'missing-model' is not configured for any auth type.\n" +
|
|
'Configured models: qwen-turbo.\n' +
|
|
'Configure models in settings.modelProviders or run /model to select an available model.',
|
|
});
|
|
});
|
|
|
|
it('should not treat model IDs prefixed with --fast as the --fast flag', async () => {
|
|
const setValue = vi.fn();
|
|
const switchModel = vi.fn().mockResolvedValue(undefined);
|
|
mockContext = createMockCommandContext({
|
|
invocation: {
|
|
raw: '/model --fast-model',
|
|
name: 'model',
|
|
args: '--fast-model',
|
|
},
|
|
services: {
|
|
config: {
|
|
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
|
model: 'qwen-plus',
|
|
authType: AuthType.USE_OPENAI,
|
|
}),
|
|
getAvailableModelsForAuthType: vi
|
|
.fn()
|
|
.mockReturnValue([{ id: '--fast-model', label: '--fast-model' }]),
|
|
switchModel,
|
|
},
|
|
settings: createMockSettings(setValue),
|
|
},
|
|
});
|
|
|
|
const result = await modelCommand.action!(mockContext, '--fast-model');
|
|
|
|
expect(switchModel).toHaveBeenCalledWith(
|
|
AuthType.USE_OPENAI,
|
|
'--fast-model',
|
|
undefined,
|
|
);
|
|
expect(setValue).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
'model.name',
|
|
'--fast-model',
|
|
);
|
|
expect(result).toEqual({
|
|
type: 'message',
|
|
messageType: 'info',
|
|
content: 'Model: --fast-model',
|
|
});
|
|
});
|
|
|
|
describe('non-interactive mode', () => {
|
|
it('should return current model without triggering dialog when no args', async () => {
|
|
mockContext = createMockCommandContext({
|
|
executionMode: 'non_interactive',
|
|
services: {
|
|
config: {
|
|
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
|
model: 'qwen-max',
|
|
authType: AuthType.QWEN_OAUTH,
|
|
}),
|
|
getModel: vi.fn().mockReturnValue('qwen-max'),
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await modelCommand.action!(mockContext, '');
|
|
|
|
expect(result).toEqual({
|
|
type: 'message',
|
|
messageType: 'info',
|
|
content: expect.stringContaining('qwen-max'),
|
|
});
|
|
expect((result as { type: string }).type).toBe('message');
|
|
});
|
|
|
|
it('should return current fast model without triggering dialog for --fast no args', async () => {
|
|
mockContext = createMockCommandContext({
|
|
executionMode: 'non_interactive',
|
|
invocation: { args: '--fast' },
|
|
services: {
|
|
config: {
|
|
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
|
model: 'qwen-max',
|
|
authType: AuthType.QWEN_OAUTH,
|
|
}),
|
|
getModel: vi.fn().mockReturnValue('qwen-max'),
|
|
},
|
|
settings: {
|
|
merged: { fastModel: 'qwen-turbo' } as Record<string, unknown>,
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await modelCommand.action!(mockContext, '--fast');
|
|
|
|
expect(result).toEqual({
|
|
type: 'message',
|
|
messageType: 'info',
|
|
content: expect.stringContaining('qwen-turbo'),
|
|
});
|
|
});
|
|
});
|
|
});
|