| |
| |
| |
| |
| |
|
|
| import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; |
|
|
| import type { SlashCommand, CommandContext } from './types.js'; |
| import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; |
| import type { Content } from '@google/genai'; |
| import { AuthType, type GeminiClient } from '@google/gemini-cli-core'; |
|
|
| import * as fsPromises from 'node:fs/promises'; |
| import { chatCommand, debugCommand } from './chatCommand.js'; |
| import { |
| serializeHistoryToMarkdown, |
| exportHistoryToFile, |
| } from '../utils/historyExportUtils.js'; |
| import type { Stats } from 'node:fs'; |
| import type { HistoryItemWithoutId } from '../types.js'; |
| import path from 'node:path'; |
|
|
| vi.mock('fs/promises', () => ({ |
| stat: vi.fn(), |
| readdir: vi.fn().mockResolvedValue(['file1.txt', 'file2.txt'] as string[]), |
| writeFile: vi.fn(), |
| })); |
|
|
| vi.mock('../utils/historyExportUtils.js', async (importOriginal) => { |
| const actual = |
| await importOriginal<typeof import('../utils/historyExportUtils.js')>(); |
| return { |
| ...actual, |
| exportHistoryToFile: vi.fn(), |
| }; |
| }); |
|
|
| describe('chatCommand', () => { |
| const mockFs = vi.mocked(fsPromises); |
| const mockExport = vi.mocked(exportHistoryToFile); |
|
|
| let mockContext: CommandContext; |
| let mockGetChat: ReturnType<typeof vi.fn>; |
| let mockSaveCheckpoint: ReturnType<typeof vi.fn>; |
| let mockLoadCheckpoint: ReturnType<typeof vi.fn>; |
| let mockDeleteCheckpoint: ReturnType<typeof vi.fn>; |
| let mockGetHistory: ReturnType<typeof vi.fn>; |
|
|
| const getSubCommand = ( |
| name: 'list' | 'save' | 'resume' | 'delete' | 'share', |
| ): SlashCommand => { |
| const subCommand = chatCommand.subCommands?.find( |
| (cmd) => cmd.name === name, |
| ); |
| if (!subCommand) { |
| throw new Error(`/chat ${name} command not found.`); |
| } |
| return subCommand; |
| }; |
|
|
| beforeEach(() => { |
| mockGetHistory = vi.fn().mockReturnValue([]); |
| mockGetChat = vi.fn().mockReturnValue({ |
| getHistory: mockGetHistory, |
| }); |
| mockSaveCheckpoint = vi.fn().mockResolvedValue(undefined); |
| mockLoadCheckpoint = vi.fn().mockResolvedValue({ history: [] }); |
| mockDeleteCheckpoint = vi.fn().mockResolvedValue(true); |
|
|
| mockContext = createMockCommandContext({ |
| services: { |
| agentContext: { |
| config: { |
| getProjectRoot: () => '/project/root', |
| getContentGeneratorConfig: () => ({ |
| authType: AuthType.LOGIN_WITH_GOOGLE, |
| }), |
| storage: { |
| getProjectTempDir: () => '/project/root/.gemini/tmp/mockhash', |
| }, |
| }, |
| geminiClient: { |
| getChat: mockGetChat, |
| } as unknown as GeminiClient, |
| }, |
| logger: { |
| saveCheckpoint: mockSaveCheckpoint, |
| loadCheckpoint: mockLoadCheckpoint, |
| deleteCheckpoint: mockDeleteCheckpoint, |
| initialize: vi.fn().mockResolvedValue(undefined), |
| }, |
| }, |
| }); |
| }); |
|
|
| afterEach(() => { |
| vi.restoreAllMocks(); |
| }); |
|
|
| it('should have the correct main command definition', () => { |
| expect(chatCommand.name).toBe('chat'); |
| expect(chatCommand.description).toBe( |
| 'Browse auto-saved conversations and manage chat checkpoints', |
| ); |
| expect(chatCommand.autoExecute).toBe(true); |
| expect(chatCommand.subCommands).toHaveLength(6); |
| }); |
|
|
| describe('list subcommand', () => { |
| let listCommand: SlashCommand; |
|
|
| beforeEach(() => { |
| listCommand = getSubCommand('list'); |
| }); |
|
|
| it('should add a chat_list item to the UI', async () => { |
| const fakeFiles = ['checkpoint-test1.json', 'checkpoint-test2.json']; |
| const date1 = new Date(); |
| const date2 = new Date(date1.getTime() + 1000); |
|
|
| |
| mockFs.readdir.mockResolvedValue(fakeFiles as any); |
| |
| mockFs.stat.mockImplementation(async (path: any): Promise<Stats> => { |
| if (path.endsWith('test1.json')) { |
| return { mtime: date1 } as Stats; |
| } |
| return { mtime: date2 } as Stats; |
| }); |
|
|
| await listCommand?.action?.(mockContext, ''); |
|
|
| expect(mockContext.ui.addItem).toHaveBeenCalledWith({ |
| type: 'chat_list', |
| chats: [ |
| { |
| name: 'test1', |
| mtime: date1.toISOString(), |
| }, |
| { |
| name: 'test2', |
| mtime: date2.toISOString(), |
| }, |
| ], |
| }); |
| }); |
| }); |
| describe('save subcommand', () => { |
| let saveCommand: SlashCommand; |
| const tag = 'my-tag'; |
| let mockCheckpointExists: ReturnType<typeof vi.fn>; |
|
|
| beforeEach(() => { |
| saveCommand = getSubCommand('save'); |
| mockCheckpointExists = vi.fn().mockResolvedValue(false); |
| mockContext.services.logger.checkpointExists = mockCheckpointExists; |
| }); |
|
|
| it('should return an error if tag is missing', async () => { |
| const result = await saveCommand?.action?.(mockContext, ' '); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: 'Missing tag. Usage: /resume save <tag>', |
| }); |
| }); |
|
|
| it('should inform if conversation history is empty or only contains system context', async () => { |
| mockGetHistory.mockReturnValue([]); |
| let result = await saveCommand?.action?.(mockContext, tag); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'info', |
| content: 'No conversation found to save.', |
| }); |
|
|
| mockGetHistory.mockReturnValue([ |
| { role: 'user', parts: [{ text: 'context for our chat' }] }, |
| ]); |
| result = await saveCommand?.action?.(mockContext, tag); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'info', |
| content: 'No conversation found to save.', |
| }); |
|
|
| mockGetHistory.mockReturnValue([ |
| { role: 'user', parts: [{ text: 'context for our chat' }] }, |
| { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] }, |
| { role: 'user', parts: [{ text: 'Hello, how are you?' }] }, |
| ]); |
| result = await saveCommand?.action?.(mockContext, tag); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'info', |
| content: `Conversation checkpoint saved with tag: ${tag}.`, |
| }); |
| }); |
|
|
| it('should return confirm_action if checkpoint already exists', async () => { |
| mockCheckpointExists.mockResolvedValue(true); |
| mockContext.invocation = { |
| raw: `/chat save ${tag}`, |
| name: 'save', |
| args: tag, |
| }; |
|
|
| const result = await saveCommand?.action?.(mockContext, tag); |
|
|
| expect(mockCheckpointExists).toHaveBeenCalledWith(tag); |
| expect(mockSaveCheckpoint).not.toHaveBeenCalled(); |
| expect(result).toMatchObject({ |
| type: 'confirm_action', |
| originalInvocation: { raw: `/chat save ${tag}` }, |
| }); |
| |
| expect(result).toHaveProperty('prompt'); |
| }); |
|
|
| it('should save the conversation if overwrite is confirmed', async () => { |
| const history: Content[] = [ |
| { role: 'user', parts: [{ text: 'context for our chat' }] }, |
| { role: 'user', parts: [{ text: 'hello' }] }, |
| ]; |
| mockGetHistory.mockReturnValue(history); |
| mockContext.overwriteConfirmed = true; |
|
|
| const result = await saveCommand?.action?.(mockContext, tag); |
|
|
| expect(mockCheckpointExists).not.toHaveBeenCalled(); |
| expect(mockSaveCheckpoint).toHaveBeenCalledWith( |
| { history, authType: AuthType.LOGIN_WITH_GOOGLE }, |
| tag, |
| ); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'info', |
| content: `Conversation checkpoint saved with tag: ${tag}.`, |
| }); |
| }); |
| }); |
|
|
| describe('resume subcommand', () => { |
| const goodTag = 'good-tag'; |
| const badTag = 'bad-tag'; |
|
|
| let resumeCommand: SlashCommand; |
| beforeEach(() => { |
| resumeCommand = getSubCommand('resume'); |
| }); |
|
|
| it('should return an error if tag is missing', async () => { |
| const result = await resumeCommand?.action?.(mockContext, ''); |
|
|
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: 'Missing tag. Usage: /resume resume <tag>', |
| }); |
| }); |
|
|
| it('should inform if checkpoint is not found', async () => { |
| mockLoadCheckpoint.mockResolvedValue({ history: [] }); |
|
|
| const result = await resumeCommand?.action?.(mockContext, badTag); |
|
|
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'info', |
| content: `No saved checkpoint found with tag: ${badTag}.`, |
| }); |
| }); |
|
|
| it('should resume a conversation with matching authType', async () => { |
| const conversation: Content[] = [ |
| { role: 'user', parts: [{ text: 'system setup' }] }, |
| { role: 'user', parts: [{ text: 'hello gemini' }] }, |
| { role: 'model', parts: [{ text: 'hello world' }] }, |
| ]; |
| mockLoadCheckpoint.mockResolvedValue({ |
| history: conversation, |
| authType: AuthType.LOGIN_WITH_GOOGLE, |
| }); |
|
|
| const result = await resumeCommand?.action?.(mockContext, goodTag); |
|
|
| expect(result).toEqual({ |
| type: 'load_history', |
| history: [ |
| { type: 'user', text: 'hello gemini' }, |
| { type: 'gemini', text: 'hello world' }, |
| ] as HistoryItemWithoutId[], |
| clientHistory: conversation, |
| }); |
| }); |
|
|
| it('should block resuming a conversation with mismatched authType', async () => { |
| const conversation: Content[] = [ |
| { role: 'user', parts: [{ text: 'system setup' }] }, |
| { role: 'user', parts: [{ text: 'hello gemini' }] }, |
| { role: 'model', parts: [{ text: 'hello world' }] }, |
| ]; |
| mockLoadCheckpoint.mockResolvedValue({ |
| history: conversation, |
| authType: AuthType.USE_GEMINI, |
| }); |
|
|
| const result = await resumeCommand?.action?.(mockContext, goodTag); |
|
|
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: `Cannot resume chat. It was saved with a different authentication method (${AuthType.USE_GEMINI}) than the current one (${AuthType.LOGIN_WITH_GOOGLE}).`, |
| }); |
| }); |
|
|
| it('should resume a legacy conversation without authType', async () => { |
| const conversation: Content[] = [ |
| { role: 'user', parts: [{ text: 'system setup' }] }, |
| { role: 'user', parts: [{ text: 'hello gemini' }] }, |
| { role: 'model', parts: [{ text: 'hello world' }] }, |
| ]; |
| mockLoadCheckpoint.mockResolvedValue({ history: conversation }); |
|
|
| const result = await resumeCommand?.action?.(mockContext, goodTag); |
|
|
| expect(result).toEqual({ |
| type: 'load_history', |
| history: [ |
| { type: 'user', text: 'hello gemini' }, |
| { type: 'gemini', text: 'hello world' }, |
| ] as HistoryItemWithoutId[], |
| clientHistory: conversation, |
| }); |
| }); |
|
|
| describe('completion', () => { |
| it('should provide completion suggestions', async () => { |
| const fakeFiles = ['checkpoint-alpha.json', 'checkpoint-beta.json']; |
| mockFs.readdir.mockImplementation( |
| (async (_: string): Promise<string[]> => |
| fakeFiles) as unknown as typeof fsPromises.readdir, |
| ); |
|
|
| mockFs.stat.mockImplementation( |
| (async (_: string): Promise<Stats> => |
| ({ |
| mtime: new Date(), |
| }) as Stats) as unknown as typeof fsPromises.stat, |
| ); |
|
|
| const result = await resumeCommand?.completion?.(mockContext, 'a'); |
|
|
| expect(result).toEqual(['alpha']); |
| }); |
|
|
| it('should suggest filenames sorted by modified time (newest first)', async () => { |
| const fakeFiles = ['checkpoint-test1.json', 'checkpoint-test2.json']; |
| const date = new Date(); |
| mockFs.readdir.mockImplementation( |
| (async (_: string): Promise<string[]> => |
| fakeFiles) as unknown as typeof fsPromises.readdir, |
| ); |
| mockFs.stat.mockImplementation((async ( |
| path: string, |
| ): Promise<Stats> => { |
| if (path.endsWith('test1.json')) { |
| return { mtime: date } as Stats; |
| } |
| return { mtime: new Date(date.getTime() + 1000) } as Stats; |
| }) as unknown as typeof fsPromises.stat); |
|
|
| const result = await resumeCommand?.completion?.(mockContext, ''); |
| |
| expect(result).toEqual(['test2', 'test1']); |
| }); |
| }); |
| }); |
|
|
| describe('delete subcommand', () => { |
| let deleteCommand: SlashCommand; |
| const tag = 'my-tag'; |
| beforeEach(() => { |
| deleteCommand = getSubCommand('delete'); |
| }); |
|
|
| it('should return an error if tag is missing', async () => { |
| const result = await deleteCommand?.action?.(mockContext, ' '); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: 'Missing tag. Usage: /resume delete <tag>', |
| }); |
| }); |
|
|
| it('should return an error if checkpoint is not found', async () => { |
| mockDeleteCheckpoint.mockResolvedValue(false); |
| const result = await deleteCommand?.action?.(mockContext, tag); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: `Error: No checkpoint found with tag '${tag}'.`, |
| }); |
| }); |
|
|
| it('should delete the conversation', async () => { |
| const result = await deleteCommand?.action?.(mockContext, tag); |
|
|
| expect(mockDeleteCheckpoint).toHaveBeenCalledWith(tag); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'info', |
| content: `Conversation checkpoint '${tag}' has been deleted.`, |
| }); |
| }); |
|
|
| describe('completion', () => { |
| it('should provide completion suggestions', async () => { |
| const fakeFiles = ['checkpoint-alpha.json', 'checkpoint-beta.json']; |
| mockFs.readdir.mockImplementation( |
| (async (_: string): Promise<string[]> => |
| fakeFiles) as unknown as typeof fsPromises.readdir, |
| ); |
|
|
| mockFs.stat.mockImplementation( |
| (async (_: string): Promise<Stats> => |
| ({ |
| mtime: new Date(), |
| }) as Stats) as unknown as typeof fsPromises.stat, |
| ); |
|
|
| const result = await deleteCommand?.completion?.(mockContext, 'a'); |
|
|
| expect(result).toEqual(['alpha']); |
| }); |
| }); |
| }); |
|
|
| describe('share subcommand', () => { |
| let shareCommand: SlashCommand; |
| const mockHistory = [ |
| { role: 'user', parts: [{ text: 'context' }] }, |
| { role: 'model', parts: [{ text: 'context response' }] }, |
| { role: 'user', parts: [{ text: 'Hello' }] }, |
| { role: 'model', parts: [{ text: 'Hi there!' }] }, |
| ]; |
|
|
| beforeEach(() => { |
| shareCommand = getSubCommand('share'); |
| vi.spyOn(process, 'cwd').mockReturnValue( |
| path.resolve('/usr/local/google/home/myuser/gemini-cli'), |
| ); |
| vi.spyOn(Date, 'now').mockReturnValue(1234567890); |
| mockGetHistory.mockReturnValue(mockHistory); |
| mockFs.writeFile.mockClear(); |
| }); |
|
|
| it('should default to a json file if no path is provided', async () => { |
| const result = await shareCommand?.action?.(mockContext, ''); |
| const expectedPath = path.join( |
| process.cwd(), |
| 'gemini-conversation-1234567890.json', |
| ); |
| expect(mockExport).toHaveBeenCalledWith({ |
| history: mockHistory, |
| filePath: expectedPath, |
| }); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'info', |
| content: `Conversation shared to ${expectedPath}`, |
| }); |
| }); |
|
|
| it('should share the conversation to a JSON file', async () => { |
| const filePath = 'my-chat.json'; |
| const result = await shareCommand?.action?.(mockContext, filePath); |
| const expectedPath = path.join(process.cwd(), 'my-chat.json'); |
| expect(mockExport).toHaveBeenCalledWith({ |
| history: mockHistory, |
| filePath: expectedPath, |
| }); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'info', |
| content: `Conversation shared to ${expectedPath}`, |
| }); |
| }); |
|
|
| it('should share the conversation to a Markdown file', async () => { |
| const filePath = 'my-chat.md'; |
| const result = await shareCommand?.action?.(mockContext, filePath); |
| const expectedPath = path.join(process.cwd(), 'my-chat.md'); |
| expect(mockExport).toHaveBeenCalledWith({ |
| history: mockHistory, |
| filePath: expectedPath, |
| }); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'info', |
| content: `Conversation shared to ${expectedPath}`, |
| }); |
| }); |
|
|
| it('should return an error for unsupported file extensions', async () => { |
| const filePath = 'my-chat.txt'; |
| const result = await shareCommand?.action?.(mockContext, filePath); |
| expect(mockExport).not.toHaveBeenCalled(); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: 'Invalid file format. Only .md and .json are supported.', |
| }); |
| }); |
|
|
| it('should inform if there is no conversation to share', async () => { |
| mockGetHistory.mockReturnValue([ |
| { role: 'user', parts: [{ text: 'context' }] }, |
| ]); |
| const result = await shareCommand?.action?.(mockContext, 'my-chat.json'); |
| expect(mockExport).not.toHaveBeenCalled(); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'info', |
| content: 'No conversation found to share.', |
| }); |
| }); |
|
|
| it('should handle errors during file writing', async () => { |
| const error = new Error('Permission denied'); |
| mockExport.mockRejectedValue(error); |
| const result = await shareCommand?.action?.(mockContext, 'my-chat.json'); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: `Error sharing conversation: ${error.message}`, |
| }); |
| }); |
|
|
| it('should output valid JSON schema', async () => { |
| const filePath = 'my-chat.json'; |
| await shareCommand?.action?.(mockContext, filePath); |
| const expectedPath = path.join(process.cwd(), 'my-chat.json'); |
| expect(mockExport).toHaveBeenCalledWith({ |
| history: mockHistory, |
| filePath: expectedPath, |
| }); |
| }); |
|
|
| it('should output correct markdown format', async () => { |
| const filePath = 'my-chat.md'; |
| await shareCommand?.action?.(mockContext, filePath); |
| const expectedPath = path.join(process.cwd(), 'my-chat.md'); |
| expect(mockExport).toHaveBeenCalledWith({ |
| history: mockHistory, |
| filePath: expectedPath, |
| }); |
| }); |
| }); |
|
|
| describe('serializeHistoryToMarkdown', () => { |
| it('should correctly serialize chat history to Markdown with icons', () => { |
| const history: Content[] = [ |
| { role: 'user', parts: [{ text: 'Hello' }] }, |
| { role: 'model', parts: [{ text: 'Hi there!' }] }, |
| { role: 'user', parts: [{ text: 'How are you?' }] }, |
| ]; |
|
|
| const expectedMarkdown = |
| '## USER π§βπ»\n\nHello\n\n---\n\n' + |
| '## MODEL β¨\n\nHi there!\n\n---\n\n' + |
| '## USER π§βπ»\n\nHow are you?'; |
|
|
| const result = serializeHistoryToMarkdown(history); |
| expect(result).toBe(expectedMarkdown); |
| }); |
|
|
| it('should handle empty history', () => { |
| const history: Content[] = []; |
| const result = serializeHistoryToMarkdown(history); |
| expect(result).toBe(''); |
| }); |
|
|
| it('should handle items with no text parts', () => { |
| const history: Content[] = [ |
| { role: 'user', parts: [{ text: 'Hello' }] }, |
| { role: 'model', parts: [] }, |
| { role: 'user', parts: [{ text: 'How are you?' }] }, |
| ]; |
|
|
| const expectedMarkdown = `## USER π§βπ» |
| |
| Hello |
| |
| --- |
| |
| ## MODEL β¨ |
| |
| |
| |
| --- |
| |
| ## USER π§βπ» |
| |
| How are you?`; |
|
|
| const result = serializeHistoryToMarkdown(history); |
| expect(result).toBe(expectedMarkdown); |
| }); |
|
|
| it('should correctly serialize function calls and responses', () => { |
| const history: Content[] = [ |
| { |
| role: 'user', |
| parts: [{ text: 'Please call a function.' }], |
| }, |
| { |
| role: 'model', |
| parts: [ |
| { |
| functionCall: { |
| name: 'my-function', |
| args: { arg1: 'value1' }, |
| }, |
| }, |
| ], |
| }, |
| { |
| role: 'user', |
| parts: [ |
| { |
| functionResponse: { |
| name: 'my-function', |
| response: { result: 'success' }, |
| }, |
| }, |
| ], |
| }, |
| ]; |
|
|
| const expectedMarkdown = `## USER π§βπ» |
| |
| Please call a function. |
| |
| --- |
| |
| ## MODEL β¨ |
| |
| **Tool Command**: |
| \`\`\`json |
| { |
| "name": "my-function", |
| "args": { |
| "arg1": "value1" |
| } |
| } |
| \`\`\` |
| |
| --- |
| |
| ## USER π§βπ» |
| |
| **Tool Response**: |
| \`\`\`json |
| { |
| "name": "my-function", |
| "response": { |
| "result": "success" |
| } |
| } |
| \`\`\``; |
|
|
| const result = serializeHistoryToMarkdown(history); |
| expect(result).toBe(expectedMarkdown); |
| }); |
|
|
| it('should handle items with undefined role', () => { |
| const history: Array<Partial<Content>> = [ |
| { role: 'user', parts: [{ text: 'Hello' }] }, |
| { parts: [{ text: 'Hi there!' }] }, |
| ]; |
|
|
| const expectedMarkdown = `## USER π§βπ» |
| |
| Hello |
| |
| --- |
| |
| ## MODEL β¨ |
| |
| Hi there!`; |
|
|
| const result = serializeHistoryToMarkdown(history as Content[]); |
| expect(result).toBe(expectedMarkdown); |
| }); |
| describe('debug subcommand', () => { |
| let mockGetLatestApiRequest: ReturnType<typeof vi.fn>; |
|
|
| beforeEach(() => { |
| mockGetLatestApiRequest = vi.fn(); |
| if (!mockContext.services.agentContext!.config) { |
| |
| (mockContext.services.agentContext!.config as any) = {}; |
| } |
| mockContext.services.agentContext!.config.getLatestApiRequest = |
| mockGetLatestApiRequest; |
| vi.spyOn(process, 'cwd').mockReturnValue('/project/root'); |
| vi.spyOn(Date, 'now').mockReturnValue(1234567890); |
| mockFs.writeFile.mockClear(); |
| }); |
|
|
| it('should return an error if no API request is found', async () => { |
| mockGetLatestApiRequest.mockReturnValue(undefined); |
|
|
| const result = await debugCommand.action?.(mockContext, ''); |
|
|
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: 'No recent API request found to export.', |
| }); |
| expect(mockFs.writeFile).not.toHaveBeenCalled(); |
| }); |
|
|
| it('should convert and write the API request to a json file', async () => { |
| const mockRequest = { |
| contents: [{ role: 'user', parts: [{ text: 'test' }] }], |
| }; |
| mockGetLatestApiRequest.mockReturnValue(mockRequest); |
|
|
| const result = await debugCommand.action?.(mockContext, ''); |
|
|
| const expectedFilename = 'gcli-request-1234567890.json'; |
| const expectedPath = path.join('/project/root', expectedFilename); |
|
|
| expect(mockFs.writeFile).toHaveBeenCalledWith( |
| expectedPath, |
| expect.stringContaining('"role": "user"'), |
| ); |
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'info', |
| content: `Debug API request saved to ${expectedFilename}`, |
| }); |
| }); |
|
|
| it('should handle errors during file write', async () => { |
| const mockRequest = { contents: [] }; |
| mockGetLatestApiRequest.mockReturnValue(mockRequest); |
| mockFs.writeFile.mockRejectedValue(new Error('Write failed')); |
|
|
| const result = await debugCommand.action?.(mockContext, ''); |
|
|
| expect(result).toEqual({ |
| type: 'message', |
| messageType: 'error', |
| content: 'Error saving debug request: Write failed', |
| }); |
| }); |
| }); |
| }); |
| }); |
|
|