mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-05 19:01:12 -07:00
149 lines
4.8 KiB
TypeScript
149 lines
4.8 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import * as os from 'node:os';
|
|
import {
|
|
deleteSessionArtifactsAsync,
|
|
deleteSubagentSessionDirAndArtifactsAsync,
|
|
validateAndSanitizeSessionId,
|
|
} from './sessionOperations.js';
|
|
|
|
describe('sessionOperations', () => {
|
|
let tempDir: string;
|
|
let chatsDir: string;
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
// Create a real temporary directory for each test
|
|
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'session-ops-test-'));
|
|
chatsDir = path.join(tempDir, 'chats');
|
|
});
|
|
|
|
afterEach(async () => {
|
|
vi.unstubAllEnvs();
|
|
// Clean up the temporary directory
|
|
if (tempDir) {
|
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('validateAndSanitizeSessionId', () => {
|
|
it('should throw for empty or dangerous IDs', () => {
|
|
expect(() => validateAndSanitizeSessionId('')).toThrow(
|
|
'Invalid sessionId',
|
|
);
|
|
expect(() => validateAndSanitizeSessionId('.')).toThrow(
|
|
'Invalid sessionId',
|
|
);
|
|
expect(() => validateAndSanitizeSessionId('..')).toThrow(
|
|
'Invalid sessionId',
|
|
);
|
|
});
|
|
|
|
it('should sanitize valid IDs', () => {
|
|
expect(validateAndSanitizeSessionId('abc/def')).toBe('abc_def');
|
|
expect(validateAndSanitizeSessionId('valid-id')).toBe('valid-id');
|
|
});
|
|
});
|
|
|
|
describe('deleteSessionArtifactsAsync', () => {
|
|
it('should delete logs and tool outputs', async () => {
|
|
const sessionId = 'test-session';
|
|
const logsDir = path.join(tempDir, 'logs');
|
|
const toolOutputsDir = path.join(
|
|
tempDir,
|
|
'tool-outputs',
|
|
`session-${sessionId}`,
|
|
);
|
|
const sessionDir = path.join(tempDir, sessionId);
|
|
|
|
await fs.mkdir(logsDir, { recursive: true });
|
|
await fs.mkdir(toolOutputsDir, { recursive: true });
|
|
await fs.mkdir(sessionDir, { recursive: true });
|
|
|
|
const logFile = path.join(logsDir, `session-${sessionId}.jsonl`);
|
|
await fs.writeFile(logFile, '{}');
|
|
|
|
// Verify files exist before call
|
|
expect(await fs.stat(logFile)).toBeTruthy();
|
|
expect(await fs.stat(toolOutputsDir)).toBeTruthy();
|
|
expect(await fs.stat(sessionDir)).toBeTruthy();
|
|
|
|
await deleteSessionArtifactsAsync(sessionId, tempDir);
|
|
|
|
// Verify files are deleted
|
|
await expect(fs.stat(logFile)).rejects.toThrow();
|
|
await expect(fs.stat(toolOutputsDir)).rejects.toThrow();
|
|
await expect(fs.stat(sessionDir)).rejects.toThrow();
|
|
});
|
|
|
|
it('should ignore ENOENT errors during deletion', async () => {
|
|
// Don't create any files. Calling delete on non-existent files should not throw.
|
|
await expect(
|
|
deleteSessionArtifactsAsync('non-existent', tempDir),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('deleteSubagentSessionDirAndArtifactsAsync', () => {
|
|
it('should iterate subagent files and delete their artifacts', async () => {
|
|
const parentSessionId = 'parent-123';
|
|
const subDir = path.join(chatsDir, parentSessionId);
|
|
await fs.mkdir(subDir, { recursive: true });
|
|
|
|
await fs.writeFile(path.join(subDir, 'sub1.json'), '{}');
|
|
await fs.writeFile(path.join(subDir, 'sub2.json'), '{}');
|
|
|
|
const logsDir = path.join(tempDir, 'logs');
|
|
await fs.mkdir(logsDir, { recursive: true });
|
|
await fs.writeFile(path.join(logsDir, 'session-sub1.jsonl'), '{}');
|
|
await fs.writeFile(path.join(logsDir, 'session-sub2.jsonl'), '{}');
|
|
|
|
await deleteSubagentSessionDirAndArtifactsAsync(
|
|
parentSessionId,
|
|
chatsDir,
|
|
tempDir,
|
|
);
|
|
|
|
// Verify subagent directory is deleted
|
|
await expect(fs.stat(subDir)).rejects.toThrow();
|
|
|
|
// Verify artifacts are deleted
|
|
await expect(
|
|
fs.stat(path.join(logsDir, 'session-sub1.jsonl')),
|
|
).rejects.toThrow();
|
|
await expect(
|
|
fs.stat(path.join(logsDir, 'session-sub2.jsonl')),
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
it('should resolve for safe path even if input contains traversals (due to sanitization)', async () => {
|
|
// Should sanitize '../unsafe' to '.._unsafe' and resolve (directory won't exist, so readdir returns [] naturally)
|
|
await expect(
|
|
deleteSubagentSessionDirAndArtifactsAsync(
|
|
'../unsafe',
|
|
chatsDir,
|
|
tempDir,
|
|
),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('should handle ENOENT for readdir gracefully', async () => {
|
|
// Non-existent directory should not throw
|
|
await expect(
|
|
deleteSubagentSessionDirAndArtifactsAsync(
|
|
'non-existent-parent',
|
|
chatsDir,
|
|
tempDir,
|
|
),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
});
|