mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
sync gemini-cli 0.1.17
Co-Authored-By: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -23,6 +23,10 @@ import { useGeminiStream } from './hooks/useGeminiStream.js';
|
||||
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
|
||||
import { StreamingState, ConsoleMessageItem } from './types.js';
|
||||
import { Tips } from './components/Tips.js';
|
||||
import { checkForUpdates, UpdateObject } from './utils/updateCheck.js';
|
||||
import { EventEmitter } from 'events';
|
||||
import { updateEventEmitter } from '../utils/updateEventEmitter.js';
|
||||
import * as auth from '../config/auth.js';
|
||||
|
||||
// Define a more complete mock server config based on actual Config
|
||||
interface MockServerConfig {
|
||||
@@ -148,13 +152,17 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
|
||||
setFlashFallbackHandler: vi.fn(),
|
||||
getSessionId: vi.fn(() => 'test-session-id'),
|
||||
getUserTier: vi.fn().mockResolvedValue(undefined),
|
||||
getIdeModeFeature: vi.fn(() => false),
|
||||
getIdeMode: vi.fn(() => false),
|
||||
getWorkspaceContext: vi.fn(() => ({
|
||||
getDirectories: vi.fn(() => []),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
const ideContextMock = {
|
||||
getOpenFilesContext: vi.fn(),
|
||||
subscribeToOpenFiles: vi.fn(() => vi.fn()), // subscribe returns an unsubscribe function
|
||||
getIdeContext: vi.fn(),
|
||||
subscribeToIdeContext: vi.fn(() => vi.fn()), // subscribe returns an unsubscribe function
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -163,6 +171,7 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
|
||||
MCPServerConfig: actualCore.MCPServerConfig,
|
||||
getAllGeminiMdFilenames: vi.fn(() => ['GEMINI.md']),
|
||||
ideContext: ideContextMock,
|
||||
isGitRepository: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -220,6 +229,21 @@ vi.mock('./components/Header.js', () => ({
|
||||
Header: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
vi.mock('./utils/updateCheck.js', () => ({
|
||||
checkForUpdates: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./config/auth.js', () => ({
|
||||
validateAuthMethod: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockedCheckForUpdates = vi.mocked(checkForUpdates);
|
||||
const { isGitRepository: mockedIsGitRepository } = vi.mocked(
|
||||
await import('@qwen-code/qwen-code-core'),
|
||||
);
|
||||
|
||||
vi.mock('node:child_process');
|
||||
|
||||
describe('App UI', () => {
|
||||
let mockConfig: MockServerConfig;
|
||||
let mockSettings: LoadedSettings;
|
||||
@@ -277,7 +301,14 @@ describe('App UI', () => {
|
||||
|
||||
// Ensure a theme is set so the theme dialog does not appear.
|
||||
mockSettings = createMockSettings({ workspace: { theme: 'Default' } });
|
||||
vi.mocked(ideContext.getOpenFilesContext).mockReturnValue(undefined);
|
||||
|
||||
// Ensure getWorkspaceContext is available if not added by the constructor
|
||||
if (!mockConfig.getWorkspaceContext) {
|
||||
mockConfig.getWorkspaceContext = vi.fn(() => ({
|
||||
getDirectories: vi.fn(() => ['/test/dir']),
|
||||
}));
|
||||
}
|
||||
vi.mocked(ideContext.getIdeContext).mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -288,11 +319,181 @@ describe('App UI', () => {
|
||||
vi.clearAllMocks(); // Clear mocks after each test
|
||||
});
|
||||
|
||||
describe('handleAutoUpdate', () => {
|
||||
let spawnEmitter: EventEmitter;
|
||||
|
||||
beforeEach(async () => {
|
||||
const { spawn } = await import('node:child_process');
|
||||
spawnEmitter = new EventEmitter();
|
||||
spawnEmitter.stdout = new EventEmitter();
|
||||
spawnEmitter.stderr = new EventEmitter();
|
||||
(spawn as vi.Mock).mockReturnValue(spawnEmitter);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.GEMINI_CLI_DISABLE_AUTOUPDATER;
|
||||
});
|
||||
|
||||
it('should not start the update process when running from git', async () => {
|
||||
mockedIsGitRepository.mockResolvedValue(true);
|
||||
const info: UpdateObject = {
|
||||
update: {
|
||||
name: '@qwen-code/qwen-code',
|
||||
latest: '1.1.0',
|
||||
current: '1.0.0',
|
||||
},
|
||||
message: 'Qwen Code update available!',
|
||||
};
|
||||
mockedCheckForUpdates.mockResolvedValue(info);
|
||||
const { spawn } = await import('node:child_process');
|
||||
|
||||
const { unmount } = render(
|
||||
<App
|
||||
config={mockConfig as unknown as ServerConfig}
|
||||
settings={mockSettings}
|
||||
version={mockVersion}
|
||||
/>,
|
||||
);
|
||||
currentUnmount = unmount;
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
expect(spawn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show a success message when update succeeds', async () => {
|
||||
mockedIsGitRepository.mockResolvedValue(false);
|
||||
const info: UpdateObject = {
|
||||
update: {
|
||||
name: '@qwen-code/qwen-code',
|
||||
latest: '1.1.0',
|
||||
current: '1.0.0',
|
||||
},
|
||||
message: 'Update available',
|
||||
};
|
||||
mockedCheckForUpdates.mockResolvedValue(info);
|
||||
|
||||
const { lastFrame, unmount } = render(
|
||||
<App
|
||||
config={mockConfig as unknown as ServerConfig}
|
||||
settings={mockSettings}
|
||||
version={mockVersion}
|
||||
/>,
|
||||
);
|
||||
currentUnmount = unmount;
|
||||
|
||||
updateEventEmitter.emit('update-success', info);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
expect(lastFrame()).toContain(
|
||||
'Update successful! The new version will be used on your next run.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show an error message when update fails', async () => {
|
||||
mockedIsGitRepository.mockResolvedValue(false);
|
||||
const info: UpdateObject = {
|
||||
update: {
|
||||
name: '@qwen-code/qwen-code',
|
||||
latest: '1.1.0',
|
||||
current: '1.0.0',
|
||||
},
|
||||
message: 'Update available',
|
||||
};
|
||||
mockedCheckForUpdates.mockResolvedValue(info);
|
||||
|
||||
const { lastFrame, unmount } = render(
|
||||
<App
|
||||
config={mockConfig as unknown as ServerConfig}
|
||||
settings={mockSettings}
|
||||
version={mockVersion}
|
||||
/>,
|
||||
);
|
||||
currentUnmount = unmount;
|
||||
|
||||
updateEventEmitter.emit('update-failed', info);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
expect(lastFrame()).toContain(
|
||||
'Automatic update failed. Please try updating manually',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show an error message when spawn fails', async () => {
|
||||
mockedIsGitRepository.mockResolvedValue(false);
|
||||
const info: UpdateObject = {
|
||||
update: {
|
||||
name: '@qwen-code/qwen-code',
|
||||
latest: '1.1.0',
|
||||
current: '1.0.0',
|
||||
},
|
||||
message: 'Update available',
|
||||
};
|
||||
mockedCheckForUpdates.mockResolvedValue(info);
|
||||
|
||||
const { lastFrame, unmount } = render(
|
||||
<App
|
||||
config={mockConfig as unknown as ServerConfig}
|
||||
settings={mockSettings}
|
||||
version={mockVersion}
|
||||
/>,
|
||||
);
|
||||
currentUnmount = unmount;
|
||||
|
||||
// We are testing the App's reaction to an `update-failed` event,
|
||||
// which is what should be emitted when a spawn error occurs elsewhere.
|
||||
updateEventEmitter.emit('update-failed', info);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
expect(lastFrame()).toContain(
|
||||
'Automatic update failed. Please try updating manually',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not auto-update if GEMINI_CLI_DISABLE_AUTOUPDATER is true', async () => {
|
||||
mockedIsGitRepository.mockResolvedValue(false);
|
||||
process.env.GEMINI_CLI_DISABLE_AUTOUPDATER = 'true';
|
||||
const info: UpdateObject = {
|
||||
update: {
|
||||
name: '@qwen-code/qwen-code',
|
||||
latest: '1.1.0',
|
||||
current: '1.0.0',
|
||||
},
|
||||
message: 'Update available',
|
||||
};
|
||||
mockedCheckForUpdates.mockResolvedValue(info);
|
||||
const { spawn } = await import('node:child_process');
|
||||
|
||||
const { unmount } = render(
|
||||
<App
|
||||
config={mockConfig as unknown as ServerConfig}
|
||||
settings={mockSettings}
|
||||
version={mockVersion}
|
||||
/>,
|
||||
);
|
||||
currentUnmount = unmount;
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
expect(spawn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display active file when available', async () => {
|
||||
vi.mocked(ideContext.getOpenFilesContext).mockReturnValue({
|
||||
activeFile: '/path/to/my-file.ts',
|
||||
recentOpenFiles: [{ filePath: '/path/to/my-file.ts', content: 'hello' }],
|
||||
selectedText: 'hello',
|
||||
vi.mocked(ideContext.getIdeContext).mockReturnValue({
|
||||
workspaceState: {
|
||||
openFiles: [
|
||||
{
|
||||
path: '/path/to/my-file.ts',
|
||||
isActive: true,
|
||||
selectedText: 'hello',
|
||||
timestamp: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = render(
|
||||
@@ -304,12 +505,14 @@ describe('App UI', () => {
|
||||
);
|
||||
currentUnmount = unmount;
|
||||
await Promise.resolve();
|
||||
expect(lastFrame()).toContain('1 recent file (ctrl+e to view)');
|
||||
expect(lastFrame()).toContain('1 open file (ctrl+e to view)');
|
||||
});
|
||||
|
||||
it('should not display active file when not available', async () => {
|
||||
vi.mocked(ideContext.getOpenFilesContext).mockReturnValue({
|
||||
activeFile: '',
|
||||
it('should not display any files when not available', async () => {
|
||||
vi.mocked(ideContext.getIdeContext).mockReturnValue({
|
||||
workspaceState: {
|
||||
openFiles: [],
|
||||
},
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = render(
|
||||
@@ -324,11 +527,54 @@ describe('App UI', () => {
|
||||
expect(lastFrame()).not.toContain('Open File');
|
||||
});
|
||||
|
||||
it('should display active file and other open files', async () => {
|
||||
vi.mocked(ideContext.getIdeContext).mockReturnValue({
|
||||
workspaceState: {
|
||||
openFiles: [
|
||||
{
|
||||
path: '/path/to/my-file.ts',
|
||||
isActive: true,
|
||||
selectedText: 'hello',
|
||||
timestamp: 0,
|
||||
},
|
||||
{
|
||||
path: '/path/to/another-file.ts',
|
||||
isActive: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
path: '/path/to/third-file.ts',
|
||||
isActive: false,
|
||||
timestamp: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = render(
|
||||
<App
|
||||
config={mockConfig as unknown as ServerConfig}
|
||||
settings={mockSettings}
|
||||
version={mockVersion}
|
||||
/>,
|
||||
);
|
||||
currentUnmount = unmount;
|
||||
await Promise.resolve();
|
||||
expect(lastFrame()).toContain('3 open files (ctrl+e to view)');
|
||||
});
|
||||
|
||||
it('should display active file and other context', async () => {
|
||||
vi.mocked(ideContext.getOpenFilesContext).mockReturnValue({
|
||||
activeFile: '/path/to/my-file.ts',
|
||||
recentOpenFiles: [{ filePath: '/path/to/my-file.ts', content: 'hello' }],
|
||||
selectedText: 'hello',
|
||||
vi.mocked(ideContext.getIdeContext).mockReturnValue({
|
||||
workspaceState: {
|
||||
openFiles: [
|
||||
{
|
||||
path: '/path/to/my-file.ts',
|
||||
isActive: true,
|
||||
selectedText: 'hello',
|
||||
timestamp: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
mockConfig.getGeminiMdFileCount.mockReturnValue(1);
|
||||
mockConfig.getAllGeminiMdFilenames.mockReturnValue(['GEMINI.md']);
|
||||
@@ -343,7 +589,7 @@ describe('App UI', () => {
|
||||
currentUnmount = unmount;
|
||||
await Promise.resolve();
|
||||
expect(lastFrame()).toContain(
|
||||
'Using: 1 recent file (ctrl+e to view) | 1 GEMINI.md file',
|
||||
'Using: 1 open file (ctrl+e to view) | 1 GEMINI.md file',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -764,4 +1010,50 @@ describe('App UI', () => {
|
||||
expect(lastFrame()).toContain('5 errors');
|
||||
});
|
||||
});
|
||||
|
||||
describe('auth validation', () => {
|
||||
it('should call validateAuthMethod when useExternalAuth is false', async () => {
|
||||
const validateAuthMethodSpy = vi.spyOn(auth, 'validateAuthMethod');
|
||||
mockSettings = createMockSettings({
|
||||
workspace: {
|
||||
selectedAuthType: 'USE_GEMINI' as AuthType,
|
||||
useExternalAuth: false,
|
||||
theme: 'Default',
|
||||
},
|
||||
});
|
||||
|
||||
const { unmount } = render(
|
||||
<App
|
||||
config={mockConfig as unknown as ServerConfig}
|
||||
settings={mockSettings}
|
||||
version={mockVersion}
|
||||
/>,
|
||||
);
|
||||
currentUnmount = unmount;
|
||||
|
||||
expect(validateAuthMethodSpy).toHaveBeenCalledWith('USE_GEMINI');
|
||||
});
|
||||
|
||||
it('should NOT call validateAuthMethod when useExternalAuth is true', async () => {
|
||||
const validateAuthMethodSpy = vi.spyOn(auth, 'validateAuthMethod');
|
||||
mockSettings = createMockSettings({
|
||||
workspace: {
|
||||
selectedAuthType: 'USE_GEMINI' as AuthType,
|
||||
useExternalAuth: true,
|
||||
theme: 'Default',
|
||||
},
|
||||
});
|
||||
|
||||
const { unmount } = render(
|
||||
<App
|
||||
config={mockConfig as unknown as ServerConfig}
|
||||
settings={mockSettings}
|
||||
version={mockVersion}
|
||||
/>,
|
||||
);
|
||||
currentUnmount = unmount;
|
||||
|
||||
expect(validateAuthMethodSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user