mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-28 05:55:17 -07:00
feat(core): Render memory hierarchically in context. (#18350)
This commit is contained in:
@@ -10,8 +10,9 @@ import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import {
|
||||
loadServerHierarchicalMemory,
|
||||
loadGlobalMemory,
|
||||
loadEnvironmentMemory,
|
||||
getGlobalMemoryPaths,
|
||||
getExtensionMemoryPaths,
|
||||
getEnvironmentMemoryPaths,
|
||||
loadJitSubdirectoryMemory,
|
||||
refreshServerHierarchicalMemory,
|
||||
} from './memoryDiscovery.js';
|
||||
@@ -19,8 +20,22 @@ import {
|
||||
setGeminiMdFilename,
|
||||
DEFAULT_CONTEXT_FILENAME,
|
||||
} from '../tools/memoryTool.js';
|
||||
import { flattenMemory } from '../config/memory.js';
|
||||
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||
import { GEMINI_DIR } from './paths.js';
|
||||
import { GEMINI_DIR, normalizePath } from './paths.js';
|
||||
import type { HierarchicalMemory } from '../config/memory.js';
|
||||
|
||||
function flattenResult(result: {
|
||||
memoryContent: HierarchicalMemory;
|
||||
fileCount: number;
|
||||
filePaths: string[];
|
||||
}) {
|
||||
return {
|
||||
...result,
|
||||
memoryContent: flattenMemory(result.memoryContent),
|
||||
filePaths: result.filePaths.map((p) => normalizePath(p)),
|
||||
};
|
||||
}
|
||||
import { Config, type GeminiCLIExtension } from '../config/config.js';
|
||||
import { Storage } from '../config/storage.js';
|
||||
import { SimpleExtensionLoader } from './extensionLoader.js';
|
||||
@@ -39,6 +54,10 @@ vi.mock('../utils/paths.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../utils/paths.js')>();
|
||||
return {
|
||||
...actual,
|
||||
normalizePath: (p: string) => {
|
||||
const resolved = path.resolve(p);
|
||||
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
||||
},
|
||||
homedir: vi.fn(),
|
||||
};
|
||||
});
|
||||
@@ -54,18 +73,20 @@ describe('memoryDiscovery', () => {
|
||||
|
||||
async function createEmptyDir(fullPath: string) {
|
||||
await fsPromises.mkdir(fullPath, { recursive: true });
|
||||
return fullPath;
|
||||
return normalizePath(fullPath);
|
||||
}
|
||||
|
||||
async function createTestFile(fullPath: string, fileContents: string) {
|
||||
await fsPromises.mkdir(path.dirname(fullPath), { recursive: true });
|
||||
await fsPromises.writeFile(fullPath, fileContents);
|
||||
return path.resolve(testRootDir, fullPath);
|
||||
return normalizePath(path.resolve(testRootDir, fullPath));
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
testRootDir = await fsPromises.mkdtemp(
|
||||
path.join(os.tmpdir(), 'folder-structure-test-'),
|
||||
testRootDir = normalizePath(
|
||||
await fsPromises.mkdtemp(
|
||||
path.join(os.tmpdir(), 'folder-structure-test-'),
|
||||
),
|
||||
);
|
||||
|
||||
vi.resetAllMocks();
|
||||
@@ -80,6 +101,9 @@ describe('memoryDiscovery', () => {
|
||||
vi.mocked(pathsHomedir).mockReturnValue(homedir);
|
||||
});
|
||||
|
||||
const normMarker = (p: string) =>
|
||||
process.platform === 'win32' ? p.toLowerCase() : p;
|
||||
|
||||
afterEach(async () => {
|
||||
vi.unstubAllEnvs();
|
||||
// Some tests set this to a different value.
|
||||
@@ -104,13 +128,15 @@ describe('memoryDiscovery', () => {
|
||||
path.join(cwd, DEFAULT_CONTEXT_FILENAME),
|
||||
'Src directory memory',
|
||||
);
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
false, // untrusted
|
||||
const result = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
false, // untrusted
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -130,9 +156,16 @@ describe('memoryDiscovery', () => {
|
||||
'Src directory memory', // Untrusted
|
||||
);
|
||||
|
||||
const filepath = path.join(homedir, GEMINI_DIR, DEFAULT_CONTEXT_FILENAME);
|
||||
await createTestFile(filepath, 'default context content'); // In user home dir (outside untrusted space).
|
||||
const { fileCount, memoryContent, filePaths } =
|
||||
const filepathInput = path.join(
|
||||
homedir,
|
||||
GEMINI_DIR,
|
||||
DEFAULT_CONTEXT_FILENAME,
|
||||
);
|
||||
const filepath = await createTestFile(
|
||||
filepathInput,
|
||||
'default context content',
|
||||
); // In user home dir (outside untrusted space).
|
||||
const { fileCount, memoryContent, filePaths } = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
@@ -140,7 +173,8 @@ describe('memoryDiscovery', () => {
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
false, // untrusted
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
expect(fileCount).toEqual(1);
|
||||
expect(memoryContent).toContain(path.relative(cwd, filepath).toString());
|
||||
@@ -149,13 +183,15 @@ describe('memoryDiscovery', () => {
|
||||
});
|
||||
|
||||
it('should return empty memory and count if no context files are found', async () => {
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
const result = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -171,17 +207,23 @@ describe('memoryDiscovery', () => {
|
||||
'default context content',
|
||||
);
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
const result = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, defaultContextFile)} ---
|
||||
expect({
|
||||
...result,
|
||||
memoryContent: flattenMemory(result.memoryContent),
|
||||
}).toEqual({
|
||||
memoryContent: `--- Global ---
|
||||
--- Context from: ${path.relative(cwd, defaultContextFile)} ---
|
||||
default context content
|
||||
--- End of Context from: ${path.relative(cwd, defaultContextFile)} ---`,
|
||||
fileCount: 1,
|
||||
@@ -198,19 +240,22 @@ default context content
|
||||
'custom context content',
|
||||
);
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
const result = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, customContextFile)} ---
|
||||
memoryContent: `--- Global ---
|
||||
--- Context from: ${normMarker(path.relative(cwd, customContextFile))} ---
|
||||
custom context content
|
||||
--- End of Context from: ${path.relative(cwd, customContextFile)} ---`,
|
||||
--- End of Context from: ${normMarker(path.relative(cwd, customContextFile))} ---`,
|
||||
fileCount: 1,
|
||||
filePaths: [customContextFile],
|
||||
});
|
||||
@@ -229,23 +274,26 @@ custom context content
|
||||
'cwd context content',
|
||||
);
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
const result = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, projectContextFile)} ---
|
||||
memoryContent: `--- Project ---
|
||||
--- Context from: ${normMarker(path.relative(cwd, projectContextFile))} ---
|
||||
project context content
|
||||
--- End of Context from: ${path.relative(cwd, projectContextFile)} ---
|
||||
--- End of Context from: ${normMarker(path.relative(cwd, projectContextFile))} ---
|
||||
|
||||
--- Context from: ${path.relative(cwd, cwdContextFile)} ---
|
||||
--- Context from: ${normMarker(path.relative(cwd, cwdContextFile))} ---
|
||||
cwd context content
|
||||
--- End of Context from: ${path.relative(cwd, cwdContextFile)} ---`,
|
||||
--- End of Context from: ${normMarker(path.relative(cwd, cwdContextFile))} ---`,
|
||||
fileCount: 2,
|
||||
filePaths: [projectContextFile, cwdContextFile],
|
||||
});
|
||||
@@ -264,23 +312,26 @@ cwd context content
|
||||
'CWD custom memory',
|
||||
);
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
const result = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${customFilename} ---
|
||||
memoryContent: `--- Project ---
|
||||
--- Context from: ${normMarker(customFilename)} ---
|
||||
CWD custom memory
|
||||
--- End of Context from: ${customFilename} ---
|
||||
--- End of Context from: ${normMarker(customFilename)} ---
|
||||
|
||||
--- Context from: ${path.join('subdir', customFilename)} ---
|
||||
--- Context from: ${normMarker(path.join('subdir', customFilename))} ---
|
||||
Subdir custom memory
|
||||
--- End of Context from: ${path.join('subdir', customFilename)} ---`,
|
||||
--- End of Context from: ${normMarker(path.join('subdir', customFilename))} ---`,
|
||||
fileCount: 2,
|
||||
filePaths: [cwdCustomFile, subdirCustomFile],
|
||||
});
|
||||
@@ -296,23 +347,26 @@ Subdir custom memory
|
||||
'Src directory memory',
|
||||
);
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
const result = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, projectRootGeminiFile)} ---
|
||||
memoryContent: `--- Project ---
|
||||
--- Context from: ${normMarker(path.relative(cwd, projectRootGeminiFile))} ---
|
||||
Project root memory
|
||||
--- End of Context from: ${path.relative(cwd, projectRootGeminiFile)} ---
|
||||
--- End of Context from: ${normMarker(path.relative(cwd, projectRootGeminiFile))} ---
|
||||
|
||||
--- Context from: ${path.relative(cwd, srcGeminiFile)} ---
|
||||
--- Context from: ${normMarker(path.relative(cwd, srcGeminiFile))} ---
|
||||
Src directory memory
|
||||
--- End of Context from: ${path.relative(cwd, srcGeminiFile)} ---`,
|
||||
--- End of Context from: ${normMarker(path.relative(cwd, srcGeminiFile))} ---`,
|
||||
fileCount: 2,
|
||||
filePaths: [projectRootGeminiFile, srcGeminiFile],
|
||||
});
|
||||
@@ -328,23 +382,26 @@ Src directory memory
|
||||
'CWD memory',
|
||||
);
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
const result = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${DEFAULT_CONTEXT_FILENAME} ---
|
||||
memoryContent: `--- Project ---
|
||||
--- Context from: ${normMarker(DEFAULT_CONTEXT_FILENAME)} ---
|
||||
CWD memory
|
||||
--- End of Context from: ${DEFAULT_CONTEXT_FILENAME} ---
|
||||
--- End of Context from: ${normMarker(DEFAULT_CONTEXT_FILENAME)} ---
|
||||
|
||||
--- Context from: ${path.join('subdir', DEFAULT_CONTEXT_FILENAME)} ---
|
||||
--- Context from: ${normMarker(path.join('subdir', DEFAULT_CONTEXT_FILENAME))} ---
|
||||
Subdir memory
|
||||
--- End of Context from: ${path.join('subdir', DEFAULT_CONTEXT_FILENAME)} ---`,
|
||||
--- End of Context from: ${normMarker(path.join('subdir', DEFAULT_CONTEXT_FILENAME))} ---`,
|
||||
fileCount: 2,
|
||||
filePaths: [cwdGeminiFile, subDirGeminiFile],
|
||||
});
|
||||
@@ -372,35 +429,39 @@ Subdir memory
|
||||
'Subdir memory',
|
||||
);
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
const result = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, defaultContextFile)} ---
|
||||
memoryContent: `--- Global ---
|
||||
--- Context from: ${normMarker(path.relative(cwd, defaultContextFile))} ---
|
||||
default context content
|
||||
--- End of Context from: ${path.relative(cwd, defaultContextFile)} ---
|
||||
--- End of Context from: ${normMarker(path.relative(cwd, defaultContextFile))} ---
|
||||
|
||||
--- Context from: ${path.relative(cwd, rootGeminiFile)} ---
|
||||
--- Project ---
|
||||
--- Context from: ${normMarker(path.relative(cwd, rootGeminiFile))} ---
|
||||
Project parent memory
|
||||
--- End of Context from: ${path.relative(cwd, rootGeminiFile)} ---
|
||||
--- End of Context from: ${normMarker(path.relative(cwd, rootGeminiFile))} ---
|
||||
|
||||
--- Context from: ${path.relative(cwd, projectRootGeminiFile)} ---
|
||||
--- Context from: ${normMarker(path.relative(cwd, projectRootGeminiFile))} ---
|
||||
Project root memory
|
||||
--- End of Context from: ${path.relative(cwd, projectRootGeminiFile)} ---
|
||||
--- End of Context from: ${normMarker(path.relative(cwd, projectRootGeminiFile))} ---
|
||||
|
||||
--- Context from: ${path.relative(cwd, cwdGeminiFile)} ---
|
||||
--- Context from: ${normMarker(path.relative(cwd, cwdGeminiFile))} ---
|
||||
CWD memory
|
||||
--- End of Context from: ${path.relative(cwd, cwdGeminiFile)} ---
|
||||
--- End of Context from: ${normMarker(path.relative(cwd, cwdGeminiFile))} ---
|
||||
|
||||
--- Context from: ${path.relative(cwd, subDirGeminiFile)} ---
|
||||
--- Context from: ${normMarker(path.relative(cwd, subDirGeminiFile))} ---
|
||||
Subdir memory
|
||||
--- End of Context from: ${path.relative(cwd, subDirGeminiFile)} ---`,
|
||||
--- End of Context from: ${normMarker(path.relative(cwd, subDirGeminiFile))} ---`,
|
||||
fileCount: 5,
|
||||
filePaths: [
|
||||
defaultContextFile,
|
||||
@@ -425,26 +486,29 @@ Subdir memory
|
||||
'My code memory',
|
||||
);
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
'tree',
|
||||
{
|
||||
respectGitIgnore: true,
|
||||
respectGeminiIgnore: true,
|
||||
customIgnoreFilePaths: [],
|
||||
},
|
||||
200, // maxDirs parameter
|
||||
const result = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
'tree',
|
||||
{
|
||||
respectGitIgnore: true,
|
||||
respectGeminiIgnore: true,
|
||||
customIgnoreFilePaths: [],
|
||||
},
|
||||
200, // maxDirs parameter
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, regularSubDirGeminiFile)} ---
|
||||
memoryContent: `--- Project ---
|
||||
--- Context from: ${normMarker(path.relative(cwd, regularSubDirGeminiFile))} ---
|
||||
My code memory
|
||||
--- End of Context from: ${path.relative(cwd, regularSubDirGeminiFile)} ---`,
|
||||
--- End of Context from: ${normMarker(path.relative(cwd, regularSubDirGeminiFile))} ---`,
|
||||
fileCount: 1,
|
||||
filePaths: [regularSubDirGeminiFile],
|
||||
});
|
||||
@@ -485,13 +549,15 @@ My code memory
|
||||
|
||||
consoleDebugSpy.mockRestore();
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
const result = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -507,24 +573,27 @@ My code memory
|
||||
'Extension memory content',
|
||||
);
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([
|
||||
{
|
||||
contextFiles: [extensionFilePath],
|
||||
isActive: true,
|
||||
} as GeminiCLIExtension,
|
||||
]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
const result = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([
|
||||
{
|
||||
contextFiles: [extensionFilePath],
|
||||
isActive: true,
|
||||
} as GeminiCLIExtension,
|
||||
]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, extensionFilePath)} ---
|
||||
memoryContent: `--- Extension ---
|
||||
--- Context from: ${normMarker(path.relative(cwd, extensionFilePath))} ---
|
||||
Extension memory content
|
||||
--- End of Context from: ${path.relative(cwd, extensionFilePath)} ---`,
|
||||
--- End of Context from: ${normMarker(path.relative(cwd, extensionFilePath))} ---`,
|
||||
fileCount: 1,
|
||||
filePaths: [extensionFilePath],
|
||||
});
|
||||
@@ -539,19 +608,22 @@ Extension memory content
|
||||
'included directory memory',
|
||||
);
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[includedDir],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
const result = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[includedDir],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, includedFile)} ---
|
||||
memoryContent: `--- Project ---
|
||||
--- Context from: ${normMarker(path.relative(cwd, includedFile))} ---
|
||||
included directory memory
|
||||
--- End of Context from: ${path.relative(cwd, includedFile)} ---`,
|
||||
--- End of Context from: ${normMarker(path.relative(cwd, includedFile))} ---`,
|
||||
fileCount: 1,
|
||||
filePaths: [includedFile],
|
||||
});
|
||||
@@ -574,13 +646,15 @@ included directory memory
|
||||
}
|
||||
|
||||
// Load memory from all directories
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
createdFiles.map((f) => path.dirname(f)),
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
const result = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
createdFiles.map((f) => path.dirname(f)),
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
),
|
||||
);
|
||||
|
||||
// Should have loaded all files
|
||||
@@ -589,8 +663,9 @@ included directory memory
|
||||
expect(result.filePaths.sort()).toEqual(createdFiles.sort());
|
||||
|
||||
// Content should include all project contents
|
||||
const flattenedMemory = flattenMemory(result.memoryContent);
|
||||
for (let i = 0; i < numDirs; i++) {
|
||||
expect(result.memoryContent).toContain(`Content from project ${i}`);
|
||||
expect(flattenedMemory).toContain(`Content from project ${i}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -609,73 +684,91 @@ included directory memory
|
||||
);
|
||||
|
||||
// Include both parent and child directories
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
parentDir,
|
||||
[childDir, parentDir], // Deliberately include duplicates
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
const result = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
parentDir,
|
||||
[childDir, parentDir], // Deliberately include duplicates
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
new SimpleExtensionLoader([]),
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
),
|
||||
);
|
||||
|
||||
// Should have both files without duplicates
|
||||
const flattenedMemory = flattenMemory(result.memoryContent);
|
||||
expect(result.fileCount).toBe(2);
|
||||
expect(result.memoryContent).toContain('Parent content');
|
||||
expect(result.memoryContent).toContain('Child content');
|
||||
expect(flattenedMemory).toContain('Parent content');
|
||||
expect(flattenedMemory).toContain('Child content');
|
||||
expect(result.filePaths.sort()).toEqual([parentFile, childFile].sort());
|
||||
|
||||
// Check that files are not duplicated
|
||||
const parentOccurrences = (
|
||||
result.memoryContent.match(/Parent content/g) || []
|
||||
).length;
|
||||
const childOccurrences = (
|
||||
result.memoryContent.match(/Child content/g) || []
|
||||
).length;
|
||||
const parentOccurrences = (flattenedMemory.match(/Parent content/g) || [])
|
||||
.length;
|
||||
const childOccurrences = (flattenedMemory.match(/Child content/g) || [])
|
||||
.length;
|
||||
expect(parentOccurrences).toBe(1);
|
||||
expect(childOccurrences).toBe(1);
|
||||
});
|
||||
|
||||
describe('loadGlobalMemory', () => {
|
||||
it('should load global memory file if it exists', async () => {
|
||||
describe('getGlobalMemoryPaths', () => {
|
||||
it('should find global memory file if it exists', async () => {
|
||||
const globalMemoryFile = await createTestFile(
|
||||
path.join(homedir, GEMINI_DIR, DEFAULT_CONTEXT_FILENAME),
|
||||
'Global memory content',
|
||||
);
|
||||
|
||||
const result = await loadGlobalMemory();
|
||||
const result = await getGlobalMemoryPaths();
|
||||
|
||||
expect(result.files).toHaveLength(1);
|
||||
expect(result.files[0].path).toBe(globalMemoryFile);
|
||||
expect(result.files[0].content).toBe('Global memory content');
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe(globalMemoryFile);
|
||||
});
|
||||
|
||||
it('should return empty content if global memory file does not exist', async () => {
|
||||
const result = await loadGlobalMemory();
|
||||
it('should return empty array if global memory file does not exist', async () => {
|
||||
const result = await getGlobalMemoryPaths();
|
||||
|
||||
expect(result.files).toHaveLength(0);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadEnvironmentMemory', () => {
|
||||
it('should load extension memory', async () => {
|
||||
describe('getExtensionMemoryPaths', () => {
|
||||
it('should return active extension context files', async () => {
|
||||
const extFile = await createTestFile(
|
||||
path.join(testRootDir, 'ext', 'GEMINI.md'),
|
||||
'Extension content',
|
||||
);
|
||||
const mockExtensionLoader = new SimpleExtensionLoader([
|
||||
const loader = new SimpleExtensionLoader([
|
||||
{
|
||||
isActive: true,
|
||||
contextFiles: [extFile],
|
||||
} as GeminiCLIExtension,
|
||||
]);
|
||||
|
||||
const result = await loadEnvironmentMemory([], mockExtensionLoader);
|
||||
const result = getExtensionMemoryPaths(loader);
|
||||
|
||||
expect(result.files).toHaveLength(1);
|
||||
expect(result.files[0].path).toBe(extFile);
|
||||
expect(result.files[0].content).toBe('Extension content');
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe(extFile);
|
||||
});
|
||||
|
||||
it('should ignore inactive extensions', async () => {
|
||||
const extFile = await createTestFile(
|
||||
path.join(testRootDir, 'ext', 'GEMINI.md'),
|
||||
'Extension content',
|
||||
);
|
||||
const loader = new SimpleExtensionLoader([
|
||||
{
|
||||
isActive: false,
|
||||
contextFiles: [extFile],
|
||||
} as GeminiCLIExtension,
|
||||
]);
|
||||
|
||||
const result = getExtensionMemoryPaths(loader);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEnvironmentMemoryPaths', () => {
|
||||
it('should NOT traverse upward beyond trusted root (even with .git)', async () => {
|
||||
// Setup: /temp/parent/repo/.git
|
||||
const parentDir = await createEmptyDir(path.join(testRootDir, 'parent'));
|
||||
@@ -698,14 +791,10 @@ included directory memory
|
||||
|
||||
// Trust srcDir. Should ONLY load srcFile.
|
||||
// Repo and Parent are NOT trusted.
|
||||
const result = await loadEnvironmentMemory(
|
||||
[srcDir],
|
||||
new SimpleExtensionLoader([]),
|
||||
);
|
||||
const result = await getEnvironmentMemoryPaths([srcDir]);
|
||||
|
||||
expect(result.files).toHaveLength(1);
|
||||
expect(result.files[0].path).toBe(srcFile);
|
||||
expect(result.files[0].content).toBe('Src content');
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe(srcFile);
|
||||
});
|
||||
|
||||
it('should NOT traverse upward beyond trusted root (no .git)', async () => {
|
||||
@@ -724,20 +813,13 @@ included directory memory
|
||||
|
||||
// Trust notesDir. Should load NOTHING because notesDir has no file,
|
||||
// and we do not traverse up to docsDir.
|
||||
const resultNotes = await loadEnvironmentMemory(
|
||||
[notesDir],
|
||||
new SimpleExtensionLoader([]),
|
||||
);
|
||||
expect(resultNotes.files).toHaveLength(0);
|
||||
const resultNotes = await getEnvironmentMemoryPaths([notesDir]);
|
||||
expect(resultNotes).toHaveLength(0);
|
||||
|
||||
// Trust docsDir. Should load docsFile, but NOT homeFile.
|
||||
const resultDocs = await loadEnvironmentMemory(
|
||||
[docsDir],
|
||||
new SimpleExtensionLoader([]),
|
||||
);
|
||||
expect(resultDocs.files).toHaveLength(1);
|
||||
expect(resultDocs.files[0].path).toBe(docsFile);
|
||||
expect(resultDocs.files[0].content).toBe('Docs content');
|
||||
const resultDocs = await getEnvironmentMemoryPaths([docsDir]);
|
||||
expect(resultDocs).toHaveLength(1);
|
||||
expect(resultDocs[0]).toBe(docsFile);
|
||||
});
|
||||
|
||||
it('should deduplicate paths when same root is trusted multiple times', async () => {
|
||||
@@ -750,13 +832,10 @@ included directory memory
|
||||
);
|
||||
|
||||
// Trust repoDir twice.
|
||||
const result = await loadEnvironmentMemory(
|
||||
[repoDir, repoDir],
|
||||
new SimpleExtensionLoader([]),
|
||||
);
|
||||
const result = await getEnvironmentMemoryPaths([repoDir, repoDir]);
|
||||
|
||||
expect(result.files).toHaveLength(1);
|
||||
expect(result.files[0].path).toBe(repoFile);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe(repoFile);
|
||||
});
|
||||
|
||||
it('should keep multiple memory files from the same directory adjacent and in order', async () => {
|
||||
@@ -777,19 +856,14 @@ included directory memory
|
||||
'Secondary content',
|
||||
);
|
||||
|
||||
const result = await loadEnvironmentMemory(
|
||||
[dir],
|
||||
new SimpleExtensionLoader([]),
|
||||
);
|
||||
const result = await getEnvironmentMemoryPaths([dir]);
|
||||
|
||||
expect(result.files).toHaveLength(2);
|
||||
expect(result).toHaveLength(2);
|
||||
// Verify order: PRIMARY should come before SECONDARY because they are
|
||||
// sorted by path and PRIMARY.md comes before SECONDARY.md alphabetically
|
||||
// if in same dir.
|
||||
expect(result.files[0].path).toBe(primaryFile);
|
||||
expect(result.files[1].path).toBe(secondaryFile);
|
||||
expect(result.files[0].content).toBe('Primary content');
|
||||
expect(result.files[1].content).toBe('Secondary content');
|
||||
expect(result[0]).toBe(primaryFile);
|
||||
expect(result[1]).toBe(secondaryFile);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -904,16 +978,18 @@ included directory memory
|
||||
model: 'fake-model',
|
||||
extensionLoader,
|
||||
});
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
config.getWorkingDir(),
|
||||
config.shouldLoadMemoryFromIncludeDirectories()
|
||||
? config.getWorkspaceContext().getDirectories()
|
||||
: [],
|
||||
config.getDebugMode(),
|
||||
config.getFileService(),
|
||||
config.getExtensionLoader(),
|
||||
config.isTrustedFolder(),
|
||||
config.getImportFormat(),
|
||||
const result = flattenResult(
|
||||
await loadServerHierarchicalMemory(
|
||||
config.getWorkingDir(),
|
||||
config.shouldLoadMemoryFromIncludeDirectories()
|
||||
? config.getWorkspaceContext().getDirectories()
|
||||
: [],
|
||||
config.getDebugMode(),
|
||||
config.getFileService(),
|
||||
config.getExtensionLoader(),
|
||||
config.isTrustedFolder(),
|
||||
config.getImportFormat(),
|
||||
),
|
||||
);
|
||||
expect(result.fileCount).equals(0);
|
||||
|
||||
@@ -937,12 +1013,11 @@ included directory memory
|
||||
const refreshResult = await refreshServerHierarchicalMemory(config);
|
||||
expect(refreshResult.fileCount).equals(1);
|
||||
expect(config.getGeminiMdFileCount()).equals(refreshResult.fileCount);
|
||||
expect(refreshResult.memoryContent).toContain(
|
||||
'Really cool custom context!',
|
||||
);
|
||||
expect(config.getUserMemory()).equals(refreshResult.memoryContent);
|
||||
const flattenedMemory = flattenMemory(refreshResult.memoryContent);
|
||||
expect(flattenedMemory).toContain('Really cool custom context!');
|
||||
expect(config.getUserMemory()).toStrictEqual(refreshResult.memoryContent);
|
||||
expect(refreshResult.filePaths[0]).toContain(
|
||||
path.join(extensionPath, 'CustomContext.md'),
|
||||
normMarker(path.join(extensionPath, 'CustomContext.md')),
|
||||
);
|
||||
expect(config.getGeminiMdFilePaths()).equals(refreshResult.filePaths);
|
||||
expect(mockEventListener).toHaveBeenCalledExactlyOnceWith({
|
||||
@@ -980,12 +1055,16 @@ included directory memory
|
||||
await refreshServerHierarchicalMemory(mockConfig);
|
||||
|
||||
expect(mockConfig.setUserMemory).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"# Instructions for MCP Server 'extension-server'",
|
||||
),
|
||||
expect.objectContaining({
|
||||
project: expect.stringContaining(
|
||||
"# Instructions for MCP Server 'extension-server'",
|
||||
),
|
||||
}),
|
||||
);
|
||||
expect(mockConfig.setUserMemory).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Always be polite.'),
|
||||
expect.objectContaining({
|
||||
project: expect.stringContaining('Always be polite.'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user