feat(extensions): implement plan directory merging logic and add tests

This commit is contained in:
Mahima Shanware
2026-02-25 21:07:28 +00:00
parent 2c82d18c9f
commit 9be5347b3e
2 changed files with 135 additions and 1 deletions
+116
View File
@@ -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',
);
});
});
+19 -1
View File
@@ -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,