/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { describe, expect } from 'vitest'; import { evalTest } from './test-helper.js'; import { READ_FILE_TOOL_NAME, EDIT_TOOL_NAME } from '@google/gemini-cli-core'; describe('Frugal reads eval', () => { /** * Ensures that the agent is frugal in its use of context by relying * primarily on ranged reads when the line number is known, and combining * nearby ranges into a single contiguous read to save tool calls. */ evalTest('USUALLY_PASSES', { name: 'should use ranged read when nearby lines are targeted', files: { 'package.json': JSON.stringify({ name: 'test-project', version: '1.0.0', type: 'module', }), 'eslint.config.mjs': `export default [ { files: ["**/*.ts"], rules: { "no-var": "error" } } ];`, 'linter_mess.ts': (() => { const lines = []; for (let i = 0; i < 1000; i++) { if (i === 500 || i === 510 || i === 520) { lines.push(`var oldVar${i} = "needs fix";`); } else { lines.push(`const goodVar${i} = "clean";`); } } return lines.join('\n'); })(), }, prompt: 'Fix all linter errors in linter_mess.ts manually by editing the file. Run eslint directly (using "npx --yes eslint") to find them. Do not run the file.', assert: async (rig) => { const logs = rig.readToolLogs(); // Check if the agent read the whole file const readCalls = logs.filter( (log) => log.toolRequest?.name === READ_FILE_TOOL_NAME, ); const targetFileReads = readCalls.filter((call) => { const args = JSON.parse(call.toolRequest.args); return args.file_path.includes('linter_mess.ts'); }); expect( targetFileReads.length, 'Agent should have used read_file to check context', ).toBeGreaterThan(0); // We expect 1-3 ranges in a single turn. expect( targetFileReads.length, 'Agent should have used 1-3 ranged reads for near errors', ).toBeLessThanOrEqual(3); const firstPromptId = targetFileReads[0].toolRequest.prompt_id; expect(firstPromptId, 'Prompt ID should be defined').toBeDefined(); expect( targetFileReads.every( (call) => call.toolRequest.prompt_id === firstPromptId, ), 'All reads should have happened in the same turn', ).toBe(true); let totalLinesRead = 0; const readRanges: { start_line: number; end_line: number }[] = []; for (const call of targetFileReads) { const args = JSON.parse(call.toolRequest.args); expect( args.end_line, 'Agent read the entire file (missing end_line) instead of using ranged read', ).toBeDefined(); const end_line = args.end_line; const start_line = args.start_line ?? 1; const linesRead = end_line - start_line + 1; totalLinesRead += linesRead; readRanges.push({ start_line, end_line }); expect(linesRead, 'Agent read too many lines at once').toBeLessThan( 1001, ); } // Ranged read shoud be frugal and just enough to satisfy the task at hand. expect( totalLinesRead, 'Agent read more of the file than expected', ).toBeLessThan(1000); // Check that we read around the error lines const errorLines = [500, 510, 520]; for (const line of errorLines) { const covered = readRanges.some( (range) => line >= range.start_line && line <= range.end_line, ); expect(covered, `Agent should have read around line ${line}`).toBe( true, ); } const editCalls = logs.filter( (log) => log.toolRequest?.name === EDIT_TOOL_NAME, ); const targetEditCalls = editCalls.filter((call) => { const args = JSON.parse(call.toolRequest.args); return args.file_path.includes('linter_mess.ts'); }); expect( targetEditCalls.length, 'Agent should have made replacement calls on the target file', ).toBeGreaterThanOrEqual(3); }, }); /** * Ensures the agent uses multiple ranged reads when the targets are far * apart to avoid the need to read the whole file. */ evalTest('USUALLY_PASSES', { name: 'should use ranged read when targets are far apart', files: { 'package.json': JSON.stringify({ name: 'test-project', version: '1.0.0', type: 'module', }), 'eslint.config.mjs': `export default [ { files: ["**/*.ts"], rules: { "no-var": "error" } } ];`, 'far_mess.ts': (() => { const lines = []; for (let i = 0; i < 1000; i++) { if (i === 100 || i === 900) { lines.push(`var oldVar${i} = "needs fix";`); } else { lines.push(`const goodVar${i} = "clean";`); } } return lines.join('\n'); })(), }, prompt: 'Fix all linter errors in far_mess.ts manually by editing the file. Run eslint directly (using "npx --yes eslint") to find them. Do not run the file.', assert: async (rig) => { const logs = rig.readToolLogs(); const readCalls = logs.filter( (log) => log.toolRequest?.name === READ_FILE_TOOL_NAME, ); const targetFileReads = readCalls.filter((call) => { const args = JSON.parse(call.toolRequest.args); return args.file_path.includes('far_mess.ts'); }); // The agent should use ranged reads to be frugal with context tokens, // even if it requires multiple calls for far-apart errors. expect( targetFileReads.length, 'Agent should have used read_file to check context', ).toBeGreaterThan(0); // We allow multiple calls since the errors are far apart. expect( targetFileReads.length, 'Agent should have used separate reads for far apart errors', ).toBeLessThanOrEqual(4); for (const call of targetFileReads) { const args = JSON.parse(call.toolRequest.args); expect( args.end_line, 'Agent should have used ranged read (end_line) to save tokens', ).toBeDefined(); } }, }); /** * Validates that the agent reads the entire file if there are lots of matches * (e.g.: 10), as it's more efficient than many small ranged reads. */ evalTest('USUALLY_PASSES', { name: 'should read the entire file when there are many matches', files: { 'package.json': JSON.stringify({ name: 'test-project', version: '1.0.0', type: 'module', }), 'eslint.config.mjs': `export default [ { files: ["**/*.ts"], rules: { "no-var": "error" } } ];`, 'many_mess.ts': (() => { const lines = []; for (let i = 0; i < 1000; i++) { if (i % 100 === 0) { lines.push(`var oldVar${i} = "needs fix";`); } else { lines.push(`const goodVar${i} = "clean";`); } } return lines.join('\n'); })(), }, prompt: 'Fix all linter errors in many_mess.ts manually by editing the file. Run eslint directly (using "npx --yes eslint") to find them. Do not run the file.', assert: async (rig) => { const logs = rig.readToolLogs(); const readCalls = logs.filter( (log) => log.toolRequest?.name === READ_FILE_TOOL_NAME, ); const targetFileReads = readCalls.filter((call) => { const args = JSON.parse(call.toolRequest.args); return args.file_path.includes('many_mess.ts'); }); expect( targetFileReads.length, 'Agent should have used read_file to check context', ).toBeGreaterThan(0); // In this case, we expect the agent to realize there are many scattered errors // and just read the whole file to be efficient with tool calls. const readEntireFile = targetFileReads.some((call) => { const args = JSON.parse(call.toolRequest.args); return args.end_line === undefined; }); expect( readEntireFile, 'Agent should have read the entire file because of the high number of scattered matches', ).toBe(true); // Check that the agent actually fixed the errors const editCalls = logs.filter( (log) => log.toolRequest?.name === EDIT_TOOL_NAME, ); const targetEditCalls = editCalls.filter((call) => { const args = JSON.parse(call.toolRequest.args); return args.file_path.includes('many_mess.ts'); }); expect( targetEditCalls.length, 'Agent should have made replacement calls on the target file', ).toBeGreaterThanOrEqual(1); }, }); });