mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 09:17:53 +00:00
feat: Multi-Directory Workspace Support (part 3: configuration in settings.json) (#5354)
Co-authored-by: Allen Hutchison <adh@google.com>
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { loadCliConfig, parseArguments } from './config.js';
|
||||
import { Settings } from './settings.js';
|
||||
import { Extension } from './extension.js';
|
||||
@@ -44,7 +46,7 @@ vi.mock('@google/gemini-cli-core', async () => {
|
||||
},
|
||||
loadEnvironment: vi.fn(),
|
||||
loadServerHierarchicalMemory: vi.fn(
|
||||
(cwd, debug, fileService, extensionPaths, _maxDirs) =>
|
||||
(cwd, dirs, debug, fileService, extensionPaths, _maxDirs) =>
|
||||
Promise.resolve({
|
||||
memoryContent: extensionPaths?.join(',') || '',
|
||||
fileCount: extensionPaths?.length || 0,
|
||||
@@ -487,6 +489,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
||||
await loadCliConfig(settings, extensions, 'session-id', argv);
|
||||
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
[],
|
||||
false,
|
||||
expect.any(Object),
|
||||
[
|
||||
@@ -1015,3 +1018,85 @@ describe('loadCliConfig ideModeFeature', () => {
|
||||
expect(config.getIdeModeFeature()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
vi.mock('fs', async () => {
|
||||
const actualFs = await vi.importActual<typeof fs>('fs');
|
||||
const MOCK_CWD1 = process.cwd();
|
||||
const MOCK_CWD2 = path.resolve(path.sep, 'home', 'user', 'project');
|
||||
|
||||
const mockPaths = new Set([
|
||||
MOCK_CWD1,
|
||||
MOCK_CWD2,
|
||||
path.resolve(path.sep, 'cli', 'path1'),
|
||||
path.resolve(path.sep, 'settings', 'path1'),
|
||||
path.join(os.homedir(), 'settings', 'path2'),
|
||||
path.join(MOCK_CWD2, 'cli', 'path2'),
|
||||
path.join(MOCK_CWD2, 'settings', 'path3'),
|
||||
]);
|
||||
|
||||
return {
|
||||
...actualFs,
|
||||
existsSync: vi.fn((p) => mockPaths.has(p.toString())),
|
||||
statSync: vi.fn((p) => {
|
||||
if (mockPaths.has(p.toString())) {
|
||||
return { isDirectory: () => true };
|
||||
}
|
||||
// Fallback for other paths if needed, though the test should be specific.
|
||||
return actualFs.statSync(p);
|
||||
}),
|
||||
realpathSync: vi.fn((p) => p),
|
||||
};
|
||||
});
|
||||
|
||||
describe('loadCliConfig with includeDirectories', () => {
|
||||
const originalArgv = process.argv;
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||
process.env.GEMINI_API_KEY = 'test-api-key';
|
||||
vi.spyOn(process, 'cwd').mockReturnValue(
|
||||
path.resolve(path.sep, 'home', 'user', 'project'),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.argv = originalArgv;
|
||||
process.env = originalEnv;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should combine and resolve paths from settings and CLI arguments', async () => {
|
||||
const mockCwd = path.resolve(path.sep, 'home', 'user', 'project');
|
||||
process.argv = [
|
||||
'node',
|
||||
'script.js',
|
||||
'--include-directories',
|
||||
`${path.resolve(path.sep, 'cli', 'path1')},${path.join(mockCwd, 'cli', 'path2')}`,
|
||||
];
|
||||
const argv = await parseArguments();
|
||||
const settings: Settings = {
|
||||
includeDirectories: [
|
||||
path.resolve(path.sep, 'settings', 'path1'),
|
||||
path.join(os.homedir(), 'settings', 'path2'),
|
||||
path.join(mockCwd, 'settings', 'path3'),
|
||||
],
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
const expected = [
|
||||
mockCwd,
|
||||
path.resolve(path.sep, 'cli', 'path1'),
|
||||
path.join(mockCwd, 'cli', 'path2'),
|
||||
path.resolve(path.sep, 'settings', 'path1'),
|
||||
path.join(os.homedir(), 'settings', 'path2'),
|
||||
path.join(mockCwd, 'settings', 'path3'),
|
||||
];
|
||||
expect(config.getWorkspaceContext().getDirectories()).toEqual(
|
||||
expect.arrayContaining(expected),
|
||||
);
|
||||
expect(config.getWorkspaceContext().getDirectories()).toHaveLength(
|
||||
expected.length,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ import { Settings } from './settings.js';
|
||||
import { Extension, annotateActiveExtensions } from './extension.js';
|
||||
import { getCliVersion } from '../utils/version.js';
|
||||
import { loadSandboxConfig } from './sandboxConfig.js';
|
||||
import { resolvePath } from '../utils/resolvePath.js';
|
||||
|
||||
// Simple console logger for now - replace with actual logger if available
|
||||
const logger = {
|
||||
@@ -65,6 +66,7 @@ export interface CliArgs {
|
||||
ideModeFeature: boolean | undefined;
|
||||
proxy: string | undefined;
|
||||
includeDirectories: string[] | undefined;
|
||||
loadMemoryFromIncludeDirectories: boolean | undefined;
|
||||
}
|
||||
|
||||
export async function parseArguments(): Promise<CliArgs> {
|
||||
@@ -212,6 +214,12 @@ export async function parseArguments(): Promise<CliArgs> {
|
||||
// Handle comma-separated values
|
||||
dirs.flatMap((dir) => dir.split(',').map((d) => d.trim())),
|
||||
})
|
||||
.option('load-memory-from-include-directories', {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'If true, when refreshing memory, GEMINI.md files should be loaded from all directories that are added. If false, GEMINI.md files should only be loaded from the primary working directory.',
|
||||
default: false,
|
||||
})
|
||||
.version(await getCliVersion()) // This will enable the --version flag based on package.json
|
||||
.alias('v', 'version')
|
||||
.help()
|
||||
@@ -239,6 +247,7 @@ export async function parseArguments(): Promise<CliArgs> {
|
||||
// TODO: Consider if App.tsx should get memory via a server call or if Config should refresh itself.
|
||||
export async function loadHierarchicalGeminiMemory(
|
||||
currentWorkingDirectory: string,
|
||||
includeDirectoriesToReadGemini: readonly string[] = [],
|
||||
debugMode: boolean,
|
||||
fileService: FileDiscoveryService,
|
||||
settings: Settings,
|
||||
@@ -264,6 +273,7 @@ export async function loadHierarchicalGeminiMemory(
|
||||
// Directly call the server function with the corrected path.
|
||||
return loadServerHierarchicalMemory(
|
||||
effectiveCwd,
|
||||
includeDirectoriesToReadGemini,
|
||||
debugMode,
|
||||
fileService,
|
||||
extensionContextFilePaths,
|
||||
@@ -325,9 +335,14 @@ export async function loadCliConfig(
|
||||
...settings.fileFiltering,
|
||||
};
|
||||
|
||||
const includeDirectories = (settings.includeDirectories || [])
|
||||
.map(resolvePath)
|
||||
.concat((argv.includeDirectories || []).map(resolvePath));
|
||||
|
||||
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
|
||||
const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory(
|
||||
process.cwd(),
|
||||
settings.loadMemoryFromIncludeDirectories ? includeDirectories : [],
|
||||
debugMode,
|
||||
fileService,
|
||||
settings,
|
||||
@@ -393,7 +408,11 @@ export async function loadCliConfig(
|
||||
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
sandbox: sandboxConfig,
|
||||
targetDir: process.cwd(),
|
||||
includeDirectories: argv.includeDirectories,
|
||||
includeDirectories,
|
||||
loadMemoryFromIncludeDirectories:
|
||||
argv.loadMemoryFromIncludeDirectories ||
|
||||
settings.loadMemoryFromIncludeDirectories ||
|
||||
false,
|
||||
debugMode,
|
||||
question: argv.promptInteractive || argv.prompt || '',
|
||||
fullContext: argv.allFiles || argv.all_files || false,
|
||||
|
||||
@@ -112,6 +112,7 @@ describe('Settings Loading and Merging', () => {
|
||||
expect(settings.merged).toEqual({
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
includeDirectories: [],
|
||||
});
|
||||
expect(settings.errors.length).toBe(0);
|
||||
});
|
||||
@@ -145,6 +146,7 @@ describe('Settings Loading and Merging', () => {
|
||||
...systemSettingsContent,
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
includeDirectories: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -178,6 +180,7 @@ describe('Settings Loading and Merging', () => {
|
||||
...userSettingsContent,
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
includeDirectories: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -209,6 +212,7 @@ describe('Settings Loading and Merging', () => {
|
||||
...workspaceSettingsContent,
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
includeDirectories: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -246,6 +250,7 @@ describe('Settings Loading and Merging', () => {
|
||||
contextFileName: 'WORKSPACE_CONTEXT.md',
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
includeDirectories: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -295,6 +300,7 @@ describe('Settings Loading and Merging', () => {
|
||||
allowMCPServers: ['server1', 'server2'],
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
includeDirectories: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -616,6 +622,40 @@ describe('Settings Loading and Merging', () => {
|
||||
expect(settings.merged.mcpServers).toEqual({});
|
||||
});
|
||||
|
||||
it('should merge includeDirectories from all scopes', () => {
|
||||
(mockFsExistsSync as Mock).mockReturnValue(true);
|
||||
const systemSettingsContent = {
|
||||
includeDirectories: ['/system/dir'],
|
||||
};
|
||||
const userSettingsContent = {
|
||||
includeDirectories: ['/user/dir1', '/user/dir2'],
|
||||
};
|
||||
const workspaceSettingsContent = {
|
||||
includeDirectories: ['/workspace/dir'],
|
||||
};
|
||||
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === getSystemSettingsPath())
|
||||
return JSON.stringify(systemSettingsContent);
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(settings.merged.includeDirectories).toEqual([
|
||||
'/system/dir',
|
||||
'/user/dir1',
|
||||
'/user/dir2',
|
||||
'/workspace/dir',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle JSON parsing errors gracefully', () => {
|
||||
(mockFsExistsSync as Mock).mockReturnValue(true); // Both files "exist"
|
||||
const invalidJsonContent = 'invalid json';
|
||||
@@ -654,6 +694,7 @@ describe('Settings Loading and Merging', () => {
|
||||
expect(settings.merged).toEqual({
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
includeDirectories: [],
|
||||
});
|
||||
|
||||
// Check that error objects are populated in settings.errors
|
||||
@@ -1090,6 +1131,7 @@ describe('Settings Loading and Merging', () => {
|
||||
...systemSettingsContent,
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
includeDirectories: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -126,6 +126,10 @@ export interface Settings {
|
||||
// Environment variables to exclude from project .env files
|
||||
excludedProjectEnvVars?: string[];
|
||||
dnsResolutionOrder?: DnsResolutionOrder;
|
||||
|
||||
includeDirectories?: string[];
|
||||
|
||||
loadMemoryFromIncludeDirectories?: boolean;
|
||||
}
|
||||
|
||||
export interface SettingsError {
|
||||
@@ -181,6 +185,11 @@ export class LoadedSettings {
|
||||
...(workspace.mcpServers || {}),
|
||||
...(system.mcpServers || {}),
|
||||
},
|
||||
includeDirectories: [
|
||||
...(system.includeDirectories || []),
|
||||
...(user.includeDirectories || []),
|
||||
...(workspace.includeDirectories || []),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user