mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-13 12:57:12 -07:00
feat(extensions): implement plan directory merging logic and add tests
This commit is contained in:
@@ -19,6 +19,8 @@ import {
|
||||
debugLogger,
|
||||
ApprovalMode,
|
||||
type MCPServerConfig,
|
||||
Storage,
|
||||
type GeminiCLIExtension,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
|
||||
import {
|
||||
@@ -3465,3 +3467,117 @@ describe('loadCliConfig mcpEnabled', () => {
|
||||
expect(config.getBlockedMcpServers()).toEqual(['serverB']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig extension plan settings', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
||||
// Mock getProjectIdentifier to avoid "Storage must be initialized before use" error
|
||||
// when accessing plansDir without a custom directory set.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
vi.spyOn(Storage.prototype as any, 'getProjectIdentifier').mockReturnValue(
|
||||
'test-project',
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should use plan directory from active extension when user has not specified one', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: { plan: true },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
|
||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
|
||||
{
|
||||
name: 'ext-plan',
|
||||
isActive: true,
|
||||
plan: { directory: 'ext-plans-dir' },
|
||||
} as unknown as GeminiCLIExtension,
|
||||
]);
|
||||
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.storage.getPlansDir()).toContain('ext-plans-dir');
|
||||
});
|
||||
|
||||
it('should prefer user-specified plan directory over extension-provided one', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: { plan: true },
|
||||
general: {
|
||||
plan: { directory: 'user-plans-dir' },
|
||||
},
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
|
||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
|
||||
{
|
||||
name: 'ext-plan',
|
||||
isActive: true,
|
||||
plan: { directory: 'ext-plans-dir' },
|
||||
} as unknown as GeminiCLIExtension,
|
||||
]);
|
||||
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.storage.getPlansDir()).toContain('user-plans-dir');
|
||||
expect(config.storage.getPlansDir()).not.toContain('ext-plans-dir');
|
||||
});
|
||||
|
||||
it('should use the first active extension plan directory and log a warning if multiple are found', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: { plan: true },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
|
||||
const warnSpy = vi.spyOn(debugLogger, 'warn');
|
||||
|
||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
|
||||
{
|
||||
name: 'ext-plan-1',
|
||||
isActive: true,
|
||||
plan: { directory: 'ext-plans-dir-1' },
|
||||
} as unknown as GeminiCLIExtension,
|
||||
{
|
||||
name: 'ext-plan-2',
|
||||
isActive: true,
|
||||
plan: { directory: 'ext-plans-dir-2' },
|
||||
} as unknown as GeminiCLIExtension,
|
||||
]);
|
||||
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.storage.getPlansDir()).toContain('ext-plans-dir-1');
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Multiple active extensions define a plan directory',
|
||||
),
|
||||
);
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('ext-plan-1'));
|
||||
});
|
||||
|
||||
it('should ignore plan directory from inactive extensions', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: { plan: true },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
|
||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
|
||||
{
|
||||
name: 'ext-plan-inactive',
|
||||
isActive: false,
|
||||
plan: { directory: 'ext-plans-dir-inactive' },
|
||||
} as unknown as GeminiCLIExtension,
|
||||
]);
|
||||
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.storage.getPlansDir()).not.toContain(
|
||||
'ext-plans-dir-inactive',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
getVersion,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
type HierarchicalMemory,
|
||||
type PlanSettings,
|
||||
coreEvents,
|
||||
GEMINI_MODEL_ALIAS_AUTO,
|
||||
getAdminErrorMessage,
|
||||
@@ -511,6 +512,21 @@ export async function loadCliConfig(
|
||||
});
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
// Filter active extensions that define a plan directory
|
||||
const activeExtensionsWithPlan = extensionManager
|
||||
.getExtensions()
|
||||
.filter((e) => e.isActive && e.plan?.directory);
|
||||
|
||||
let extensionPlanSettings: PlanSettings | undefined;
|
||||
if (activeExtensionsWithPlan.length > 0) {
|
||||
if (activeExtensionsWithPlan.length > 1) {
|
||||
debugLogger.warn(
|
||||
`Multiple active extensions define a plan directory. Using plan directory from extension: "${activeExtensionsWithPlan[0].name}"`,
|
||||
);
|
||||
}
|
||||
extensionPlanSettings = activeExtensionsWithPlan[0].plan;
|
||||
}
|
||||
|
||||
const experimentalJitContext = settings.experimental?.jitContext ?? false;
|
||||
|
||||
let memoryContent: string | HierarchicalMemory = '';
|
||||
@@ -827,7 +843,9 @@ export async function loadCliConfig(
|
||||
enableAgents: settings.experimental?.enableAgents,
|
||||
plan: settings.experimental?.plan,
|
||||
directWebFetch: settings.experimental?.directWebFetch,
|
||||
planSettings: settings.general?.plan,
|
||||
planSettings: settings.general?.plan?.directory
|
||||
? settings.general.plan
|
||||
: (extensionPlanSettings ?? settings.general?.plan),
|
||||
enableEventDrivenScheduler: true,
|
||||
skillsSupport: settings.skills?.enabled ?? true,
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
|
||||
Reference in New Issue
Block a user