mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-25 12:34:38 -07:00
Merge branch 'main' into memory_usage3
This commit is contained in:
@@ -72,7 +72,6 @@ describe('ACP telemetry', () => {
|
||||
GEMINI_TELEMETRY_ENABLED: 'true',
|
||||
GEMINI_TELEMETRY_TARGET: 'local',
|
||||
GEMINI_TELEMETRY_OUTFILE: telemetryPath,
|
||||
// GEMINI_DEV_TRACING not set: fake responses aren't instrumented for spans
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -55,8 +55,8 @@ describe('file-system', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to write a file', async () => {
|
||||
await rig.setup('should be able to write a file', {
|
||||
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', '');
|
||||
|
||||
@@ -165,14 +165,15 @@ describe('Hooks Agent Flow', () => {
|
||||
|
||||
// BeforeModel hook to track message counts across LLM calls
|
||||
const messageCountFile = join(rig.testDir!, 'message-counts.json');
|
||||
const escapedPath = JSON.stringify(messageCountFile);
|
||||
const beforeModelScript = `
|
||||
const fs = require('fs');
|
||||
const input = JSON.parse(fs.readFileSync(0, 'utf-8'));
|
||||
const messageCount = input.llm_request?.contents?.length || 0;
|
||||
let counts = [];
|
||||
try { counts = JSON.parse(fs.readFileSync(${JSON.stringify(messageCountFile)}, 'utf-8')); } catch (e) {}
|
||||
try { counts = JSON.parse(fs.readFileSync(${escapedPath}, 'utf-8')); } catch (e) {}
|
||||
counts.push(messageCount);
|
||||
fs.writeFileSync(${JSON.stringify(messageCountFile)}, JSON.stringify(counts));
|
||||
fs.writeFileSync(${escapedPath}, JSON.stringify(counts));
|
||||
console.log(JSON.stringify({ decision: 'allow' }));
|
||||
`;
|
||||
const beforeModelScriptPath = rig.createScript(
|
||||
|
||||
@@ -81,7 +81,9 @@ describe('JSON output', () => {
|
||||
const message = (thrown as Error).message;
|
||||
|
||||
// Use a regex to find the first complete JSON object in the string
|
||||
const jsonMatch = message.match(/{[\s\S]*}/);
|
||||
// We expect the JSON to start with a quote (e.g. {"error": ...}) to avoid
|
||||
// matching random error objects printed to stderr (like ENOENT).
|
||||
const jsonMatch = message.match(/{\s*"[\s\S]*}/);
|
||||
|
||||
// Fail if no JSON-like text was found
|
||||
expect(
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig, checkModelOutputContent } from './test-helper.js';
|
||||
import { TestRig, checkModelOutputContent, GEMINI_DIR } from './test-helper.js';
|
||||
|
||||
describe('Plan Mode', () => {
|
||||
let rig: TestRig;
|
||||
@@ -62,50 +64,98 @@ describe('Plan Mode', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow write_file only in the plans directory in plan mode', async () => {
|
||||
await rig.setup(
|
||||
'should allow write_file only in the plans directory in plan mode',
|
||||
{
|
||||
settings: {
|
||||
experimental: { plan: true },
|
||||
tools: {
|
||||
core: ['write_file', 'read_file', 'list_directory'],
|
||||
allowed: ['write_file'],
|
||||
it('should allow write_file to the plans directory in plan mode', async () => {
|
||||
const plansDir = '.gemini/tmp/foo/123/plans';
|
||||
const testName =
|
||||
'should allow write_file to the plans directory in plan mode';
|
||||
|
||||
await rig.setup(testName, {
|
||||
settings: {
|
||||
experimental: { plan: true },
|
||||
tools: {
|
||||
core: ['write_file', 'read_file', 'list_directory'],
|
||||
},
|
||||
general: {
|
||||
defaultApprovalMode: 'plan',
|
||||
plan: {
|
||||
directory: plansDir,
|
||||
},
|
||||
general: { defaultApprovalMode: 'plan' },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// We ask the agent to create a plan for a feature, which should trigger a write_file in the plans directory.
|
||||
// Verify that write_file outside of plan directory fails
|
||||
await rig.run({
|
||||
approvalMode: 'plan',
|
||||
stdin:
|
||||
'Create a file called plan.md in the plans directory. Then create a file called hello.txt in the current directory',
|
||||
});
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const writeLogs = toolLogs.filter(
|
||||
(l) => l.toolRequest.name === 'write_file',
|
||||
// Disable the interactive terminal setup prompt in tests
|
||||
writeFileSync(
|
||||
join(rig.homeDir!, GEMINI_DIR, 'state.json'),
|
||||
JSON.stringify({ terminalSetupPromptShown: true }, null, 2),
|
||||
);
|
||||
|
||||
const planWrite = writeLogs.find(
|
||||
const run = await rig.runInteractive({
|
||||
approvalMode: 'plan',
|
||||
});
|
||||
|
||||
await run.type('Create a file called plan.md in the plans directory.');
|
||||
await run.type('\r');
|
||||
|
||||
await rig.expectToolCallSuccess(['write_file'], 30000, (args) =>
|
||||
args.includes('plan.md'),
|
||||
);
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const planWrite = toolLogs.find(
|
||||
(l) =>
|
||||
l.toolRequest.name === 'write_file' &&
|
||||
l.toolRequest.args.includes('plans') &&
|
||||
l.toolRequest.args.includes('plan.md'),
|
||||
);
|
||||
expect(planWrite?.toolRequest.success).toBe(true);
|
||||
});
|
||||
|
||||
const blockedWrite = writeLogs.find((l) =>
|
||||
l.toolRequest.args.includes('hello.txt'),
|
||||
it('should deny write_file to non-plans directory in plan mode', async () => {
|
||||
const plansDir = '.gemini/tmp/foo/123/plans';
|
||||
const testName =
|
||||
'should deny write_file to non-plans directory in plan mode';
|
||||
|
||||
await rig.setup(testName, {
|
||||
settings: {
|
||||
experimental: { plan: true },
|
||||
tools: {
|
||||
core: ['write_file', 'read_file', 'list_directory'],
|
||||
},
|
||||
general: {
|
||||
defaultApprovalMode: 'plan',
|
||||
plan: {
|
||||
directory: plansDir,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Disable the interactive terminal setup prompt in tests
|
||||
writeFileSync(
|
||||
join(rig.homeDir!, GEMINI_DIR, 'state.json'),
|
||||
JSON.stringify({ terminalSetupPromptShown: true }, null, 2),
|
||||
);
|
||||
|
||||
// Model is undeterministic, sometimes a blocked write appears in tool logs and sometimes it doesn't
|
||||
if (blockedWrite) {
|
||||
expect(blockedWrite?.toolRequest.success).toBe(false);
|
||||
}
|
||||
const run = await rig.runInteractive({
|
||||
approvalMode: 'plan',
|
||||
});
|
||||
|
||||
expect(planWrite?.toolRequest.success).toBe(true);
|
||||
await run.type('Create a file called hello.txt in the current directory.');
|
||||
await run.type('\r');
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const writeLog = toolLogs.find(
|
||||
(l) =>
|
||||
l.toolRequest.name === 'write_file' &&
|
||||
l.toolRequest.args.includes('hello.txt'),
|
||||
);
|
||||
|
||||
// In Plan Mode, writes outside the plans directory should be blocked.
|
||||
// Model is undeterministic, sometimes it doesn't even try, but if it does, it must fail.
|
||||
if (writeLog) {
|
||||
expect(writeLog.toolRequest.success).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should be able to enter plan mode from default mode', async () => {
|
||||
@@ -119,6 +169,12 @@ describe('Plan Mode', () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Disable the interactive terminal setup prompt in tests
|
||||
writeFileSync(
|
||||
join(rig.homeDir!, GEMINI_DIR, 'state.json'),
|
||||
JSON.stringify({ terminalSetupPromptShown: true }, null, 2),
|
||||
);
|
||||
|
||||
// Start in default mode and ask to enter plan mode.
|
||||
await rig.run({
|
||||
approvalMode: 'default',
|
||||
@@ -126,10 +182,7 @@ describe('Plan Mode', () => {
|
||||
'I want to perform a complex refactoring. Please enter plan mode so we can design it first.',
|
||||
});
|
||||
|
||||
const enterPlanCallFound = await rig.waitForToolCall(
|
||||
'enter_plan_mode',
|
||||
10000,
|
||||
);
|
||||
const enterPlanCallFound = await rig.waitForToolCall('enter_plan_mode');
|
||||
expect(enterPlanCallFound, 'Expected enter_plan_mode to be called').toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -102,7 +102,10 @@ describe('ripgrep-real-direct', () => {
|
||||
'console.log("hello");\n',
|
||||
);
|
||||
|
||||
const invocation = tool.build({ pattern: 'hello', include: '*.js' });
|
||||
const invocation = tool.build({
|
||||
pattern: 'hello',
|
||||
include_pattern: '*.js',
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 1 match');
|
||||
|
||||
@@ -18,6 +18,7 @@ const { shell } = getShellConfiguration();
|
||||
function getLineCountCommand(): { command: string; tool: string } {
|
||||
switch (shell) {
|
||||
case 'powershell':
|
||||
return { command: `Measure-Object -Line`, tool: 'Measure-Object' };
|
||||
case 'cmd':
|
||||
return { command: `find /c /v`, tool: 'find' };
|
||||
case 'bash':
|
||||
@@ -238,8 +239,12 @@ describe('run_shell_command', () => {
|
||||
});
|
||||
|
||||
it('should succeed in yolo mode', async () => {
|
||||
const isWindows = process.platform === 'win32';
|
||||
await rig.setup('should succeed in yolo mode', {
|
||||
settings: { tools: { core: ['run_shell_command'] } },
|
||||
settings: {
|
||||
tools: { core: ['run_shell_command'] },
|
||||
shell: isWindows ? { enableInteractiveShell: false } : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const testFile = rig.createFile('test.txt', 'Lorem\nIpsum\nDolor\n');
|
||||
|
||||
@@ -22,8 +22,8 @@ describe('write_file', () => {
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('should be able to write a file', async () => {
|
||||
await rig.setup('should be able to write a file', {
|
||||
it('should be able to write a joke to a file', async () => {
|
||||
await rig.setup('should be able to write a joke to a file', {
|
||||
settings: { tools: { core: ['write_file', 'read_file'] } },
|
||||
});
|
||||
const prompt = `show me an example of using the write tool. put a dad joke in dad.txt`;
|
||||
|
||||
Reference in New Issue
Block a user