qwen-code/packages/cli/src/ui/hooks/useCodingPlanUpdates.test.ts
pomelo-nwu 24ea2b6964 feat: add qwen3-coder-next to Coding Plan (Global/Intl region)
- Added qwen3-coder-next model to the Global/Intl Coding Plan template
- Removed thinking mode from qwen3-coder-next (both China and Global regions)
- Updated test expectations to reflect the new model count

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-23 10:51:00 +08:00

552 lines
16 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { useCodingPlanUpdates } from './useCodingPlanUpdates.js';
import {
CODING_PLAN_ENV_KEY,
getCodingPlanConfig,
CodingPlanRegion,
} from '../../constants/codingPlan.js';
import { AuthType } from '@qwen-code/qwen-code-core';
// Get region configs for testing
const chinaConfig = getCodingPlanConfig(CodingPlanRegion.CHINA);
const globalConfig = getCodingPlanConfig(CodingPlanRegion.GLOBAL);
describe('useCodingPlanUpdates', () => {
const mockSettings = {
merged: {
modelProviders: {},
codingPlan: {},
},
setValue: vi.fn(),
isTrusted: true,
workspace: { settings: {} },
user: { settings: {} },
};
const mockConfig = {
reloadModelProvidersConfig: vi.fn(),
refreshAuth: vi.fn(),
getModel: vi.fn().mockReturnValue('qwen-max'),
};
const mockAddItem = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
delete process.env[CODING_PLAN_ENV_KEY];
});
describe('version comparison', () => {
it('should not show update prompt when no version is stored', () => {
mockSettings.merged.codingPlan = {};
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
expect(result.current.codingPlanUpdateRequest).toBeUndefined();
});
it('should not show update prompt when China region versions match', () => {
mockSettings.merged.codingPlan = {
region: CodingPlanRegion.CHINA,
version: chinaConfig.version,
};
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
expect(result.current.codingPlanUpdateRequest).toBeUndefined();
});
it('should not show update prompt when Global region versions match', () => {
mockSettings.merged.codingPlan = {
region: CodingPlanRegion.GLOBAL,
version: globalConfig.version,
};
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
expect(result.current.codingPlanUpdateRequest).toBeUndefined();
});
it('should default to China region when region is not specified', async () => {
// No region specified, should default to China
mockSettings.merged.codingPlan = {
version: 'old-version-hash',
};
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
// Should prompt for China region since it defaults to China
expect(result.current.codingPlanUpdateRequest?.prompt).toContain(
chinaConfig.regionName,
);
});
it('should show update prompt when China region versions differ', async () => {
mockSettings.merged.codingPlan = {
region: CodingPlanRegion.CHINA,
version: 'old-version-hash',
};
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
expect(result.current.codingPlanUpdateRequest?.prompt).toContain(
chinaConfig.regionName,
);
});
it('should show update prompt when Global region versions differ', async () => {
mockSettings.merged.codingPlan = {
region: CodingPlanRegion.GLOBAL,
version: 'old-version-hash',
};
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
expect(result.current.codingPlanUpdateRequest?.prompt).toContain(
globalConfig.regionName,
);
});
});
describe('update execution', () => {
it('should execute China region update when user confirms', async () => {
mockSettings.merged.codingPlan = {
region: CodingPlanRegion.CHINA,
version: 'old-version-hash',
};
mockSettings.merged.modelProviders = {
[AuthType.USE_OPENAI]: [
{
id: 'test-model-china-1',
baseUrl: chinaConfig.baseUrl,
envKey: CODING_PLAN_ENV_KEY,
},
{
id: 'custom-model',
baseUrl: 'https://custom.example.com',
envKey: 'CUSTOM_API_KEY',
},
],
};
mockConfig.refreshAuth.mockResolvedValue(undefined);
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
// Confirm the update
await result.current.codingPlanUpdateRequest!.onConfirm(true);
// Wait for async update to complete
await waitFor(() => {
// Should update model providers (at least 2 calls: modelProviders + version + region)
expect(mockSettings.setValue).toHaveBeenCalled();
});
// Should update version with correct hash
expect(mockSettings.setValue).toHaveBeenCalledWith(
expect.anything(),
'codingPlan.version',
chinaConfig.version,
);
// Should update region
expect(mockSettings.setValue).toHaveBeenCalledWith(
expect.anything(),
'codingPlan.region',
CodingPlanRegion.CHINA,
);
// Should reload and refresh auth
expect(mockConfig.reloadModelProvidersConfig).toHaveBeenCalled();
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_OPENAI);
// Should show success message with region info
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'info',
text: expect.stringContaining(chinaConfig.regionName),
}),
expect.any(Number),
);
});
it('should execute Global region update when user confirms', async () => {
mockSettings.merged.codingPlan = {
region: CodingPlanRegion.GLOBAL,
version: 'old-version-hash',
};
mockSettings.merged.modelProviders = {
[AuthType.USE_OPENAI]: [
{
id: 'test-model-global-1',
baseUrl: globalConfig.baseUrl,
envKey: CODING_PLAN_ENV_KEY,
},
{
id: 'custom-model',
baseUrl: 'https://custom.example.com',
envKey: 'CUSTOM_API_KEY',
},
],
};
mockConfig.refreshAuth.mockResolvedValue(undefined);
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
// Confirm the update
await result.current.codingPlanUpdateRequest!.onConfirm(true);
// Wait for async update to complete
await waitFor(() => {
expect(mockSettings.setValue).toHaveBeenCalled();
});
// Should update version with correct hash (single version field)
expect(mockSettings.setValue).toHaveBeenCalledWith(
expect.anything(),
'codingPlan.version',
globalConfig.version,
);
// Should update region
expect(mockSettings.setValue).toHaveBeenCalledWith(
expect.anything(),
'codingPlan.region',
CodingPlanRegion.GLOBAL,
);
// Should reload and refresh auth
expect(mockConfig.reloadModelProvidersConfig).toHaveBeenCalled();
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_OPENAI);
// Should show success message with Global region info
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'info',
text: expect.stringContaining(globalConfig.regionName),
}),
expect.any(Number),
);
});
it('should not execute update when user declines', async () => {
mockSettings.merged.codingPlan = {
region: CodingPlanRegion.CHINA,
version: 'old-version-hash',
};
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
// Decline the update
await result.current.codingPlanUpdateRequest!.onConfirm(false);
// Should not update anything
expect(mockSettings.setValue).not.toHaveBeenCalled();
expect(mockConfig.reloadModelProvidersConfig).not.toHaveBeenCalled();
});
it('should replace all Coding Plan configs during update (mutually exclusive)', async () => {
// Since regions are mutually exclusive, when updating one region,
// all Coding Plan configs should be replaced (not preserving other region configs)
mockSettings.merged.codingPlan = {
region: CodingPlanRegion.CHINA,
version: 'old-version-hash',
};
const chinaModelConfig = {
id: 'test-model-china-1',
baseUrl: chinaConfig.baseUrl,
envKey: CODING_PLAN_ENV_KEY,
};
const globalModelConfig = {
id: 'test-model-global-1',
baseUrl: globalConfig.baseUrl,
envKey: CODING_PLAN_ENV_KEY,
};
const customConfig = {
id: 'custom-model',
baseUrl: 'https://custom.example.com',
envKey: 'CUSTOM_API_KEY',
};
mockSettings.merged.modelProviders = {
[AuthType.USE_OPENAI]: [
chinaModelConfig,
globalModelConfig,
customConfig,
],
};
mockConfig.refreshAuth.mockResolvedValue(undefined);
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
await result.current.codingPlanUpdateRequest!.onConfirm(true);
// Wait for async update to complete
await waitFor(() => {
expect(mockSettings.setValue).toHaveBeenCalled();
});
// Get the updated configs passed to setValue
const setValueCalls = mockSettings.setValue.mock.calls;
const modelProvidersCall = setValueCalls.find((call: unknown[]) =>
(call[1] as string).includes('modelProviders'),
);
expect(modelProvidersCall).toBeDefined();
const updatedConfigs = modelProvidersCall![2] as Array<
Record<string, unknown>
>;
// Should have new China configs + custom config only (global config removed since regions are mutually exclusive)
// The China template has 6 models, so we expect 6 (from template) + 1 (custom) = 7
expect(updatedConfigs.length).toBe(7);
// Should NOT contain the Global config (mutually exclusive)
expect(
updatedConfigs.some(
(c: Record<string, unknown>) => c['baseUrl'] === globalConfig.baseUrl,
),
).toBe(false);
// Should contain the custom config
expect(
updatedConfigs.some(
(c: Record<string, unknown>) => c['id'] === 'custom-model',
),
).toBe(true);
// All configs should use the unified env key
updatedConfigs.forEach((config) => {
if (config['envKey'] === CODING_PLAN_ENV_KEY) {
expect(config['baseUrl']).toBe(chinaConfig.baseUrl);
}
});
// Should reload and refresh auth
expect(mockConfig.reloadModelProvidersConfig).toHaveBeenCalled();
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_OPENAI);
});
it('should preserve non-Coding Plan configs during update', async () => {
mockSettings.merged.codingPlan = {
region: CodingPlanRegion.CHINA,
version: 'old-version-hash',
};
const customConfig = {
id: 'custom-model',
baseUrl: 'https://custom.example.com',
envKey: 'CUSTOM_API_KEY',
};
mockSettings.merged.modelProviders = {
[AuthType.USE_OPENAI]: [
{
id: 'test-model-china-1',
baseUrl: chinaConfig.baseUrl,
envKey: CODING_PLAN_ENV_KEY,
},
customConfig,
],
};
mockConfig.refreshAuth.mockResolvedValue(undefined);
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
await result.current.codingPlanUpdateRequest!.onConfirm(true);
// Wait for async update to complete
await waitFor(() => {
// Should preserve custom config - verify setValue was called
expect(mockSettings.setValue).toHaveBeenCalled();
});
// Get the updated configs passed to setValue
const setValueCalls = mockSettings.setValue.mock.calls;
const modelProvidersCall = setValueCalls.find((call: unknown[]) =>
(call[1] as string).includes('modelProviders'),
);
// Should preserve custom config
expect(modelProvidersCall).toBeDefined();
const updatedConfigs = modelProvidersCall![2] as Array<
Record<string, unknown>
>;
expect(
updatedConfigs.some(
(c: Record<string, unknown>) => c['id'] === 'custom-model',
),
).toBe(true);
});
it('should handle update errors gracefully', async () => {
mockSettings.merged.codingPlan = {
region: CodingPlanRegion.CHINA,
version: 'old-version-hash',
};
mockSettings.merged.modelProviders = {
[AuthType.USE_OPENAI]: [
{
id: 'test-model-china-1',
baseUrl: chinaConfig.baseUrl,
envKey: CODING_PLAN_ENV_KEY,
},
],
};
// Simulate an error during refreshAuth
mockConfig.refreshAuth.mockRejectedValue(new Error('Network error'));
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
await result.current.codingPlanUpdateRequest!.onConfirm(true);
// Should show error message
await waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
}),
expect.any(Number),
);
});
});
});
describe('dismissUpdate', () => {
it('should clear update request when dismissed', async () => {
mockSettings.merged.codingPlan = {
region: CodingPlanRegion.CHINA,
version: 'old-version-hash',
};
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
result.current.dismissCodingPlanUpdate();
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeUndefined();
});
});
});
});