/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { vi, describe, it, expect, beforeEach, Mock } from 'vitest'; import { memoryCommand } from './memoryCommand.js'; import { type CommandContext, SlashCommand } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; import { MessageType } from '../types.js'; import { LoadedSettings } from '../../config/settings.js'; import { getErrorMessage, loadServerHierarchicalMemory, type FileDiscoveryService, } from '@google/gemini-cli-core'; vi.mock('@google/gemini-cli-core', async (importOriginal) => { const original = await importOriginal(); return { ...original, getErrorMessage: vi.fn((error: unknown) => { if (error instanceof Error) return error.message; return String(error); }), loadServerHierarchicalMemory: vi.fn(), }; }); const mockLoadServerHierarchicalMemory = loadServerHierarchicalMemory as Mock; describe('memoryCommand', () => { let mockContext: CommandContext; const getSubCommand = (name: 'show' | 'add' | 'refresh'): SlashCommand => { const subCommand = memoryCommand.subCommands?.find( (cmd) => cmd.name === name, ); if (!subCommand) { throw new Error(`/memory ${name} command not found.`); } return subCommand; }; describe('/memory show', () => { let showCommand: SlashCommand; let mockGetUserMemory: Mock; let mockGetGeminiMdFileCount: Mock; beforeEach(() => { showCommand = getSubCommand('show'); mockGetUserMemory = vi.fn(); mockGetGeminiMdFileCount = vi.fn(); mockContext = createMockCommandContext({ services: { config: { getUserMemory: mockGetUserMemory, getGeminiMdFileCount: mockGetGeminiMdFileCount, }, }, }); }); it('should display a message if memory is empty', async () => { if (!showCommand.action) throw new Error('Command has no action'); mockGetUserMemory.mockReturnValue(''); mockGetGeminiMdFileCount.mockReturnValue(0); await showCommand.action(mockContext, ''); expect(mockContext.ui.addItem).toHaveBeenCalledWith( { type: MessageType.INFO, text: 'Memory is currently empty.', }, expect.any(Number), ); }); it('should display the memory content and file count if it exists', async () => { if (!showCommand.action) throw new Error('Command has no action'); const memoryContent = 'This is a test memory.'; mockGetUserMemory.mockReturnValue(memoryContent); mockGetGeminiMdFileCount.mockReturnValue(1); await showCommand.action(mockContext, ''); expect(mockContext.ui.addItem).toHaveBeenCalledWith( { type: MessageType.INFO, text: `Current memory content from 1 file(s):\n\n---\n${memoryContent}\n---`, }, expect.any(Number), ); }); }); describe('/memory add', () => { let addCommand: SlashCommand; beforeEach(() => { addCommand = getSubCommand('add'); mockContext = createMockCommandContext(); }); it('should return an error message if no arguments are provided', () => { if (!addCommand.action) throw new Error('Command has no action'); const result = addCommand.action(mockContext, ' '); expect(result).toEqual({ type: 'message', messageType: 'error', content: 'Usage: /memory add ', }); expect(mockContext.ui.addItem).not.toHaveBeenCalled(); }); it('should return a tool action and add an info message when arguments are provided', () => { if (!addCommand.action) throw new Error('Command has no action'); const fact = 'remember this'; const result = addCommand.action(mockContext, ` ${fact} `); expect(mockContext.ui.addItem).toHaveBeenCalledWith( { type: MessageType.INFO, text: `Attempting to save to memory: "${fact}"`, }, expect.any(Number), ); expect(result).toEqual({ type: 'tool', toolName: 'save_memory', toolArgs: { fact }, }); }); }); describe('/memory refresh', () => { let refreshCommand: SlashCommand; let mockSetUserMemory: Mock; let mockSetGeminiMdFileCount: Mock; beforeEach(() => { refreshCommand = getSubCommand('refresh'); mockSetUserMemory = vi.fn(); mockSetGeminiMdFileCount = vi.fn(); const mockConfig = { setUserMemory: mockSetUserMemory, setGeminiMdFileCount: mockSetGeminiMdFileCount, getWorkingDir: () => '/test/dir', getDebugMode: () => false, getFileService: () => ({}) as FileDiscoveryService, getExtensionContextFilePaths: () => [], getFileFilteringOptions: () => ({ ignore: [], include: [], }), }; mockContext = createMockCommandContext({ services: { config: Promise.resolve(mockConfig), settings: { merged: { memoryDiscoveryMaxDirs: 1000, }, } as LoadedSettings, }, }); mockLoadServerHierarchicalMemory.mockClear(); }); it('should display success message when memory is refreshed with content', async () => { if (!refreshCommand.action) throw new Error('Command has no action'); const refreshResult = { memoryContent: 'new memory content', fileCount: 2, }; mockLoadServerHierarchicalMemory.mockResolvedValue(refreshResult); await refreshCommand.action(mockContext, ''); expect(mockContext.ui.addItem).toHaveBeenCalledWith( { type: MessageType.INFO, text: 'Refreshing memory from source files...', }, expect.any(Number), ); expect(loadServerHierarchicalMemory).toHaveBeenCalledOnce(); expect(mockSetUserMemory).toHaveBeenCalledWith( refreshResult.memoryContent, ); expect(mockSetGeminiMdFileCount).toHaveBeenCalledWith( refreshResult.fileCount, ); expect(mockContext.ui.addItem).toHaveBeenCalledWith( { type: MessageType.INFO, text: 'Memory refreshed successfully. Loaded 18 characters from 2 file(s).', }, expect.any(Number), ); }); it('should display success message when memory is refreshed with no content', async () => { if (!refreshCommand.action) throw new Error('Command has no action'); const refreshResult = { memoryContent: '', fileCount: 0 }; mockLoadServerHierarchicalMemory.mockResolvedValue(refreshResult); await refreshCommand.action(mockContext, ''); expect(loadServerHierarchicalMemory).toHaveBeenCalledOnce(); expect(mockSetUserMemory).toHaveBeenCalledWith(''); expect(mockSetGeminiMdFileCount).toHaveBeenCalledWith(0); expect(mockContext.ui.addItem).toHaveBeenCalledWith( { type: MessageType.INFO, text: 'Memory refreshed successfully. No memory content found.', }, expect.any(Number), ); }); it('should display an error message if refreshing fails', async () => { if (!refreshCommand.action) throw new Error('Command has no action'); const error = new Error('Failed to read memory files.'); mockLoadServerHierarchicalMemory.mockRejectedValue(error); await refreshCommand.action(mockContext, ''); expect(loadServerHierarchicalMemory).toHaveBeenCalledOnce(); expect(mockSetUserMemory).not.toHaveBeenCalled(); expect(mockSetGeminiMdFileCount).not.toHaveBeenCalled(); expect(mockContext.ui.addItem).toHaveBeenCalledWith( { type: MessageType.ERROR, text: `Error refreshing memory: ${error.message}`, }, expect.any(Number), ); expect(getErrorMessage).toHaveBeenCalledWith(error); }); it('should not throw if config service is unavailable', async () => { if (!refreshCommand.action) throw new Error('Command has no action'); const nullConfigContext = createMockCommandContext({ services: { config: null }, }); await expect( refreshCommand.action(nullConfigContext, ''), ).resolves.toBeUndefined(); expect(nullConfigContext.ui.addItem).toHaveBeenCalledWith( { type: MessageType.INFO, text: 'Refreshing memory from source files...', }, expect.any(Number), ); expect(loadServerHierarchicalMemory).not.toHaveBeenCalled(); }); }); });