Merge tag 'v0.1.15' into feature/yiheng/sync-gemini-cli-0.1.15

This commit is contained in:
奕桁
2025-08-01 23:06:11 +08:00
340 changed files with 36528 additions and 22931 deletions

View File

@@ -15,11 +15,13 @@ import {
AccessibilitySettings,
SandboxConfig,
GeminiClient,
ideContext,
} from '@qwen-code/qwen-code-core';
import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js';
import process from 'node:process';
import { useGeminiStream } from './hooks/useGeminiStream.js';
import { StreamingState } from './types.js';
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
import { StreamingState, ConsoleMessageItem } from './types.js';
import { Tips } from './components/Tips.js';
// Define a more complete mock server config based on actual Config
@@ -58,6 +60,12 @@ interface MockServerConfig {
getToolCallCommand: Mock<() => string | undefined>;
getMcpServerCommand: Mock<() => string | undefined>;
getMcpServers: Mock<() => Record<string, MCPServerConfig> | undefined>;
getExtensions: Mock<
() => Array<{ name: string; version: string; isActive: boolean }>
>;
getBlockedMcpServers: Mock<
() => Array<{ name: string; extensionName: string }>
>;
getUserAgent: Mock<() => string>;
getUserMemory: Mock<() => string>;
setUserMemory: Mock<(newUserMemory: string) => void>;
@@ -118,6 +126,9 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
getToolCallCommand: vi.fn(() => opts.toolCallCommand),
getMcpServerCommand: vi.fn(() => opts.mcpServerCommand),
getMcpServers: vi.fn(() => opts.mcpServers),
getPromptRegistry: vi.fn(),
getExtensions: vi.fn(() => []),
getBlockedMcpServers: vi.fn(() => []),
getUserAgent: vi.fn(() => opts.userAgent || 'test-agent'),
getUserMemory: vi.fn(() => opts.userMemory || ''),
setUserMemory: vi.fn(),
@@ -129,19 +140,29 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
getShowMemoryUsage: vi.fn(() => opts.showMemoryUsage ?? false),
getAccessibility: vi.fn(() => opts.accessibility ?? {}),
getProjectRoot: vi.fn(() => opts.targetDir),
getGeminiClient: vi.fn(() => ({})),
getGeminiClient: vi.fn(() => ({
getUserTier: vi.fn(),
})),
getCheckpointingEnabled: vi.fn(() => opts.checkpointing ?? true),
getAllGeminiMdFilenames: vi.fn(() => ['GEMINI.md']),
setFlashFallbackHandler: vi.fn(),
getSessionId: vi.fn(() => 'test-session-id'),
getUserTier: vi.fn().mockResolvedValue(undefined),
getIdeMode: vi.fn(() => false),
};
});
const ideContextMock = {
getOpenFilesContext: vi.fn(),
subscribeToOpenFiles: vi.fn(() => vi.fn()), // subscribe returns an unsubscribe function
};
return {
...actualCore,
Config: ConfigClassMock,
MCPServerConfig: actualCore.MCPServerConfig,
getAllGeminiMdFilenames: vi.fn(() => ['GEMINI.md']),
ideContext: ideContextMock,
};
});
@@ -172,6 +193,14 @@ vi.mock('./hooks/useLogger', () => ({
})),
}));
vi.mock('./hooks/useConsoleMessages.js', () => ({
useConsoleMessages: vi.fn(() => ({
consoleMessages: [],
handleNewMessage: vi.fn(),
clearConsoleMessages: vi.fn(),
})),
}));
vi.mock('../config/config.js', async (importOriginal) => {
const actual = await importOriginal();
return {
@@ -213,7 +242,7 @@ describe('App UI', () => {
settings: settings.user || {},
};
const workspaceSettingsFile: SettingsFile = {
path: '/workspace/.qwen/settings.json',
path: '/workspace/.gemini/settings.json',
settings: settings.workspace || {},
};
return new LoadedSettings(
@@ -248,6 +277,7 @@ 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);
});
afterEach(() => {
@@ -258,8 +288,68 @@ describe('App UI', () => {
vi.clearAllMocks(); // Clear mocks after each test
});
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',
});
const { lastFrame, unmount } = render(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettings}
version={mockVersion}
/>,
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain('1 recent file (ctrl+e to view)');
});
it('should not display active file when not available', async () => {
vi.mocked(ideContext.getOpenFilesContext).mockReturnValue({
activeFile: '',
});
const { lastFrame, unmount } = render(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettings}
version={mockVersion}
/>,
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).not.toContain('Open File');
});
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',
});
mockConfig.getGeminiMdFileCount.mockReturnValue(1);
mockConfig.getAllGeminiMdFilenames.mockReturnValue(['GEMINI.md']);
const { lastFrame, unmount } = render(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettings}
version={mockVersion}
/>,
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain(
'Using: 1 recent file (ctrl+e to view) | 1 GEMINI.md file',
);
});
it('should display default "GEMINI.md" in footer when contextFileName is not set and count is 1', async () => {
mockConfig.getGeminiMdFileCount.mockReturnValue(1);
mockConfig.getAllGeminiMdFilenames.mockReturnValue(['GEMINI.md']);
// For this test, ensure showMemoryUsage is false or debugMode is false if it relies on that
mockConfig.getDebugMode.mockReturnValue(false);
mockConfig.getShowMemoryUsage.mockReturnValue(false);
@@ -273,11 +363,15 @@ describe('App UI', () => {
);
currentUnmount = unmount;
await Promise.resolve(); // Wait for any async updates
expect(lastFrame()).toContain('Using 1 GEMINI.md file');
expect(lastFrame()).toContain('Using: 1 GEMINI.md file');
});
it('should display default "GEMINI.md" with plural when contextFileName is not set and count is > 1', async () => {
mockConfig.getGeminiMdFileCount.mockReturnValue(2);
mockConfig.getAllGeminiMdFilenames.mockReturnValue([
'GEMINI.md',
'GEMINI.md',
]);
mockConfig.getDebugMode.mockReturnValue(false);
mockConfig.getShowMemoryUsage.mockReturnValue(false);
@@ -290,7 +384,7 @@ describe('App UI', () => {
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain('Using 2 GEMINI.md files');
expect(lastFrame()).toContain('Using: 2 GEMINI.md files');
});
it('should display custom contextFileName in footer when set and count is 1', async () => {
@@ -298,6 +392,7 @@ describe('App UI', () => {
workspace: { contextFileName: 'AGENTS.md', theme: 'Default' },
});
mockConfig.getGeminiMdFileCount.mockReturnValue(1);
mockConfig.getAllGeminiMdFilenames.mockReturnValue(['AGENTS.md']);
mockConfig.getDebugMode.mockReturnValue(false);
mockConfig.getShowMemoryUsage.mockReturnValue(false);
@@ -310,7 +405,7 @@ describe('App UI', () => {
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain('Using 1 AGENTS.md file');
expect(lastFrame()).toContain('Using: 1 AGENTS.md file');
});
it('should display a generic message when multiple context files with different names are provided', async () => {
@@ -321,6 +416,10 @@ describe('App UI', () => {
},
});
mockConfig.getGeminiMdFileCount.mockReturnValue(2);
mockConfig.getAllGeminiMdFilenames.mockReturnValue([
'AGENTS.md',
'CONTEXT.md',
]);
mockConfig.getDebugMode.mockReturnValue(false);
mockConfig.getShowMemoryUsage.mockReturnValue(false);
@@ -333,7 +432,7 @@ describe('App UI', () => {
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain('Using 2 context files');
expect(lastFrame()).toContain('Using: 2 context files');
});
it('should display custom contextFileName with plural when set and count is > 1', async () => {
@@ -341,6 +440,11 @@ describe('App UI', () => {
workspace: { contextFileName: 'MY_NOTES.TXT', theme: 'Default' },
});
mockConfig.getGeminiMdFileCount.mockReturnValue(3);
mockConfig.getAllGeminiMdFilenames.mockReturnValue([
'MY_NOTES.TXT',
'MY_NOTES.TXT',
'MY_NOTES.TXT',
]);
mockConfig.getDebugMode.mockReturnValue(false);
mockConfig.getShowMemoryUsage.mockReturnValue(false);
@@ -353,7 +457,7 @@ describe('App UI', () => {
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain('Using 3 MY_NOTES.TXT files');
expect(lastFrame()).toContain('Using: 3 MY_NOTES.TXT files');
});
it('should not display context file message if count is 0, even if contextFileName is set', async () => {
@@ -361,6 +465,7 @@ describe('App UI', () => {
workspace: { contextFileName: 'ANY_FILE.MD', theme: 'Default' },
});
mockConfig.getGeminiMdFileCount.mockReturnValue(0);
mockConfig.getAllGeminiMdFilenames.mockReturnValue([]);
mockConfig.getDebugMode.mockReturnValue(false);
mockConfig.getShowMemoryUsage.mockReturnValue(false);
@@ -378,6 +483,10 @@ describe('App UI', () => {
it('should display GEMINI.md and MCP server count when both are present', async () => {
mockConfig.getGeminiMdFileCount.mockReturnValue(2);
mockConfig.getAllGeminiMdFilenames.mockReturnValue([
'GEMINI.md',
'GEMINI.md',
]);
mockConfig.getMcpServers.mockReturnValue({
server1: {} as MCPServerConfig,
});
@@ -393,11 +502,12 @@ describe('App UI', () => {
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain('server');
expect(lastFrame()).toContain('1 MCP server');
});
it('should display only MCP server count when GEMINI.md count is 0', async () => {
mockConfig.getGeminiMdFileCount.mockReturnValue(0);
mockConfig.getAllGeminiMdFilenames.mockReturnValue([]);
mockConfig.getMcpServers.mockReturnValue({
server1: {} as MCPServerConfig,
server2: {} as MCPServerConfig,
@@ -414,7 +524,7 @@ describe('App UI', () => {
);
currentUnmount = unmount;
await Promise.resolve();
expect(lastFrame()).toContain('Using 2 MCP servers');
expect(lastFrame()).toContain('Using: 2 MCP servers (ctrl+t to view)');
});
it('should display Tips component by default', async () => {
@@ -527,7 +637,7 @@ describe('App UI', () => {
);
currentUnmount = unmount;
expect(lastFrame()).toContain('Select Theme');
expect(lastFrame()).toContain("I'm Feeling Lucky (esc to cancel");
});
it('should display a message if NO_COLOR is set', async () => {
@@ -542,13 +652,43 @@ describe('App UI', () => {
);
currentUnmount = unmount;
expect(lastFrame()).toContain(
'Theme configuration unavailable due to NO_COLOR env variable.',
);
expect(lastFrame()).toContain("I'm Feeling Lucky (esc to cancel");
expect(lastFrame()).not.toContain('Select Theme');
});
});
it('should render the initial UI correctly', () => {
const { lastFrame, unmount } = render(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettings}
version={mockVersion}
/>,
);
currentUnmount = unmount;
expect(lastFrame()).toMatchSnapshot();
});
it('should render correctly with the prompt input box', () => {
vi.mocked(useGeminiStream).mockReturnValue({
streamingState: StreamingState.Idle,
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: null,
});
const { lastFrame, unmount } = render(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettings}
version={mockVersion}
/>,
);
currentUnmount = unmount;
expect(lastFrame()).toMatchSnapshot();
});
describe('with initial prompt from --prompt-interactive', () => {
it('should submit the initial prompt automatically', async () => {
const mockSubmitQuery = vi.fn();
@@ -565,6 +705,7 @@ describe('App UI', () => {
mockConfig.getGeminiClient.mockReturnValue({
isInitialized: vi.fn(() => true),
getUserTier: vi.fn(),
} as unknown as GeminiClient);
const { unmount, rerender } = render(
@@ -592,4 +733,35 @@ describe('App UI', () => {
);
});
});
describe('errorCount', () => {
it('should correctly sum the counts of error messages', async () => {
const mockConsoleMessages: ConsoleMessageItem[] = [
{ type: 'error', content: 'First error', count: 1 },
{ type: 'log', content: 'some log', count: 1 },
{ type: 'error', content: 'Second error', count: 3 },
{ type: 'warn', content: 'a warning', count: 1 },
{ type: 'error', content: 'Third error', count: 1 },
];
vi.mocked(useConsoleMessages).mockReturnValue({
consoleMessages: mockConsoleMessages,
handleNewMessage: vi.fn(),
clearConsoleMessages: vi.fn(),
});
const { lastFrame, unmount } = render(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettings}
version={mockVersion}
/>,
);
currentUnmount = unmount;
await Promise.resolve();
// Total error count should be 1 + 3 + 1 = 5
expect(lastFrame()).toContain('5 errors');
});
});
});