mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-22 07:41:23 -07:00
feat(core): add project-level memory scope to save_memory tool (#24161)
This commit is contained in:
@@ -19,7 +19,9 @@ import {
|
||||
getCurrentGeminiMdFilename,
|
||||
getAllGeminiMdFilenames,
|
||||
DEFAULT_CONTEXT_FILENAME,
|
||||
getProjectMemoryFilePath,
|
||||
} from './memoryTool.js';
|
||||
import type { Storage } from '../config/storage.js';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
@@ -113,9 +115,7 @@ describe('MemoryTool', () => {
|
||||
it('should have correct name, displayName, description, and schema', () => {
|
||||
expect(memoryTool.name).toBe('save_memory');
|
||||
expect(memoryTool.displayName).toBe('SaveMemory');
|
||||
expect(memoryTool.description).toContain(
|
||||
'Saves concise global user context',
|
||||
);
|
||||
expect(memoryTool.description).toContain('Saves concise user context');
|
||||
expect(memoryTool.schema).toBeDefined();
|
||||
expect(memoryTool.schema.name).toBe('save_memory');
|
||||
expect(memoryTool.schema.parametersJsonSchema).toStrictEqual({
|
||||
@@ -127,6 +127,12 @@ describe('MemoryTool', () => {
|
||||
description:
|
||||
'The specific fact or piece of information to remember. Should be a clear, self-contained statement.',
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
enum: ['global', 'project'],
|
||||
description:
|
||||
"Where to save the memory. 'global' (default) saves to a file loaded in every workspace. 'project' saves to a project-specific file private to the user, not committed to the repo.",
|
||||
},
|
||||
},
|
||||
required: ['fact'],
|
||||
});
|
||||
@@ -378,4 +384,93 @@ describe('MemoryTool', () => {
|
||||
expect(() => memoryTool.build(attackParams)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('project-scope memory', () => {
|
||||
const mockProjectMemoryDir = path.join(
|
||||
'/mock',
|
||||
'.gemini',
|
||||
'memory',
|
||||
'test-project',
|
||||
);
|
||||
|
||||
function createMockStorage(): Storage {
|
||||
return {
|
||||
getProjectMemoryDir: () => mockProjectMemoryDir,
|
||||
} as unknown as Storage;
|
||||
}
|
||||
|
||||
it('should reject scope=project when storage is not initialized', () => {
|
||||
const bus = createMockMessageBus();
|
||||
const memoryToolNoStorage = new MemoryTool(bus);
|
||||
const params = { fact: 'project fact', scope: 'project' as const };
|
||||
|
||||
expect(memoryToolNoStorage.validateToolParams(params)).toBe(
|
||||
'Project-level memory is not available: storage is not initialized.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should write to global path when scope is not specified', async () => {
|
||||
const bus = createMockMessageBus();
|
||||
getMockMessageBusInstance(bus).defaultToolDecision = 'ask_user';
|
||||
const memoryToolWithStorage = new MemoryTool(bus, createMockStorage());
|
||||
const params = { fact: 'global fact' };
|
||||
const invocation = memoryToolWithStorage.build(params);
|
||||
await invocation.execute(mockAbortSignal);
|
||||
|
||||
const expectedFilePath = path.join(
|
||||
os.homedir(),
|
||||
GEMINI_DIR,
|
||||
getCurrentGeminiMdFilename(),
|
||||
);
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
expectedFilePath,
|
||||
expect.any(String),
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
|
||||
it('should write to project memory path when scope is project', async () => {
|
||||
const bus = createMockMessageBus();
|
||||
getMockMessageBusInstance(bus).defaultToolDecision = 'ask_user';
|
||||
const memoryToolWithStorage = new MemoryTool(bus, createMockStorage());
|
||||
const params = {
|
||||
fact: 'project-specific fact',
|
||||
scope: 'project' as const,
|
||||
};
|
||||
const invocation = memoryToolWithStorage.build(params);
|
||||
await invocation.execute(mockAbortSignal);
|
||||
|
||||
const expectedFilePath = path.join(
|
||||
mockProjectMemoryDir,
|
||||
getCurrentGeminiMdFilename(),
|
||||
);
|
||||
expect(fs.mkdir).toHaveBeenCalledWith(mockProjectMemoryDir, {
|
||||
recursive: true,
|
||||
});
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
expectedFilePath,
|
||||
expect.stringContaining('- project-specific fact'),
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use project path in confirmation details when scope is project', async () => {
|
||||
const bus = createMockMessageBus();
|
||||
getMockMessageBusInstance(bus).defaultToolDecision = 'ask_user';
|
||||
const memoryToolWithStorage = new MemoryTool(bus, createMockStorage());
|
||||
const params = { fact: 'project fact', scope: 'project' as const };
|
||||
const invocation = memoryToolWithStorage.build(params);
|
||||
const result = await invocation.shouldConfirmExecute(mockAbortSignal);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).not.toBe(false);
|
||||
|
||||
if (result && result.type === 'edit') {
|
||||
expect(result.fileName).toBe(
|
||||
getProjectMemoryFilePath(createMockStorage()),
|
||||
);
|
||||
expect(result.newContent).toContain('- project fact');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user