sync gemini-cli 0.1.17

Co-Authored-By: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Yiheng Xu
2025-08-05 16:44:06 +08:00
235 changed files with 16997 additions and 3736 deletions

View File

@@ -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();
});
});
});