mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-13 04:48:09 -07:00
fix(core): address extension context stickiness and symlink path resolution
This commit addresses two bugs identified during review: 1. Cleared the sticky `activeExtensionContext` when the standard `/plan` command is executed, ensuring subsequent prompts correctly target the default global plan directory. 2. Fixed a path resolution regression in `Storage.getPlansDir()` by constructing the fallback ENOENT path directly against the real project root. This prevents `isSubpath` validation failures and potential traversal vulnerabilities when the project root is a symlink.
This commit is contained in:
@@ -23,6 +23,8 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
isInteractive: vi.fn(() => false),
|
||||
isInitialized: vi.fn(() => true),
|
||||
setTerminalBackground: vi.fn(),
|
||||
setActiveExtensionContext: vi.fn(),
|
||||
hasExtensionPlanDir: vi.fn(() => true),
|
||||
storage: {
|
||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/gemini-test'),
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
|
||||
@@ -3182,6 +3182,39 @@ describe('AppContainer State Management', () => {
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('clears activeExtensionContext when /plan is explicitly executed', async () => {
|
||||
const { checkPermissions } = await import(
|
||||
'./hooks/atCommandProcessor.js'
|
||||
);
|
||||
vi.mocked(checkPermissions).mockResolvedValue([]);
|
||||
|
||||
mockedUseSlashCommandProcessor.mockReturnValue({
|
||||
handleSlashCommand: vi.fn(),
|
||||
slashCommands: [{ name: 'plan', description: 'test', action: vi.fn() }],
|
||||
pendingHistoryItems: [],
|
||||
commandContext: {},
|
||||
shellConfirmationRequest: null,
|
||||
confirmationRequest: null,
|
||||
});
|
||||
|
||||
const spySetActiveExtensionContext = vi.spyOn(
|
||||
mockConfig,
|
||||
'setActiveExtensionContext',
|
||||
);
|
||||
|
||||
const { unmount } = await act(async () => renderAppContainer());
|
||||
|
||||
expect(capturedUIActions).toBeTruthy();
|
||||
|
||||
await act(async () =>
|
||||
capturedUIActions.handleFinalSubmit('/plan my task'),
|
||||
);
|
||||
|
||||
expect(spySetActiveExtensionContext).toHaveBeenCalledWith(undefined);
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Overflow Hint Handling', () => {
|
||||
|
||||
@@ -1313,9 +1313,13 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
slashCommands ?? [],
|
||||
);
|
||||
|
||||
if (parsedCommand.extensionContext && config) {
|
||||
if (config.hasExtensionPlanDir(parsedCommand.extensionContext)) {
|
||||
config.setActiveExtensionContext(parsedCommand.extensionContext);
|
||||
if (config) {
|
||||
if (parsedCommand.extensionContext) {
|
||||
if (config.hasExtensionPlanDir(parsedCommand.extensionContext)) {
|
||||
config.setActiveExtensionContext(parsedCommand.extensionContext);
|
||||
}
|
||||
} else if (parsedCommand.commandToExecute?.name === 'plan') {
|
||||
config.setActiveExtensionContext(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -358,6 +358,26 @@ describe('Storage – additional helpers', () => {
|
||||
expectedError:
|
||||
"Custom plans directory 'symlink-to-outside' resolves to '/outside/project/root', which is outside the project root '/tmp/project'.",
|
||||
},
|
||||
{
|
||||
name: 'non-existent plan dir in a symlinked project root',
|
||||
customDir: 'new-plans',
|
||||
setup: () => {
|
||||
vi.mocked(fs.realpathSync).mockImplementation((p: fs.PathLike) => {
|
||||
const pStr = p.toString();
|
||||
if (pStr === projectRoot) {
|
||||
return '/private/tmp/project';
|
||||
}
|
||||
if (pStr.includes('new-plans')) {
|
||||
const err = new Error('ENOENT') as NodeJS.ErrnoException;
|
||||
err.code = 'ENOENT';
|
||||
throw err;
|
||||
}
|
||||
return pStr;
|
||||
});
|
||||
return () => vi.mocked(fs.realpathSync).mockRestore();
|
||||
},
|
||||
expected: path.resolve(projectRoot, 'new-plans'),
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach(({ name, customDir, expected, expectedError, setup }) => {
|
||||
|
||||
@@ -333,6 +333,8 @@ export class Storage {
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
// Construct the fallback path safely against the real project root
|
||||
realResolvedPath = path.resolve(realProjectRoot, customDir);
|
||||
}
|
||||
|
||||
if (!isSubpath(realProjectRoot, realResolvedPath)) {
|
||||
|
||||
Reference in New Issue
Block a user