fix(core): silently skip GEMINI.md paths that are directories (EISDIR) (#25662)

Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
This commit is contained in:
Martin
2026-05-01 05:06:56 +08:00
committed by GitHub
parent d494195602
commit 80e3bb9689
2 changed files with 118 additions and 11 deletions
@@ -15,6 +15,7 @@ import {
getEnvironmentMemoryPaths,
loadJitSubdirectoryMemory,
refreshServerHierarchicalMemory,
readGeminiMdFiles,
} from './memoryDiscovery.js';
import {
setGeminiMdFilename,
@@ -682,6 +683,97 @@ included directory memory
expect(childOccurrences).toBe(1);
});
describe('EISDIR handling for GEMINI.md as a directory', () => {
it('readGeminiMdFiles returns null content (without throwing) when path is a directory', async () => {
const dirAsFilePath = await createEmptyDir(
path.join(cwd, DEFAULT_CONTEXT_FILENAME),
);
const results = await readGeminiMdFiles([dirAsFilePath]);
expect(results).toHaveLength(1);
expect(results[0].filePath).toBe(dirAsFilePath);
expect(results[0].content).toBeNull();
});
it('loadServerHierarchicalMemory ignores a GEMINI.md directory and returns empty memory', async () => {
// Create a directory named GEMINI.md where a regular file would be expected.
await createEmptyDir(path.join(cwd, DEFAULT_CONTEXT_FILENAME));
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
// EISDIR is silently skipped, so memory is empty (no readable file
// contents) and no exception propagates.
expect(result.memoryContent).toBe('');
});
it('falls back to a real GEMINI.md file at a higher level when a directory shadows the same name lower in the tree', async () => {
// Lower in the tree (cwd): a directory named GEMINI.md (invalid).
await createEmptyDir(path.join(cwd, DEFAULT_CONTEXT_FILENAME));
// Higher in the tree (projectRoot): a real GEMINI.md file (valid).
const projectContextFile = await createTestFile(
path.join(projectRoot, DEFAULT_CONTEXT_FILENAME),
'Project root memory content',
);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
// The directory at cwd is silently skipped; the actual file at
// projectRoot is still discovered and loaded normally.
expect(result.memoryContent).toContain('Project root memory content');
expect(result.filePaths).toContain(projectContextFile);
});
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'));
const symlinkPath = path.join(cwd, DEFAULT_CONTEXT_FILENAME);
try {
await fsPromises.symlink(realDir, symlinkPath, 'dir');
} catch (err) {
// Symlink creation may be unsupported on some Windows setups (no
// SeCreateSymbolicLinkPrivilege). Skip the test there rather than fail.
if (
err instanceof Error &&
(err as NodeJS.ErrnoException).code === 'EPERM'
) {
return;
}
throw err;
}
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
// A symlink resolving to a directory triggers EISDIR on read in the
// same way a plain directory does and must be skipped silently.
expect(result.memoryContent).toBe('');
});
});
describe('getGlobalMemoryPaths', () => {
it('should find global memory file if it exists', async () => {
const globalMemoryFile = await createTestFile(
+26 -11
View File
@@ -406,19 +406,34 @@ export async function readGeminiMdFiles(
return { filePath, content: processedResult.content };
} catch (error: unknown) {
const isTestEnv =
process.env['NODE_ENV'] === 'test' || process.env['VITEST'];
if (!isTestEnv) {
const message =
error instanceof Error ? error.message : String(error);
logger.warn(
`Warning: Could not read ${getAllGeminiMdFilenames()} file at ${filePath}. Error: ${message}`,
const isEISDIR =
error instanceof Error &&
(error as NodeJS.ErrnoException).code === 'EISDIR';
if (isEISDIR) {
// A directory exists where a GEMINI.md file is expected.
// This is valid in some project structures (e.g. a folder named
// GEMINI.md held for organisational purposes) — skip it silently
// instead of surfacing a confusing warning to the user.
debugLogger.debug(
'[DEBUG] [MemoryDiscovery] Skipping directory at GEMINI.md path:',
filePath,
);
} else {
const isTestEnv =
process.env['NODE_ENV'] === 'test' || process.env['VITEST'];
if (!isTestEnv) {
const message =
error instanceof Error ? error.message : String(error);
logger.warn(
`Warning: Could not read ${getAllGeminiMdFilenames()} file at ${filePath}. Error: ${message}`,
);
}
debugLogger.debug(
'[DEBUG] [MemoryDiscovery] Failed to read:',
filePath,
);
}
debugLogger.debug(
'[DEBUG] [MemoryDiscovery] Failed to read:',
filePath,
);
return { filePath, content: null }; // Still include it with null content
}
},