mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-29 04:00:36 +00:00
test: add tests for confirmation-bus, prompt-registry, and cli/core modules (#2272)
* test: add tests for confirmation-bus, prompt-registry, and cli/core modules Add 42 new tests covering previously untested core modules: - MessageBus: publish, subscribe/unsubscribe, request-response pattern (13 tests) - PromptRegistry: register, dedup, query by server, clear, remove (11 tests) - performInitialAuth: success, failure, no authType cases (3 tests) - validateTheme: found, not found, no config cases (4 tests) - initializeApp: i18n, auth, theme, IDE mode, auth dialog logic (11 tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: improve test quality - copyright headers, env safety, cleanup - Fix copyright headers from Google LLC to Qwen Code in all 5 test files - Use vi.stubEnv() instead of manual process.env mutation in initializer test - Add removeAllListeners() cleanup in message-bus debug test - Add void prefix to un-awaited publish() calls in message-bus test - Verify invoke reference preserved after prompt rename in prompt-registry test Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(message-bus): add AbortSignal coverage for request() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e0aeee5414
commit
d22b7e61ee
5 changed files with 740 additions and 75 deletions
66
packages/cli/src/core/auth.test.ts
Normal file
66
packages/cli/src/core/auth.test.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Code
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { performInitialAuth } from './auth.js';
|
||||
|
||||
const mockLogAuth = vi.fn();
|
||||
vi.mock('@qwen-code/qwen-code-core', () => ({
|
||||
getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)),
|
||||
logAuth: (...args: unknown[]) => mockLogAuth(...args),
|
||||
AuthEvent: vi.fn().mockImplementation((type, method, status, message?) => ({
|
||||
type,
|
||||
method,
|
||||
status,
|
||||
message,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('performInitialAuth', () => {
|
||||
let mockConfig: {
|
||||
refreshAuth: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockConfig = {
|
||||
refreshAuth: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should return null when authType is undefined', async () => {
|
||||
const result = await performInitialAuth(mockConfig as never, undefined);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockConfig.refreshAuth).not.toHaveBeenCalled();
|
||||
expect(mockLogAuth).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return null on successful authentication', async () => {
|
||||
mockConfig.refreshAuth.mockResolvedValue(undefined);
|
||||
|
||||
const result = await performInitialAuth(
|
||||
mockConfig as never,
|
||||
'api_key' as never,
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith('api_key', true);
|
||||
expect(mockLogAuth).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return error message on authentication failure', async () => {
|
||||
mockConfig.refreshAuth.mockRejectedValue(new Error('Invalid API key'));
|
||||
|
||||
const result = await performInitialAuth(
|
||||
mockConfig as never,
|
||||
'api_key' as never,
|
||||
);
|
||||
|
||||
expect(result).toBe('Failed to login. Message: Invalid API key');
|
||||
expect(mockLogAuth).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,109 +1,190 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* Copyright 2025 Qwen Code
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { AuthType } from '@qwen-code/qwen-code-core';
|
||||
import type { LoadedSettings } from '../config/settings.js';
|
||||
import type { Config } from '@qwen-code/qwen-code-core';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { initializeApp } from './initializer.js';
|
||||
import { performInitialAuth } from './auth.js';
|
||||
import { validateTheme } from './theme.js';
|
||||
import { initializeI18n } from '../i18n/index.js';
|
||||
|
||||
const mockPerformInitialAuth = vi.fn();
|
||||
const mockValidateTheme = vi.fn();
|
||||
const mockInitializeI18n = vi.fn();
|
||||
|
||||
vi.mock('./auth.js', () => ({
|
||||
performInitialAuth: vi.fn(),
|
||||
performInitialAuth: (...args: unknown[]) => mockPerformInitialAuth(...args),
|
||||
}));
|
||||
|
||||
vi.mock('./theme.js', () => ({
|
||||
validateTheme: vi.fn(),
|
||||
validateTheme: (...args: unknown[]) => mockValidateTheme(...args),
|
||||
}));
|
||||
|
||||
vi.mock('../i18n/index.js', () => ({
|
||||
initializeI18n: vi.fn(),
|
||||
initializeI18n: (...args: unknown[]) => mockInitializeI18n(...args),
|
||||
}));
|
||||
|
||||
const mockConnect = vi.fn();
|
||||
const mockGetInstance = vi.fn().mockResolvedValue({ connect: mockConnect });
|
||||
const mockLogIdeConnection = vi.fn();
|
||||
|
||||
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
|
||||
const actual = await importOriginal<Record<string, unknown>>();
|
||||
return {
|
||||
...actual,
|
||||
IdeClient: { getInstance: () => mockGetInstance() },
|
||||
IdeConnectionEvent: vi.fn().mockImplementation((type) => ({ type })),
|
||||
IdeConnectionType: { START: 'start' },
|
||||
logIdeConnection: (...args: unknown[]) => mockLogIdeConnection(...args),
|
||||
};
|
||||
});
|
||||
|
||||
describe('initializeApp', () => {
|
||||
let mockConfig: {
|
||||
getModelsConfig: ReturnType<typeof vi.fn>;
|
||||
getIdeMode: ReturnType<typeof vi.fn>;
|
||||
getGeminiMdFileCount: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let mockSettings: {
|
||||
merged: Record<string, unknown>;
|
||||
setValue: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
delete process.env['QWEN_CODE_LANG'];
|
||||
|
||||
vi.mocked(initializeI18n).mockResolvedValue(undefined);
|
||||
vi.mocked(validateTheme).mockReturnValue(null);
|
||||
});
|
||||
|
||||
function createMockConfig(
|
||||
options: {
|
||||
authType?: AuthType;
|
||||
wasAuthTypeExplicitlyProvided?: boolean;
|
||||
geminiMdFileCount?: number;
|
||||
ideMode?: boolean;
|
||||
} = {},
|
||||
): Config {
|
||||
const {
|
||||
authType = AuthType.USE_OPENAI,
|
||||
wasAuthTypeExplicitlyProvided = true,
|
||||
geminiMdFileCount = 0,
|
||||
ideMode = false,
|
||||
} = options;
|
||||
|
||||
return {
|
||||
mockConfig = {
|
||||
getModelsConfig: vi.fn().mockReturnValue({
|
||||
getCurrentAuthType: vi.fn().mockReturnValue(authType),
|
||||
wasAuthTypeExplicitlyProvided: vi
|
||||
.fn()
|
||||
.mockReturnValue(wasAuthTypeExplicitlyProvided),
|
||||
getCurrentAuthType: vi.fn().mockReturnValue('api_key'),
|
||||
wasAuthTypeExplicitlyProvided: vi.fn().mockReturnValue(false),
|
||||
}),
|
||||
getIdeMode: vi.fn().mockReturnValue(ideMode),
|
||||
getGeminiMdFileCount: vi.fn().mockReturnValue(geminiMdFileCount),
|
||||
} as unknown as Config;
|
||||
}
|
||||
getIdeMode: vi.fn().mockReturnValue(false),
|
||||
getGeminiMdFileCount: vi.fn().mockReturnValue(0),
|
||||
};
|
||||
|
||||
function createMockSettings(): LoadedSettings {
|
||||
return {
|
||||
merged: {
|
||||
general: {
|
||||
language: 'en',
|
||||
},
|
||||
},
|
||||
mockSettings = {
|
||||
merged: { general: { language: 'en' } },
|
||||
setValue: vi.fn(),
|
||||
} as unknown as LoadedSettings;
|
||||
}
|
||||
};
|
||||
|
||||
it('should not clear selected auth type when initial auth fails', async () => {
|
||||
vi.mocked(performInitialAuth).mockResolvedValue(
|
||||
'Failed to login. Message: missing OLLAMA_API_KEY',
|
||||
);
|
||||
|
||||
const config = createMockConfig({
|
||||
authType: AuthType.USE_OPENAI,
|
||||
wasAuthTypeExplicitlyProvided: true,
|
||||
});
|
||||
const settings = createMockSettings();
|
||||
|
||||
const result = await initializeApp(config, settings);
|
||||
|
||||
expect(result.authError).toBe(
|
||||
'Failed to login. Message: missing OLLAMA_API_KEY',
|
||||
);
|
||||
expect(result.shouldOpenAuthDialog).toBe(true);
|
||||
expect(settings.setValue).not.toHaveBeenCalled();
|
||||
mockPerformInitialAuth.mockResolvedValue(null);
|
||||
mockValidateTheme.mockReturnValue(null);
|
||||
mockInitializeI18n.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should not open auth dialog when auth is explicit and succeeds', async () => {
|
||||
vi.mocked(performInitialAuth).mockResolvedValue(null);
|
||||
it('should initialize i18n with language from settings', async () => {
|
||||
await initializeApp(mockConfig as never, mockSettings as never);
|
||||
|
||||
const config = createMockConfig({
|
||||
authType: AuthType.USE_OPENAI,
|
||||
wasAuthTypeExplicitlyProvided: true,
|
||||
});
|
||||
const settings = createMockSettings();
|
||||
expect(mockInitializeI18n).toHaveBeenCalledWith('en');
|
||||
});
|
||||
|
||||
const result = await initializeApp(config, settings);
|
||||
it('should initialize i18n with QWEN_CODE_LANG env var if set', async () => {
|
||||
vi.stubEnv('QWEN_CODE_LANG', 'zh');
|
||||
|
||||
await initializeApp(mockConfig as never, mockSettings as never);
|
||||
expect(mockInitializeI18n).toHaveBeenCalledWith('zh');
|
||||
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should return no errors on successful initialization', async () => {
|
||||
const result = await initializeApp(
|
||||
mockConfig as never,
|
||||
mockSettings as never,
|
||||
);
|
||||
|
||||
expect(result.authError).toBeNull();
|
||||
expect(result.themeError).toBeNull();
|
||||
expect(result.geminiMdFileCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should return authError when auth fails', async () => {
|
||||
mockPerformInitialAuth.mockResolvedValue('Auth failed');
|
||||
|
||||
const result = await initializeApp(
|
||||
mockConfig as never,
|
||||
mockSettings as never,
|
||||
);
|
||||
|
||||
expect(result.authError).toBe('Auth failed');
|
||||
expect(result.shouldOpenAuthDialog).toBe(true);
|
||||
// initializeApp does not clear the selected auth type on failure
|
||||
expect(mockSettings.setValue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return themeError when theme validation fails', async () => {
|
||||
mockValidateTheme.mockReturnValue('Theme not found');
|
||||
|
||||
const result = await initializeApp(
|
||||
mockConfig as never,
|
||||
mockSettings as never,
|
||||
);
|
||||
|
||||
expect(result.themeError).toBe('Theme not found');
|
||||
});
|
||||
|
||||
it('should set shouldOpenAuthDialog when auth was not explicitly provided', async () => {
|
||||
mockConfig
|
||||
.getModelsConfig()
|
||||
.wasAuthTypeExplicitlyProvided.mockReturnValue(false);
|
||||
|
||||
const result = await initializeApp(
|
||||
mockConfig as never,
|
||||
mockSettings as never,
|
||||
);
|
||||
|
||||
expect(result.shouldOpenAuthDialog).toBe(true);
|
||||
});
|
||||
|
||||
it('should set shouldOpenAuthDialog when auth error occurs', async () => {
|
||||
mockConfig
|
||||
.getModelsConfig()
|
||||
.wasAuthTypeExplicitlyProvided.mockReturnValue(true);
|
||||
mockPerformInitialAuth.mockResolvedValue('Auth failed');
|
||||
|
||||
const result = await initializeApp(
|
||||
mockConfig as never,
|
||||
mockSettings as never,
|
||||
);
|
||||
|
||||
expect(result.shouldOpenAuthDialog).toBe(true);
|
||||
});
|
||||
|
||||
it('should not open auth dialog when auth was explicitly provided and succeeds', async () => {
|
||||
mockConfig
|
||||
.getModelsConfig()
|
||||
.wasAuthTypeExplicitlyProvided.mockReturnValue(true);
|
||||
|
||||
const result = await initializeApp(
|
||||
mockConfig as never,
|
||||
mockSettings as never,
|
||||
);
|
||||
|
||||
expect(result.shouldOpenAuthDialog).toBe(false);
|
||||
});
|
||||
|
||||
it('should connect to IDE when in IDE mode', async () => {
|
||||
mockConfig.getIdeMode.mockReturnValue(true);
|
||||
|
||||
await initializeApp(mockConfig as never, mockSettings as never);
|
||||
|
||||
expect(mockGetInstance).toHaveBeenCalled();
|
||||
expect(mockConnect).toHaveBeenCalled();
|
||||
expect(mockLogIdeConnection).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not connect to IDE when not in IDE mode', async () => {
|
||||
mockConfig.getIdeMode.mockReturnValue(false);
|
||||
|
||||
await initializeApp(mockConfig as never, mockSettings as never);
|
||||
|
||||
expect(mockGetInstance).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should default language to auto when no setting is provided', async () => {
|
||||
mockSettings.merged = {};
|
||||
|
||||
await initializeApp(mockConfig as never, mockSettings as never);
|
||||
|
||||
expect(mockInitializeI18n).toHaveBeenCalledWith('auto');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
64
packages/cli/src/core/theme.test.ts
Normal file
64
packages/cli/src/core/theme.test.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Code
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { validateTheme } from './theme.js';
|
||||
|
||||
const mockFindThemeByName = vi.fn();
|
||||
vi.mock('../ui/themes/theme-manager.js', () => ({
|
||||
themeManager: {
|
||||
findThemeByName: (...args: unknown[]) => mockFindThemeByName(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../i18n/index.js', () => ({
|
||||
t: (msg: string, params?: Record<string, string>) => {
|
||||
if (params) {
|
||||
return msg.replace(
|
||||
/\{\{(\w+)\}\}/g,
|
||||
(_, key) => params[key] ?? `{{${key}}}`,
|
||||
);
|
||||
}
|
||||
return msg;
|
||||
},
|
||||
}));
|
||||
|
||||
describe('validateTheme', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return null when no theme is configured', () => {
|
||||
const settings = { merged: { ui: {} } };
|
||||
const result = validateTheme(settings as never);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when theme is found', () => {
|
||||
mockFindThemeByName.mockReturnValue({ name: 'dark' });
|
||||
const settings = { merged: { ui: { theme: 'dark' } } };
|
||||
|
||||
const result = validateTheme(settings as never);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockFindThemeByName).toHaveBeenCalledWith('dark');
|
||||
});
|
||||
|
||||
it('should return error message when theme is not found', () => {
|
||||
mockFindThemeByName.mockReturnValue(undefined);
|
||||
const settings = { merged: { ui: { theme: 'nonexistent-theme' } } };
|
||||
|
||||
const result = validateTheme(settings as never);
|
||||
|
||||
expect(result).toBe('Theme "nonexistent-theme" not found.');
|
||||
});
|
||||
|
||||
it('should return null when ui section is undefined', () => {
|
||||
const settings = { merged: {} };
|
||||
const result = validateTheme(settings as never);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue