mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-16 08:10:46 -07:00
276 lines
8.5 KiB
TypeScript
276 lines
8.5 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { TestRig } from './test-helper.js';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
|
|
describe('run_shell_command streaming to file regression', () => {
|
|
let rig: TestRig;
|
|
|
|
beforeEach(() => {
|
|
rig = new TestRig();
|
|
});
|
|
|
|
afterEach(async () => await rig.cleanup());
|
|
|
|
it('should stream large outputs to a file and verify full content presence', async () => {
|
|
await rig.setup(
|
|
'should stream large outputs to a file and verify full content presence',
|
|
{
|
|
settings: { tools: { core: ['run_shell_command'] } },
|
|
},
|
|
);
|
|
|
|
const numLines = 20000;
|
|
const testFileName = 'large_output_test.txt';
|
|
const testFilePath = path.join(rig.testDir!, testFileName);
|
|
|
|
// Create a ~20MB file with unique content at start and end
|
|
const startMarker = 'START_OF_FILE_MARKER';
|
|
const endMarker = 'END_OF_FILE_MARKER';
|
|
|
|
const stream = fs.createWriteStream(testFilePath);
|
|
stream.write(startMarker + '\n');
|
|
for (let i = 0; i < numLines; i++) {
|
|
stream.write(`Line ${i + 1}: ` + 'A'.repeat(1000) + '\n');
|
|
}
|
|
stream.write(endMarker + '\n');
|
|
await new Promise((resolve) => stream.end(resolve));
|
|
|
|
const fileSize = fs.statSync(testFilePath).size;
|
|
expect(fileSize).toBeGreaterThan(20000000);
|
|
|
|
const prompt = `Use run_shell_command to cat ${testFileName} and say 'Done.'`;
|
|
await rig.run({ args: prompt });
|
|
|
|
let savedFilePath = '';
|
|
const tmpdir = path.join(rig.homeDir!, '.gemini', 'tmp');
|
|
if (fs.existsSync(tmpdir)) {
|
|
const findFiles = (dir: string): string[] => {
|
|
let results: string[] = [];
|
|
const list = fs.readdirSync(dir, { withFileTypes: true });
|
|
for (const file of list) {
|
|
const fullPath = path.join(dir, file.name);
|
|
if (file.isDirectory()) {
|
|
results = results.concat(findFiles(fullPath));
|
|
} else if (file.isFile() && file.name.endsWith('.txt')) {
|
|
results.push(fullPath);
|
|
} else if (file.isFile() && file.name.endsWith('.log')) {
|
|
results.push(fullPath);
|
|
}
|
|
}
|
|
return results;
|
|
};
|
|
|
|
const files = findFiles(tmpdir);
|
|
files.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
|
|
for (const p of files) {
|
|
try {
|
|
const stat = fs.statSync(p);
|
|
if (stat.size >= 20000000) {
|
|
savedFilePath = p;
|
|
break;
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
if (!savedFilePath) {
|
|
const fileStats = files.map((p) => {
|
|
try {
|
|
return { p, size: fs.statSync(p).size };
|
|
} catch {
|
|
return { p, size: 'error' };
|
|
}
|
|
});
|
|
rig.log('Available files:', JSON.stringify(fileStats, null, 2));
|
|
}
|
|
}
|
|
|
|
expect(
|
|
savedFilePath,
|
|
`Expected to find a saved output file >= 20MB in ${tmpdir}`,
|
|
).toBeTruthy();
|
|
|
|
const savedContent = fs.readFileSync(savedFilePath, 'utf8');
|
|
expect(savedContent).toContain(startMarker);
|
|
expect(savedContent).toContain(endMarker);
|
|
expect(savedContent.length).toBeGreaterThanOrEqual(fileSize);
|
|
|
|
fs.unlinkSync(savedFilePath);
|
|
}, 120000);
|
|
|
|
it('should stream very large (50MB) outputs to a file and verify full content presence', async () => {
|
|
await rig.setup(
|
|
'should stream very large (50MB) outputs to a file and verify full content presence',
|
|
{
|
|
settings: { tools: { core: ['run_shell_command'] } },
|
|
},
|
|
);
|
|
|
|
const numLines = 1000000;
|
|
const testFileName = 'very_large_output_test.txt';
|
|
const testFilePath = path.join(rig.testDir!, testFileName);
|
|
|
|
// Create a ~50MB file with unique content at start and end
|
|
const startMarker = 'START_OF_FILE_MARKER';
|
|
const endMarker = 'END_OF_FILE_MARKER';
|
|
|
|
const stream = fs.createWriteStream(testFilePath);
|
|
stream.write(startMarker + '\n');
|
|
for (let i = 0; i < numLines; i++) {
|
|
stream.write(`Line ${i + 1}: ` + 'A'.repeat(40) + '\n');
|
|
}
|
|
stream.write(endMarker + '\n');
|
|
await new Promise((resolve) => stream.end(resolve));
|
|
|
|
const fileSize = fs.statSync(testFilePath).size;
|
|
expect(fileSize).toBeGreaterThan(45000000);
|
|
|
|
const prompt = `Use run_shell_command to cat ${testFileName} and say 'Done.'`;
|
|
await rig.run({ args: prompt });
|
|
|
|
let savedFilePath = '';
|
|
const tmpdir = path.join(rig.homeDir!, '.gemini', 'tmp');
|
|
if (fs.existsSync(tmpdir)) {
|
|
const findFiles = (dir: string): string[] => {
|
|
let results: string[] = [];
|
|
const list = fs.readdirSync(dir, { withFileTypes: true });
|
|
for (const file of list) {
|
|
const fullPath = path.join(dir, file.name);
|
|
if (file.isDirectory()) {
|
|
results = results.concat(findFiles(fullPath));
|
|
} else if (file.isFile() && file.name.endsWith('.txt')) {
|
|
results.push(fullPath);
|
|
} else if (file.isFile() && file.name.endsWith('.log')) {
|
|
results.push(fullPath);
|
|
}
|
|
}
|
|
return results;
|
|
};
|
|
|
|
const files = findFiles(tmpdir);
|
|
files.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
|
|
for (const p of files) {
|
|
try {
|
|
const stat = fs.statSync(p);
|
|
if (stat.size >= 20000000) {
|
|
savedFilePath = p;
|
|
break;
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
if (!savedFilePath) {
|
|
const fileStats = files.map((p) => {
|
|
try {
|
|
return { p, size: fs.statSync(p).size };
|
|
} catch {
|
|
return { p, size: 'error' };
|
|
}
|
|
});
|
|
rig.log('Available files:', JSON.stringify(fileStats, null, 2));
|
|
}
|
|
}
|
|
|
|
expect(
|
|
savedFilePath,
|
|
`Expected to find a saved output file >= 20MB in ${tmpdir}`,
|
|
).toBeTruthy();
|
|
|
|
const savedContent = fs.readFileSync(savedFilePath, 'utf8');
|
|
expect(savedContent).toContain(startMarker);
|
|
expect(savedContent).toContain(endMarker);
|
|
expect(savedContent.length).toBeGreaterThanOrEqual(fileSize);
|
|
|
|
fs.unlinkSync(savedFilePath);
|
|
}, 120000);
|
|
|
|
it('should produce clean output resolving carriage returns and backspaces', async () => {
|
|
await rig.setup(
|
|
'should produce clean output resolving carriage returns and backspaces',
|
|
{
|
|
settings: {
|
|
tools: { core: ['run_shell_command'] },
|
|
},
|
|
},
|
|
);
|
|
|
|
const script = `
|
|
import sys
|
|
import time
|
|
|
|
# Fill buffer to force file streaming/truncation
|
|
# 45000 chars to be safe (default threshold is 40000)
|
|
print('A' * 45000)
|
|
sys.stdout.flush()
|
|
|
|
# Test sequence
|
|
print('XXXXX', end='', flush=True)
|
|
time.sleep(0.5)
|
|
print('\\rYYYYY', end='', flush=True)
|
|
time.sleep(0.5)
|
|
print('\\nNext Line', end='', flush=True)
|
|
`;
|
|
const scriptPath = path.join(rig.testDir!, 'test_script.py');
|
|
fs.writeFileSync(scriptPath, script);
|
|
|
|
const prompt = `run_shell_command python3 "${scriptPath}"`;
|
|
await rig.run({ args: prompt });
|
|
|
|
let savedFilePath = '';
|
|
const tmpdir = path.join(rig.homeDir!, '.gemini', 'tmp');
|
|
if (fs.existsSync(tmpdir)) {
|
|
const findFiles = (dir: string): string[] => {
|
|
let results: string[] = [];
|
|
const list = fs.readdirSync(dir, { withFileTypes: true });
|
|
for (const file of list) {
|
|
const fullPath = path.join(dir, file.name);
|
|
if (file.isDirectory()) {
|
|
results = results.concat(findFiles(fullPath));
|
|
} else if (file.isFile() && file.name.endsWith('.txt')) {
|
|
results.push(fullPath);
|
|
}
|
|
}
|
|
return results;
|
|
};
|
|
|
|
const files = findFiles(tmpdir);
|
|
files.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
|
|
if (files.length > 0) {
|
|
savedFilePath = files[0];
|
|
}
|
|
}
|
|
|
|
expect(savedFilePath, 'Output file should exist').toBeTruthy();
|
|
const content = fs.readFileSync(savedFilePath, 'utf8');
|
|
|
|
// Verify it contains the large chunk
|
|
expect(content).toContain('AAAA');
|
|
|
|
// Verify cleanup logic:
|
|
// 1. The final text "YYYYY" should be present.
|
|
expect(content).toContain('YYYYY');
|
|
// 2. The next line should be present.
|
|
expect(content).toContain('Next Line');
|
|
|
|
// 3. Verify overwrite happened.
|
|
// In raw output, we would have "XXXXX...YYYYY".
|
|
// In processed output, "YYYYY" overwrites "XXXXX".
|
|
// We confirm that escape codes are stripped (processed text).
|
|
|
|
// 4. Check for ANSI escape codes (like \\x1b) just in case
|
|
expect(content).not.toContain('\x1b');
|
|
}, 60000);
|
|
});
|