mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-19 02:20:42 -07:00
On resume (-r), the CLI was loading and replaying the entire session recording, including messages that had already been compressed away. For long-running Forever Mode sessions this made resume extremely slow. Add lastCompressionIndex to ConversationRecord, stamped when compression succeeds. On resume, only messages from that index onward are loaded into the client history and UI. Fully backward compatible — old sessions without the field load all messages as before.
851 lines
23 KiB
TypeScript
851 lines
23 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,
|
|
hasUserOrAssistantMessage,
|
|
convertSessionToHistoryFormats,
|
|
SessionError,
|
|
} from './sessionUtils.js';
|
|
import type {
|
|
Config,
|
|
MessageRecord,
|
|
ConversationRecord,
|
|
} from '@google/gemini-cli-core';
|
|
import { SESSION_FILE_PREFIX } from '@google/gemini-cli-core';
|
|
import { MessageType } from '../ui/types.js';
|
|
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 deduplicate sessions by ID', async () => {
|
|
const sessionId = randomUUID();
|
|
|
|
// Create test session files
|
|
const chatsDir = path.join(tmpDir, 'chats');
|
|
await fs.mkdir(chatsDir, { recursive: true });
|
|
|
|
const sessionOriginal = {
|
|
sessionId,
|
|
projectHash: 'test-hash',
|
|
startTime: '2024-01-01T10:00:00.000Z',
|
|
lastUpdated: '2024-01-01T10:30:00.000Z',
|
|
messages: [
|
|
{
|
|
type: 'user',
|
|
content: 'Original',
|
|
id: 'msg1',
|
|
timestamp: '2024-01-01T10:00:00.000Z',
|
|
},
|
|
],
|
|
};
|
|
|
|
const sessionDuplicate = {
|
|
sessionId,
|
|
projectHash: 'test-hash',
|
|
startTime: '2024-01-01T10:00:00.000Z',
|
|
lastUpdated: '2024-01-01T11:00:00.000Z', // Newer
|
|
messages: [
|
|
{
|
|
type: 'user',
|
|
content: 'Newer Duplicate',
|
|
id: 'msg1',
|
|
timestamp: '2024-01-01T10:00:00.000Z',
|
|
},
|
|
],
|
|
};
|
|
|
|
// File 1
|
|
await fs.writeFile(
|
|
path.join(
|
|
chatsDir,
|
|
`${SESSION_FILE_PREFIX}2024-01-01T10-00-${sessionId.slice(0, 8)}.json`,
|
|
),
|
|
JSON.stringify(sessionOriginal, null, 2),
|
|
);
|
|
|
|
// File 2 (Simulate a copy or newer version with same ID)
|
|
await fs.writeFile(
|
|
path.join(
|
|
chatsDir,
|
|
`${SESSION_FILE_PREFIX}2024-01-01T11-00-${sessionId.slice(0, 8)}.json`,
|
|
),
|
|
JSON.stringify(sessionDuplicate, null, 2),
|
|
);
|
|
|
|
const sessionSelector = new SessionSelector(config);
|
|
const sessions = await sessionSelector.listSessions();
|
|
|
|
expect(sessions.length).toBe(1);
|
|
expect(sessions[0].id).toBe(sessionId);
|
|
// Should keep the one with later lastUpdated
|
|
expect(sessions[0].lastUpdated).toBe('2024-01-01T11:00:00.000Z');
|
|
});
|
|
|
|
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(SessionError);
|
|
|
|
await expect(sessionSelector.resolveSession('999')).rejects.toThrow(
|
|
SessionError,
|
|
);
|
|
});
|
|
|
|
it('should throw SessionError with NO_SESSIONS_FOUND when resolving latest with no sessions', async () => {
|
|
// Empty chats directory — no session files
|
|
const chatsDir = path.join(tmpDir, 'chats');
|
|
await fs.mkdir(chatsDir, { recursive: true });
|
|
|
|
const emptyConfig = {
|
|
storage: {
|
|
getProjectTempDir: () => tmpDir,
|
|
},
|
|
getSessionId: () => 'current-session-id',
|
|
} as Partial<Config> as Config;
|
|
|
|
const sessionSelector = new SessionSelector(emptyConfig);
|
|
|
|
await expect(sessionSelector.resolveSession('latest')).rejects.toSatisfy(
|
|
(error) => {
|
|
expect(error).toBeInstanceOf(SessionError);
|
|
expect((error as SessionError).code).toBe('NO_SESSIONS_FOUND');
|
|
return true;
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should not list sessions with only system messages', async () => {
|
|
const sessionIdWithUser = randomUUID();
|
|
const sessionIdSystemOnly = randomUUID();
|
|
|
|
// Create test session files
|
|
const chatsDir = path.join(tmpDir, 'chats');
|
|
await fs.mkdir(chatsDir, { recursive: true });
|
|
|
|
// Session with user message - should be listed
|
|
const sessionWithUser = {
|
|
sessionId: sessionIdWithUser,
|
|
projectHash: 'test-hash',
|
|
startTime: '2024-01-01T10:00:00.000Z',
|
|
lastUpdated: '2024-01-01T10:30:00.000Z',
|
|
messages: [
|
|
{
|
|
type: 'user',
|
|
content: 'Hello world',
|
|
id: 'msg1',
|
|
timestamp: '2024-01-01T10:00:00.000Z',
|
|
},
|
|
],
|
|
};
|
|
|
|
// Session with only system messages - should NOT be listed
|
|
const sessionSystemOnly = {
|
|
sessionId: sessionIdSystemOnly,
|
|
projectHash: 'test-hash',
|
|
startTime: '2024-01-01T11:00:00.000Z',
|
|
lastUpdated: '2024-01-01T11:30:00.000Z',
|
|
messages: [
|
|
{
|
|
type: 'info',
|
|
content: 'Session started',
|
|
id: 'msg1',
|
|
timestamp: '2024-01-01T11:00:00.000Z',
|
|
},
|
|
{
|
|
type: 'error',
|
|
content: 'An error occurred',
|
|
id: 'msg2',
|
|
timestamp: '2024-01-01T11:01:00.000Z',
|
|
},
|
|
],
|
|
};
|
|
|
|
await fs.writeFile(
|
|
path.join(
|
|
chatsDir,
|
|
`${SESSION_FILE_PREFIX}2024-01-01T10-00-${sessionIdWithUser.slice(0, 8)}.json`,
|
|
),
|
|
JSON.stringify(sessionWithUser, null, 2),
|
|
);
|
|
|
|
await fs.writeFile(
|
|
path.join(
|
|
chatsDir,
|
|
`${SESSION_FILE_PREFIX}2024-01-01T11-00-${sessionIdSystemOnly.slice(0, 8)}.json`,
|
|
),
|
|
JSON.stringify(sessionSystemOnly, null, 2),
|
|
);
|
|
|
|
const sessionSelector = new SessionSelector(config);
|
|
const sessions = await sessionSelector.listSessions();
|
|
|
|
// Should only list the session with user message
|
|
expect(sessions.length).toBe(1);
|
|
expect(sessions[0].id).toBe(sessionIdWithUser);
|
|
});
|
|
|
|
it('should list session with gemini message even without user message', async () => {
|
|
const sessionIdGeminiOnly = randomUUID();
|
|
|
|
// Create test session files
|
|
const chatsDir = path.join(tmpDir, 'chats');
|
|
await fs.mkdir(chatsDir, { recursive: true });
|
|
|
|
// Session with only gemini message - should be listed
|
|
const sessionGeminiOnly = {
|
|
sessionId: sessionIdGeminiOnly,
|
|
projectHash: 'test-hash',
|
|
startTime: '2024-01-01T10:00:00.000Z',
|
|
lastUpdated: '2024-01-01T10:30:00.000Z',
|
|
messages: [
|
|
{
|
|
type: 'gemini',
|
|
content: 'Hello, how can I help?',
|
|
id: 'msg1',
|
|
timestamp: '2024-01-01T10:00:00.000Z',
|
|
},
|
|
],
|
|
};
|
|
|
|
await fs.writeFile(
|
|
path.join(
|
|
chatsDir,
|
|
`${SESSION_FILE_PREFIX}2024-01-01T10-00-${sessionIdGeminiOnly.slice(0, 8)}.json`,
|
|
),
|
|
JSON.stringify(sessionGeminiOnly, null, 2),
|
|
);
|
|
|
|
const sessionSelector = new SessionSelector(config);
|
|
const sessions = await sessionSelector.listSessions();
|
|
|
|
// Should list the session with gemini message
|
|
expect(sessions.length).toBe(1);
|
|
expect(sessions[0].id).toBe(sessionIdGeminiOnly);
|
|
});
|
|
|
|
it('should not list sessions marked as subagent', async () => {
|
|
const mainSessionId = randomUUID();
|
|
const subagentSessionId = randomUUID();
|
|
|
|
// Create test session files
|
|
const chatsDir = path.join(tmpDir, 'chats');
|
|
await fs.mkdir(chatsDir, { recursive: true });
|
|
|
|
// Main session - should be listed
|
|
const mainSession = {
|
|
sessionId: mainSessionId,
|
|
projectHash: 'test-hash',
|
|
startTime: '2024-01-01T10:00:00.000Z',
|
|
lastUpdated: '2024-01-01T10:30:00.000Z',
|
|
messages: [
|
|
{
|
|
type: 'user',
|
|
content: 'Hello world',
|
|
id: 'msg1',
|
|
timestamp: '2024-01-01T10:00:00.000Z',
|
|
},
|
|
],
|
|
kind: 'main',
|
|
};
|
|
|
|
// Subagent session - should NOT be listed
|
|
const subagentSession = {
|
|
sessionId: subagentSessionId,
|
|
projectHash: 'test-hash',
|
|
startTime: '2024-01-01T11:00:00.000Z',
|
|
lastUpdated: '2024-01-01T11:30:00.000Z',
|
|
messages: [
|
|
{
|
|
type: 'user',
|
|
content: 'Internal subagent task',
|
|
id: 'msg1',
|
|
timestamp: '2024-01-01T11:00:00.000Z',
|
|
},
|
|
],
|
|
kind: 'subagent',
|
|
};
|
|
|
|
await fs.writeFile(
|
|
path.join(
|
|
chatsDir,
|
|
`${SESSION_FILE_PREFIX}2024-01-01T10-00-${mainSessionId.slice(0, 8)}.json`,
|
|
),
|
|
JSON.stringify(mainSession, null, 2),
|
|
);
|
|
|
|
await fs.writeFile(
|
|
path.join(
|
|
chatsDir,
|
|
`${SESSION_FILE_PREFIX}2024-01-01T11-00-${subagentSessionId.slice(0, 8)}.json`,
|
|
),
|
|
JSON.stringify(subagentSession, null, 2),
|
|
);
|
|
|
|
const sessionSelector = new SessionSelector(config);
|
|
const sessions = await sessionSelector.listSessions();
|
|
|
|
// Should only list the main session
|
|
expect(sessions.length).toBe(1);
|
|
expect(sessions[0].id).toBe(mainSessionId);
|
|
});
|
|
});
|
|
|
|
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 not 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(longMessage);
|
|
});
|
|
|
|
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('hasUserOrAssistantMessage', () => {
|
|
it('should return true when session has user message', () => {
|
|
const messages = [
|
|
{
|
|
type: 'user',
|
|
content: 'Hello',
|
|
id: 'msg1',
|
|
timestamp: '2024-01-01T10:00:00.000Z',
|
|
},
|
|
] as MessageRecord[];
|
|
|
|
expect(hasUserOrAssistantMessage(messages)).toBe(true);
|
|
});
|
|
|
|
it('should return true when session has gemini message', () => {
|
|
const messages = [
|
|
{
|
|
type: 'gemini',
|
|
content: 'Hello, how can I help?',
|
|
id: 'msg1',
|
|
timestamp: '2024-01-01T10:00:00.000Z',
|
|
},
|
|
] as MessageRecord[];
|
|
|
|
expect(hasUserOrAssistantMessage(messages)).toBe(true);
|
|
});
|
|
|
|
it('should return true when session has both user and gemini messages', () => {
|
|
const messages = [
|
|
{
|
|
type: 'user',
|
|
content: 'Hello',
|
|
id: 'msg1',
|
|
timestamp: '2024-01-01T10:00:00.000Z',
|
|
},
|
|
{
|
|
type: 'gemini',
|
|
content: 'Hi there!',
|
|
id: 'msg2',
|
|
timestamp: '2024-01-01T10:01:00.000Z',
|
|
},
|
|
] as MessageRecord[];
|
|
|
|
expect(hasUserOrAssistantMessage(messages)).toBe(true);
|
|
});
|
|
|
|
it('should return false when session only has info messages', () => {
|
|
const messages = [
|
|
{
|
|
type: 'info',
|
|
content: 'Session started',
|
|
id: 'msg1',
|
|
timestamp: '2024-01-01T10:00:00.000Z',
|
|
},
|
|
] as MessageRecord[];
|
|
|
|
expect(hasUserOrAssistantMessage(messages)).toBe(false);
|
|
});
|
|
|
|
it('should return false when session only has error messages', () => {
|
|
const messages = [
|
|
{
|
|
type: 'error',
|
|
content: 'An error occurred',
|
|
id: 'msg1',
|
|
timestamp: '2024-01-01T10:00:00.000Z',
|
|
},
|
|
] as MessageRecord[];
|
|
|
|
expect(hasUserOrAssistantMessage(messages)).toBe(false);
|
|
});
|
|
|
|
it('should return false when session only has warning messages', () => {
|
|
const messages = [
|
|
{
|
|
type: 'warning',
|
|
content: 'Warning message',
|
|
id: 'msg1',
|
|
timestamp: '2024-01-01T10:00:00.000Z',
|
|
},
|
|
] as MessageRecord[];
|
|
|
|
expect(hasUserOrAssistantMessage(messages)).toBe(false);
|
|
});
|
|
|
|
it('should return false when session only has system messages (mixed)', () => {
|
|
const messages = [
|
|
{
|
|
type: 'info',
|
|
content: 'Session started',
|
|
id: 'msg1',
|
|
timestamp: '2024-01-01T10:00:00.000Z',
|
|
},
|
|
{
|
|
type: 'error',
|
|
content: 'An error occurred',
|
|
id: 'msg2',
|
|
timestamp: '2024-01-01T10:01:00.000Z',
|
|
},
|
|
{
|
|
type: 'warning',
|
|
content: 'Warning message',
|
|
id: 'msg3',
|
|
timestamp: '2024-01-01T10:02:00.000Z',
|
|
},
|
|
] as MessageRecord[];
|
|
|
|
expect(hasUserOrAssistantMessage(messages)).toBe(false);
|
|
});
|
|
|
|
it('should return true when session has user message among system messages', () => {
|
|
const messages = [
|
|
{
|
|
type: 'info',
|
|
content: 'Session started',
|
|
id: 'msg1',
|
|
timestamp: '2024-01-01T10:00:00.000Z',
|
|
},
|
|
{
|
|
type: 'user',
|
|
content: 'Hello',
|
|
id: 'msg2',
|
|
timestamp: '2024-01-01T10:01:00.000Z',
|
|
},
|
|
{
|
|
type: 'error',
|
|
content: 'An error occurred',
|
|
id: 'msg3',
|
|
timestamp: '2024-01-01T10:02:00.000Z',
|
|
},
|
|
] as MessageRecord[];
|
|
|
|
expect(hasUserOrAssistantMessage(messages)).toBe(true);
|
|
});
|
|
|
|
it('should return false for empty messages array', () => {
|
|
const messages: MessageRecord[] = [];
|
|
expect(hasUserOrAssistantMessage(messages)).toBe(false);
|
|
});
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|
|
|
|
describe('convertSessionToHistoryFormats', () => {
|
|
const messages: ConversationRecord['messages'] = [
|
|
{
|
|
id: '1',
|
|
type: 'user',
|
|
timestamp: '2024-01-01T10:00:00Z',
|
|
content: 'First question',
|
|
},
|
|
{
|
|
id: '2',
|
|
type: 'gemini',
|
|
timestamp: '2024-01-01T10:01:00Z',
|
|
content: 'First answer',
|
|
},
|
|
{
|
|
id: '3',
|
|
type: 'user',
|
|
timestamp: '2024-01-01T10:02:00Z',
|
|
content: 'Second question',
|
|
},
|
|
{
|
|
id: '4',
|
|
type: 'gemini',
|
|
timestamp: '2024-01-01T10:03:00Z',
|
|
content: 'Second answer',
|
|
},
|
|
];
|
|
|
|
it('should convert all messages when startIndex is undefined', () => {
|
|
const { uiHistory } = convertSessionToHistoryFormats(messages);
|
|
|
|
expect(uiHistory).toHaveLength(4);
|
|
expect(uiHistory[0]).toEqual({
|
|
type: MessageType.USER,
|
|
text: 'First question',
|
|
});
|
|
expect(uiHistory[1]).toEqual({
|
|
type: MessageType.GEMINI,
|
|
text: 'First answer',
|
|
});
|
|
expect(uiHistory[2]).toEqual({
|
|
type: MessageType.USER,
|
|
text: 'Second question',
|
|
});
|
|
expect(uiHistory[3]).toEqual({
|
|
type: MessageType.GEMINI,
|
|
text: 'Second answer',
|
|
});
|
|
});
|
|
|
|
it('should show only post-compression messages with a leading info message when startIndex is provided', () => {
|
|
const { uiHistory } = convertSessionToHistoryFormats(messages, 2);
|
|
|
|
// Should have: 1 info message + 2 remaining messages
|
|
expect(uiHistory).toHaveLength(3);
|
|
|
|
// First item is the compression info message
|
|
expect(uiHistory[0].type).toBe(MessageType.INFO);
|
|
expect((uiHistory[0] as { type: string; text: string }).text).toContain(
|
|
'2 messages',
|
|
);
|
|
expect((uiHistory[0] as { type: string; text: string }).text).toContain(
|
|
'compressed',
|
|
);
|
|
|
|
// Remaining items are the post-compression messages
|
|
expect(uiHistory[1]).toEqual({
|
|
type: MessageType.USER,
|
|
text: 'Second question',
|
|
});
|
|
expect(uiHistory[2]).toEqual({
|
|
type: MessageType.GEMINI,
|
|
text: 'Second answer',
|
|
});
|
|
});
|
|
});
|