2025-05-29 22:30:18 +00:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-08-22 17:47:32 +05:30
import { describe , it , expect , beforeEach , afterEach , vi } from 'vitest' ;
2026-03-04 05:42:59 +05:30
import { ReadFileTool , type ReadFileToolParams } from './read-file.js' ;
2025-08-08 04:33:42 -07:00
import { ToolErrorType } from './tool-error.js' ;
2025-08-25 22:11:27 +02:00
import path from 'node:path' ;
2026-01-27 13:17:40 -08:00
import { isSubpath } from '../utils/paths.js' ;
2025-08-25 22:11:27 +02:00
import os from 'node:os' ;
import fs from 'node:fs' ;
import fsp from 'node:fs/promises' ;
2025-08-26 00:04:53 +02:00
import type { Config } from '../config/config.js' ;
2025-06-14 10:25:34 -04:00
import { FileDiscoveryService } from '../services/fileDiscoveryService.js' ;
2025-08-18 16:29:45 -06:00
import { StandardFileSystemService } from '../services/fileSystemService.js' ;
2025-07-31 05:38:20 +09:00
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js' ;
2025-11-06 15:03:52 -08:00
import { WorkspaceContext } from '../utils/workspaceContext.js' ;
2026-01-04 14:59:35 -05:00
import { createMockMessageBus } from '../test-utils/mock-message-bus.js' ;
2026-01-27 17:19:13 -08:00
import { GEMINI_IGNORE_FILE_NAME } from '../config/constants.js' ;
2025-05-29 22:30:18 +00:00
2025-08-22 17:47:32 +05:30
vi . mock ( '../telemetry/loggers.js' , ( ) = > ( {
logFileOperation : vi.fn ( ) ,
} ) ) ;
2025-05-29 22:30:18 +00:00
describe ( 'ReadFileTool' , ( ) = > {
let tempRootDir : string ;
let tool : ReadFileTool ;
const abortSignal = new AbortController ( ) . signal ;
2025-07-25 10:31:22 -07:00
beforeEach ( async ( ) = > {
2025-05-29 22:30:18 +00:00
// Create a unique temporary root directory for each test run
2025-11-06 15:03:52 -08:00
const realTmp = await fsp . realpath ( os . tmpdir ( ) ) ;
tempRootDir = await fsp . mkdtemp ( path . join ( realTmp , 'read-file-tool-root-' ) ) ;
2025-07-25 10:31:22 -07:00
2025-06-05 10:15:27 -07:00
const mockConfigInstance = {
2025-07-25 10:31:22 -07:00
getFileService : ( ) = > new FileDiscoveryService ( tempRootDir ) ,
2025-08-18 16:29:45 -06:00
getFileSystemService : ( ) = > new StandardFileSystemService ( ) ,
2025-07-14 22:55:49 -07:00
getTargetDir : ( ) = > tempRootDir ,
2025-07-31 05:38:20 +09:00
getWorkspaceContext : ( ) = > createMockWorkspaceContext ( tempRootDir ) ,
2025-10-24 18:55:12 -07:00
getFileFilteringOptions : ( ) = > ( {
respectGitIgnore : true ,
respectGeminiIgnore : true ,
} ) ,
2025-09-05 15:37:29 -07:00
storage : {
getProjectTempDir : ( ) = > path . join ( tempRootDir , '.temp' ) ,
} ,
2025-11-11 02:03:32 -08:00
isInteractive : ( ) = > false ,
2026-01-27 13:17:40 -08:00
isPathAllowed ( this : Config , absolutePath : string ) : boolean {
const workspaceContext = this . getWorkspaceContext ( ) ;
if ( workspaceContext . isPathWithinWorkspace ( absolutePath ) ) {
return true ;
}
const projectTempDir = this . storage . getProjectTempDir ( ) ;
return isSubpath ( path . resolve ( projectTempDir ) , absolutePath ) ;
} ,
validatePathAccess ( this : Config , absolutePath : string ) : string | null {
if ( this . isPathAllowed ( absolutePath ) ) {
return null ;
}
const workspaceDirs = this . getWorkspaceContext ( ) . getDirectories ( ) ;
const projectTempDir = this . storage . getProjectTempDir ( ) ;
return ` Path not in workspace: Attempted path " ${ absolutePath } " resolves outside the allowed workspace directories: ${ workspaceDirs . join ( ', ' ) } or the project temp directory: ${ projectTempDir } ` ;
} ,
2025-06-14 10:25:34 -04:00
} as unknown as Config ;
2026-01-04 14:59:35 -05:00
tool = new ReadFileTool ( mockConfigInstance , createMockMessageBus ( ) ) ;
2025-05-29 22:30:18 +00:00
} ) ;
2025-07-25 10:31:22 -07:00
afterEach ( async ( ) = > {
2025-05-29 22:30:18 +00:00
// Clean up the temporary root directory
if ( fs . existsSync ( tempRootDir ) ) {
2025-07-25 10:31:22 -07:00
await fsp . rm ( tempRootDir , { recursive : true , force : true } ) ;
2025-05-29 22:30:18 +00:00
}
} ) ;
2025-08-06 10:50:02 -07:00
describe ( 'build' , ( ) = > {
it ( 'should return an invocation for valid params (absolute path within root)' , ( ) = > {
2025-05-29 22:30:18 +00:00
const params : ReadFileToolParams = {
2025-11-06 15:03:52 -08:00
file_path : path.join ( tempRootDir , 'test.txt' ) ,
2025-05-29 22:30:18 +00:00
} ;
2025-08-06 10:50:02 -07:00
const result = tool . build ( params ) ;
2025-08-08 04:33:42 -07:00
expect ( typeof result ) . not . toBe ( 'string' ) ;
2025-05-29 22:30:18 +00:00
} ) ;
2025-11-06 15:03:52 -08:00
it ( 'should return an invocation for valid params (relative path within root)' , ( ) = > {
2025-05-29 22:30:18 +00:00
const params : ReadFileToolParams = {
2025-11-06 15:03:52 -08:00
file_path : 'test.txt' ,
2025-05-29 22:30:18 +00:00
} ;
2025-11-06 15:03:52 -08:00
const result = tool . build ( params ) ;
expect ( typeof result ) . not . toBe ( 'string' ) ;
2025-12-12 17:43:43 -08:00
const invocation = result ;
2025-11-06 15:03:52 -08:00
expect ( invocation . toolLocations ( ) [ 0 ] . path ) . toBe (
path . join ( tempRootDir , 'test.txt' ) ,
2025-05-29 22:30:18 +00:00
) ;
} ) ;
2025-08-08 04:33:42 -07:00
it ( 'should throw error if path is outside root' , ( ) = > {
const params : ReadFileToolParams = {
2025-11-06 15:03:52 -08:00
file_path : '/outside/root.txt' ,
2025-08-08 04:33:42 -07:00
} ;
2026-01-27 13:17:40 -08:00
expect ( ( ) = > tool . build ( params ) ) . toThrow ( /Path not in workspace/ ) ;
2025-05-29 22:30:18 +00:00
} ) ;
2025-09-05 15:37:29 -07:00
it ( 'should allow access to files in project temp directory' , ( ) = > {
const tempDir = path . join ( tempRootDir , '.temp' ) ;
const params : ReadFileToolParams = {
2025-11-06 15:03:52 -08:00
file_path : path.join ( tempDir , 'temp-file.txt' ) ,
2025-09-05 15:37:29 -07:00
} ;
const result = tool . build ( params ) ;
expect ( typeof result ) . not . toBe ( 'string' ) ;
} ) ;
it ( 'should show temp directory in error message when path is outside workspace and temp dir' , ( ) = > {
const params : ReadFileToolParams = {
2025-11-06 15:03:52 -08:00
file_path : '/completely/outside/path.txt' ,
2025-09-05 15:37:29 -07:00
} ;
2026-01-27 13:17:40 -08:00
expect ( ( ) = > tool . build ( params ) ) . toThrow ( /Path not in workspace/ ) ;
2025-09-05 15:37:29 -07:00
} ) ;
2025-08-19 13:55:06 -07:00
it ( 'should throw error if path is empty' , ( ) = > {
const params : ReadFileToolParams = {
2025-11-06 15:03:52 -08:00
file_path : '' ,
2025-08-19 13:55:06 -07:00
} ;
expect ( ( ) = > tool . build ( params ) ) . toThrow (
2025-11-06 15:03:52 -08:00
/The 'file_path' parameter must be non-empty./ ,
2025-08-19 13:55:06 -07:00
) ;
} ) ;
2026-02-20 17:59:18 -05:00
it ( 'should throw error if start_line is less than 1' , ( ) = > {
2025-05-29 22:30:18 +00:00
const params : ReadFileToolParams = {
2025-11-06 15:03:52 -08:00
file_path : path.join ( tempRootDir , 'test.txt' ) ,
2026-02-20 17:59:18 -05:00
start_line : 0 ,
2025-05-29 22:30:18 +00:00
} ;
2026-02-20 17:59:18 -05:00
expect ( ( ) = > tool . build ( params ) ) . toThrow ( 'start_line must be at least 1' ) ;
} ) ;
it ( 'should throw error if end_line is less than 1' , ( ) = > {
const params : ReadFileToolParams = {
file_path : path.join ( tempRootDir , 'test.txt' ) ,
end_line : 0 ,
} ;
expect ( ( ) = > tool . build ( params ) ) . toThrow ( 'end_line must be at least 1' ) ;
2025-05-29 22:30:18 +00:00
} ) ;
2026-02-20 17:59:18 -05:00
it ( 'should throw error if start_line is greater than end_line' , ( ) = > {
2025-08-08 04:33:42 -07:00
const params : ReadFileToolParams = {
2025-11-06 15:03:52 -08:00
file_path : path.join ( tempRootDir , 'test.txt' ) ,
2026-02-20 17:59:18 -05:00
start_line : 10 ,
end_line : 5 ,
2025-05-29 22:30:18 +00:00
} ;
2025-08-08 04:33:42 -07:00
expect ( ( ) = > tool . build ( params ) ) . toThrow (
2026-02-20 17:59:18 -05:00
'start_line cannot be greater than end_line' ,
2025-05-29 22:30:18 +00:00
) ;
2025-08-08 04:33:42 -07:00
} ) ;
} ) ;
describe ( 'getDescription' , ( ) = > {
2026-02-20 17:59:18 -05:00
it ( 'should return relative path without ranges' , ( ) = > {
2025-08-08 04:33:42 -07:00
const subDir = path . join ( tempRootDir , 'sub' , 'dir' ) ;
const params : ReadFileToolParams = {
2025-11-06 15:03:52 -08:00
file_path : path.join ( subDir , 'file.txt' ) ,
2025-05-29 22:30:18 +00:00
} ;
2025-08-08 04:33:42 -07:00
const invocation = tool . build ( params ) ;
expect ( typeof invocation ) . not . toBe ( 'string' ) ;
2025-12-12 17:43:43 -08:00
expect ( invocation . getDescription ( ) ) . toBe (
path . join ( 'sub' , 'dir' , 'file.txt' ) ,
) ;
2025-05-29 22:30:18 +00:00
} ) ;
2025-08-08 04:33:42 -07:00
it ( 'should return shortened path when file path is deep' , ( ) = > {
const deepPath = path . join (
tempRootDir ,
'very' ,
'deep' ,
'directory' ,
'structure' ,
'that' ,
'exceeds' ,
'the' ,
'normal' ,
'limit' ,
'file.txt' ,
2025-05-29 22:30:18 +00:00
) ;
2025-11-06 15:03:52 -08:00
const params : ReadFileToolParams = { file_path : deepPath } ;
2025-08-08 04:33:42 -07:00
const invocation = tool . build ( params ) ;
expect ( typeof invocation ) . not . toBe ( 'string' ) ;
2025-12-12 17:43:43 -08:00
const desc = invocation . getDescription ( ) ;
2025-08-08 04:33:42 -07:00
expect ( desc ) . toContain ( '...' ) ;
expect ( desc ) . toContain ( 'file.txt' ) ;
2025-05-29 22:30:18 +00:00
} ) ;
2025-08-08 04:33:42 -07:00
it ( 'should handle non-normalized file paths correctly' , ( ) = > {
const subDir = path . join ( tempRootDir , 'sub' , 'dir' ) ;
const params : ReadFileToolParams = {
2025-11-06 15:03:52 -08:00
file_path : path.join ( subDir , '..' , 'dir' , 'file.txt' ) ,
2025-08-08 04:33:42 -07:00
} ;
const invocation = tool . build ( params ) ;
expect ( typeof invocation ) . not . toBe ( 'string' ) ;
2025-12-12 17:43:43 -08:00
expect ( invocation . getDescription ( ) ) . toBe (
path . join ( 'sub' , 'dir' , 'file.txt' ) ,
) ;
2025-08-08 04:33:42 -07:00
} ) ;
2025-05-29 22:30:18 +00:00
2025-08-08 04:33:42 -07:00
it ( 'should return . if path is the root directory' , ( ) = > {
2025-11-06 15:03:52 -08:00
const params : ReadFileToolParams = { file_path : tempRootDir } ;
2025-08-08 04:33:42 -07:00
const invocation = tool . build ( params ) ;
expect ( typeof invocation ) . not . toBe ( 'string' ) ;
2025-12-12 17:43:43 -08:00
expect ( invocation . getDescription ( ) ) . toBe ( '.' ) ;
2025-05-29 22:30:18 +00:00
} ) ;
2025-08-08 04:33:42 -07:00
} ) ;
2025-05-29 22:30:18 +00:00
2025-11-06 15:03:52 -08:00
describe ( 'execute' , ( ) = > {
it ( 'should successfully read a file with a relative path' , async ( ) = > {
const filePath = path . join ( tempRootDir , 'textfile.txt' ) ;
const fileContent = 'This is a test file.' ;
await fsp . writeFile ( filePath , fileContent , 'utf-8' ) ;
const params : ReadFileToolParams = { file_path : 'textfile.txt' } ;
2025-12-12 17:43:43 -08:00
const invocation = tool . build ( params ) ;
2025-10-27 09:47:13 +05:30
2025-11-06 15:03:52 -08:00
expect ( await invocation . execute ( abortSignal ) ) . toEqual ( {
llmContent : fileContent ,
returnDisplay : '' ,
} ) ;
2025-10-27 09:47:13 +05:30
} ) ;
2025-08-08 04:33:42 -07:00
it ( 'should return error if file does not exist' , async ( ) = > {
const filePath = path . join ( tempRootDir , 'nonexistent.txt' ) ;
2025-11-06 15:03:52 -08:00
const params : ReadFileToolParams = { file_path : filePath } ;
2025-12-12 17:43:43 -08:00
const invocation = tool . build ( params ) ;
2025-08-08 04:33:42 -07:00
const result = await invocation . execute ( abortSignal ) ;
expect ( result ) . toEqual ( {
llmContent :
'Could not read file because no file was found at the specified path.' ,
returnDisplay : 'File not found.' ,
error : {
message : ` File not found: ${ filePath } ` ,
type : ToolErrorType . FILE_NOT_FOUND ,
} ,
2025-05-29 22:30:18 +00:00
} ) ;
2025-08-08 04:33:42 -07:00
} ) ;
2025-05-29 22:30:18 +00:00
2025-08-08 04:33:42 -07:00
it ( 'should return success result for a text file' , async ( ) = > {
const filePath = path . join ( tempRootDir , 'textfile.txt' ) ;
const fileContent = 'This is a test file.' ;
await fsp . writeFile ( filePath , fileContent , 'utf-8' ) ;
2025-11-06 15:03:52 -08:00
const params : ReadFileToolParams = { file_path : filePath } ;
2025-12-12 17:43:43 -08:00
const invocation = tool . build ( params ) ;
2025-08-08 04:33:42 -07:00
expect ( await invocation . execute ( abortSignal ) ) . toEqual ( {
llmContent : fileContent ,
returnDisplay : '' ,
2025-05-29 22:30:18 +00:00
} ) ;
2025-08-08 04:33:42 -07:00
} ) ;
2025-05-29 22:30:18 +00:00
2025-08-08 04:33:42 -07:00
it ( 'should return error if path is a directory' , async ( ) = > {
const dirPath = path . join ( tempRootDir , 'directory' ) ;
await fsp . mkdir ( dirPath ) ;
2025-11-06 15:03:52 -08:00
const params : ReadFileToolParams = { file_path : dirPath } ;
2025-12-12 17:43:43 -08:00
const invocation = tool . build ( params ) ;
2025-08-08 04:33:42 -07:00
const result = await invocation . execute ( abortSignal ) ;
expect ( result ) . toEqual ( {
llmContent :
'Could not read file because the provided path is a directory, not a file.' ,
returnDisplay : 'Path is a directory.' ,
error : {
message : ` Path is a directory, not a file: ${ dirPath } ` ,
2025-08-20 16:13:29 -07:00
type : ToolErrorType . TARGET_IS_DIRECTORY ,
2025-08-08 04:33:42 -07:00
} ,
2025-07-25 10:31:22 -07:00
} ) ;
2025-08-08 04:33:42 -07:00
} ) ;
2025-05-29 22:30:18 +00:00
2025-08-08 04:33:42 -07:00
it ( 'should return error for a file that is too large' , async ( ) = > {
const filePath = path . join ( tempRootDir , 'largefile.txt' ) ;
// 21MB of content exceeds 20MB limit
const largeContent = 'x' . repeat ( 21 * 1024 * 1024 ) ;
await fsp . writeFile ( filePath , largeContent , 'utf-8' ) ;
2025-11-06 15:03:52 -08:00
const params : ReadFileToolParams = { file_path : filePath } ;
2025-12-12 17:43:43 -08:00
const invocation = tool . build ( params ) ;
2025-08-08 04:33:42 -07:00
const result = await invocation . execute ( abortSignal ) ;
expect ( result ) . toHaveProperty ( 'error' ) ;
expect ( result . error ? . type ) . toBe ( ToolErrorType . FILE_TOO_LARGE ) ;
expect ( result . error ? . message ) . toContain (
'File size exceeds the 20MB limit' ,
) ;
} ) ;
2025-06-05 10:15:27 -07:00
2025-08-08 04:33:42 -07:00
it ( 'should handle text file with lines exceeding maximum length' , async ( ) = > {
const filePath = path . join ( tempRootDir , 'longlines.txt' ) ;
const longLine = 'a' . repeat ( 2500 ) ; // Exceeds MAX_LINE_LENGTH_TEXT_FILE (2000)
const fileContent = ` Short line \ n ${ longLine } \ nAnother short line ` ;
await fsp . writeFile ( filePath , fileContent , 'utf-8' ) ;
2025-11-06 15:03:52 -08:00
const params : ReadFileToolParams = { file_path : filePath } ;
2025-12-12 17:43:43 -08:00
const invocation = tool . build ( params ) ;
2025-08-08 04:33:42 -07:00
const result = await invocation . execute ( abortSignal ) ;
expect ( result . llmContent ) . toContain (
'IMPORTANT: The file content has been truncated' ,
) ;
expect ( result . llmContent ) . toContain ( '--- FILE CONTENT (truncated) ---' ) ;
expect ( result . returnDisplay ) . toContain ( 'some lines were shortened' ) ;
} ) ;
2025-07-25 10:31:22 -07:00
2025-08-08 04:33:42 -07:00
it ( 'should handle image file and return appropriate content' , async ( ) = > {
const imagePath = path . join ( tempRootDir , 'image.png' ) ;
// Minimal PNG header
const pngHeader = Buffer . from ( [
0x89 , 0x50 , 0x4e , 0x47 , 0x0d , 0x0a , 0x1a , 0x0a ,
] ) ;
await fsp . writeFile ( imagePath , pngHeader ) ;
2025-11-06 15:03:52 -08:00
const params : ReadFileToolParams = { file_path : imagePath } ;
2025-12-12 17:43:43 -08:00
const invocation = tool . build ( params ) ;
2025-08-08 04:33:42 -07:00
const result = await invocation . execute ( abortSignal ) ;
expect ( result . llmContent ) . toEqual ( {
inlineData : {
data : pngHeader.toString ( 'base64' ) ,
mimeType : 'image/png' ,
} ,
2025-07-25 10:31:22 -07:00
} ) ;
2025-08-08 04:33:42 -07:00
expect ( result . returnDisplay ) . toBe ( 'Read image file: image.png' ) ;
} ) ;
2025-07-25 10:31:22 -07:00
2025-08-08 04:33:42 -07:00
it ( 'should handle PDF file and return appropriate content' , async ( ) = > {
const pdfPath = path . join ( tempRootDir , 'document.pdf' ) ;
// Minimal PDF header
const pdfHeader = Buffer . from ( '%PDF-1.4' ) ;
await fsp . writeFile ( pdfPath , pdfHeader ) ;
2025-11-06 15:03:52 -08:00
const params : ReadFileToolParams = { file_path : pdfPath } ;
2025-12-12 17:43:43 -08:00
const invocation = tool . build ( params ) ;
2025-08-08 04:33:42 -07:00
const result = await invocation . execute ( abortSignal ) ;
expect ( result . llmContent ) . toEqual ( {
inlineData : {
data : pdfHeader.toString ( 'base64' ) ,
mimeType : 'application/pdf' ,
} ,
2025-07-25 10:31:22 -07:00
} ) ;
2025-08-08 04:33:42 -07:00
expect ( result . returnDisplay ) . toBe ( 'Read pdf file: document.pdf' ) ;
2025-06-05 10:15:27 -07:00
} ) ;
2025-07-31 05:38:20 +09:00
2025-08-08 04:33:42 -07:00
it ( 'should handle binary file and skip content' , async ( ) = > {
const binPath = path . join ( tempRootDir , 'binary.bin' ) ;
// Binary data with null bytes
const binaryData = Buffer . from ( [ 0x00 , 0xff , 0x00 , 0xff ] ) ;
await fsp . writeFile ( binPath , binaryData ) ;
2025-11-06 15:03:52 -08:00
const params : ReadFileToolParams = { file_path : binPath } ;
2025-12-12 17:43:43 -08:00
const invocation = tool . build ( params ) ;
2025-08-08 04:33:42 -07:00
const result = await invocation . execute ( abortSignal ) ;
expect ( result . llmContent ) . toBe (
'Cannot display content of binary file: binary.bin' ,
) ;
expect ( result . returnDisplay ) . toBe ( 'Skipped binary file: binary.bin' ) ;
2025-07-31 05:38:20 +09:00
} ) ;
2025-08-08 04:33:42 -07:00
it ( 'should handle SVG file as text' , async ( ) = > {
const svgPath = path . join ( tempRootDir , 'image.svg' ) ;
const svgContent = '<svg><circle cx="50" cy="50" r="40"/></svg>' ;
await fsp . writeFile ( svgPath , svgContent , 'utf-8' ) ;
2025-11-06 15:03:52 -08:00
const params : ReadFileToolParams = { file_path : svgPath } ;
2025-12-12 17:43:43 -08:00
const invocation = tool . build ( params ) ;
2025-08-08 04:33:42 -07:00
const result = await invocation . execute ( abortSignal ) ;
expect ( result . llmContent ) . toBe ( svgContent ) ;
expect ( result . returnDisplay ) . toBe ( 'Read SVG as text: image.svg' ) ;
} ) ;
it ( 'should handle large SVG file' , async ( ) = > {
const svgPath = path . join ( tempRootDir , 'large.svg' ) ;
// Create SVG content larger than 1MB
const largeContent = '<svg>' + 'x' . repeat ( 1024 * 1024 + 1 ) + '</svg>' ;
await fsp . writeFile ( svgPath , largeContent , 'utf-8' ) ;
2025-11-06 15:03:52 -08:00
const params : ReadFileToolParams = { file_path : svgPath } ;
2025-12-12 17:43:43 -08:00
const invocation = tool . build ( params ) ;
2025-08-08 04:33:42 -07:00
const result = await invocation . execute ( abortSignal ) ;
expect ( result . llmContent ) . toBe (
'Cannot display content of SVG file larger than 1MB: large.svg' ,
) ;
expect ( result . returnDisplay ) . toBe (
'Skipped large SVG file (>1MB): large.svg' ,
2025-07-31 05:38:20 +09:00
) ;
} ) ;
2025-08-08 04:33:42 -07:00
it ( 'should handle empty file' , async ( ) = > {
const emptyPath = path . join ( tempRootDir , 'empty.txt' ) ;
await fsp . writeFile ( emptyPath , '' , 'utf-8' ) ;
2025-11-06 15:03:52 -08:00
const params : ReadFileToolParams = { file_path : emptyPath } ;
2025-12-12 17:43:43 -08:00
const invocation = tool . build ( params ) ;
2025-08-08 04:33:42 -07:00
const result = await invocation . execute ( abortSignal ) ;
expect ( result . llmContent ) . toBe ( '' ) ;
expect ( result . returnDisplay ) . toBe ( '' ) ;
} ) ;
2026-02-20 17:59:18 -05:00
it ( 'should support start_line and end_line for text files' , async ( ) = > {
2025-08-08 04:33:42 -07:00
const filePath = path . join ( tempRootDir , 'paginated.txt' ) ;
const lines = Array . from ( { length : 20 } , ( _ , i ) = > ` Line ${ i + 1 } ` ) ;
const fileContent = lines . join ( '\n' ) ;
await fsp . writeFile ( filePath , fileContent , 'utf-8' ) ;
2025-07-31 05:38:20 +09:00
const params : ReadFileToolParams = {
2025-11-06 15:03:52 -08:00
file_path : filePath ,
2026-02-20 17:59:18 -05:00
start_line : 6 ,
end_line : 8 ,
2025-07-31 05:38:20 +09:00
} ;
2025-12-12 17:43:43 -08:00
const invocation = tool . build ( params ) ;
2025-08-08 04:33:42 -07:00
const result = await invocation . execute ( abortSignal ) ;
expect ( result . llmContent ) . toContain (
'IMPORTANT: The file content has been truncated' ,
) ;
expect ( result . llmContent ) . toContain (
'Status: Showing lines 6-8 of 20 total lines' ,
2025-07-31 05:38:20 +09:00
) ;
2025-08-08 04:33:42 -07:00
expect ( result . llmContent ) . toContain ( 'Line 6' ) ;
expect ( result . llmContent ) . toContain ( 'Line 7' ) ;
expect ( result . llmContent ) . toContain ( 'Line 8' ) ;
expect ( result . returnDisplay ) . toBe (
'Read lines 6-8 of 20 from paginated.txt' ,
) ;
} ) ;
2025-09-05 15:37:29 -07:00
it ( 'should successfully read files from project temp directory' , async ( ) = > {
const tempDir = path . join ( tempRootDir , '.temp' ) ;
await fsp . mkdir ( tempDir , { recursive : true } ) ;
const tempFilePath = path . join ( tempDir , 'temp-output.txt' ) ;
const tempFileContent = 'This is temporary output content' ;
await fsp . writeFile ( tempFilePath , tempFileContent , 'utf-8' ) ;
2025-11-06 15:03:52 -08:00
const params : ReadFileToolParams = { file_path : tempFilePath } ;
2025-12-12 17:43:43 -08:00
const invocation = tool . build ( params ) ;
2025-09-05 15:37:29 -07:00
const result = await invocation . execute ( abortSignal ) ;
expect ( result . llmContent ) . toBe ( tempFileContent ) ;
expect ( result . returnDisplay ) . toBe ( '' ) ;
} ) ;
2025-08-08 04:33:42 -07:00
describe ( 'with .geminiignore' , ( ) = > {
beforeEach ( async ( ) = > {
await fsp . writeFile (
2026-01-27 17:19:13 -08:00
path . join ( tempRootDir , GEMINI_IGNORE_FILE_NAME ) ,
2025-08-08 04:33:42 -07:00
[ 'foo.*' , 'ignored/' ] . join ( '\n' ) ,
) ;
2025-11-06 15:03:52 -08:00
const mockConfigInstance = {
getFileService : ( ) = > new FileDiscoveryService ( tempRootDir ) ,
getFileSystemService : ( ) = > new StandardFileSystemService ( ) ,
getTargetDir : ( ) = > tempRootDir ,
getWorkspaceContext : ( ) = > new WorkspaceContext ( tempRootDir ) ,
getFileFilteringOptions : ( ) = > ( {
respectGitIgnore : true ,
respectGeminiIgnore : true ,
} ) ,
storage : {
getProjectTempDir : ( ) = > path . join ( tempRootDir , '.temp' ) ,
} ,
2026-01-27 13:17:40 -08:00
isPathAllowed ( this : Config , absolutePath : string ) : boolean {
const workspaceContext = this . getWorkspaceContext ( ) ;
if ( workspaceContext . isPathWithinWorkspace ( absolutePath ) ) {
return true ;
}
const projectTempDir = this . storage . getProjectTempDir ( ) ;
return isSubpath ( path . resolve ( projectTempDir ) , absolutePath ) ;
} ,
validatePathAccess (
this : Config ,
absolutePath : string ,
) : string | null {
if ( this . isPathAllowed ( absolutePath ) ) {
return null ;
}
const workspaceDirs = this . getWorkspaceContext ( ) . getDirectories ( ) ;
const projectTempDir = this . storage . getProjectTempDir ( ) ;
return ` Path not in workspace: Attempted path " ${ absolutePath } " resolves outside the allowed workspace directories: ${ workspaceDirs . join ( ', ' ) } or the project temp directory: ${ projectTempDir } ` ;
} ,
2025-11-06 15:03:52 -08:00
} as unknown as Config ;
2026-01-04 14:59:35 -05:00
tool = new ReadFileTool ( mockConfigInstance , createMockMessageBus ( ) ) ;
2025-08-08 04:33:42 -07:00
} ) ;
it ( 'should throw error if path is ignored by a .geminiignore pattern' , async ( ) = > {
const ignoredFilePath = path . join ( tempRootDir , 'foo.bar' ) ;
await fsp . writeFile ( ignoredFilePath , 'content' , 'utf-8' ) ;
const params : ReadFileToolParams = {
2025-11-06 15:03:52 -08:00
file_path : ignoredFilePath ,
2025-08-08 04:33:42 -07:00
} ;
2025-10-24 18:55:12 -07:00
const expectedError = ` File path ' ${ ignoredFilePath } ' is ignored by configured ignore patterns. ` ;
2025-08-08 04:33:42 -07:00
expect ( ( ) = > tool . build ( params ) ) . toThrow ( expectedError ) ;
} ) ;
it ( 'should throw error if file is in an ignored directory' , async ( ) = > {
const ignoredDirPath = path . join ( tempRootDir , 'ignored' ) ;
await fsp . mkdir ( ignoredDirPath , { recursive : true } ) ;
const ignoredFilePath = path . join ( ignoredDirPath , 'file.txt' ) ;
await fsp . writeFile ( ignoredFilePath , 'content' , 'utf-8' ) ;
const params : ReadFileToolParams = {
2025-11-06 15:03:52 -08:00
file_path : ignoredFilePath ,
2025-08-08 04:33:42 -07:00
} ;
2025-10-24 18:55:12 -07:00
const expectedError = ` File path ' ${ ignoredFilePath } ' is ignored by configured ignore patterns. ` ;
2025-08-08 04:33:42 -07:00
expect ( ( ) = > tool . build ( params ) ) . toThrow ( expectedError ) ;
} ) ;
it ( 'should allow reading non-ignored files' , async ( ) = > {
const allowedFilePath = path . join ( tempRootDir , 'allowed.txt' ) ;
await fsp . writeFile ( allowedFilePath , 'content' , 'utf-8' ) ;
const params : ReadFileToolParams = {
2025-11-06 15:03:52 -08:00
file_path : allowedFilePath ,
2025-08-08 04:33:42 -07:00
} ;
const invocation = tool . build ( params ) ;
expect ( typeof invocation ) . not . toBe ( 'string' ) ;
} ) ;
2026-01-27 17:19:13 -08:00
it ( 'should allow reading ignored files if respectGeminiIgnore is false' , async ( ) = > {
const ignoredFilePath = path . join ( tempRootDir , 'foo.bar' ) ;
await fsp . writeFile ( ignoredFilePath , 'content' , 'utf-8' ) ;
const configNoIgnore = {
getFileService : ( ) = > new FileDiscoveryService ( tempRootDir ) ,
getFileSystemService : ( ) = > new StandardFileSystemService ( ) ,
getTargetDir : ( ) = > tempRootDir ,
getWorkspaceContext : ( ) = > new WorkspaceContext ( tempRootDir ) ,
getFileFilteringOptions : ( ) = > ( {
respectGitIgnore : true ,
respectGeminiIgnore : false ,
} ) ,
storage : {
getProjectTempDir : ( ) = > path . join ( tempRootDir , '.temp' ) ,
} ,
isInteractive : ( ) = > false ,
isPathAllowed ( this : Config , absolutePath : string ) : boolean {
const workspaceContext = this . getWorkspaceContext ( ) ;
if ( workspaceContext . isPathWithinWorkspace ( absolutePath ) ) {
return true ;
}
const projectTempDir = this . storage . getProjectTempDir ( ) ;
return isSubpath ( path . resolve ( projectTempDir ) , absolutePath ) ;
} ,
validatePathAccess (
this : Config ,
absolutePath : string ,
) : string | null {
if ( this . isPathAllowed ( absolutePath ) ) {
return null ;
}
const workspaceDirs = this . getWorkspaceContext ( ) . getDirectories ( ) ;
const projectTempDir = this . storage . getProjectTempDir ( ) ;
return ` Path not in workspace: Attempted path " ${ absolutePath } " resolves outside the allowed workspace directories: ${ workspaceDirs . join ( ', ' ) } or the project temp directory: ${ projectTempDir } ` ;
} ,
} as unknown as Config ;
const toolNoIgnore = new ReadFileTool (
configNoIgnore ,
createMockMessageBus ( ) ,
) ;
const params : ReadFileToolParams = {
file_path : ignoredFilePath ,
} ;
const invocation = toolNoIgnore . build ( params ) ;
expect ( typeof invocation ) . not . toBe ( 'string' ) ;
} ) ;
2025-07-31 05:38:20 +09:00
} ) ;
} ) ;
2026-02-09 15:46:23 -05:00
describe ( 'getSchema' , ( ) = > {
it ( 'should return the base schema when no modelId is provided' , ( ) = > {
const schema = tool . getSchema ( ) ;
expect ( schema . name ) . toBe ( ReadFileTool . Name ) ;
expect ( schema . description ) . toMatchSnapshot ( ) ;
2026-02-20 17:59:18 -05:00
expect (
( schema . parametersJsonSchema as { properties : Record < string , unknown > } )
. properties ,
) . not . toHaveProperty ( 'offset' ) ;
2026-02-09 15:46:23 -05:00
} ) ;
it ( 'should return the schema from the resolver when modelId is provided' , ( ) = > {
const modelId = 'gemini-2.0-flash' ;
const schema = tool . getSchema ( modelId ) ;
expect ( schema . name ) . toBe ( ReadFileTool . Name ) ;
expect ( schema . description ) . toMatchSnapshot ( ) ;
} ) ;
2026-03-02 15:11:58 -05:00
it ( 'should return the Gemini 3 schema when a Gemini 3 modelId is provided' , ( ) = > {
const modelId = 'gemini-3-pro-preview' ;
const schema = tool . getSchema ( modelId ) ;
expect ( schema . name ) . toBe ( ReadFileTool . Name ) ;
expect ( schema . description ) . toMatchSnapshot ( ) ;
expect ( schema . description ) . toContain ( 'surgical reads' ) ;
} ) ;
2026-02-09 15:46:23 -05:00
} ) ;
2025-05-29 22:30:18 +00:00
} ) ;