mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-30 23:14:32 -07:00
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:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user