Files
gemini-cli/packages/cli/src/utils/sessionUtils.test.ts
bl-ue 6893d27441 feat(sessions): add resuming to geminiChat and add CLI flags for session management (#10719)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-11 01:31:00 +00:00

364 lines
9.9 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
SessionSelector,
extractFirstUserMessage,
formatRelativeTime,
} from './sessionUtils.js';
import type { Config, MessageRecord } from '@google/gemini-cli-core';
import { SESSION_FILE_PREFIX } from '@google/gemini-cli-core';
import * as fs from 'node:fs/promises';
import path from 'node:path';
import { randomUUID } from 'node:crypto';
describe('SessionSelector', () => {
let tmpDir: string;
let config: Config;
beforeEach(async () => {
// Create a temporary directory for testing
tmpDir = path.join(process.cwd(), '.tmp-test-sessions');
await fs.mkdir(tmpDir, { recursive: true });
// Mock config
config = {
storage: {
getProjectTempDir: () => tmpDir,
},
getSessionId: () => 'current-session-id',
} as Partial<Config> as Config;
});
afterEach(async () => {
// Clean up test files
try {
await fs.rm(tmpDir, { recursive: true, force: true });
} catch (_error) {
// Ignore cleanup errors
}
});
it('should resolve session by UUID', async () => {
const sessionId1 = randomUUID();
const sessionId2 = randomUUID();
// Create test session files
const chatsDir = path.join(tmpDir, 'chats');
await fs.mkdir(chatsDir, { recursive: true });
const session1 = {
sessionId: sessionId1,
projectHash: 'test-hash',
startTime: '2024-01-01T10:00:00.000Z',
lastUpdated: '2024-01-01T10:30:00.000Z',
messages: [
{
type: 'user',
content: 'Test message 1',
id: 'msg1',
timestamp: '2024-01-01T10:00:00.000Z',
},
],
};
const session2 = {
sessionId: sessionId2,
projectHash: 'test-hash',
startTime: '2024-01-01T11:00:00.000Z',
lastUpdated: '2024-01-01T11:30:00.000Z',
messages: [
{
type: 'user',
content: 'Test message 2',
id: 'msg2',
timestamp: '2024-01-01T11:00:00.000Z',
},
],
};
await fs.writeFile(
path.join(
chatsDir,
`${SESSION_FILE_PREFIX}2024-01-01T10-00-${sessionId1.slice(0, 8)}.json`,
),
JSON.stringify(session1, null, 2),
);
await fs.writeFile(
path.join(
chatsDir,
`${SESSION_FILE_PREFIX}2024-01-01T11-00-${sessionId2.slice(0, 8)}.json`,
),
JSON.stringify(session2, null, 2),
);
const sessionSelector = new SessionSelector(config);
// Test resolving by UUID
const result1 = await sessionSelector.resolveSession(sessionId1);
expect(result1.sessionData.sessionId).toBe(sessionId1);
expect(result1.sessionData.messages[0].content).toBe('Test message 1');
const result2 = await sessionSelector.resolveSession(sessionId2);
expect(result2.sessionData.sessionId).toBe(sessionId2);
expect(result2.sessionData.messages[0].content).toBe('Test message 2');
});
it('should resolve session by index', async () => {
const sessionId1 = randomUUID();
const sessionId2 = randomUUID();
// Create test session files
const chatsDir = path.join(tmpDir, 'chats');
await fs.mkdir(chatsDir, { recursive: true });
const session1 = {
sessionId: sessionId1,
projectHash: 'test-hash',
startTime: '2024-01-01T10:00:00.000Z',
lastUpdated: '2024-01-01T10:30:00.000Z',
messages: [
{
type: 'user',
content: 'First session',
id: 'msg1',
timestamp: '2024-01-01T10:00:00.000Z',
},
],
};
const session2 = {
sessionId: sessionId2,
projectHash: 'test-hash',
startTime: '2024-01-01T11:00:00.000Z',
lastUpdated: '2024-01-01T11:30:00.000Z',
messages: [
{
type: 'user',
content: 'Second session',
id: 'msg2',
timestamp: '2024-01-01T11:00:00.000Z',
},
],
};
await fs.writeFile(
path.join(
chatsDir,
`${SESSION_FILE_PREFIX}2024-01-01T10-00-${sessionId1.slice(0, 8)}.json`,
),
JSON.stringify(session1, null, 2),
);
await fs.writeFile(
path.join(
chatsDir,
`${SESSION_FILE_PREFIX}2024-01-01T11-00-${sessionId2.slice(0, 8)}.json`,
),
JSON.stringify(session2, null, 2),
);
const sessionSelector = new SessionSelector(config);
// Test resolving by index (1-based)
const result1 = await sessionSelector.resolveSession('1');
expect(result1.sessionData.messages[0].content).toBe('First session');
const result2 = await sessionSelector.resolveSession('2');
expect(result2.sessionData.messages[0].content).toBe('Second session');
});
it('should resolve latest session', async () => {
const sessionId1 = randomUUID();
const sessionId2 = randomUUID();
// Create test session files
const chatsDir = path.join(tmpDir, 'chats');
await fs.mkdir(chatsDir, { recursive: true });
const session1 = {
sessionId: sessionId1,
projectHash: 'test-hash',
startTime: '2024-01-01T10:00:00.000Z',
lastUpdated: '2024-01-01T10:30:00.000Z',
messages: [
{
type: 'user',
content: 'First session',
id: 'msg1',
timestamp: '2024-01-01T10:00:00.000Z',
},
],
};
const session2 = {
sessionId: sessionId2,
projectHash: 'test-hash',
startTime: '2024-01-01T11:00:00.000Z',
lastUpdated: '2024-01-01T11:30:00.000Z',
messages: [
{
type: 'user',
content: 'Latest session',
id: 'msg2',
timestamp: '2024-01-01T11:00:00.000Z',
},
],
};
await fs.writeFile(
path.join(
chatsDir,
`${SESSION_FILE_PREFIX}2024-01-01T10-00-${sessionId1.slice(0, 8)}.json`,
),
JSON.stringify(session1, null, 2),
);
await fs.writeFile(
path.join(
chatsDir,
`${SESSION_FILE_PREFIX}2024-01-01T11-00-${sessionId2.slice(0, 8)}.json`,
),
JSON.stringify(session2, null, 2),
);
const sessionSelector = new SessionSelector(config);
// Test resolving latest
const result = await sessionSelector.resolveSession('latest');
expect(result.sessionData.messages[0].content).toBe('Latest session');
});
it('should throw error for invalid session identifier', async () => {
const sessionId1 = randomUUID();
// Create test session files
const chatsDir = path.join(tmpDir, 'chats');
await fs.mkdir(chatsDir, { recursive: true });
const session1 = {
sessionId: sessionId1,
projectHash: 'test-hash',
startTime: '2024-01-01T10:00:00.000Z',
lastUpdated: '2024-01-01T10:30:00.000Z',
messages: [
{
type: 'user',
content: 'Test message 1',
id: 'msg1',
timestamp: '2024-01-01T10:00:00.000Z',
},
],
};
await fs.writeFile(
path.join(
chatsDir,
`${SESSION_FILE_PREFIX}2024-01-01T10-00-${sessionId1.slice(0, 8)}.json`,
),
JSON.stringify(session1, null, 2),
);
const sessionSelector = new SessionSelector(config);
await expect(
sessionSelector.resolveSession('invalid-uuid'),
).rejects.toThrow('Invalid session identifier "invalid-uuid"');
await expect(sessionSelector.resolveSession('999')).rejects.toThrow(
'Invalid session identifier "999"',
);
});
});
describe('extractFirstUserMessage', () => {
it('should extract first non-resume user message', () => {
const messages = [
{
type: 'user',
content: '/resume',
id: 'msg1',
timestamp: '2024-01-01T10:00:00.000Z',
},
{
type: 'user',
content: 'Hello world',
id: 'msg2',
timestamp: '2024-01-01T10:01:00.000Z',
},
] as MessageRecord[];
expect(extractFirstUserMessage(messages)).toBe('Hello world');
});
it('should truncate long messages', () => {
const longMessage = 'a'.repeat(150);
const messages = [
{
type: 'user',
content: longMessage,
id: 'msg1',
timestamp: '2024-01-01T10:00:00.000Z',
},
] as MessageRecord[];
const result = extractFirstUserMessage(messages);
expect(result).toBe('a'.repeat(97) + '...');
expect(result.length).toBe(100);
});
it('should return "Empty conversation" for no user messages', () => {
const messages = [
{
type: 'gemini',
content: 'Hello',
id: 'msg1',
timestamp: '2024-01-01T10:00:00.000Z',
},
] as MessageRecord[];
expect(extractFirstUserMessage(messages)).toBe('Empty conversation');
});
});
describe('formatRelativeTime', () => {
it('should format time correctly', () => {
const now = new Date();
// 5 minutes ago
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000);
expect(formatRelativeTime(fiveMinutesAgo.toISOString())).toBe(
'5 minutes ago',
);
// 1 minute ago
const oneMinuteAgo = new Date(now.getTime() - 1 * 60 * 1000);
expect(formatRelativeTime(oneMinuteAgo.toISOString())).toBe('1 minute ago');
// 2 hours ago
const twoHoursAgo = new Date(now.getTime() - 2 * 60 * 60 * 1000);
expect(formatRelativeTime(twoHoursAgo.toISOString())).toBe('2 hours ago');
// 1 hour ago
const oneHourAgo = new Date(now.getTime() - 1 * 60 * 60 * 1000);
expect(formatRelativeTime(oneHourAgo.toISOString())).toBe('1 hour ago');
// 3 days ago
const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000);
expect(formatRelativeTime(threeDaysAgo.toISOString())).toBe('3 days ago');
// 1 day ago
const oneDayAgo = new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000);
expect(formatRelativeTime(oneDayAgo.toISOString())).toBe('1 day ago');
// Just now (within 60 seconds)
const thirtySecondsAgo = new Date(now.getTime() - 30 * 1000);
expect(formatRelativeTime(thirtySecondsAgo.toISOString())).toBe('Just now');
});
});