mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
sync gemini-cli 0.1.17
Co-Authored-By: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -18,6 +18,19 @@ import {
|
||||
} from '../core/contentGenerator.js';
|
||||
import { GeminiClient } from '../core/client.js';
|
||||
import { GitService } from '../services/gitService.js';
|
||||
import { IdeClient } from '../ide/ide-client.js';
|
||||
|
||||
vi.mock('fs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('fs')>();
|
||||
return {
|
||||
...actual,
|
||||
existsSync: vi.fn().mockReturnValue(true),
|
||||
statSync: vi.fn().mockReturnValue({
|
||||
isDirectory: vi.fn().mockReturnValue(true),
|
||||
}),
|
||||
realpathSync: vi.fn((path) => path),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock dependencies that might be called during Config construction or createServerConfig
|
||||
vi.mock('../tools/tool-registry', () => {
|
||||
@@ -107,6 +120,7 @@ describe('Server Config (config.ts)', () => {
|
||||
telemetry: TELEMETRY_SETTINGS,
|
||||
sessionId: SESSION_ID,
|
||||
model: MODEL,
|
||||
ideClient: IdeClient.getInstance(false),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -152,6 +166,10 @@ describe('Server Config (config.ts)', () => {
|
||||
|
||||
(createContentGeneratorConfig as Mock).mockReturnValue(mockContentConfig);
|
||||
|
||||
// Set fallback mode to true to ensure it gets reset
|
||||
config.setFallbackMode(true);
|
||||
expect(config.isInFallbackMode()).toBe(true);
|
||||
|
||||
await config.refreshAuth(authType);
|
||||
|
||||
expect(createContentGeneratorConfig).toHaveBeenCalledWith(
|
||||
@@ -163,6 +181,89 @@ describe('Server Config (config.ts)', () => {
|
||||
expect(config.getContentGeneratorConfig().model).toBe(newModel);
|
||||
expect(config.getModel()).toBe(newModel); // getModel() should return the updated model
|
||||
expect(GeminiClient).toHaveBeenCalledWith(config);
|
||||
// Verify that fallback mode is reset
|
||||
expect(config.isInFallbackMode()).toBe(false);
|
||||
});
|
||||
|
||||
it('should preserve conversation history when refreshing auth', async () => {
|
||||
const config = new Config(baseParams);
|
||||
const authType = AuthType.USE_GEMINI;
|
||||
const mockContentConfig = {
|
||||
model: 'gemini-pro',
|
||||
apiKey: 'test-key',
|
||||
};
|
||||
|
||||
(createContentGeneratorConfig as Mock).mockReturnValue(mockContentConfig);
|
||||
|
||||
// Mock the existing client with some history
|
||||
const mockExistingHistory = [
|
||||
{ role: 'user', parts: [{ text: 'Hello' }] },
|
||||
{ role: 'model', parts: [{ text: 'Hi there!' }] },
|
||||
{ role: 'user', parts: [{ text: 'How are you?' }] },
|
||||
];
|
||||
|
||||
const mockExistingClient = {
|
||||
isInitialized: vi.fn().mockReturnValue(true),
|
||||
getHistory: vi.fn().mockReturnValue(mockExistingHistory),
|
||||
};
|
||||
|
||||
const mockNewClient = {
|
||||
isInitialized: vi.fn().mockReturnValue(true),
|
||||
getHistory: vi.fn().mockReturnValue([]),
|
||||
setHistory: vi.fn(),
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
// Set the existing client
|
||||
(
|
||||
config as unknown as { geminiClient: typeof mockExistingClient }
|
||||
).geminiClient = mockExistingClient;
|
||||
(GeminiClient as Mock).mockImplementation(() => mockNewClient);
|
||||
|
||||
await config.refreshAuth(authType);
|
||||
|
||||
// Verify that existing history was retrieved
|
||||
expect(mockExistingClient.getHistory).toHaveBeenCalled();
|
||||
|
||||
// Verify that new client was created and initialized
|
||||
expect(GeminiClient).toHaveBeenCalledWith(config);
|
||||
expect(mockNewClient.initialize).toHaveBeenCalledWith(mockContentConfig);
|
||||
|
||||
// Verify that history was restored to the new client
|
||||
expect(mockNewClient.setHistory).toHaveBeenCalledWith(
|
||||
mockExistingHistory,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle case when no existing client is initialized', async () => {
|
||||
const config = new Config(baseParams);
|
||||
const authType = AuthType.USE_GEMINI;
|
||||
const mockContentConfig = {
|
||||
model: 'gemini-pro',
|
||||
apiKey: 'test-key',
|
||||
};
|
||||
|
||||
(createContentGeneratorConfig as Mock).mockReturnValue(mockContentConfig);
|
||||
|
||||
const mockNewClient = {
|
||||
isInitialized: vi.fn().mockReturnValue(true),
|
||||
getHistory: vi.fn().mockReturnValue([]),
|
||||
setHistory: vi.fn(),
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
// No existing client
|
||||
(config as unknown as { geminiClient: null }).geminiClient = null;
|
||||
(GeminiClient as Mock).mockImplementation(() => mockNewClient);
|
||||
|
||||
await config.refreshAuth(authType);
|
||||
|
||||
// Verify that new client was created and initialized
|
||||
expect(GeminiClient).toHaveBeenCalledWith(config);
|
||||
expect(mockNewClient.initialize).toHaveBeenCalledWith(mockContentConfig);
|
||||
|
||||
// Verify that setHistory was not called since there was no existing history
|
||||
expect(mockNewClient.setHistory).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -213,6 +314,23 @@ describe('Server Config (config.ts)', () => {
|
||||
expect(config.getFileFilteringRespectGitIgnore()).toBe(false);
|
||||
});
|
||||
|
||||
it('should initialize WorkspaceContext with includeDirectories', () => {
|
||||
const includeDirectories = ['/path/to/dir1', '/path/to/dir2'];
|
||||
const paramsWithIncludeDirs: ConfigParameters = {
|
||||
...baseParams,
|
||||
includeDirectories,
|
||||
};
|
||||
const config = new Config(paramsWithIncludeDirs);
|
||||
const workspaceContext = config.getWorkspaceContext();
|
||||
const directories = workspaceContext.getDirectories();
|
||||
|
||||
// Should include the target directory plus the included directories
|
||||
expect(directories).toHaveLength(3);
|
||||
expect(directories).toContain(path.resolve(baseParams.targetDir));
|
||||
expect(directories).toContain('/path/to/dir1');
|
||||
expect(directories).toContain('/path/to/dir2');
|
||||
});
|
||||
|
||||
it('Config constructor should set telemetry to true when provided as true', () => {
|
||||
const paramsWithTelemetry: ConfigParameters = {
|
||||
...baseParams,
|
||||
|
||||
@@ -47,9 +47,11 @@ import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js'
|
||||
import { shouldAttemptBrowserLaunch } from '../utils/browser.js';
|
||||
import { MCPOAuthConfig } from '../mcp/oauth-provider.js';
|
||||
import { IdeClient } from '../ide/ide-client.js';
|
||||
import type { Content } from '@google/genai';
|
||||
|
||||
// Re-export OAuth config type
|
||||
export type { MCPOAuthConfig };
|
||||
import { WorkspaceContext } from '../utils/workspaceContext.js';
|
||||
|
||||
export enum ApprovalMode {
|
||||
DEFAULT = 'default',
|
||||
@@ -81,6 +83,7 @@ export interface GeminiCLIExtension {
|
||||
name: string;
|
||||
version: string;
|
||||
isActive: boolean;
|
||||
path: string;
|
||||
}
|
||||
export interface FileFilteringOptions {
|
||||
respectGitIgnore: boolean;
|
||||
@@ -171,6 +174,7 @@ export interface ConfigParameters {
|
||||
proxy?: string;
|
||||
cwd: string;
|
||||
fileDiscoveryService?: FileDiscoveryService;
|
||||
includeDirectories?: string[];
|
||||
bugCommand?: BugCommandSettings;
|
||||
model: string;
|
||||
extensionContextFilePaths?: string[];
|
||||
@@ -183,6 +187,7 @@ export interface ConfigParameters {
|
||||
blockedMcpServers?: Array<{ name: string; extensionName: string }>;
|
||||
noBrowser?: boolean;
|
||||
summarizeToolOutput?: Record<string, SummarizeToolOutputSettings>;
|
||||
ideModeFeature?: boolean;
|
||||
ideMode?: boolean;
|
||||
ideClient?: IdeClient;
|
||||
enableOpenAILogging?: boolean;
|
||||
@@ -206,6 +211,7 @@ export class Config {
|
||||
private readonly embeddingModel: string;
|
||||
private readonly sandbox: SandboxConfig | undefined;
|
||||
private readonly targetDir: string;
|
||||
private workspaceContext: WorkspaceContext;
|
||||
private readonly debugMode: boolean;
|
||||
private readonly question: string | undefined;
|
||||
private readonly fullContext: boolean;
|
||||
@@ -237,14 +243,15 @@ export class Config {
|
||||
private readonly model: string;
|
||||
private readonly extensionContextFilePaths: string[];
|
||||
private readonly noBrowser: boolean;
|
||||
private readonly ideMode: boolean;
|
||||
private readonly ideClient: IdeClient | undefined;
|
||||
private readonly ideModeFeature: boolean;
|
||||
private ideMode: boolean;
|
||||
private ideClient: IdeClient;
|
||||
private inFallbackMode = false;
|
||||
private readonly systemPromptMappings?: Array<{
|
||||
baseUrls?: string[];
|
||||
modelNames?: string[];
|
||||
template?: string;
|
||||
}>;
|
||||
private modelSwitchedDuringSession: boolean = false;
|
||||
private readonly maxSessionTurns: number;
|
||||
private readonly sessionTokenLimit: number;
|
||||
private readonly maxFolderItems: number;
|
||||
@@ -272,6 +279,10 @@ export class Config {
|
||||
params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL;
|
||||
this.sandbox = params.sandbox;
|
||||
this.targetDir = path.resolve(params.targetDir);
|
||||
this.workspaceContext = new WorkspaceContext(
|
||||
this.targetDir,
|
||||
params.includeDirectories ?? [],
|
||||
);
|
||||
this.debugMode = params.debugMode;
|
||||
this.question = params.question;
|
||||
this.fullContext = params.fullContext ?? false;
|
||||
@@ -317,8 +328,11 @@ export class Config {
|
||||
this._blockedMcpServers = params.blockedMcpServers ?? [];
|
||||
this.noBrowser = params.noBrowser ?? false;
|
||||
this.summarizeToolOutput = params.summarizeToolOutput;
|
||||
this.ideModeFeature = params.ideModeFeature ?? false;
|
||||
this.ideMode = params.ideMode ?? false;
|
||||
this.ideClient = params.ideClient;
|
||||
this.ideClient =
|
||||
params.ideClient ??
|
||||
IdeClient.getInstance(this.ideMode && this.ideModeFeature);
|
||||
this.systemPromptMappings = params.systemPromptMappings;
|
||||
this.enableOpenAILogging = params.enableOpenAILogging ?? false;
|
||||
this.sampling_params = params.sampling_params;
|
||||
@@ -352,16 +366,33 @@ export class Config {
|
||||
}
|
||||
|
||||
async refreshAuth(authMethod: AuthType) {
|
||||
this.contentGeneratorConfig = createContentGeneratorConfig(
|
||||
// Save the current conversation history before creating a new client
|
||||
let existingHistory: Content[] = [];
|
||||
if (this.geminiClient && this.geminiClient.isInitialized()) {
|
||||
existingHistory = this.geminiClient.getHistory();
|
||||
}
|
||||
|
||||
// Create new content generator config
|
||||
const newContentGeneratorConfig = createContentGeneratorConfig(
|
||||
this,
|
||||
authMethod,
|
||||
);
|
||||
|
||||
this.geminiClient = new GeminiClient(this);
|
||||
await this.geminiClient.initialize(this.contentGeneratorConfig);
|
||||
// Create and initialize new client in local variable first
|
||||
const newGeminiClient = new GeminiClient(this);
|
||||
await newGeminiClient.initialize(newContentGeneratorConfig);
|
||||
|
||||
// Only assign to instance properties after successful initialization
|
||||
this.contentGeneratorConfig = newContentGeneratorConfig;
|
||||
this.geminiClient = newGeminiClient;
|
||||
|
||||
// Restore the conversation history to the new client
|
||||
if (existingHistory.length > 0) {
|
||||
this.geminiClient.setHistory(existingHistory);
|
||||
}
|
||||
|
||||
// Reset the session flag since we're explicitly changing auth and using default model
|
||||
this.modelSwitchedDuringSession = false;
|
||||
this.inFallbackMode = false;
|
||||
}
|
||||
|
||||
getSessionId(): string {
|
||||
@@ -379,19 +410,15 @@ export class Config {
|
||||
setModel(newModel: string): void {
|
||||
if (this.contentGeneratorConfig) {
|
||||
this.contentGeneratorConfig.model = newModel;
|
||||
this.modelSwitchedDuringSession = true;
|
||||
}
|
||||
}
|
||||
|
||||
isModelSwitchedDuringSession(): boolean {
|
||||
return this.modelSwitchedDuringSession;
|
||||
isInFallbackMode(): boolean {
|
||||
return this.inFallbackMode;
|
||||
}
|
||||
|
||||
resetModelToDefault(): void {
|
||||
if (this.contentGeneratorConfig) {
|
||||
this.contentGeneratorConfig.model = this.model; // Reset to the original default model
|
||||
this.modelSwitchedDuringSession = false;
|
||||
}
|
||||
setFallbackMode(active: boolean): void {
|
||||
this.inFallbackMode = active;
|
||||
}
|
||||
|
||||
setFlashFallbackHandler(handler: FlashFallbackHandler): void {
|
||||
@@ -426,6 +453,17 @@ export class Config {
|
||||
return this.sandbox;
|
||||
}
|
||||
|
||||
isRestrictiveSandbox(): boolean {
|
||||
const sandboxConfig = this.getSandbox();
|
||||
const seatbeltProfile = process.env.SEATBELT_PROFILE;
|
||||
return (
|
||||
!!sandboxConfig &&
|
||||
sandboxConfig.command === 'sandbox-exec' &&
|
||||
!!seatbeltProfile &&
|
||||
seatbeltProfile.startsWith('restrictive-')
|
||||
);
|
||||
}
|
||||
|
||||
getTargetDir(): string {
|
||||
return this.targetDir;
|
||||
}
|
||||
@@ -434,6 +472,10 @@ export class Config {
|
||||
return this.targetDir;
|
||||
}
|
||||
|
||||
getWorkspaceContext(): WorkspaceContext {
|
||||
return this.workspaceContext;
|
||||
}
|
||||
|
||||
getToolRegistry(): Promise<ToolRegistry> {
|
||||
return Promise.resolve(this.toolRegistry);
|
||||
}
|
||||
@@ -620,12 +662,28 @@ export class Config {
|
||||
return this.summarizeToolOutput;
|
||||
}
|
||||
|
||||
getIdeModeFeature(): boolean {
|
||||
return this.ideModeFeature;
|
||||
}
|
||||
|
||||
getIdeClient(): IdeClient {
|
||||
return this.ideClient;
|
||||
}
|
||||
|
||||
getIdeMode(): boolean {
|
||||
return this.ideMode;
|
||||
}
|
||||
|
||||
getIdeClient(): IdeClient | undefined {
|
||||
return this.ideClient;
|
||||
setIdeMode(value: boolean): void {
|
||||
this.ideMode = value;
|
||||
}
|
||||
|
||||
setIdeClientDisconnected(): void {
|
||||
this.ideClient.setDisconnected();
|
||||
}
|
||||
|
||||
setIdeClientConnected(): void {
|
||||
this.ideClient.reconnect(this.ideMode && this.ideModeFeature);
|
||||
}
|
||||
|
||||
getEnableOpenAILogging(): boolean {
|
||||
|
||||
@@ -4,20 +4,29 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { Config } from './config.js';
|
||||
import { DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_FLASH_MODEL } from './models.js';
|
||||
import { IdeClient } from '../ide/ide-client.js';
|
||||
import fs from 'node:fs';
|
||||
|
||||
vi.mock('node:fs');
|
||||
|
||||
describe('Flash Model Fallback Configuration', () => {
|
||||
let config: Config;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.statSync).mockReturnValue({
|
||||
isDirectory: () => true,
|
||||
} as fs.Stats);
|
||||
config = new Config({
|
||||
sessionId: 'test-session',
|
||||
targetDir: '/test',
|
||||
debugMode: false,
|
||||
cwd: '/test',
|
||||
model: DEFAULT_GEMINI_MODEL,
|
||||
ideClient: IdeClient.getInstance(false),
|
||||
});
|
||||
|
||||
// Initialize contentGeneratorConfig for testing
|
||||
@@ -29,26 +38,11 @@ describe('Flash Model Fallback Configuration', () => {
|
||||
};
|
||||
});
|
||||
|
||||
// These tests do not actually test fallback. isInFallbackMode() only returns true,
|
||||
// when setFallbackMode is marked as true. This is to decouple setting a model
|
||||
// with the fallback mechanism. This will be necessary we introduce more
|
||||
// intelligent model routing.
|
||||
describe('setModel', () => {
|
||||
it('should update the model and mark as switched during session', () => {
|
||||
expect(config.getModel()).toBe(DEFAULT_GEMINI_MODEL);
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(false);
|
||||
|
||||
config.setModel(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
|
||||
expect(config.getModel()).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle multiple model switches during session', () => {
|
||||
config.setModel(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(true);
|
||||
|
||||
config.setModel('gemini-1.5-pro');
|
||||
expect(config.getModel()).toBe('gemini-1.5-pro');
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(true);
|
||||
});
|
||||
|
||||
it('should only mark as switched if contentGeneratorConfig exists', () => {
|
||||
// Create config without initializing contentGeneratorConfig
|
||||
const newConfig = new Config({
|
||||
@@ -57,11 +51,12 @@ describe('Flash Model Fallback Configuration', () => {
|
||||
debugMode: false,
|
||||
cwd: '/test',
|
||||
model: DEFAULT_GEMINI_MODEL,
|
||||
ideClient: IdeClient.getInstance(false),
|
||||
});
|
||||
|
||||
// Should not crash when contentGeneratorConfig is undefined
|
||||
newConfig.setModel(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(newConfig.isModelSwitchedDuringSession()).toBe(false);
|
||||
expect(newConfig.isInFallbackMode()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,60 +75,32 @@ describe('Flash Model Fallback Configuration', () => {
|
||||
debugMode: false,
|
||||
cwd: '/test',
|
||||
model: 'custom-model',
|
||||
ideClient: IdeClient.getInstance(false),
|
||||
});
|
||||
|
||||
expect(newConfig.getModel()).toBe('custom-model');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isModelSwitchedDuringSession', () => {
|
||||
describe('isInFallbackMode', () => {
|
||||
it('should start as false for new session', () => {
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(false);
|
||||
expect(config.isInFallbackMode()).toBe(false);
|
||||
});
|
||||
|
||||
it('should remain false if no model switch occurs', () => {
|
||||
// Perform other operations that don't involve model switching
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(false);
|
||||
expect(config.isInFallbackMode()).toBe(false);
|
||||
});
|
||||
|
||||
it('should persist switched state throughout session', () => {
|
||||
config.setModel(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(true);
|
||||
// Setting state for fallback mode as is expected of clients
|
||||
config.setFallbackMode(true);
|
||||
expect(config.isInFallbackMode()).toBe(true);
|
||||
|
||||
// Should remain true even after getting model
|
||||
config.getModel();
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetModelToDefault', () => {
|
||||
it('should reset model to default and clear session switch flag', () => {
|
||||
// Switch to Flash first
|
||||
config.setModel(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(config.getModel()).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(true);
|
||||
|
||||
// Reset to default
|
||||
config.resetModelToDefault();
|
||||
|
||||
// Should be back to default with flag cleared
|
||||
expect(config.getModel()).toBe(DEFAULT_GEMINI_MODEL);
|
||||
expect(config.isModelSwitchedDuringSession()).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle case where contentGeneratorConfig is not initialized', () => {
|
||||
// Create config without initializing contentGeneratorConfig
|
||||
const newConfig = new Config({
|
||||
sessionId: 'test-session-2',
|
||||
targetDir: '/test',
|
||||
debugMode: false,
|
||||
cwd: '/test',
|
||||
model: DEFAULT_GEMINI_MODEL,
|
||||
});
|
||||
|
||||
// Should not crash when contentGeneratorConfig is undefined
|
||||
expect(() => newConfig.resetModelToDefault()).not.toThrow();
|
||||
expect(newConfig.isModelSwitchedDuringSession()).toBe(false);
|
||||
expect(config.isInFallbackMode()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,4 +6,6 @@
|
||||
|
||||
export const DEFAULT_GEMINI_MODEL = 'qwen3-coder-plus';
|
||||
export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-2.5-flash';
|
||||
export const DEFAULT_GEMINI_FLASH_LITE_MODEL = 'gemini-2.5-flash-lite';
|
||||
|
||||
export const DEFAULT_GEMINI_EMBEDDING_MODEL = 'gemini-embedding-001';
|
||||
|
||||
Reference in New Issue
Block a user