mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-04 22:21:15 -07:00
585 lines
21 KiB
TypeScript
585 lines
21 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import {
|
|
describe,
|
|
it,
|
|
expect,
|
|
vi,
|
|
beforeEach,
|
|
afterEach,
|
|
type Mock,
|
|
} from 'vitest';
|
|
|
|
import fs from 'node:fs';
|
|
import * as actualNodeFs from 'node:fs'; // For setup/teardown
|
|
import fsPromises from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import os from 'node:os';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import mime from 'mime/lite';
|
|
|
|
import {
|
|
isWithinRoot,
|
|
isBinaryFile,
|
|
detectFileType,
|
|
processSingleFileContent,
|
|
detectBOM,
|
|
readFileWithEncoding,
|
|
readWasmBinaryFromDisk,
|
|
saveTruncatedToolOutput,
|
|
formatTruncatedToolOutput,
|
|
getRealPath,
|
|
isEmpty,
|
|
} from './fileUtils.js';
|
|
import { StandardFileSystemService } from '../services/fileSystemService.js';
|
|
import { ToolErrorType } from '../tools/tool-error.js';
|
|
|
|
vi.mock('mime/lite', () => ({
|
|
default: { getType: vi.fn() },
|
|
getType: vi.fn(),
|
|
}));
|
|
|
|
const mockMimeGetType = mime.getType as Mock;
|
|
|
|
describe('fileUtils', () => {
|
|
let tempRootDir: string;
|
|
const originalProcessCwd = process.cwd;
|
|
|
|
let testTextFilePath: string;
|
|
let testImageFilePath: string;
|
|
let testPdfFilePath: string;
|
|
let testAudioFilePath: string;
|
|
let testBinaryFilePath: string;
|
|
let nonexistentFilePath: string;
|
|
let directoryPath: string;
|
|
|
|
beforeEach(() => {
|
|
vi.resetAllMocks(); // Reset all mocks, including mime.getType
|
|
|
|
tempRootDir = actualNodeFs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'fileUtils-test-'),
|
|
);
|
|
process.cwd = vi.fn(() => tempRootDir); // Mock cwd if necessary for relative path logic within tests
|
|
|
|
testTextFilePath = path.join(tempRootDir, 'test.txt');
|
|
testImageFilePath = path.join(tempRootDir, 'image.png');
|
|
testPdfFilePath = path.join(tempRootDir, 'document.pdf');
|
|
testAudioFilePath = path.join(tempRootDir, 'audio.mp3');
|
|
testBinaryFilePath = path.join(tempRootDir, 'app.exe');
|
|
nonexistentFilePath = path.join(tempRootDir, 'nonexistent.txt');
|
|
directoryPath = path.join(tempRootDir, 'subdir');
|
|
|
|
actualNodeFs.mkdirSync(directoryPath, { recursive: true }); // Ensure subdir exists
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (actualNodeFs.existsSync(tempRootDir)) {
|
|
actualNodeFs.rmSync(tempRootDir, { recursive: true, force: true });
|
|
}
|
|
process.cwd = originalProcessCwd;
|
|
vi.restoreAllMocks(); // Restore any spies
|
|
});
|
|
|
|
describe('readWasmBinaryFromDisk', () => {
|
|
it('loads a WASM binary from disk as a Uint8Array', async () => {
|
|
const wasmFixtureUrl = new URL(
|
|
'./__fixtures__/dummy.wasm',
|
|
import.meta.url,
|
|
);
|
|
const wasmFixturePath = fileURLToPath(wasmFixtureUrl);
|
|
const result = await readWasmBinaryFromDisk(wasmFixturePath);
|
|
const expectedBytes = new Uint8Array(
|
|
await fsPromises.readFile(wasmFixturePath),
|
|
);
|
|
|
|
expect(result).toBeInstanceOf(Uint8Array);
|
|
expect(result).toEqual(expectedBytes);
|
|
});
|
|
});
|
|
|
|
describe('detectBOM', () => {
|
|
it('detects UTF-8 BOM', () => {
|
|
const buf = Buffer.from([0xef, 0xbb, 0xbf, 0x61, 0x62, 0x63]);
|
|
expect(detectBOM(buf)).toEqual({ encoding: 'utf8', bomLength: 3 });
|
|
});
|
|
|
|
it('detects UTF-16 LE BOM', () => {
|
|
const buf = Buffer.from([0xff, 0xfe, 0x61, 0x00]);
|
|
expect(detectBOM(buf)).toEqual({ encoding: 'utf16le', bomLength: 2 });
|
|
});
|
|
|
|
it('detects UTF-16 BE BOM', () => {
|
|
const buf = Buffer.from([0xfe, 0xff, 0x00, 0x61]);
|
|
expect(detectBOM(buf)).toEqual({ encoding: 'utf16be', bomLength: 2 });
|
|
});
|
|
|
|
it('detects UTF-32 LE BOM', () => {
|
|
const buf = Buffer.from([0xff, 0xfe, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00]);
|
|
expect(detectBOM(buf)).toEqual({ encoding: 'utf32le', bomLength: 4 });
|
|
});
|
|
|
|
it('detects UTF-32 BE BOM', () => {
|
|
const buf = Buffer.from([0x00, 0x00, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x61]);
|
|
expect(detectBOM(buf)).toEqual({ encoding: 'utf32be', bomLength: 4 });
|
|
});
|
|
|
|
it('returns null for no BOM', () => {
|
|
const buf = Buffer.from([0x61, 0x62, 0x63]);
|
|
expect(detectBOM(buf)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('readFileWithEncoding', () => {
|
|
it('reads UTF-8 file without BOM', async () => {
|
|
actualNodeFs.writeFileSync(testTextFilePath, 'hello');
|
|
expect(await readFileWithEncoding(testTextFilePath)).toBe('hello');
|
|
});
|
|
|
|
it('reads UTF-8 file with BOM', async () => {
|
|
const buf = Buffer.from([0xef, 0xbb, 0xbf, 0x68, 0x69]);
|
|
actualNodeFs.writeFileSync(testTextFilePath, buf);
|
|
expect(await readFileWithEncoding(testTextFilePath)).toBe('hi');
|
|
});
|
|
|
|
it('reads UTF-16 LE file with BOM', async () => {
|
|
const buf = Buffer.from([0xff, 0xfe, 0x68, 0x00, 0x69, 0x00]);
|
|
actualNodeFs.writeFileSync(testTextFilePath, buf);
|
|
expect(await readFileWithEncoding(testTextFilePath)).toBe('hi');
|
|
});
|
|
|
|
it('reads UTF-16 BE file with BOM', async () => {
|
|
const buf = Buffer.from([0xfe, 0xff, 0x00, 0x68, 0x00, 0x69]);
|
|
actualNodeFs.writeFileSync(testTextFilePath, buf);
|
|
expect(await readFileWithEncoding(testTextFilePath)).toBe('hi');
|
|
});
|
|
|
|
it('reads UTF-32 LE file with BOM', async () => {
|
|
const buf = Buffer.from([
|
|
0xff, 0xfe, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00,
|
|
]);
|
|
actualNodeFs.writeFileSync(testTextFilePath, buf);
|
|
expect(await readFileWithEncoding(testTextFilePath)).toBe('hi');
|
|
});
|
|
|
|
it('reads UTF-32 BE file with BOM', async () => {
|
|
const buf = Buffer.from([
|
|
0x00, 0x00, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x69,
|
|
]);
|
|
actualNodeFs.writeFileSync(testTextFilePath, buf);
|
|
expect(await readFileWithEncoding(testTextFilePath)).toBe('hi');
|
|
});
|
|
});
|
|
|
|
describe('isWithinRoot', () => {
|
|
it('should return true if path is within root', () => {
|
|
const root = '/path/to/project';
|
|
const file = '/path/to/project/src/index.js';
|
|
expect(isWithinRoot(file, root)).toBe(true);
|
|
});
|
|
|
|
it('should return true if path is root itself', () => {
|
|
const root = '/path/to/project';
|
|
expect(isWithinRoot(root, root)).toBe(true);
|
|
});
|
|
|
|
it('should return false if path is outside root', () => {
|
|
const root = '/path/to/project';
|
|
const file = '/path/to/other/project/src/index.js';
|
|
expect(isWithinRoot(file, root)).toBe(false);
|
|
});
|
|
|
|
it('should handle trailing separators in root', () => {
|
|
const root = '/path/to/project/';
|
|
const file = '/path/to/project/src/index.js';
|
|
expect(isWithinRoot(file, root)).toBe(true);
|
|
});
|
|
|
|
it('should handle relative paths and resolve them', () => {
|
|
const root = './project';
|
|
const file = './project/src/index.js';
|
|
// Resolving against process.cwd which is mocked in beforeEach
|
|
const resolvedRoot = path.resolve(tempRootDir, root);
|
|
const resolvedFile = path.resolve(tempRootDir, file);
|
|
expect(isWithinRoot(resolvedFile, resolvedRoot)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('isEmpty', () => {
|
|
it('returns true for 0-byte file', async () => {
|
|
actualNodeFs.writeFileSync(testTextFilePath, '');
|
|
expect(await isEmpty(testTextFilePath)).toBe(true);
|
|
});
|
|
|
|
it('returns true for whitespace-only file', async () => {
|
|
actualNodeFs.writeFileSync(testTextFilePath, ' \n \r\t ');
|
|
expect(await isEmpty(testTextFilePath)).toBe(true);
|
|
});
|
|
|
|
it('returns true for UTF-8 BOM + whitespace', async () => {
|
|
const buf = Buffer.from([0xef, 0xbb, 0xbf, 0x20, 0x0a]);
|
|
actualNodeFs.writeFileSync(testTextFilePath, buf);
|
|
expect(await isEmpty(testTextFilePath)).toBe(true);
|
|
});
|
|
|
|
it('returns false for file with content', async () => {
|
|
actualNodeFs.writeFileSync(testTextFilePath, 'hello');
|
|
expect(await isEmpty(testTextFilePath)).toBe(false);
|
|
});
|
|
|
|
it('returns true if file is missing', async () => {
|
|
expect(await isEmpty(nonexistentFilePath)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('isBinaryFile', () => {
|
|
it('should return false for an empty file', async () => {
|
|
actualNodeFs.writeFileSync(testTextFilePath, '');
|
|
expect(await isBinaryFile(testTextFilePath)).toBe(false);
|
|
});
|
|
|
|
it('should return false for a plain text file', async () => {
|
|
actualNodeFs.writeFileSync(testTextFilePath, 'This is some plain text.');
|
|
expect(await isBinaryFile(testTextFilePath)).toBe(false);
|
|
});
|
|
|
|
it('should return true for a file with null bytes', async () => {
|
|
const content = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x21]);
|
|
actualNodeFs.writeFileSync(testBinaryFilePath, content);
|
|
expect(await isBinaryFile(testBinaryFilePath)).toBe(true);
|
|
});
|
|
|
|
it('should return true for a file with high non-printable character ratio', async () => {
|
|
const content = Buffer.alloc(100);
|
|
for (let i = 0; i < 40; i++) {
|
|
content[i] = 1; // Non-printable SOH
|
|
}
|
|
actualNodeFs.writeFileSync(testBinaryFilePath, content);
|
|
expect(await isBinaryFile(testBinaryFilePath)).toBe(true);
|
|
});
|
|
|
|
it('should return false for a UTF-16 LE file with BOM', async () => {
|
|
const buf = Buffer.from([0xff, 0xfe, 0x61, 0x00, 0x62, 0x00]);
|
|
actualNodeFs.writeFileSync(testTextFilePath, buf);
|
|
expect(await isBinaryFile(testTextFilePath)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('detectFileType', () => {
|
|
it("should return 'text' for .ts file", async () => {
|
|
const tsFilePath = path.join(tempRootDir, 'script.ts');
|
|
actualNodeFs.writeFileSync(tsFilePath, 'console.log("hello");');
|
|
expect(await detectFileType(tsFilePath)).toBe('text');
|
|
});
|
|
|
|
it("should return 'image' for .png file", async () => {
|
|
mockMimeGetType.mockReturnValue('image/png');
|
|
actualNodeFs.writeFileSync(testImageFilePath, 'fake-image-data');
|
|
expect(await detectFileType(testImageFilePath)).toBe('image');
|
|
});
|
|
|
|
it("should return 'pdf' for .pdf file", async () => {
|
|
mockMimeGetType.mockReturnValue('application/pdf');
|
|
actualNodeFs.writeFileSync(testPdfFilePath, 'fake-pdf-data');
|
|
expect(await detectFileType(testPdfFilePath)).toBe('pdf');
|
|
});
|
|
|
|
it("should return 'audio' for .mp3 file", async () => {
|
|
mockMimeGetType.mockReturnValue('audio/mpeg');
|
|
// Mock isBinaryFile to return true to confirm it's recognized as audio
|
|
vi.spyOn(actualNodeFs.promises, 'open').mockImplementation(
|
|
async () =>
|
|
({
|
|
stat: async () => ({ size: 100 }),
|
|
read: async (buf: Buffer) => {
|
|
buf[0] = 0x00; // null byte to make it look binary
|
|
return { bytesRead: 1 };
|
|
},
|
|
close: async () => {},
|
|
}) as unknown as fs.promises.FileHandle,
|
|
);
|
|
|
|
actualNodeFs.writeFileSync(testAudioFilePath, 'fake-audio-data');
|
|
expect(await detectFileType(testAudioFilePath)).toBe('audio');
|
|
});
|
|
|
|
it("should return 'text' for audio extension with text content", async () => {
|
|
mockMimeGetType.mockReturnValue('audio/mpeg');
|
|
// No null bytes, should be text
|
|
actualNodeFs.writeFileSync(testAudioFilePath, 'Not really audio');
|
|
expect(await detectFileType(testAudioFilePath)).toBe('text');
|
|
});
|
|
|
|
it("should return 'binary' for known binary extension", async () => {
|
|
const exeFilePath = path.join(tempRootDir, 'app.exe');
|
|
actualNodeFs.writeFileSync(exeFilePath, 'fake-exe-data');
|
|
expect(await detectFileType(exeFilePath)).toBe('binary');
|
|
});
|
|
|
|
it("should return 'binary' for unknown extension with binary content", async () => {
|
|
const unknownFilePath = path.join(tempRootDir, 'file.dat');
|
|
const content = Buffer.from([0x00, 0x01, 0x02, 0x03]);
|
|
actualNodeFs.writeFileSync(unknownFilePath, content);
|
|
expect(await detectFileType(unknownFilePath)).toBe('binary');
|
|
});
|
|
|
|
it("should return 'svg' for .svg extension", async () => {
|
|
const svgPath = path.join(tempRootDir, 'icon.svg');
|
|
actualNodeFs.writeFileSync(svgPath, '<svg></svg>');
|
|
expect(await detectFileType(svgPath)).toBe('svg');
|
|
});
|
|
});
|
|
|
|
describe('processSingleFileContent', () => {
|
|
beforeEach(() => {
|
|
// Ensure files exist for statSync checks before readFile might be mocked
|
|
if (actualNodeFs.existsSync(testTextFilePath))
|
|
actualNodeFs.unlinkSync(testTextFilePath);
|
|
if (actualNodeFs.existsSync(testImageFilePath))
|
|
actualNodeFs.unlinkSync(testImageFilePath);
|
|
if (actualNodeFs.existsSync(testPdfFilePath))
|
|
actualNodeFs.unlinkSync(testPdfFilePath);
|
|
if (actualNodeFs.existsSync(testAudioFilePath))
|
|
actualNodeFs.unlinkSync(testAudioFilePath);
|
|
if (actualNodeFs.existsSync(testBinaryFilePath))
|
|
actualNodeFs.unlinkSync(testBinaryFilePath);
|
|
});
|
|
|
|
it('should read a text file successfully', async () => {
|
|
const content = 'Line 1\nLine 2\nLine 3';
|
|
actualNodeFs.writeFileSync(testTextFilePath, content);
|
|
const result = await processSingleFileContent(
|
|
testTextFilePath,
|
|
tempRootDir,
|
|
new StandardFileSystemService(),
|
|
);
|
|
expect(result.llmContent).toBe(content);
|
|
expect(result.returnDisplay).toBe('');
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
|
|
it('should read all lines when full flag is true', async () => {
|
|
// Create a file with more than DEFAULT_MAX_LINES_TEXT_FILE lines (2000 lines)
|
|
const lines = Array.from({ length: 2500 }, (_, i) => `Line ${i + 1}`);
|
|
const content = lines.join('\n');
|
|
actualNodeFs.writeFileSync(testTextFilePath, content);
|
|
|
|
// Without full flag, it should be truncated
|
|
const truncatedResult = await processSingleFileContent(
|
|
testTextFilePath,
|
|
tempRootDir,
|
|
new StandardFileSystemService(),
|
|
);
|
|
expect(truncatedResult.originalLineCount).toBe(2500);
|
|
expect(truncatedResult.isTruncated).toBe(true);
|
|
expect(
|
|
(truncatedResult.llmContent as string).split('\n').length,
|
|
).toBeLessThan(2500);
|
|
|
|
// With full flag, it should return all lines
|
|
const fullResult = await processSingleFileContent(
|
|
testTextFilePath,
|
|
tempRootDir,
|
|
new StandardFileSystemService(),
|
|
undefined,
|
|
undefined,
|
|
true,
|
|
);
|
|
expect(fullResult.originalLineCount).toBe(2500);
|
|
expect(fullResult.isTruncated).toBe(false);
|
|
expect((fullResult.llmContent as string).split('\n').length).toBe(2500);
|
|
});
|
|
|
|
it('should handle file not found', async () => {
|
|
const result = await processSingleFileContent(
|
|
nonexistentFilePath,
|
|
tempRootDir,
|
|
new StandardFileSystemService(),
|
|
);
|
|
expect(result.returnDisplay).toBe('File not found.');
|
|
expect(result.errorType).toBe(ToolErrorType.FILE_NOT_FOUND);
|
|
});
|
|
|
|
it('should handle directory path', async () => {
|
|
const result = await processSingleFileContent(
|
|
directoryPath,
|
|
tempRootDir,
|
|
new StandardFileSystemService(),
|
|
);
|
|
expect(result.returnDisplay).toBe('Path is a directory.');
|
|
expect(result.errorType).toBe(ToolErrorType.TARGET_IS_DIRECTORY);
|
|
});
|
|
|
|
it('should handle large file error', async () => {
|
|
// Ensure file exists so it doesn't fail on existsSync check
|
|
actualNodeFs.writeFileSync(testTextFilePath, 'some content');
|
|
// Mock stat to return a large size
|
|
vi.spyOn(fs.promises, 'stat').mockResolvedValue({
|
|
size: 30 * 1024 * 1024,
|
|
isDirectory: () => false,
|
|
} as unknown as fs.Stats);
|
|
|
|
const result = await processSingleFileContent(
|
|
testTextFilePath,
|
|
tempRootDir,
|
|
new StandardFileSystemService(),
|
|
);
|
|
expect(result.returnDisplay).toMatch(/File size exceeds the .*MB limit/);
|
|
expect(result.errorType).toBe(ToolErrorType.FILE_TOO_LARGE);
|
|
});
|
|
|
|
it('should handle binary file', async () => {
|
|
const content = Buffer.from([0x00, 0x01, 0x02, 0x03]);
|
|
actualNodeFs.writeFileSync(testBinaryFilePath, content);
|
|
const result = await processSingleFileContent(
|
|
testBinaryFilePath,
|
|
tempRootDir,
|
|
new StandardFileSystemService(),
|
|
);
|
|
expect(result.returnDisplay).toContain('Skipped binary file');
|
|
expect(result.llmContent).toContain(
|
|
'Cannot display content of binary file',
|
|
);
|
|
});
|
|
|
|
it('should handle image file', async () => {
|
|
mockMimeGetType.mockReturnValue('image/png');
|
|
const content = Buffer.from('fake-image-data');
|
|
actualNodeFs.writeFileSync(testImageFilePath, content);
|
|
const result = await processSingleFileContent(
|
|
testImageFilePath,
|
|
tempRootDir,
|
|
new StandardFileSystemService(),
|
|
);
|
|
expect(result.returnDisplay).toContain('Read image file');
|
|
expect(
|
|
(result.llmContent as { inlineData: { data: string } }).inlineData.data,
|
|
).toBe(content.toString('base64'));
|
|
expect(
|
|
(result.llmContent as { inlineData: { mimeType: string } }).inlineData
|
|
.mimeType,
|
|
).toBe('image/png');
|
|
});
|
|
|
|
it('should handle PDF file', async () => {
|
|
mockMimeGetType.mockReturnValue('application/pdf');
|
|
const content = Buffer.from('fake-pdf-data');
|
|
actualNodeFs.writeFileSync(testPdfFilePath, content);
|
|
const result = await processSingleFileContent(
|
|
testPdfFilePath,
|
|
tempRootDir,
|
|
new StandardFileSystemService(),
|
|
);
|
|
expect(result.returnDisplay).toContain('Read pdf file');
|
|
expect(
|
|
(result.llmContent as { inlineData: { data: string } }).inlineData.data,
|
|
).toBe(content.toString('base64'));
|
|
});
|
|
|
|
it('should handle truncation of text files', async () => {
|
|
const lines = Array.from({ length: 3000 }, (_, i) => `Line ${i + 1}`);
|
|
actualNodeFs.writeFileSync(testTextFilePath, lines.join('\n'));
|
|
const result = await processSingleFileContent(
|
|
testTextFilePath,
|
|
tempRootDir,
|
|
new StandardFileSystemService(),
|
|
);
|
|
expect(result.isTruncated).toBe(true);
|
|
expect(result.originalLineCount).toBe(3000);
|
|
expect(result.linesShown).toEqual([1, 2000]);
|
|
});
|
|
|
|
it('should read a specific line range', async () => {
|
|
const lines = Array.from({ length: 100 }, (_, i) => `Line ${i + 1}`);
|
|
actualNodeFs.writeFileSync(testTextFilePath, lines.join('\n'));
|
|
const result = await processSingleFileContent(
|
|
testTextFilePath,
|
|
tempRootDir,
|
|
new StandardFileSystemService(),
|
|
10,
|
|
20,
|
|
);
|
|
expect(result.originalLineCount).toBe(100);
|
|
expect(result.linesShown).toEqual([10, 20]);
|
|
const contentLines = (result.llmContent as string).split('\n');
|
|
expect(contentLines.length).toBe(11);
|
|
expect(contentLines[0]).toBe('Line 10');
|
|
expect(contentLines[10]).toBe('Line 20');
|
|
});
|
|
|
|
it('should handle SVG file as text', async () => {
|
|
const svgContent = '<svg><rect/></svg>';
|
|
const svgPath = path.join(tempRootDir, 'test.svg');
|
|
actualNodeFs.writeFileSync(svgPath, svgContent);
|
|
const result = await processSingleFileContent(
|
|
svgPath,
|
|
tempRootDir,
|
|
new StandardFileSystemService(),
|
|
);
|
|
expect(result.llmContent).toBe(svgContent);
|
|
expect(result.returnDisplay).toContain('Read SVG as text');
|
|
});
|
|
});
|
|
|
|
describe('saveTruncatedToolOutput', () => {
|
|
it('saves content to a temporary file', async () => {
|
|
const content = 'large output';
|
|
const { outputFile } = await saveTruncatedToolOutput(
|
|
content,
|
|
'testTool',
|
|
'call123',
|
|
tempRootDir,
|
|
);
|
|
expect(actualNodeFs.existsSync(outputFile)).toBe(true);
|
|
expect(actualNodeFs.readFileSync(outputFile, 'utf8')).toBe(content);
|
|
expect(outputFile).toContain('testtool_call123.txt');
|
|
});
|
|
|
|
it('handles session ID in path', async () => {
|
|
const content = 'session content';
|
|
const { outputFile } = await saveTruncatedToolOutput(
|
|
content,
|
|
'testTool',
|
|
'call123',
|
|
tempRootDir,
|
|
'session-456',
|
|
);
|
|
// sanitization of 'session-456' results in 'session-456'
|
|
expect(outputFile).toContain('session-session-456');
|
|
expect(actualNodeFs.readFileSync(outputFile, 'utf8')).toBe(content);
|
|
});
|
|
});
|
|
|
|
describe('formatTruncatedToolOutput', () => {
|
|
it('does not truncate if within limit', () => {
|
|
const content = 'short';
|
|
expect(formatTruncatedToolOutput(content, 'out.txt', 100)).toBe(content);
|
|
});
|
|
|
|
it('truncates content and adds link to file', () => {
|
|
const content = 'A'.repeat(1000);
|
|
const result = formatTruncatedToolOutput(content, 'out.txt', 100);
|
|
expect(result).toContain('Output too large');
|
|
expect(result).toContain('out.txt');
|
|
expect(result).toContain('characters omitted');
|
|
});
|
|
});
|
|
|
|
describe('getRealPath', () => {
|
|
it('returns real path for existing file', () => {
|
|
actualNodeFs.writeFileSync(testTextFilePath, 'test');
|
|
const real = getRealPath(testTextFilePath);
|
|
expect(path.isAbsolute(real)).toBe(true);
|
|
});
|
|
|
|
it('returns resolved path for non-existent file', () => {
|
|
const real = getRealPath(nonexistentFilePath);
|
|
expect(real).toBe(path.resolve(nonexistentFilePath));
|
|
});
|
|
});
|
|
});
|