mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
@@ -22,6 +22,7 @@ import type { Settings } from './settings.js';
|
||||
import * as ServerConfig from '@google/gemini-cli-core';
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
import { ExtensionManager } from './extension-manager.js';
|
||||
import { RESUME_LATEST } from '../utils/sessionUtils.js';
|
||||
|
||||
vi.mock('./trustedFolders.js', () => ({
|
||||
isWorkspaceTrusted: vi
|
||||
@@ -482,6 +483,20 @@ describe('parseArguments', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should return RESUME_LATEST constant when --resume is passed without a value', async () => {
|
||||
const originalIsTTY = process.stdin.isTTY;
|
||||
process.stdin.isTTY = true; // Make it interactive to avoid validation error
|
||||
process.argv = ['node', 'script.js', '--resume'];
|
||||
|
||||
try {
|
||||
const argv = await parseArguments({} as Settings);
|
||||
expect(argv.resume).toBe(RESUME_LATEST);
|
||||
expect(argv.resume).toBe('latest');
|
||||
} finally {
|
||||
process.stdin.isTTY = originalIsTTY;
|
||||
}
|
||||
});
|
||||
|
||||
it('should support comma-separated values for --allowed-tools', async () => {
|
||||
process.argv = [
|
||||
'node',
|
||||
|
||||
@@ -37,6 +37,7 @@ import { getCliVersion } from '../utils/version.js';
|
||||
import { loadSandboxConfig } from './sandboxConfig.js';
|
||||
import { resolvePath } from '../utils/resolvePath.js';
|
||||
import { appEvents } from '../utils/events.js';
|
||||
import { RESUME_LATEST } from '../utils/sessionUtils.js';
|
||||
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
import { createPolicyEngineConfig } from './policy.js';
|
||||
@@ -61,7 +62,7 @@ export interface CliArgs {
|
||||
experimentalAcp: boolean | undefined;
|
||||
extensions: string[] | undefined;
|
||||
listExtensions: boolean | undefined;
|
||||
resume: string | 'latest' | undefined;
|
||||
resume: string | typeof RESUME_LATEST | undefined;
|
||||
listSessions: boolean | undefined;
|
||||
deleteSession: string | undefined;
|
||||
includeDirectories: string[] | undefined;
|
||||
@@ -189,7 +190,7 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
// When --resume not passed at all: this `coerce` function is not called at all, and
|
||||
// `yargsInstance.argv.resume` is undefined.
|
||||
if (value === '') {
|
||||
return 'latest';
|
||||
return RESUME_LATEST;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
@@ -16,6 +16,12 @@ import {
|
||||
import * as fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
/**
|
||||
* Constant for the resume "latest" identifier.
|
||||
* Used when --resume is passed without a value to select the most recent session.
|
||||
*/
|
||||
export const RESUME_LATEST = 'latest';
|
||||
|
||||
/**
|
||||
* Session information for display and selection purposes.
|
||||
*/
|
||||
@@ -267,7 +273,7 @@ export class SessionSelector {
|
||||
async resolveSession(resumeArg: string): Promise<SessionSelectionResult> {
|
||||
let selectedSession: SessionInfo;
|
||||
|
||||
if (resumeArg === 'latest') {
|
||||
if (resumeArg === RESUME_LATEST) {
|
||||
const sessions = await this.listSessions();
|
||||
|
||||
if (sessions.length === 0) {
|
||||
|
||||
667
packages/cli/src/utils/sessions.test.ts
Normal file
667
packages/cli/src/utils/sessions.test.ts
Normal file
@@ -0,0 +1,667 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { ChatRecordingService } from '@google/gemini-cli-core';
|
||||
import { listSessions, deleteSession } from './sessions.js';
|
||||
import { SessionSelector, type SessionInfo } from './sessionUtils.js';
|
||||
|
||||
// Mock the SessionSelector and ChatRecordingService
|
||||
vi.mock('./sessionUtils.js', () => ({
|
||||
SessionSelector: vi.fn(),
|
||||
formatRelativeTime: vi.fn(() => 'some time ago'),
|
||||
}));
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async () => {
|
||||
const actual = await vi.importActual('@google/gemini-cli-core');
|
||||
return {
|
||||
...actual,
|
||||
ChatRecordingService: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('listSessions', () => {
|
||||
let mockConfig: Config;
|
||||
let mockListSessions: ReturnType<typeof vi.fn>;
|
||||
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock config
|
||||
mockConfig = {
|
||||
storage: {
|
||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/test-project'),
|
||||
},
|
||||
getSessionId: vi.fn().mockReturnValue('current-session-id'),
|
||||
} as unknown as Config;
|
||||
|
||||
// Create mock listSessions method
|
||||
mockListSessions = vi.fn();
|
||||
|
||||
// Mock SessionSelector constructor to return object with listSessions method
|
||||
vi.mocked(SessionSelector).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
listSessions: mockListSessions,
|
||||
}) as unknown as InstanceType<typeof SessionSelector>,
|
||||
);
|
||||
|
||||
// Spy on console.log
|
||||
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
consoleLogSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should display message when no previous sessions were found', async () => {
|
||||
// Arrange: Return empty array from listSessions
|
||||
mockListSessions.mockResolvedValue([]);
|
||||
|
||||
// Act
|
||||
await listSessions(mockConfig);
|
||||
|
||||
// Assert
|
||||
expect(mockListSessions).toHaveBeenCalledOnce();
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'No previous sessions found for this project.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should list sessions when sessions are found', async () => {
|
||||
// Arrange: Create test sessions
|
||||
const now = new Date('2025-01-20T12:00:00.000Z');
|
||||
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
||||
const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const mockSessions: SessionInfo[] = [
|
||||
{
|
||||
id: 'session-1',
|
||||
file: 'session-2025-01-18T12-00-00-session-1',
|
||||
fileName: 'session-2025-01-18T12-00-00-session-1.json',
|
||||
startTime: twoDaysAgo.toISOString(),
|
||||
lastUpdated: twoDaysAgo.toISOString(),
|
||||
firstUserMessage: 'First user message',
|
||||
isCurrentSession: false,
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
id: 'session-2',
|
||||
file: 'session-2025-01-20T11-00-00-session-2',
|
||||
fileName: 'session-2025-01-20T11-00-00-session-2.json',
|
||||
startTime: oneHourAgo.toISOString(),
|
||||
lastUpdated: oneHourAgo.toISOString(),
|
||||
firstUserMessage: 'Second user message',
|
||||
isCurrentSession: false,
|
||||
index: 2,
|
||||
},
|
||||
{
|
||||
id: 'current-session-id',
|
||||
file: 'session-2025-01-20T12-00-00-current-s',
|
||||
fileName: 'session-2025-01-20T12-00-00-current-s.json',
|
||||
startTime: now.toISOString(),
|
||||
lastUpdated: now.toISOString(),
|
||||
firstUserMessage: 'Current session',
|
||||
isCurrentSession: true,
|
||||
index: 3,
|
||||
},
|
||||
];
|
||||
|
||||
mockListSessions.mockResolvedValue(mockSessions);
|
||||
|
||||
// Act
|
||||
await listSessions(mockConfig);
|
||||
|
||||
// Assert
|
||||
expect(mockListSessions).toHaveBeenCalledOnce();
|
||||
|
||||
// Check that the header was displayed
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'\nAvailable sessions for this project (3):\n',
|
||||
);
|
||||
|
||||
// Check that each session was logged
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('1. First user message'),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[session-1]'),
|
||||
);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('2. Second user message'),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[session-2]'),
|
||||
);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('3. Current session'),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(', current)'),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[current-session-id]'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should sort sessions by start time (oldest first)', async () => {
|
||||
// Arrange: Create sessions in non-chronological order
|
||||
const session1Time = new Date('2025-01-18T12:00:00.000Z');
|
||||
const session2Time = new Date('2025-01-19T12:00:00.000Z');
|
||||
const session3Time = new Date('2025-01-20T12:00:00.000Z');
|
||||
|
||||
const mockSessions: SessionInfo[] = [
|
||||
{
|
||||
id: 'session-2',
|
||||
file: 'session-2',
|
||||
fileName: 'session-2.json',
|
||||
startTime: session2Time.toISOString(), // Middle
|
||||
lastUpdated: session2Time.toISOString(),
|
||||
firstUserMessage: 'Middle session',
|
||||
isCurrentSession: false,
|
||||
index: 2,
|
||||
},
|
||||
{
|
||||
id: 'session-1',
|
||||
file: 'session-1',
|
||||
fileName: 'session-1.json',
|
||||
startTime: session1Time.toISOString(), // Oldest
|
||||
lastUpdated: session1Time.toISOString(),
|
||||
firstUserMessage: 'Oldest session',
|
||||
isCurrentSession: false,
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
id: 'session-3',
|
||||
file: 'session-3',
|
||||
fileName: 'session-3.json',
|
||||
startTime: session3Time.toISOString(), // Newest
|
||||
lastUpdated: session3Time.toISOString(),
|
||||
firstUserMessage: 'Newest session',
|
||||
isCurrentSession: false,
|
||||
index: 3,
|
||||
},
|
||||
];
|
||||
|
||||
mockListSessions.mockResolvedValue(mockSessions);
|
||||
|
||||
// Act
|
||||
await listSessions(mockConfig);
|
||||
|
||||
// Assert
|
||||
// Get all the session log calls (skip the header)
|
||||
const sessionCalls = consoleLogSpy.mock.calls.filter(
|
||||
(call): call is [string] =>
|
||||
typeof call[0] === 'string' &&
|
||||
call[0].includes('[session-') &&
|
||||
!call[0].includes('Available sessions'),
|
||||
);
|
||||
|
||||
// Verify they are sorted by start time (oldest first)
|
||||
expect(sessionCalls[0][0]).toContain('1. Oldest session');
|
||||
expect(sessionCalls[1][0]).toContain('2. Middle session');
|
||||
expect(sessionCalls[2][0]).toContain('3. Newest session');
|
||||
});
|
||||
|
||||
it('should format session output with relative time and session ID', async () => {
|
||||
// Arrange
|
||||
const now = new Date('2025-01-20T12:00:00.000Z');
|
||||
const mockSessions: SessionInfo[] = [
|
||||
{
|
||||
id: 'abc123def456',
|
||||
file: 'session-file',
|
||||
fileName: 'session-file.json',
|
||||
startTime: now.toISOString(),
|
||||
lastUpdated: now.toISOString(),
|
||||
firstUserMessage: 'Test message',
|
||||
isCurrentSession: false,
|
||||
index: 1,
|
||||
},
|
||||
];
|
||||
|
||||
mockListSessions.mockResolvedValue(mockSessions);
|
||||
|
||||
// Act
|
||||
await listSessions(mockConfig);
|
||||
|
||||
// Assert
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('1. Test message'),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('some time ago'),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[abc123def456]'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle single session', async () => {
|
||||
// Arrange
|
||||
const now = new Date('2025-01-20T12:00:00.000Z');
|
||||
const mockSessions: SessionInfo[] = [
|
||||
{
|
||||
id: 'single-session',
|
||||
file: 'session-file',
|
||||
fileName: 'session-file.json',
|
||||
startTime: now.toISOString(),
|
||||
lastUpdated: now.toISOString(),
|
||||
firstUserMessage: 'Only session',
|
||||
isCurrentSession: true,
|
||||
index: 1,
|
||||
},
|
||||
];
|
||||
|
||||
mockListSessions.mockResolvedValue(mockSessions);
|
||||
|
||||
// Act
|
||||
await listSessions(mockConfig);
|
||||
|
||||
// Assert
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'\nAvailable sessions for this project (1):\n',
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('1. Only session'),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(', current)'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteSession', () => {
|
||||
let mockConfig: Config;
|
||||
let mockListSessions: ReturnType<typeof vi.fn>;
|
||||
let mockDeleteSession: ReturnType<typeof vi.fn>;
|
||||
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock config
|
||||
mockConfig = {
|
||||
storage: {
|
||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/test-project'),
|
||||
},
|
||||
getSessionId: vi.fn().mockReturnValue('current-session-id'),
|
||||
} as unknown as Config;
|
||||
|
||||
// Create mock methods
|
||||
mockListSessions = vi.fn();
|
||||
mockDeleteSession = vi.fn();
|
||||
|
||||
// Mock SessionSelector constructor
|
||||
vi.mocked(SessionSelector).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
listSessions: mockListSessions,
|
||||
}) as unknown as InstanceType<typeof SessionSelector>,
|
||||
);
|
||||
|
||||
// Mock ChatRecordingService
|
||||
vi.mocked(ChatRecordingService).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
deleteSession: mockDeleteSession,
|
||||
}) as unknown as InstanceType<typeof ChatRecordingService>,
|
||||
);
|
||||
|
||||
// Spy on console methods
|
||||
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
consoleLogSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should display error when no sessions are found', async () => {
|
||||
// Arrange
|
||||
mockListSessions.mockResolvedValue([]);
|
||||
|
||||
// Act
|
||||
await deleteSession(mockConfig, '1');
|
||||
|
||||
// Assert
|
||||
expect(mockListSessions).toHaveBeenCalledOnce();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'No sessions found for this project.',
|
||||
);
|
||||
expect(mockDeleteSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete session by UUID', async () => {
|
||||
// Arrange
|
||||
const now = new Date('2025-01-20T12:00:00.000Z');
|
||||
const mockSessions: SessionInfo[] = [
|
||||
{
|
||||
id: 'session-uuid-123',
|
||||
file: 'session-file-123',
|
||||
fileName: 'session-file-123.json',
|
||||
startTime: now.toISOString(),
|
||||
lastUpdated: now.toISOString(),
|
||||
firstUserMessage: 'Test session',
|
||||
isCurrentSession: false,
|
||||
index: 1,
|
||||
},
|
||||
];
|
||||
|
||||
mockListSessions.mockResolvedValue(mockSessions);
|
||||
mockDeleteSession.mockImplementation(() => {});
|
||||
|
||||
// Act
|
||||
await deleteSession(mockConfig, 'session-uuid-123');
|
||||
|
||||
// Assert
|
||||
expect(mockListSessions).toHaveBeenCalledOnce();
|
||||
expect(mockDeleteSession).toHaveBeenCalledWith('session-file-123');
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'Deleted session 1: Test session (some time ago)',
|
||||
);
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete session by index', async () => {
|
||||
// Arrange
|
||||
const now = new Date('2025-01-20T12:00:00.000Z');
|
||||
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
||||
|
||||
const mockSessions: SessionInfo[] = [
|
||||
{
|
||||
id: 'session-1',
|
||||
file: 'session-file-1',
|
||||
fileName: 'session-file-1.json',
|
||||
startTime: oneHourAgo.toISOString(),
|
||||
lastUpdated: oneHourAgo.toISOString(),
|
||||
firstUserMessage: 'First session',
|
||||
isCurrentSession: false,
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
id: 'session-2',
|
||||
file: 'session-file-2',
|
||||
fileName: 'session-file-2.json',
|
||||
startTime: now.toISOString(),
|
||||
lastUpdated: now.toISOString(),
|
||||
firstUserMessage: 'Second session',
|
||||
isCurrentSession: false,
|
||||
index: 2,
|
||||
},
|
||||
];
|
||||
|
||||
mockListSessions.mockResolvedValue(mockSessions);
|
||||
mockDeleteSession.mockImplementation(() => {});
|
||||
|
||||
// Act
|
||||
await deleteSession(mockConfig, '2');
|
||||
|
||||
// Assert
|
||||
expect(mockListSessions).toHaveBeenCalledOnce();
|
||||
expect(mockDeleteSession).toHaveBeenCalledWith('session-file-2');
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'Deleted session 2: Second session (some time ago)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should display error for invalid session identifier (non-numeric)', async () => {
|
||||
// Arrange
|
||||
const now = new Date('2025-01-20T12:00:00.000Z');
|
||||
const mockSessions: SessionInfo[] = [
|
||||
{
|
||||
id: 'session-1',
|
||||
file: 'session-file-1',
|
||||
fileName: 'session-file-1.json',
|
||||
startTime: now.toISOString(),
|
||||
lastUpdated: now.toISOString(),
|
||||
firstUserMessage: 'Test session',
|
||||
isCurrentSession: false,
|
||||
index: 1,
|
||||
},
|
||||
];
|
||||
|
||||
mockListSessions.mockResolvedValue(mockSessions);
|
||||
|
||||
// Act
|
||||
await deleteSession(mockConfig, 'invalid-id');
|
||||
|
||||
// Assert
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Invalid session identifier "invalid-id". Use --list-sessions to see available sessions.',
|
||||
);
|
||||
expect(mockDeleteSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should display error for invalid session identifier (out of range)', async () => {
|
||||
// Arrange
|
||||
const now = new Date('2025-01-20T12:00:00.000Z');
|
||||
const mockSessions: SessionInfo[] = [
|
||||
{
|
||||
id: 'session-1',
|
||||
file: 'session-file-1',
|
||||
fileName: 'session-file-1.json',
|
||||
startTime: now.toISOString(),
|
||||
lastUpdated: now.toISOString(),
|
||||
firstUserMessage: 'Test session',
|
||||
isCurrentSession: false,
|
||||
index: 1,
|
||||
},
|
||||
];
|
||||
|
||||
mockListSessions.mockResolvedValue(mockSessions);
|
||||
|
||||
// Act
|
||||
await deleteSession(mockConfig, '999');
|
||||
|
||||
// Assert
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Invalid session identifier "999". Use --list-sessions to see available sessions.',
|
||||
);
|
||||
expect(mockDeleteSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should display error for invalid session identifier (zero)', async () => {
|
||||
// Arrange
|
||||
const now = new Date('2025-01-20T12:00:00.000Z');
|
||||
const mockSessions: SessionInfo[] = [
|
||||
{
|
||||
id: 'session-1',
|
||||
file: 'session-file-1',
|
||||
fileName: 'session-file-1.json',
|
||||
startTime: now.toISOString(),
|
||||
lastUpdated: now.toISOString(),
|
||||
firstUserMessage: 'Test session',
|
||||
isCurrentSession: false,
|
||||
index: 1,
|
||||
},
|
||||
];
|
||||
|
||||
mockListSessions.mockResolvedValue(mockSessions);
|
||||
|
||||
// Act
|
||||
await deleteSession(mockConfig, '0');
|
||||
|
||||
// Assert
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Invalid session identifier "0". Use --list-sessions to see available sessions.',
|
||||
);
|
||||
expect(mockDeleteSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent deletion of current session', async () => {
|
||||
// Arrange
|
||||
const now = new Date('2025-01-20T12:00:00.000Z');
|
||||
const mockSessions: SessionInfo[] = [
|
||||
{
|
||||
id: 'current-session-id',
|
||||
file: 'current-session-file',
|
||||
fileName: 'current-session-file.json',
|
||||
startTime: now.toISOString(),
|
||||
lastUpdated: now.toISOString(),
|
||||
firstUserMessage: 'Current session',
|
||||
isCurrentSession: true,
|
||||
index: 1,
|
||||
},
|
||||
];
|
||||
|
||||
mockListSessions.mockResolvedValue(mockSessions);
|
||||
|
||||
// Act - try to delete by index
|
||||
await deleteSession(mockConfig, '1');
|
||||
|
||||
// Assert
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Cannot delete the current active session.',
|
||||
);
|
||||
expect(mockDeleteSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent deletion of current session by UUID', async () => {
|
||||
// Arrange
|
||||
const now = new Date('2025-01-20T12:00:00.000Z');
|
||||
const mockSessions: SessionInfo[] = [
|
||||
{
|
||||
id: 'current-session-id',
|
||||
file: 'current-session-file',
|
||||
fileName: 'current-session-file.json',
|
||||
startTime: now.toISOString(),
|
||||
lastUpdated: now.toISOString(),
|
||||
firstUserMessage: 'Current session',
|
||||
isCurrentSession: true,
|
||||
index: 1,
|
||||
},
|
||||
];
|
||||
|
||||
mockListSessions.mockResolvedValue(mockSessions);
|
||||
|
||||
// Act - try to delete by UUID
|
||||
await deleteSession(mockConfig, 'current-session-id');
|
||||
|
||||
// Assert
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Cannot delete the current active session.',
|
||||
);
|
||||
expect(mockDeleteSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle deletion errors gracefully', async () => {
|
||||
// Arrange
|
||||
const now = new Date('2025-01-20T12:00:00.000Z');
|
||||
const mockSessions: SessionInfo[] = [
|
||||
{
|
||||
id: 'session-1',
|
||||
file: 'session-file-1',
|
||||
fileName: 'session-file-1.json',
|
||||
startTime: now.toISOString(),
|
||||
lastUpdated: now.toISOString(),
|
||||
firstUserMessage: 'Test session',
|
||||
isCurrentSession: false,
|
||||
index: 1,
|
||||
},
|
||||
];
|
||||
|
||||
mockListSessions.mockResolvedValue(mockSessions);
|
||||
mockDeleteSession.mockImplementation(() => {
|
||||
throw new Error('File deletion failed');
|
||||
});
|
||||
|
||||
// Act
|
||||
await deleteSession(mockConfig, '1');
|
||||
|
||||
// Assert
|
||||
expect(mockDeleteSession).toHaveBeenCalledWith('session-file-1');
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to delete session: File deletion failed',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle non-Error deletion failures', async () => {
|
||||
// Arrange
|
||||
const now = new Date('2025-01-20T12:00:00.000Z');
|
||||
const mockSessions: SessionInfo[] = [
|
||||
{
|
||||
id: 'session-1',
|
||||
file: 'session-file-1',
|
||||
fileName: 'session-file-1.json',
|
||||
startTime: now.toISOString(),
|
||||
lastUpdated: now.toISOString(),
|
||||
firstUserMessage: 'Test session',
|
||||
isCurrentSession: false,
|
||||
index: 1,
|
||||
},
|
||||
];
|
||||
|
||||
mockListSessions.mockResolvedValue(mockSessions);
|
||||
mockDeleteSession.mockImplementation(() => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
throw 'Unknown error type';
|
||||
});
|
||||
|
||||
// Act
|
||||
await deleteSession(mockConfig, '1');
|
||||
|
||||
// Assert
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to delete session: Unknown error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should sort sessions before finding by index', async () => {
|
||||
// Arrange: Create sessions in non-chronological order
|
||||
const session1Time = new Date('2025-01-18T12:00:00.000Z');
|
||||
const session2Time = new Date('2025-01-19T12:00:00.000Z');
|
||||
const session3Time = new Date('2025-01-20T12:00:00.000Z');
|
||||
|
||||
const mockSessions: SessionInfo[] = [
|
||||
{
|
||||
id: 'session-3',
|
||||
file: 'session-file-3',
|
||||
fileName: 'session-file-3.json',
|
||||
startTime: session3Time.toISOString(), // Newest
|
||||
lastUpdated: session3Time.toISOString(),
|
||||
firstUserMessage: 'Newest session',
|
||||
isCurrentSession: false,
|
||||
index: 3,
|
||||
},
|
||||
{
|
||||
id: 'session-1',
|
||||
file: 'session-file-1',
|
||||
fileName: 'session-file-1.json',
|
||||
startTime: session1Time.toISOString(), // Oldest
|
||||
lastUpdated: session1Time.toISOString(),
|
||||
firstUserMessage: 'Oldest session',
|
||||
isCurrentSession: false,
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
id: 'session-2',
|
||||
file: 'session-file-2',
|
||||
fileName: 'session-file-2.json',
|
||||
startTime: session2Time.toISOString(), // Middle
|
||||
lastUpdated: session2Time.toISOString(),
|
||||
firstUserMessage: 'Middle session',
|
||||
isCurrentSession: false,
|
||||
index: 2,
|
||||
},
|
||||
];
|
||||
|
||||
mockListSessions.mockResolvedValue(mockSessions);
|
||||
mockDeleteSession.mockImplementation(() => {});
|
||||
|
||||
// Act - delete index 1 (should be oldest session after sorting)
|
||||
await deleteSession(mockConfig, '1');
|
||||
|
||||
// Assert
|
||||
expect(mockDeleteSession).toHaveBeenCalledWith('session-file-1');
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Oldest session'),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user