mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-02 01:11:24 -07:00
406 lines
12 KiB
TypeScript
406 lines
12 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
import { cleanupExpiredSessions } from './sessionCleanup.js';
|
|
import type { Settings } from '../config/settings.js';
|
|
import {
|
|
SESSION_FILE_PREFIX,
|
|
type Config,
|
|
debugLogger,
|
|
} from '@google/gemini-cli-core';
|
|
|
|
// Create a mock config for integration testing
|
|
function createTestConfig(): Config {
|
|
return {
|
|
storage: {
|
|
getProjectTempDir: () => '/tmp/nonexistent-test-dir',
|
|
},
|
|
getSessionId: () => 'test-session-id',
|
|
getDebugMode: () => false,
|
|
initialize: async () => undefined,
|
|
} as unknown as Config;
|
|
}
|
|
|
|
describe('Session Cleanup Integration', () => {
|
|
it('should gracefully handle non-existent directories', async () => {
|
|
const config = createTestConfig();
|
|
const settings: Settings = {
|
|
general: {
|
|
sessionRetention: {
|
|
enabled: true,
|
|
maxAge: '30d',
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await cleanupExpiredSessions(config, settings);
|
|
|
|
// Should return empty result for non-existent directory
|
|
expect(result.disabled).toBe(false);
|
|
expect(result.scanned).toBe(0);
|
|
expect(result.deleted).toBe(0);
|
|
expect(result.skipped).toBe(0);
|
|
expect(result.failed).toBe(0);
|
|
});
|
|
|
|
it('should not impact startup when disabled', async () => {
|
|
const config = createTestConfig();
|
|
const settings: Settings = {
|
|
general: {
|
|
sessionRetention: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await cleanupExpiredSessions(config, settings);
|
|
|
|
expect(result.disabled).toBe(true);
|
|
expect(result.scanned).toBe(0);
|
|
expect(result.deleted).toBe(0);
|
|
expect(result.skipped).toBe(0);
|
|
expect(result.failed).toBe(0);
|
|
});
|
|
|
|
it('should handle missing sessionRetention configuration', async () => {
|
|
// Create test session files to verify they are NOT deleted when config is missing
|
|
const fs = await import('node:fs/promises');
|
|
const path = await import('node:path');
|
|
const os = await import('node:os');
|
|
|
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gemini-test-'));
|
|
const chatsDir = path.join(tempDir, 'chats');
|
|
await fs.mkdir(chatsDir, { recursive: true });
|
|
|
|
// Create an old session file that would normally be deleted
|
|
const oldDate = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000); // 60 days ago
|
|
const sessionFile = path.join(
|
|
chatsDir,
|
|
`${SESSION_FILE_PREFIX}2024-01-01T10-00-00-test123.json`,
|
|
);
|
|
await fs.writeFile(
|
|
sessionFile,
|
|
JSON.stringify({
|
|
sessionId: 'test123',
|
|
messages: [],
|
|
startTime: oldDate.toISOString(),
|
|
lastUpdated: oldDate.toISOString(),
|
|
}),
|
|
);
|
|
|
|
const config = createTestConfig();
|
|
config.storage.getProjectTempDir = vi.fn().mockReturnValue(tempDir);
|
|
|
|
const settings: Settings = {};
|
|
|
|
const result = await cleanupExpiredSessions(config, settings);
|
|
|
|
expect(result.disabled).toBe(true);
|
|
expect(result.scanned).toBe(0); // Should not even scan when config is missing
|
|
expect(result.deleted).toBe(0);
|
|
expect(result.skipped).toBe(0);
|
|
expect(result.failed).toBe(0);
|
|
|
|
// Verify the session file still exists (was not deleted)
|
|
const filesAfter = await fs.readdir(chatsDir);
|
|
expect(filesAfter).toContain(
|
|
`${SESSION_FILE_PREFIX}2024-01-01T10-00-00-test123.json`,
|
|
);
|
|
|
|
// Cleanup
|
|
await fs.rm(tempDir, { recursive: true });
|
|
});
|
|
|
|
it('should validate configuration and fail gracefully', async () => {
|
|
const errorSpy = vi.spyOn(debugLogger, 'warn').mockImplementation(() => {});
|
|
const config = createTestConfig();
|
|
|
|
const settings: Settings = {
|
|
general: {
|
|
sessionRetention: {
|
|
enabled: true,
|
|
maxAge: 'invalid-format',
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await cleanupExpiredSessions(config, settings);
|
|
|
|
expect(result.disabled).toBe(true);
|
|
expect(result.scanned).toBe(0);
|
|
expect(result.deleted).toBe(0);
|
|
expect(result.skipped).toBe(0);
|
|
expect(result.failed).toBe(0);
|
|
|
|
// Verify error logging provides visibility into the validation failure
|
|
expect(errorSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining(
|
|
'Session cleanup disabled: Error: Invalid retention period format',
|
|
),
|
|
);
|
|
|
|
errorSpy.mockRestore();
|
|
});
|
|
|
|
it('should clean up expired sessions when they exist', async () => {
|
|
// Create a temporary directory with test sessions
|
|
const fs = await import('node:fs/promises');
|
|
const path = await import('node:path');
|
|
const os = await import('node:os');
|
|
|
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gemini-test-'));
|
|
const chatsDir = path.join(tempDir, 'chats');
|
|
await fs.mkdir(chatsDir, { recursive: true });
|
|
|
|
// Create test session files with different ages
|
|
const now = new Date();
|
|
const oldDate = new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000); // 35 days ago
|
|
const recentDate = new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000); // 5 days ago
|
|
|
|
// Create an old session file that should be deleted
|
|
const oldSessionFile = path.join(
|
|
chatsDir,
|
|
`${SESSION_FILE_PREFIX}2024-12-01T10-00-00-old12345.json`,
|
|
);
|
|
await fs.writeFile(
|
|
oldSessionFile,
|
|
JSON.stringify({
|
|
sessionId: 'old12345',
|
|
messages: [{ type: 'user', content: 'test message' }],
|
|
startTime: oldDate.toISOString(),
|
|
lastUpdated: oldDate.toISOString(),
|
|
}),
|
|
);
|
|
|
|
// Create a recent session file that should be kept
|
|
const recentSessionFile = path.join(
|
|
chatsDir,
|
|
`${SESSION_FILE_PREFIX}2025-01-15T10-00-00-recent789.json`,
|
|
);
|
|
await fs.writeFile(
|
|
recentSessionFile,
|
|
JSON.stringify({
|
|
sessionId: 'recent789',
|
|
messages: [{ type: 'user', content: 'test message' }],
|
|
startTime: recentDate.toISOString(),
|
|
lastUpdated: recentDate.toISOString(),
|
|
}),
|
|
);
|
|
|
|
// Create a current session file that should always be kept
|
|
const currentSessionFile = path.join(
|
|
chatsDir,
|
|
`${SESSION_FILE_PREFIX}2025-01-20T10-00-00-current123.json`,
|
|
);
|
|
await fs.writeFile(
|
|
currentSessionFile,
|
|
JSON.stringify({
|
|
sessionId: 'current123',
|
|
messages: [{ type: 'user', content: 'test message' }],
|
|
startTime: now.toISOString(),
|
|
lastUpdated: now.toISOString(),
|
|
}),
|
|
);
|
|
|
|
// Configure test with real temp directory
|
|
const config: Config = {
|
|
storage: {
|
|
getProjectTempDir: () => tempDir,
|
|
},
|
|
getSessionId: () => 'current123',
|
|
getDebugMode: () => false,
|
|
initialize: async () => undefined,
|
|
} as unknown as Config;
|
|
|
|
const settings: Settings = {
|
|
general: {
|
|
sessionRetention: {
|
|
enabled: true,
|
|
maxAge: '30d', // Keep sessions for 30 days
|
|
},
|
|
},
|
|
};
|
|
|
|
try {
|
|
const result = await cleanupExpiredSessions(config, settings);
|
|
|
|
// Verify the result
|
|
expect(result.disabled).toBe(false);
|
|
expect(result.scanned).toBe(3); // Should scan all 3 sessions
|
|
expect(result.deleted).toBe(1); // Should delete the old session (35 days old)
|
|
expect(result.skipped).toBe(2); // Should keep recent and current sessions
|
|
expect(result.failed).toBe(0);
|
|
|
|
// Verify files on disk
|
|
const remainingFiles = await fs.readdir(chatsDir);
|
|
expect(remainingFiles).toHaveLength(2); // Only 2 files should remain
|
|
expect(remainingFiles).toContain(
|
|
`${SESSION_FILE_PREFIX}2025-01-15T10-00-00-recent789.json`,
|
|
);
|
|
expect(remainingFiles).toContain(
|
|
`${SESSION_FILE_PREFIX}2025-01-20T10-00-00-current123.json`,
|
|
);
|
|
expect(remainingFiles).not.toContain(
|
|
`${SESSION_FILE_PREFIX}2024-12-01T10-00-00-old12345.json`,
|
|
);
|
|
} finally {
|
|
// Clean up test directory
|
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('should delete subagent files and their artifacts when parent expires', async () => {
|
|
// Create a temporary directory with test sessions
|
|
const fs = await import('node:fs/promises');
|
|
const path = await import('node:path');
|
|
const os = await import('node:os');
|
|
|
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gemini-test-'));
|
|
const chatsDir = path.join(tempDir, 'chats');
|
|
const logsDir = path.join(tempDir, 'logs');
|
|
const toolOutputsDir = path.join(tempDir, 'tool-outputs');
|
|
|
|
await fs.mkdir(chatsDir, { recursive: true });
|
|
await fs.mkdir(logsDir, { recursive: true });
|
|
await fs.mkdir(toolOutputsDir, { recursive: true });
|
|
|
|
const now = new Date();
|
|
const oldDate = new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000); // 5 days ago
|
|
|
|
// The shortId that ties them together
|
|
const sharedShortId = 'abcdef12';
|
|
|
|
const parentSessionId = 'parent-uuid-123';
|
|
const parentFile = path.join(
|
|
chatsDir,
|
|
`${SESSION_FILE_PREFIX}2024-01-01T10-00-00-${sharedShortId}.json`,
|
|
);
|
|
await fs.writeFile(
|
|
parentFile,
|
|
JSON.stringify({
|
|
sessionId: parentSessionId,
|
|
messages: [],
|
|
startTime: oldDate.toISOString(),
|
|
lastUpdated: oldDate.toISOString(),
|
|
}),
|
|
);
|
|
|
|
const subagentSessionId = 'subagent-uuid-456';
|
|
const subagentFile = path.join(
|
|
chatsDir,
|
|
`${SESSION_FILE_PREFIX}2024-01-01T10-05-00-${sharedShortId}.json`,
|
|
);
|
|
await fs.writeFile(
|
|
subagentFile,
|
|
JSON.stringify({
|
|
sessionId: subagentSessionId,
|
|
messages: [],
|
|
startTime: oldDate.toISOString(),
|
|
lastUpdated: oldDate.toISOString(),
|
|
}),
|
|
);
|
|
|
|
const parentLogFile = path.join(
|
|
logsDir,
|
|
`session-${parentSessionId}.jsonl`,
|
|
);
|
|
await fs.writeFile(parentLogFile, '{"log": "parent"}');
|
|
|
|
const parentToolOutputsDir = path.join(
|
|
toolOutputsDir,
|
|
`session-${parentSessionId}`,
|
|
);
|
|
await fs.mkdir(parentToolOutputsDir, { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(parentToolOutputsDir, 'some-output.txt'),
|
|
'data',
|
|
);
|
|
|
|
const subagentLogFile = path.join(
|
|
logsDir,
|
|
`session-${subagentSessionId}.jsonl`,
|
|
);
|
|
await fs.writeFile(subagentLogFile, '{"log": "subagent"}');
|
|
|
|
const subagentToolOutputsDir = path.join(
|
|
toolOutputsDir,
|
|
`session-${subagentSessionId}`,
|
|
);
|
|
await fs.mkdir(subagentToolOutputsDir, { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(subagentToolOutputsDir, 'some-output.txt'),
|
|
'data',
|
|
);
|
|
|
|
const currentShortId = 'current1';
|
|
const currentFile = path.join(
|
|
chatsDir,
|
|
`${SESSION_FILE_PREFIX}2025-01-20T10-00-00-${currentShortId}.json`,
|
|
);
|
|
await fs.writeFile(
|
|
currentFile,
|
|
JSON.stringify({
|
|
sessionId: 'current-session',
|
|
messages: [
|
|
{
|
|
type: 'user',
|
|
content: [{ type: 'text', text: 'hello' }],
|
|
timestamp: now.toISOString(),
|
|
},
|
|
],
|
|
startTime: now.toISOString(),
|
|
lastUpdated: now.toISOString(),
|
|
}),
|
|
);
|
|
|
|
// Configure test
|
|
const config: Config = {
|
|
storage: {
|
|
getProjectTempDir: () => tempDir,
|
|
},
|
|
getSessionId: () => 'current-session', // Mock CLI instance ID
|
|
getDebugMode: () => false,
|
|
initialize: async () => undefined,
|
|
} as unknown as Config;
|
|
|
|
const settings: Settings = {
|
|
general: {
|
|
sessionRetention: {
|
|
enabled: true,
|
|
maxAge: '1d', // Expire things older than 1 day
|
|
},
|
|
},
|
|
};
|
|
|
|
try {
|
|
const result = await cleanupExpiredSessions(config, settings);
|
|
|
|
// Verify the cleanup result object
|
|
// It scanned 3 files. It should delete 2 (parent + subagent), and keep 1 (current)
|
|
expect(result.disabled).toBe(false);
|
|
expect(result.scanned).toBe(3);
|
|
expect(result.deleted).toBe(2);
|
|
expect(result.skipped).toBe(1);
|
|
|
|
// Verify on-disk file states
|
|
const chats = await fs.readdir(chatsDir);
|
|
expect(chats).toHaveLength(1);
|
|
expect(chats).toContain(
|
|
`${SESSION_FILE_PREFIX}2025-01-20T10-00-00-${currentShortId}.json`,
|
|
); // Only current is left
|
|
|
|
const logs = await fs.readdir(logsDir);
|
|
expect(logs).toHaveLength(0); // Both parent and subagent logs were deleted
|
|
|
|
const tools = await fs.readdir(toolOutputsDir);
|
|
expect(tools).toHaveLength(0); // Both parent and subagent tool output dirs were deleted
|
|
} finally {
|
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|