mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-05 19:01:12 -07:00
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
592 lines
16 KiB
TypeScript
592 lines
16 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { convertSessionToHistoryFormats } from './useSessionBrowser.js';
|
|
import { MessageType, ToolCallStatus } from '../types.js';
|
|
import type { MessageRecord } from '@google/gemini-cli-core';
|
|
|
|
describe('convertSessionToHistoryFormats', () => {
|
|
it('should convert empty messages array', () => {
|
|
const result = convertSessionToHistoryFormats([]);
|
|
|
|
expect(result.uiHistory).toEqual([]);
|
|
expect(result.clientHistory).toEqual([]);
|
|
});
|
|
|
|
it('should convert basic user and gemini messages', () => {
|
|
const messages: MessageRecord[] = [
|
|
{
|
|
id: 'msg-1',
|
|
timestamp: '2025-01-01T00:01:00Z',
|
|
content: 'Hello',
|
|
type: 'user',
|
|
},
|
|
{
|
|
id: 'msg-2',
|
|
timestamp: '2025-01-01T00:02:00Z',
|
|
content: 'Hi there!',
|
|
type: 'gemini',
|
|
},
|
|
];
|
|
|
|
const result = convertSessionToHistoryFormats(messages);
|
|
|
|
expect(result.uiHistory).toHaveLength(2);
|
|
expect(result.uiHistory[0]).toEqual({
|
|
type: MessageType.USER,
|
|
text: 'Hello',
|
|
});
|
|
expect(result.uiHistory[1]).toEqual({
|
|
type: MessageType.GEMINI,
|
|
text: 'Hi there!',
|
|
});
|
|
|
|
expect(result.clientHistory).toHaveLength(2);
|
|
expect(result.clientHistory[0]).toEqual({
|
|
role: 'user',
|
|
parts: [{ text: 'Hello' }],
|
|
});
|
|
expect(result.clientHistory[1]).toEqual({
|
|
role: 'model',
|
|
parts: [{ text: 'Hi there!' }],
|
|
});
|
|
});
|
|
|
|
it('should filter out slash commands from client history', () => {
|
|
const messages: MessageRecord[] = [
|
|
{
|
|
id: 'msg-1',
|
|
timestamp: '2025-01-01T00:01:00Z',
|
|
content: '/help',
|
|
type: 'user',
|
|
},
|
|
{
|
|
id: 'msg-2',
|
|
timestamp: '2025-01-01T00:02:00Z',
|
|
content: '?quit',
|
|
type: 'user',
|
|
},
|
|
{
|
|
id: 'msg-3',
|
|
timestamp: '2025-01-01T00:03:00Z',
|
|
content: 'Regular message',
|
|
type: 'user',
|
|
},
|
|
];
|
|
|
|
const result = convertSessionToHistoryFormats(messages);
|
|
|
|
// All messages should appear in UI history
|
|
expect(result.uiHistory).toHaveLength(3);
|
|
|
|
// Only non-slash commands should appear in client history
|
|
expect(result.clientHistory).toHaveLength(1);
|
|
expect(result.clientHistory[0]).toEqual({
|
|
role: 'user',
|
|
parts: [{ text: 'Regular message' }],
|
|
});
|
|
});
|
|
|
|
it('should handle tool calls correctly', () => {
|
|
const messages: MessageRecord[] = [
|
|
{
|
|
id: 'msg-1',
|
|
timestamp: '2025-01-01T00:01:00Z',
|
|
content: "I'll help you with that.",
|
|
type: 'gemini',
|
|
toolCalls: [
|
|
{
|
|
id: 'tool-1',
|
|
name: 'bash',
|
|
displayName: 'Execute Command',
|
|
description: 'Run bash command',
|
|
args: { command: 'ls -la' },
|
|
status: 'success',
|
|
timestamp: '2025-01-01T00:01:30Z',
|
|
resultDisplay: 'total 4\ndrwxr-xr-x 2 user user 4096 Jan 1 00:00 .',
|
|
renderOutputAsMarkdown: false,
|
|
},
|
|
{
|
|
id: 'tool-2',
|
|
name: 'read',
|
|
displayName: 'Read File',
|
|
description: 'Read file contents',
|
|
args: { path: '/etc/hosts' },
|
|
status: 'error',
|
|
timestamp: '2025-01-01T00:01:45Z',
|
|
resultDisplay: 'Permission denied',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = convertSessionToHistoryFormats(messages);
|
|
|
|
expect(result.uiHistory).toHaveLength(2); // text message + tool group
|
|
expect(result.uiHistory[0]).toEqual({
|
|
type: MessageType.GEMINI,
|
|
text: "I'll help you with that.",
|
|
});
|
|
|
|
expect(result.uiHistory[1].type).toBe('tool_group');
|
|
// This if-statement is only necessary because TypeScript can't tell that the toBe() assertion
|
|
// protects the .tools access below.
|
|
if (result.uiHistory[1].type === 'tool_group') {
|
|
expect(result.uiHistory[1].tools).toHaveLength(2);
|
|
expect(result.uiHistory[1].tools[0]).toEqual({
|
|
callId: 'tool-1',
|
|
name: 'Execute Command',
|
|
description: 'Run bash command',
|
|
renderOutputAsMarkdown: false,
|
|
status: ToolCallStatus.Success,
|
|
resultDisplay: 'total 4\ndrwxr-xr-x 2 user user 4096 Jan 1 00:00 .',
|
|
confirmationDetails: undefined,
|
|
});
|
|
expect(result.uiHistory[1].tools[1]).toEqual({
|
|
callId: 'tool-2',
|
|
name: 'Read File',
|
|
description: 'Read file contents',
|
|
renderOutputAsMarkdown: true, // default value
|
|
status: ToolCallStatus.Error,
|
|
resultDisplay: 'Permission denied',
|
|
confirmationDetails: undefined,
|
|
});
|
|
}
|
|
});
|
|
|
|
it('should skip empty tool calls arrays', () => {
|
|
const messages: MessageRecord[] = [
|
|
{
|
|
id: 'msg-1',
|
|
timestamp: '2025-01-01T00:01:00Z',
|
|
content: 'Message with empty tools',
|
|
type: 'gemini',
|
|
toolCalls: [],
|
|
},
|
|
];
|
|
|
|
const result = convertSessionToHistoryFormats(messages);
|
|
|
|
expect(result.uiHistory).toHaveLength(1); // Only text message
|
|
expect(result.uiHistory[0]).toEqual({
|
|
type: MessageType.GEMINI,
|
|
text: 'Message with empty tools',
|
|
});
|
|
});
|
|
|
|
it('should not add tool calls for user messages', () => {
|
|
const messages: MessageRecord[] = [
|
|
{
|
|
id: 'msg-1',
|
|
timestamp: '2025-01-01T00:01:00Z',
|
|
content: 'User message',
|
|
type: 'user',
|
|
// This would be invalid in real usage, but testing robustness
|
|
toolCalls: [
|
|
{
|
|
id: 'tool-1',
|
|
name: 'invalid',
|
|
args: {},
|
|
status: 'success',
|
|
timestamp: '2025-01-01T00:01:30Z',
|
|
},
|
|
],
|
|
} as MessageRecord,
|
|
];
|
|
|
|
const result = convertSessionToHistoryFormats(messages);
|
|
|
|
expect(result.uiHistory).toHaveLength(1); // Only user message, no tool group
|
|
expect(result.uiHistory[0]).toEqual({
|
|
type: MessageType.USER,
|
|
text: 'User message',
|
|
});
|
|
});
|
|
|
|
it('should handle missing tool call fields gracefully', () => {
|
|
const messages: MessageRecord[] = [
|
|
{
|
|
id: 'msg-1',
|
|
timestamp: '2025-01-01T00:01:00Z',
|
|
content: 'Message with minimal tool',
|
|
type: 'gemini',
|
|
toolCalls: [
|
|
{
|
|
id: 'tool-1',
|
|
name: 'minimal_tool',
|
|
args: {},
|
|
status: 'success',
|
|
timestamp: '2025-01-01T00:01:30Z',
|
|
// Missing optional fields
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = convertSessionToHistoryFormats(messages);
|
|
|
|
expect(result.uiHistory).toHaveLength(2);
|
|
expect(result.uiHistory[1].type).toBe('tool_group');
|
|
if (result.uiHistory[1].type === 'tool_group') {
|
|
expect(result.uiHistory[1].tools[0]).toEqual({
|
|
callId: 'tool-1',
|
|
name: 'minimal_tool', // Falls back to name when displayName missing
|
|
description: '', // Default empty string
|
|
renderOutputAsMarkdown: true, // Default value
|
|
status: ToolCallStatus.Success,
|
|
resultDisplay: undefined,
|
|
confirmationDetails: undefined,
|
|
});
|
|
} else {
|
|
throw new Error('unreachable');
|
|
}
|
|
});
|
|
|
|
describe('tool calls in client history', () => {
|
|
it('should convert tool calls to correct Gemini client history format', () => {
|
|
const messages: MessageRecord[] = [
|
|
{
|
|
id: 'msg-1',
|
|
timestamp: '2025-01-01T00:01:00Z',
|
|
content: 'List files',
|
|
type: 'user',
|
|
},
|
|
{
|
|
id: 'msg-2',
|
|
timestamp: '2025-01-01T00:02:00Z',
|
|
content: "I'll list the files for you.",
|
|
type: 'gemini',
|
|
toolCalls: [
|
|
{
|
|
id: 'tool-1',
|
|
name: 'list_directory',
|
|
args: { path: '/home/user' },
|
|
result: {
|
|
functionResponse: {
|
|
id: 'list_directory-1753650620141-f3b8b9e73919d',
|
|
name: 'list_directory',
|
|
response: {
|
|
output: 'file1.txt\nfile2.txt',
|
|
},
|
|
},
|
|
},
|
|
status: 'success',
|
|
timestamp: '2025-01-01T00:02:30Z',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = convertSessionToHistoryFormats(messages);
|
|
|
|
// Should have: user message, model with function call, user with function response
|
|
expect(result.clientHistory).toHaveLength(3);
|
|
|
|
// User message
|
|
expect(result.clientHistory[0]).toEqual({
|
|
role: 'user',
|
|
parts: [{ text: 'List files' }],
|
|
});
|
|
|
|
// Model message with function call
|
|
expect(result.clientHistory[1]).toEqual({
|
|
role: 'model',
|
|
parts: [
|
|
{ text: "I'll list the files for you." },
|
|
{
|
|
functionCall: {
|
|
name: 'list_directory',
|
|
args: { path: '/home/user' },
|
|
id: 'tool-1',
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
// Function response
|
|
expect(result.clientHistory[2]).toEqual({
|
|
role: 'user',
|
|
parts: [
|
|
{
|
|
functionResponse: {
|
|
id: 'list_directory-1753650620141-f3b8b9e73919d',
|
|
name: 'list_directory',
|
|
response: { output: 'file1.txt\nfile2.txt' },
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should handle tool calls without text content', () => {
|
|
const messages: MessageRecord[] = [
|
|
{
|
|
id: 'msg-1',
|
|
timestamp: '2025-01-01T00:01:00Z',
|
|
content: '',
|
|
type: 'gemini',
|
|
toolCalls: [
|
|
{
|
|
id: 'tool-1',
|
|
name: 'bash',
|
|
args: { command: 'ls' },
|
|
result: 'file1.txt\nfile2.txt',
|
|
status: 'success',
|
|
timestamp: '2025-01-01T00:01:30Z',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = convertSessionToHistoryFormats(messages);
|
|
|
|
expect(result.clientHistory).toHaveLength(2);
|
|
|
|
// Model message with only function call (no text)
|
|
expect(result.clientHistory[0]).toEqual({
|
|
role: 'model',
|
|
parts: [
|
|
{
|
|
functionCall: {
|
|
name: 'bash',
|
|
args: { command: 'ls' },
|
|
id: 'tool-1',
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
// Function response
|
|
expect(result.clientHistory[1]).toEqual({
|
|
role: 'user',
|
|
parts: [
|
|
{
|
|
functionResponse: {
|
|
id: 'tool-1',
|
|
name: 'bash',
|
|
response: {
|
|
output: 'file1.txt\nfile2.txt',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should handle multiple tool calls in one message', () => {
|
|
const messages: MessageRecord[] = [
|
|
{
|
|
id: 'msg-1',
|
|
timestamp: '2025-01-01T00:01:00Z',
|
|
content: 'Running multiple commands',
|
|
type: 'gemini',
|
|
toolCalls: [
|
|
{
|
|
id: 'tool-1',
|
|
name: 'bash',
|
|
args: { command: 'pwd' },
|
|
result: '/home/user',
|
|
status: 'success',
|
|
timestamp: '2025-01-01T00:01:30Z',
|
|
},
|
|
{
|
|
id: 'tool-2',
|
|
name: 'bash',
|
|
args: { command: 'ls' },
|
|
result: [
|
|
{
|
|
functionResponse: {
|
|
id: 'tool-2',
|
|
name: 'bash',
|
|
response: {
|
|
output: 'file1.txt',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
functionResponse: {
|
|
id: 'tool-2',
|
|
name: 'bash',
|
|
response: {
|
|
output: 'file2.txt',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
status: 'success',
|
|
timestamp: '2025-01-01T00:01:35Z',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = convertSessionToHistoryFormats(messages);
|
|
|
|
// Should have: model with both function calls, then one response
|
|
expect(result.clientHistory).toHaveLength(2);
|
|
|
|
// Model message with both function calls
|
|
expect(result.clientHistory[0]).toEqual({
|
|
role: 'model',
|
|
parts: [
|
|
{ text: 'Running multiple commands' },
|
|
{
|
|
functionCall: {
|
|
name: 'bash',
|
|
args: { command: 'pwd' },
|
|
id: 'tool-1',
|
|
},
|
|
},
|
|
{
|
|
functionCall: {
|
|
name: 'bash',
|
|
args: { command: 'ls' },
|
|
id: 'tool-2',
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
// First function response
|
|
expect(result.clientHistory[1]).toEqual({
|
|
role: 'user',
|
|
parts: [
|
|
{
|
|
functionResponse: {
|
|
id: 'tool-1',
|
|
name: 'bash',
|
|
response: { output: '/home/user' },
|
|
},
|
|
},
|
|
{
|
|
functionResponse: {
|
|
id: 'tool-2',
|
|
name: 'bash',
|
|
response: { output: 'file1.txt' },
|
|
},
|
|
},
|
|
{
|
|
functionResponse: {
|
|
id: 'tool-2',
|
|
name: 'bash',
|
|
response: { output: 'file2.txt' },
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should handle Part array results from tools', () => {
|
|
const messages: MessageRecord[] = [
|
|
{
|
|
id: 'msg-1',
|
|
timestamp: '2025-01-01T00:01:00Z',
|
|
content: 'Reading file',
|
|
type: 'gemini',
|
|
toolCalls: [
|
|
{
|
|
id: 'tool-1',
|
|
name: 'read_file',
|
|
args: { path: 'test.txt' },
|
|
result: [
|
|
{
|
|
functionResponse: {
|
|
id: 'tool-1',
|
|
name: 'read_file',
|
|
response: {
|
|
output: 'Hello',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
functionResponse: {
|
|
id: 'tool-1',
|
|
name: 'read_file',
|
|
response: {
|
|
output: ' World',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
status: 'success',
|
|
timestamp: '2025-01-01T00:01:30Z',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = convertSessionToHistoryFormats(messages);
|
|
|
|
expect(result.clientHistory).toHaveLength(2);
|
|
|
|
// Function response should extract both function responses
|
|
expect(result.clientHistory[1]).toEqual({
|
|
role: 'user',
|
|
parts: [
|
|
{
|
|
functionResponse: {
|
|
id: 'tool-1',
|
|
name: 'read_file',
|
|
response: {
|
|
output: 'Hello',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
functionResponse: {
|
|
id: 'tool-1',
|
|
name: 'read_file',
|
|
response: {
|
|
output: ' World',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should skip tool calls without results', () => {
|
|
const messages: MessageRecord[] = [
|
|
{
|
|
id: 'msg-1',
|
|
timestamp: '2025-01-01T00:01:00Z',
|
|
content: 'Testing tool',
|
|
type: 'gemini',
|
|
toolCalls: [
|
|
{
|
|
id: 'tool-1',
|
|
name: 'test_tool',
|
|
args: { arg: 'value' },
|
|
// No result field
|
|
status: 'error',
|
|
timestamp: '2025-01-01T00:01:30Z',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = convertSessionToHistoryFormats(messages);
|
|
|
|
// Should only have the model message with function call, no function response
|
|
expect(result.clientHistory).toHaveLength(1);
|
|
|
|
expect(result.clientHistory[0]).toEqual({
|
|
role: 'model',
|
|
parts: [
|
|
{ text: 'Testing tool' },
|
|
{
|
|
functionCall: {
|
|
name: 'test_tool',
|
|
args: { arg: 'value' },
|
|
id: 'tool-1',
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
});
|