File size: 9,235 Bytes
739fc44 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | /**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { existsSync } from 'node:fs';
import * as path from 'node:path';
import {
TestRig,
printDebugInfo,
assertModelHasOutput,
checkModelOutputContent,
} from './test-helper.js';
describe('file-system', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => await rig.cleanup());
it('should be able to read a file', async () => {
await rig.setup('should be able to read a file', {
settings: { tools: { core: ['read_file'] } },
});
rig.createFile('test.txt', 'hello world');
const result = await rig.run({
args: `read the file test.txt and show me its contents`,
});
const foundToolCall = await rig.waitForToolCall('read_file');
// Add debugging information
if (!foundToolCall || !result.includes('hello world')) {
printDebugInfo(rig, result, {
'Found tool call': foundToolCall,
'Contains hello world': result.includes('hello world'),
});
}
expect(
foundToolCall,
'Expected to find a read_file tool call',
).toBeTruthy();
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: 'hello world',
testName: 'File read test',
});
});
it('should be able to write a hello world message to a file', async () => {
await rig.setup('should be able to write a hello world message to a file', {
settings: { tools: { core: ['write_file', 'replace', 'read_file'] } },
});
rig.createFile('test.txt', '');
const result = await rig.run({
args: `edit test.txt to have a hello world message`,
});
// Accept multiple valid tools for editing files
const foundToolCall = await rig.waitForAnyToolCall([
'write_file',
'edit',
'replace',
]);
// Add debugging information
if (!foundToolCall) {
printDebugInfo(rig, result);
}
expect(
foundToolCall,
'Expected to find a write_file, edit, or replace tool call',
).toBeTruthy();
assertModelHasOutput(result);
checkModelOutputContent(result, { testName: 'File write test' });
const fileContent = rig.readFile('test.txt');
// Add debugging for file content
if (!fileContent.toLowerCase().includes('hello')) {
const writeCalls = rig
.readToolLogs()
.filter((t) => t.toolRequest.name === 'write_file')
.map((t) => t.toolRequest.args);
printDebugInfo(rig, result, {
'File content mismatch': true,
'Expected to contain': 'hello',
'Actual content': fileContent,
'Write tool calls': JSON.stringify(writeCalls),
});
}
expect(
fileContent.toLowerCase().includes('hello'),
'Expected file to contain hello',
).toBeTruthy();
// Log success info if verbose
if (process.env['VERBOSE'] === 'true') {
console.log('File written successfully with hello message.');
}
});
it('should correctly handle file paths with spaces', async () => {
await rig.setup('should correctly handle file paths with spaces', {
settings: { tools: { core: ['write_file', 'read_file'] } },
});
const fileName = 'my test file.txt';
const result = await rig.run({
args: `write "hello" to "${fileName}" and then stop. Do not perform any other actions.`,
timeout: 600000, // 10 min — real LLM can be slow in Docker sandbox
});
const foundToolCall = await rig.waitForToolCall('write_file');
if (!foundToolCall) {
printDebugInfo(rig, result);
}
expect(
foundToolCall,
'Expected to find a write_file tool call',
).toBeTruthy();
const newFileContent = rig.readFile(fileName);
// Trim to tolerate models that idiomatically append a trailing newline.
// This test is about path-with-spaces handling, not whitespace fidelity.
expect(newFileContent.trim()).toBe('hello');
});
it('should perform a read-then-write sequence', async () => {
await rig.setup('should perform a read-then-write sequence', {
settings: { tools: { core: ['read_file', 'replace', 'write_file'] } },
});
const fileName = 'version.txt';
rig.createFile(fileName, '1.0.0');
const prompt = `Read the version from ${fileName} and write the next version 1.0.1 back to the file.`;
const result = await rig.run({ args: prompt });
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const readCall = toolLogs.find(
(log) => log.toolRequest.name === 'read_file',
);
const writeCall = toolLogs.find(
(log) =>
log.toolRequest.name === 'write_file' ||
log.toolRequest.name === 'replace',
);
if (!readCall || !writeCall) {
printDebugInfo(rig, result, { readCall, writeCall });
}
expect(readCall, 'Expected to find a read_file tool call').toBeDefined();
expect(
writeCall,
'Expected to find a write_file or replace tool call',
).toBeDefined();
const newFileContent = rig.readFile(fileName);
expect(newFileContent.trimEnd()).toBe('1.0.1');
});
it.skip('should replace multiple instances of a string', async () => {
rig.setup('should replace multiple instances of a string');
const fileName = 'ambiguous.txt';
const fileContent = 'Hey there, \ntest line\ntest line';
const expectedContent = 'Hey there, \nnew line\nnew line';
rig.createFile(fileName, fileContent);
const result = await rig.run({
args: `rewrite the file ${fileName} to replace all instances of "test line" with "new line"`,
});
const validTools = ['write_file', 'edit'];
const foundToolCall = await rig.waitForAnyToolCall(validTools);
if (!foundToolCall) {
printDebugInfo(rig, result, {
'Tool call found': foundToolCall,
'Tool logs': rig.readToolLogs(),
});
}
expect(
foundToolCall,
`Expected to find one of ${validTools.join(', ')} tool calls`,
).toBeTruthy();
const toolLogs = rig.readToolLogs();
const successfulEdit = toolLogs.some(
(log) =>
validTools.includes(log.toolRequest.name) && log.toolRequest.success,
);
if (!successfulEdit) {
console.error(
`Expected a successful edit tool call (${validTools.join(', ')}), but none was found.`,
);
printDebugInfo(rig, result);
}
expect(
successfulEdit,
`Expected a successful edit tool call (${validTools.join(', ')})`,
).toBeTruthy();
const newFileContent = rig.readFile(fileName);
if (newFileContent !== expectedContent) {
printDebugInfo(rig, result, {
'Final file content': newFileContent,
'Expected file content': expectedContent,
'Tool logs': rig.readToolLogs(),
});
}
expect(newFileContent).toBe(expectedContent);
});
it('should fail safely when trying to edit a non-existent file', async () => {
await rig.setup(
'should fail safely when trying to edit a non-existent file',
{ settings: { tools: { core: ['read_file', 'replace'] } } },
);
const fileName = 'non_existent.txt';
const result = await rig.run({
args: `In ${fileName}, replace "a" with "b"`,
});
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const readAttempt = toolLogs.find(
(log) => log.toolRequest.name === 'read_file',
);
const writeAttempt = toolLogs.find(
(log) => log.toolRequest.name === 'write_file',
);
const successfulReplace = toolLogs.find(
(log) => log.toolRequest.name === 'replace' && log.toolRequest.success,
);
// The model can either investigate (and fail) or do nothing.
// If it chose to investigate by reading, that read must have failed.
if (readAttempt && readAttempt.toolRequest.success) {
console.error(
'A read_file attempt succeeded for a non-existent file when it should have failed.',
);
printDebugInfo(rig, result);
}
if (readAttempt) {
expect(
readAttempt.toolRequest.success,
'If model tries to read the file, that attempt must fail',
).toBe(false);
}
// CRITICAL: Verify that no matter what the model did, it never successfully
// wrote or replaced anything.
if (writeAttempt) {
console.error(
'A write_file attempt was made when no file should be written.',
);
printDebugInfo(rig, result);
}
expect(
writeAttempt,
'write_file should not have been called',
).toBeUndefined();
if (successfulReplace) {
console.error('A successful replace occurred when it should not have.');
printDebugInfo(rig, result);
}
expect(
successfulReplace,
'A successful replace should not have occurred',
).toBeUndefined();
// Final verification: ensure the file was not created.
const filePath = path.join(rig.testDir!, fileName);
const fileExists = existsSync(filePath);
expect(fileExists, 'The non-existent file should not be created').toBe(
false,
);
});
});
|