diff --git a/packages/core/src/utils/memoryDiscovery.test.ts b/packages/core/src/utils/memoryDiscovery.test.ts index c1e38e5c95..aac56e54f1 100644 --- a/packages/core/src/utils/memoryDiscovery.test.ts +++ b/packages/core/src/utils/memoryDiscovery.test.ts @@ -46,6 +46,15 @@ import { Config, type GeminiCLIExtension } from '../config/config.js'; import { Storage } from '../config/storage.js'; import { SimpleExtensionLoader } from './extensionLoader.js'; import { CoreEvent, coreEvents } from './events.js'; +import * as fs from 'node:fs'; + +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + realpathSync: vi.fn(actual.realpathSync), + }; +}); vi.mock('os', async (importOriginal) => { const actualOs = await importOriginal(); @@ -743,6 +752,40 @@ included directory memory expect(result.filePaths).toContain(projectContextFile); }); + it('should not crash when fs.realpathSync throws EISDIR on virtual drive roots', async () => { + // Mock realpathSync to throw EISDIR for a specific path, simulating + // the Windows virtual drive issue (#25216). + vi.mocked(fs.realpathSync).mockImplementation((p: fs.PathLike) => { + const pathStr = p.toString(); + if (pathStr.includes('virtual-drive')) { + const error = new Error( + "EISDIR: illegal operation on a directory, realpath 'A:\\a'", + ); + + (error as NodeJS.ErrnoException).code = 'EISDIR'; + throw error; + } + // For other paths, we need to return something sensible. + // Since it's a mock, we can just return the path string itself. + return pathStr; + }); + + const virtualDriveCwd = path.join(testRootDir, 'virtual-drive'); + await fsPromises.mkdir(virtualDriveCwd, { recursive: true }); + + // This should now succeed instead of throwing + const result = await loadServerHierarchicalMemory( + virtualDriveCwd, + [], + new FileDiscoveryService(projectRoot), + new SimpleExtensionLoader([]), + DEFAULT_FOLDER_TRUST, + ); + + expect(result).toBeDefined(); + expect(result.fileCount).toBe(0); + }); + it('silently skips a GEMINI.md symlink that points to a directory', async () => { // Create a real directory elsewhere and symlink GEMINI.md to it. const realDir = await createEmptyDir(path.join(cwd, '.geminimd-target')); diff --git a/packages/core/src/utils/memoryDiscovery.ts b/packages/core/src/utils/memoryDiscovery.ts index c546ca5b46..811ae08818 100644 --- a/packages/core/src/utils/memoryDiscovery.ts +++ b/packages/core/src/utils/memoryDiscovery.ts @@ -24,6 +24,7 @@ import { isSubpath, normalizePath, toAbsolutePath, + resolveToRealPath, } from './paths.js'; import type { ExtensionLoader } from './extensionLoader.js'; import { debugLogger } from './debugLogger.js'; @@ -702,10 +703,8 @@ export async function loadServerHierarchicalMemory( boundaryMarkers: readonly string[] = ['.git'], ): Promise { // FIX: Use real, canonical paths for a reliable comparison to handle symlinks. - const realCwd = normalizePath( - await fs.realpath(path.resolve(currentWorkingDirectory)), - ); - const realHome = normalizePath(await fs.realpath(path.resolve(homedir()))); + const realCwd = normalizePath(resolveToRealPath(currentWorkingDirectory)); + const realHome = normalizePath(resolveToRealPath(homedir())); const isHomeDirectory = realCwd === realHome; // If it is the home directory, pass an empty string to the core memory