diff options
| author | Allen Hutchison <[email protected]> | 2025-05-16 16:36:50 -0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-05-16 16:36:50 -0700 |
| commit | 1bdec55fe1c658069a45df0aa8e4923ba1954e41 (patch) | |
| tree | 32aa334cb0590f06830f52ed7c0d84e2d4ed7db3 /packages/cli/src/ui/hooks/slashCommandProcessor.test.ts | |
| parent | d9bd2b0e144560c8a82806bfb021a028c7cd43c9 (diff) | |
feat: Implement CLI and model memory management (#371)
Co-authored-by: N. Taylor Mullen <[email protected]>
Diffstat (limited to 'packages/cli/src/ui/hooks/slashCommandProcessor.test.ts')
| -rw-r--r-- | packages/cli/src/ui/hooks/slashCommandProcessor.test.ts | 248 |
1 files changed, 248 insertions, 0 deletions
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts new file mode 100644 index 00000000..4055dfd3 --- /dev/null +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts @@ -0,0 +1,248 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +const { mockProcessExit } = vi.hoisted(() => ({ + mockProcessExit: vi.fn((_code?: number): never => undefined as never), +})); + +vi.mock('node:process', () => ({ + exit: mockProcessExit, + cwd: vi.fn(() => '/mock/cwd'), +})); + +vi.mock('node:fs/promises', () => ({ + readFile: vi.fn(), + writeFile: vi.fn(), + mkdir: vi.fn(), +})); + +import { act, renderHook } from '@testing-library/react'; +import { vi, describe, it, expect, beforeEach, Mock } from 'vitest'; +import { useSlashCommandProcessor } from './slashCommandProcessor.js'; +import { MessageType } from '../types.js'; +import * as memoryUtils from '../../config/memoryUtils.js'; +import { type Config, MemoryTool } from '@gemini-code/server'; +import * as fsPromises from 'node:fs/promises'; + +// Import the module for mocking its functions +import * as ShowMemoryCommandModule from './useShowMemoryCommand.js'; + +// Mock dependencies +vi.mock('./useShowMemoryCommand.js', () => ({ + SHOW_MEMORY_COMMAND_NAME: '/memory show', + createShowMemoryAction: vi.fn(() => vi.fn()), +})); + +// Spy on the static method we want to mock +const performAddMemoryEntrySpy = vi.spyOn(MemoryTool, 'performAddMemoryEntry'); + +describe('useSlashCommandProcessor', () => { + let mockAddItem: ReturnType<typeof vi.fn>; + let mockClearItems: ReturnType<typeof vi.fn>; + let mockRefreshStatic: ReturnType<typeof vi.fn>; + let mockSetShowHelp: ReturnType<typeof vi.fn>; + let mockOnDebugMessage: ReturnType<typeof vi.fn>; + let mockOpenThemeDialog: ReturnType<typeof vi.fn>; + let mockPerformMemoryRefresh: ReturnType<typeof vi.fn>; + let mockConfig: Config; + + beforeEach(() => { + mockAddItem = vi.fn(); + mockClearItems = vi.fn(); + mockRefreshStatic = vi.fn(); + mockSetShowHelp = vi.fn(); + mockOnDebugMessage = vi.fn(); + mockOpenThemeDialog = vi.fn(); + mockPerformMemoryRefresh = vi.fn().mockResolvedValue(undefined); + mockConfig = { getDebugMode: vi.fn(() => false) } as unknown as Config; + + // Clear mocks for fsPromises if they were used directly or indirectly + vi.mocked(fsPromises.readFile).mockClear(); + vi.mocked(fsPromises.writeFile).mockClear(); + vi.mocked(fsPromises.mkdir).mockClear(); + + performAddMemoryEntrySpy.mockReset(); // Reset the spy + vi.spyOn(memoryUtils, 'deleteLastMemoryEntry').mockImplementation(vi.fn()); + vi.spyOn(memoryUtils, 'deleteAllAddedMemoryEntries').mockImplementation( + vi.fn(), + ); + + vi.mocked(memoryUtils.deleteLastMemoryEntry).mockClear(); + vi.mocked(memoryUtils.deleteAllAddedMemoryEntries).mockClear(); + + mockProcessExit.mockClear(); + (ShowMemoryCommandModule.createShowMemoryAction as Mock).mockClear(); + mockPerformMemoryRefresh.mockClear(); + }); + + const getProcessor = () => { + const { result } = renderHook(() => + useSlashCommandProcessor( + mockConfig, + mockAddItem, + mockClearItems, + mockRefreshStatic, + mockSetShowHelp, + mockOnDebugMessage, + mockOpenThemeDialog, + mockPerformMemoryRefresh, + ), + ); + return result.current; + }; + + describe('/memory add', () => { + it('should call MemoryTool.performAddMemoryEntry and refresh on valid input', async () => { + performAddMemoryEntrySpy.mockResolvedValue(undefined); + const { handleSlashCommand } = getProcessor(); + const fact = 'Remember this fact'; + await act(async () => { + handleSlashCommand(`/memory add ${fact}`); + }); + expect(mockAddItem).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + type: MessageType.USER, + text: `/memory add ${fact}`, + }), + expect.any(Number), + ); + expect(performAddMemoryEntrySpy).toHaveBeenCalledWith( + fact, + memoryUtils.getGlobalMemoryFilePath(), // Ensure this path is correct + { + readFile: fsPromises.readFile, + writeFile: fsPromises.writeFile, + mkdir: fsPromises.mkdir, + }, + ); + expect(mockPerformMemoryRefresh).toHaveBeenCalled(); + expect(mockAddItem).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + type: MessageType.INFO, + text: `Successfully added to memory: "${fact}"`, + }), + expect.any(Number), + ); + }); + + it('should show usage error if no text is provided', async () => { + const { handleSlashCommand } = getProcessor(); + await act(async () => { + handleSlashCommand('/memory add '); + }); + expect(performAddMemoryEntrySpy).not.toHaveBeenCalled(); + expect(mockAddItem).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + type: MessageType.ERROR, + text: 'Usage: /memory add <text to remember>', + }), + expect.any(Number), + ); + }); + + it('should handle error from MemoryTool.performAddMemoryEntry', async () => { + const fact = 'Another fact'; + performAddMemoryEntrySpy.mockRejectedValue( + new Error('[MemoryTool] Failed to add memory entry: Disk full'), + ); + const { handleSlashCommand } = getProcessor(); + await act(async () => { + handleSlashCommand(`/memory add ${fact}`); + }); + expect(performAddMemoryEntrySpy).toHaveBeenCalledWith( + fact, + memoryUtils.getGlobalMemoryFilePath(), + { + readFile: fsPromises.readFile, + writeFile: fsPromises.writeFile, + mkdir: fsPromises.mkdir, + }, + ); + expect(mockAddItem).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + type: MessageType.ERROR, + text: 'Failed to add memory: [MemoryTool] Failed to add memory entry: Disk full', + }), + expect.any(Number), + ); + }); + }); + + describe('/memory show', () => { + it('should call the showMemoryAction', async () => { + const mockReturnedShowAction = vi.fn(); + vi.mocked(ShowMemoryCommandModule.createShowMemoryAction).mockReturnValue( + mockReturnedShowAction, + ); + const { handleSlashCommand } = getProcessor(); + await act(async () => { + handleSlashCommand('/memory show'); + }); + expect( + ShowMemoryCommandModule.createShowMemoryAction, + ).toHaveBeenCalledWith(mockConfig, expect.any(Function)); + expect(mockReturnedShowAction).toHaveBeenCalled(); + }); + }); + + describe('/memory refresh', () => { + it('should call performMemoryRefresh', async () => { + const { handleSlashCommand } = getProcessor(); + await act(async () => { + handleSlashCommand('/memory refresh'); + }); + expect(mockPerformMemoryRefresh).toHaveBeenCalled(); + }); + }); + + describe('Unknown /memory subcommand', () => { + it('should show an error for unknown /memory subcommand', async () => { + const { handleSlashCommand } = getProcessor(); + await act(async () => { + handleSlashCommand('/memory foobar'); + }); + expect(mockAddItem).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + type: MessageType.ERROR, + text: 'Unknown /memory command: foobar. Available: show, refresh, add', + }), + expect.any(Number), + ); + }); + }); + + describe('Other commands', () => { + it('/help should open help', async () => { + const { handleSlashCommand } = getProcessor(); + await act(async () => { + handleSlashCommand('/help'); + }); + expect(mockSetShowHelp).toHaveBeenCalledWith(true); + }); + }); + + describe('Unknown command', () => { + it('should show an error for a general unknown command', async () => { + const { handleSlashCommand } = getProcessor(); + await act(async () => { + handleSlashCommand('/unknowncommand'); + }); + expect(mockAddItem).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + type: MessageType.ERROR, + text: 'Unknown command: /unknowncommand', + }), + expect.any(Number), + ); + }); + }); +}); |
