Files
gemini-cli/packages/cli/src/ui/hooks/useSessionBrowser.test.ts
2026-03-07 04:20:27 +00:00

335 lines
9.5 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook } from '../../test-utils/render.js';
import { act } from 'react';
import {
useSessionBrowser,
convertSessionToHistoryFormats,
} from './useSessionBrowser.js';
import * as fs from 'node:fs/promises';
import path from 'node:path';
import { getSessionFiles, type SessionInfo } from '../../utils/sessionUtils.js';
import {
type Config,
type ConversationRecord,
type MessageRecord,
CoreToolCallStatus,
} from '@google/gemini-cli-core';
import {
coreEvents,
convertSessionToClientHistory,
} from '@google/gemini-cli-core';
// Mock modules
vi.mock('fs/promises');
vi.mock('path');
vi.mock('../../utils/sessionUtils.js', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../../utils/sessionUtils.js')>();
return {
...actual,
getSessionFiles: vi.fn(),
};
});
const MOCKED_PROJECT_TEMP_DIR = '/test/project/temp';
const MOCKED_CHATS_DIR = '/test/project/temp/chats';
const MOCKED_SESSION_ID = 'test-session-123';
const MOCKED_CURRENT_SESSION_ID = 'current-session-id';
describe('useSessionBrowser', () => {
const mockedFs = vi.mocked(fs);
const mockedPath = vi.mocked(path);
const mockedGetSessionFiles = vi.mocked(getSessionFiles);
const mockConfig = {
storage: {
getProjectTempDir: vi.fn(),
},
setSessionId: vi.fn(),
getSessionId: vi.fn(),
getGeminiClient: vi.fn().mockReturnValue({
getChatRecordingService: vi.fn().mockReturnValue({
deleteSession: vi.fn(),
}),
}),
} as unknown as Config;
const mockOnLoadHistory = vi.fn();
beforeEach(() => {
vi.resetAllMocks();
vi.spyOn(coreEvents, 'emitFeedback').mockImplementation(() => {});
mockedPath.join.mockImplementation((...args) => args.join('/'));
vi.mocked(mockConfig.storage.getProjectTempDir).mockReturnValue(
MOCKED_PROJECT_TEMP_DIR,
);
vi.mocked(mockConfig.getSessionId).mockReturnValue(
MOCKED_CURRENT_SESSION_ID,
);
});
it('should successfully resume a session', async () => {
const MOCKED_FILENAME = 'session-2025-01-01-test-session-123.json';
const mockConversation: ConversationRecord = {
sessionId: 'existing-session-456',
messages: [{ type: 'user', content: 'Hello' } as MessageRecord],
} as ConversationRecord;
const mockSession = {
id: MOCKED_SESSION_ID,
fileName: MOCKED_FILENAME,
} as SessionInfo;
mockedGetSessionFiles.mockResolvedValue([mockSession]);
mockedFs.readFile.mockResolvedValue(JSON.stringify(mockConversation));
const { result } = renderHook(() =>
useSessionBrowser(mockConfig, mockOnLoadHistory),
);
await act(async () => {
await result.current.handleResumeSession(mockSession);
});
expect(mockedFs.readFile).toHaveBeenCalledWith(
`${MOCKED_CHATS_DIR}/${MOCKED_FILENAME}`,
'utf8',
);
expect(mockConfig.setSessionId).toHaveBeenCalledWith(
'existing-session-456',
);
expect(result.current.isSessionBrowserOpen).toBe(false);
expect(mockOnLoadHistory).toHaveBeenCalled();
});
it('should handle file read error', async () => {
const MOCKED_FILENAME = 'session-2025-01-01-test-session-123.json';
const mockSession = {
id: MOCKED_SESSION_ID,
fileName: MOCKED_FILENAME,
} as SessionInfo;
mockedFs.readFile.mockRejectedValue(new Error('File not found'));
const { result } = renderHook(() =>
useSessionBrowser(mockConfig, mockOnLoadHistory),
);
await act(async () => {
await result.current.handleResumeSession(mockSession);
});
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'error',
'Error resuming session:',
expect.any(Error),
);
expect(result.current.isSessionBrowserOpen).toBe(false);
});
it('should handle JSON parse error', async () => {
const MOCKED_FILENAME = 'invalid.json';
const mockSession = {
id: MOCKED_SESSION_ID,
fileName: MOCKED_FILENAME,
} as SessionInfo;
mockedFs.readFile.mockResolvedValue('invalid json');
const { result } = renderHook(() =>
useSessionBrowser(mockConfig, mockOnLoadHistory),
);
await act(async () => {
await result.current.handleResumeSession(mockSession);
});
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'error',
'Error resuming session:',
expect.any(Error),
);
expect(result.current.isSessionBrowserOpen).toBe(false);
});
});
// The convertSessionToHistoryFormats tests are self-contained and do not need changes.
describe('convertSessionToHistoryFormats', () => {
it('should convert empty messages array', () => {
const result = convertSessionToHistoryFormats([]);
expect(result.uiHistory).toEqual([]);
expect(convertSessionToClientHistory([])).toEqual([]);
});
it('should convert basic user and model messages', () => {
const messages: MessageRecord[] = [
{ type: 'user', content: 'Hello' } as MessageRecord,
{ type: 'gemini', content: 'Hi there' } as MessageRecord,
];
const result = convertSessionToHistoryFormats(messages);
expect(result.uiHistory).toHaveLength(2);
expect(result.uiHistory[0]).toMatchObject({ type: 'user', text: 'Hello' });
expect(result.uiHistory[1]).toMatchObject({
type: 'gemini',
text: 'Hi there',
});
const clientHistory = convertSessionToClientHistory(messages);
expect(clientHistory).toHaveLength(2);
expect(clientHistory[0]).toEqual({
role: 'user',
parts: [{ text: 'Hello' }],
});
expect(clientHistory[1]).toEqual({
role: 'model',
parts: [{ text: 'Hi there' }],
});
});
it('should convert thinking tokens (thoughts) to thinking history items', () => {
const messages: MessageRecord[] = [
{
type: 'gemini',
content: 'Hi there',
thoughts: [
{
subject: 'Thinking...',
description: 'I should say hello.',
timestamp: new Date().toISOString(),
},
],
} as MessageRecord,
];
const result = convertSessionToHistoryFormats(messages);
expect(result.uiHistory).toHaveLength(2);
expect(result.uiHistory[0]).toMatchObject({
type: 'thinking',
thought: {
subject: 'Thinking...',
description: 'I should say hello.',
},
});
expect(result.uiHistory[1]).toMatchObject({
type: 'gemini',
text: 'Hi there',
});
});
it('should prioritize displayContent for UI history but use content for client history', () => {
const messages: MessageRecord[] = [
{
type: 'user',
content: [{ text: 'Expanded content' }],
displayContent: [{ text: 'User input' }],
} as MessageRecord,
];
const result = convertSessionToHistoryFormats(messages);
expect(result.uiHistory).toHaveLength(1);
expect(result.uiHistory[0]).toMatchObject({
type: 'user',
text: 'User input',
});
const clientHistory = convertSessionToClientHistory(messages);
expect(clientHistory).toHaveLength(1);
expect(clientHistory[0]).toEqual({
role: 'user',
parts: [{ text: 'Expanded content' }],
});
});
it('should filter out slash commands from client history but keep in UI', () => {
const messages: MessageRecord[] = [
{ type: 'user', content: '/help' } as MessageRecord,
{ type: 'info', content: 'Help text' } as MessageRecord,
];
const result = convertSessionToHistoryFormats(messages);
expect(result.uiHistory).toHaveLength(2);
expect(result.uiHistory[0]).toMatchObject({ type: 'user', text: '/help' });
expect(result.uiHistory[1]).toMatchObject({
type: 'info',
text: 'Help text',
});
expect(convertSessionToClientHistory(messages)).toHaveLength(0);
});
it('should handle tool calls and responses', () => {
const messages: MessageRecord[] = [
{ type: 'user', content: 'What time is it?' } as MessageRecord,
{
type: 'gemini',
content: '',
toolCalls: [
{
id: 'call_1',
name: 'get_time',
args: {},
status: CoreToolCallStatus.Success,
result: '12:00',
},
],
} as unknown as MessageRecord,
];
const result = convertSessionToHistoryFormats(messages);
expect(result.uiHistory).toHaveLength(2);
expect(result.uiHistory[0]).toMatchObject({
type: 'user',
text: 'What time is it?',
});
expect(result.uiHistory[1]).toMatchObject({
type: 'tool_group',
tools: [
expect.objectContaining({
callId: 'call_1',
name: 'get_time',
status: CoreToolCallStatus.Success,
}),
],
});
const clientHistory = convertSessionToClientHistory(messages);
expect(clientHistory).toHaveLength(3); // User, Model (call), User (response)
expect(clientHistory[0]).toEqual({
role: 'user',
parts: [{ text: 'What time is it?' }],
});
expect(clientHistory[1]).toEqual({
role: 'model',
parts: [
{
functionCall: {
name: 'get_time',
args: {},
id: 'call_1',
},
},
],
});
expect(clientHistory[2]).toEqual({
role: 'user',
parts: [
{
functionResponse: {
id: 'call_1',
name: 'get_time',
response: { output: '12:00' },
},
},
],
});
});
});