mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Merge tag 'v0.3.0' into chore/sync-gemini-cli-v0.3.0
This commit is contained in:
@@ -4,19 +4,18 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
main,
|
||||
setupUnhandledRejectionHandler,
|
||||
validateDnsResolutionOrder,
|
||||
startInteractiveUI,
|
||||
} from './gemini.js';
|
||||
import {
|
||||
LoadedSettings,
|
||||
SettingsFile,
|
||||
loadSettings,
|
||||
} from './config/settings.js';
|
||||
import type { SettingsFile } from './config/settings.js';
|
||||
import { LoadedSettings, loadSettings } from './config/settings.js';
|
||||
import { appEvents, AppEvent } from './utils/events.js';
|
||||
import type { Config } from '@qwen-code/qwen-code-core';
|
||||
import { FatalConfigError } from '@qwen-code/qwen-code-core';
|
||||
|
||||
// Custom error to identify mock process.exit calls
|
||||
class MockProcessExitError extends Error {
|
||||
@@ -76,7 +75,6 @@ vi.mock('./utils/sandbox.js', () => ({
|
||||
}));
|
||||
|
||||
describe('gemini.tsx main function', () => {
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
let loadSettingsMock: ReturnType<typeof vi.mocked<typeof loadSettings>>;
|
||||
let originalEnvGeminiSandbox: string | undefined;
|
||||
let originalEnvSandbox: string | undefined;
|
||||
@@ -98,7 +96,6 @@ describe('gemini.tsx main function', () => {
|
||||
delete process.env['GEMINI_SANDBOX'];
|
||||
delete process.env['SANDBOX'];
|
||||
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
initialUnhandledRejectionListeners =
|
||||
process.listeners('unhandledRejection');
|
||||
});
|
||||
@@ -127,7 +124,7 @@ describe('gemini.tsx main function', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should call process.exit(1) if settings have errors', async () => {
|
||||
it('should throw InvalidConfigurationError if settings have errors', async () => {
|
||||
const settingsError = {
|
||||
message: 'Test settings error',
|
||||
path: '/test/settings.json',
|
||||
@@ -144,37 +141,23 @@ describe('gemini.tsx main function', () => {
|
||||
path: '/system/settings.json',
|
||||
settings: {},
|
||||
};
|
||||
const systemDefaultsFile: SettingsFile = {
|
||||
path: '/system/system-defaults.json',
|
||||
settings: {},
|
||||
};
|
||||
const mockLoadedSettings = new LoadedSettings(
|
||||
systemSettingsFile,
|
||||
systemDefaultsFile,
|
||||
userSettingsFile,
|
||||
workspaceSettingsFile,
|
||||
[settingsError],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
loadSettingsMock.mockReturnValue(mockLoadedSettings);
|
||||
|
||||
try {
|
||||
await main();
|
||||
// If main completes without throwing, the test should fail because process.exit was expected
|
||||
expect.fail('main function did not exit as expected');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(MockProcessExitError);
|
||||
if (error instanceof MockProcessExitError) {
|
||||
expect(error.code).toBe(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify console.error was called with the error message
|
||||
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
|
||||
expect(stripAnsi(String(consoleErrorSpy.mock.calls[0][0]))).toBe(
|
||||
'Error in /test/settings.json: Test settings error',
|
||||
);
|
||||
expect(stripAnsi(String(consoleErrorSpy.mock.calls[1][0]))).toBe(
|
||||
'Please fix /test/settings.json and try again.',
|
||||
);
|
||||
|
||||
// Verify process.exit was called.
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
await expect(main()).rejects.toThrow(FatalConfigError);
|
||||
});
|
||||
|
||||
it('should log unhandled promise rejections and open debug console on first error', async () => {
|
||||
@@ -250,3 +233,100 @@ describe('validateDnsResolutionOrder', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('startInteractiveUI', () => {
|
||||
// Mock dependencies
|
||||
const mockConfig = {
|
||||
getProjectRoot: () => '/root',
|
||||
getScreenReader: () => false,
|
||||
} as Config;
|
||||
const mockSettings = {
|
||||
merged: {
|
||||
ui: {
|
||||
hideWindowTitle: false,
|
||||
},
|
||||
},
|
||||
} as LoadedSettings;
|
||||
const mockStartupWarnings = ['warning1'];
|
||||
const mockWorkspaceRoot = '/root';
|
||||
|
||||
vi.mock('./utils/version.js', () => ({
|
||||
getCliVersion: vi.fn(() => Promise.resolve('1.0.0')),
|
||||
}));
|
||||
|
||||
vi.mock('./ui/utils/kittyProtocolDetector.js', () => ({
|
||||
detectAndEnableKittyProtocol: vi.fn(() => Promise.resolve()),
|
||||
}));
|
||||
|
||||
vi.mock('./ui/utils/updateCheck.js', () => ({
|
||||
checkForUpdates: vi.fn(() => Promise.resolve(null)),
|
||||
}));
|
||||
|
||||
vi.mock('./utils/cleanup.js', () => ({
|
||||
cleanupCheckpoints: vi.fn(() => Promise.resolve()),
|
||||
registerCleanup: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('ink', () => ({
|
||||
render: vi.fn().mockReturnValue({ unmount: vi.fn() }),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render the UI with proper React context and exitOnCtrlC disabled', async () => {
|
||||
const { render } = await import('ink');
|
||||
const renderSpy = vi.mocked(render);
|
||||
|
||||
await startInteractiveUI(
|
||||
mockConfig,
|
||||
mockSettings,
|
||||
mockStartupWarnings,
|
||||
mockWorkspaceRoot,
|
||||
);
|
||||
|
||||
// Verify render was called with correct options
|
||||
expect(renderSpy).toHaveBeenCalledTimes(1);
|
||||
const [reactElement, options] = renderSpy.mock.calls[0];
|
||||
|
||||
// Verify render options
|
||||
expect(options).toEqual({
|
||||
exitOnCtrlC: false,
|
||||
isScreenReaderEnabled: false,
|
||||
});
|
||||
|
||||
// Verify React element structure is valid (but don't deep dive into JSX internals)
|
||||
expect(reactElement).toBeDefined();
|
||||
});
|
||||
|
||||
it('should perform all startup tasks in correct order', async () => {
|
||||
const { getCliVersion } = await import('./utils/version.js');
|
||||
const { detectAndEnableKittyProtocol } = await import(
|
||||
'./ui/utils/kittyProtocolDetector.js'
|
||||
);
|
||||
const { checkForUpdates } = await import('./ui/utils/updateCheck.js');
|
||||
const { registerCleanup } = await import('./utils/cleanup.js');
|
||||
|
||||
await startInteractiveUI(
|
||||
mockConfig,
|
||||
mockSettings,
|
||||
mockStartupWarnings,
|
||||
mockWorkspaceRoot,
|
||||
);
|
||||
|
||||
// Verify all startup tasks were called
|
||||
expect(getCliVersion).toHaveBeenCalledTimes(1);
|
||||
expect(detectAndEnableKittyProtocol).toHaveBeenCalledTimes(1);
|
||||
expect(registerCleanup).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Verify cleanup handler is registered with unmount function
|
||||
const cleanupFn = vi.mocked(registerCleanup).mock.calls[0][0];
|
||||
expect(typeof cleanupFn).toBe('function');
|
||||
|
||||
// checkForUpdates should be called asynchronously (not waited for)
|
||||
// We need a small delay to let it execute
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(checkForUpdates).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user