2026-02-24 23:56:16 -05:00
/ * *
* @license
* Copyright 2026 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2026-03-26 14:45:03 -04:00
import { writeFileSync , mkdirSync } from 'node:fs' ;
import { join } from 'node:path' ;
2026-02-24 23:56:16 -05:00
import { describe , it , expect , beforeEach , afterEach } from 'vitest' ;
2026-03-26 14:45:03 -04:00
import { GEMINI_DIR , TestRig , checkModelOutputContent } from './test-helper.js' ;
2026-02-24 23:56:16 -05:00
describe ( 'Plan Mode' , ( ) = > {
let rig : TestRig ;
beforeEach ( ( ) = > {
rig = new TestRig ( ) ;
} ) ;
afterEach ( async ( ) = > await rig . cleanup ( ) ) ;
it ( 'should allow read-only tools but deny write tools in plan mode' , async ( ) = > {
await rig . setup (
'should allow read-only tools but deny write tools in plan mode' ,
{
settings : {
2026-03-31 12:10:13 -04:00
general : {
plan : { enabled : true } ,
} ,
2026-02-24 23:56:16 -05:00
tools : {
core : [
'run_shell_command' ,
'list_directory' ,
'write_file' ,
'read_file' ,
] ,
} ,
} ,
} ,
) ;
const result = await rig . run ( {
approvalMode : 'plan' ,
2026-03-25 12:06:45 -04:00
args : 'Please list the files in the current directory, and then attempt to create a new file named "denied.txt" using a shell command.' ,
2026-02-24 23:56:16 -05:00
} ) ;
const toolLogs = rig . readToolLogs ( ) ;
const lsLog = toolLogs . find ( ( l ) = > l . toolRequest . name === 'list_directory' ) ;
2026-03-25 12:06:45 -04:00
const shellLog = toolLogs . find (
( l ) = > l . toolRequest . name === 'run_shell_command' ,
) ;
2026-02-24 23:56:16 -05:00
2026-03-25 12:06:45 -04:00
expect ( lsLog , 'Expected list_directory to be called' ) . toBeDefined ( ) ;
2026-02-24 23:56:16 -05:00
expect ( lsLog ? . toolRequest . success ) . toBe ( true ) ;
2026-03-25 12:06:45 -04:00
expect (
shellLog ,
'Expected run_shell_command to be blocked (not even called)' ,
) . toBeUndefined ( ) ;
2026-02-24 23:56:16 -05:00
checkModelOutputContent ( result , {
expectedContent : [ 'Plan Mode' , 'read-only' ] ,
testName : 'Plan Mode restrictions test' ,
} ) ;
} ) ;
2026-03-02 14:51:44 -05:00
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 : {
tools : {
core : [ 'write_file' , 'read_file' , 'list_directory' ] ,
} ,
general : {
2026-03-31 12:10:13 -04:00
plan : { enabled : true , directory : plansDir } ,
2026-03-02 14:51:44 -05:00
defaultApprovalMode : 'plan' ,
2026-02-24 23:56:16 -05:00
} ,
} ,
2026-03-02 14:51:44 -05:00
} ) ;
2026-03-25 12:06:45 -04:00
await rig . run ( {
2026-02-24 23:56:16 -05:00
approvalMode : 'plan' ,
2026-03-25 12:06:45 -04:00
args : 'Create a file called plan.md in the plans directory.' ,
2026-02-24 23:56:16 -05:00
} ) ;
2026-03-02 14:51:44 -05:00
const toolLogs = rig . readToolLogs ( ) ;
const planWrite = toolLogs . find (
2026-02-24 23:56:16 -05:00
( l ) = >
2026-03-02 14:51:44 -05:00
l . toolRequest . name === 'write_file' &&
2026-02-24 23:56:16 -05:00
l . toolRequest . args . includes ( 'plans' ) &&
l . toolRequest . args . includes ( 'plan.md' ) ,
) ;
2026-03-25 12:06:45 -04:00
if ( ! planWrite ) {
console . error (
'All tool calls found:' ,
toolLogs . map ( ( l ) = > ( {
name : l.toolRequest.name ,
args : l.toolRequest.args ,
} ) ) ,
) ;
}
expect (
planWrite ,
'Expected write_file to be called for plan.md' ,
) . toBeDefined ( ) ;
expect (
planWrite ? . toolRequest . success ,
2026-04-16 11:20:27 -07:00
` Expected write_file to succeed, but it failed with error: ${ 'error' in ( planWrite ? . toolRequest || { } ) ? (planWrite?.toolRequest as unknown as Record<string, string>)['error'] : 'unknown'} ` ,
2026-03-25 12:06:45 -04:00
) . toBe ( true ) ;
2026-03-02 14:51:44 -05:00
} ) ;
2026-02-24 23:56:16 -05:00
2026-03-30 23:17:36 -04:00
it ( 'should deny write_file to non-plans directory in plan mode' , async ( ) = > {
2026-03-02 14:51:44 -05:00
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 : {
tools : {
core : [ 'write_file' , 'read_file' , 'list_directory' ] ,
} ,
general : {
2026-03-31 12:10:13 -04:00
plan : { enabled : true , directory : plansDir } ,
2026-03-02 14:51:44 -05:00
defaultApprovalMode : 'plan' ,
} ,
} ,
} ) ;
2026-03-25 12:06:45 -04:00
await rig . run ( {
2026-03-02 14:51:44 -05:00
approvalMode : 'plan' ,
2026-03-30 23:17:36 -04:00
args : 'Attempt to create a file named "hello.txt" in the current directory. Do not create a plan file, try to write hello.txt directly.' ,
2026-03-02 14:51:44 -05:00
} ) ;
2026-02-24 23:56:16 -05:00
2026-03-02 14:51:44 -05:00
const toolLogs = rig . readToolLogs ( ) ;
const writeLog = toolLogs . find (
( l ) = >
l . toolRequest . name === 'write_file' &&
l . toolRequest . args . includes ( 'hello.txt' ) ,
) ;
if ( writeLog ) {
2026-03-25 12:06:45 -04:00
expect (
writeLog . toolRequest . success ,
'Expected write_file to non-plans dir to fail' ,
) . toBe ( false ) ;
2026-03-02 14:51:44 -05:00
}
2026-02-24 23:56:16 -05:00
} ) ;
it ( 'should be able to enter plan mode from default mode' , async ( ) = > {
await rig . setup ( 'should be able to enter plan mode from default mode' , {
settings : {
2026-03-31 12:10:13 -04:00
general : {
plan : { enabled : true } ,
} ,
2026-02-24 23:56:16 -05:00
tools : {
core : [ 'enter_plan_mode' ] ,
allowed : [ 'enter_plan_mode' ] ,
} ,
} ,
} ) ;
await rig . run ( {
approvalMode : 'default' ,
2026-03-25 12:06:45 -04:00
args : 'I want to perform a complex refactoring. Please enter plan mode so we can design it first.' ,
2026-02-24 23:56:16 -05:00
} ) ;
const toolLogs = rig . readToolLogs ( ) ;
const enterLog = toolLogs . find (
( l ) = > l . toolRequest . name === 'enter_plan_mode' ,
) ;
2026-03-25 12:06:45 -04:00
expect ( enterLog , 'Expected enter_plan_mode to be called' ) . toBeDefined ( ) ;
2026-02-24 23:56:16 -05:00
expect ( enterLog ? . toolRequest . success ) . toBe ( true ) ;
} ) ;
2026-03-25 12:06:45 -04:00
it ( 'should allow write_file to the plans directory in plan mode even without a session ID' , async ( ) = > {
const plansDir = '.gemini/tmp/foo/plans' ;
const testName =
'should allow write_file to the plans directory in plan mode even without a session ID' ;
await rig . setup ( testName , {
settings : {
tools : {
core : [ 'write_file' , 'read_file' , 'list_directory' ] ,
} ,
general : {
2026-03-31 12:10:13 -04:00
plan : { enabled : true , directory : plansDir } ,
2026-03-25 12:06:45 -04:00
defaultApprovalMode : 'plan' ,
} ,
} ,
} ) ;
await rig . run ( {
approvalMode : 'plan' ,
args : 'Create a file called plan-no-session.md in the plans directory.' ,
} ) ;
const toolLogs = rig . readToolLogs ( ) ;
const planWrite = toolLogs . find (
( l ) = >
l . toolRequest . name === 'write_file' &&
l . toolRequest . args . includes ( 'plans' ) &&
l . toolRequest . args . includes ( 'plan-no-session.md' ) ,
) ;
if ( ! planWrite ) {
console . error (
'All tool calls found:' ,
toolLogs . map ( ( l ) = > ( {
name : l.toolRequest.name ,
args : l.toolRequest.args ,
} ) ) ,
) ;
}
expect (
planWrite ,
'Expected write_file to be called for plan-no-session.md' ,
) . toBeDefined ( ) ;
expect (
planWrite ? . toolRequest . success ,
2026-04-16 11:20:27 -07:00
` Expected write_file to succeed, but it failed with error: ${ 'error' in ( planWrite ? . toolRequest || { } ) ? (planWrite?.toolRequest as unknown as Record<string, string>)['error'] : 'unknown'} ` ,
2026-03-25 12:06:45 -04:00
) . toBe ( true ) ;
} ) ;
2026-03-26 14:45:03 -04:00
it ( 'should switch from a pro model to a flash model after exiting plan mode' , async ( ) = > {
const plansDir = 'plans-folder' ;
const planFilename = 'my-plan.md' ;
await rig . setup ( 'should-switch-to-flash' , {
settings : {
model : {
name : 'auto-gemini-2.5' ,
} ,
experimental : { plan : true } ,
tools : {
core : [ 'exit_plan_mode' , 'run_shell_command' ] ,
allowed : [ 'exit_plan_mode' , 'run_shell_command' ] ,
} ,
general : {
defaultApprovalMode : 'plan' ,
plan : {
directory : plansDir ,
} ,
} ,
} ,
} ) ;
writeFileSync (
join ( rig . homeDir ! , GEMINI_DIR , 'state.json' ) ,
JSON . stringify ( { terminalSetupPromptShown : true } , null , 2 ) ,
) ;
const fullPlansDir = join ( rig . testDir ! , plansDir ) ;
mkdirSync ( fullPlansDir , { recursive : true } ) ;
writeFileSync ( join ( fullPlansDir , planFilename ) , 'Execute echo hello' ) ;
await rig . run ( {
approvalMode : 'plan' ,
stdin : ` Exit plan mode using ${ planFilename } and then run a shell command \` echo hello \` . ` ,
} ) ;
const exitCallFound = await rig . waitForToolCall ( 'exit_plan_mode' ) ;
expect ( exitCallFound , 'Expected exit_plan_mode to be called' ) . toBe ( true ) ;
const shellCallFound = await rig . waitForToolCall ( 'run_shell_command' ) ;
expect ( shellCallFound , 'Expected run_shell_command to be called' ) . toBe (
true ,
) ;
const apiRequests = rig . readAllApiRequest ( ) ;
2026-04-16 11:20:27 -07:00
const modelNames = apiRequests . map (
( r ) = >
( 'model' in ( r . attributes || { } )
? ( r . attributes as unknown as Record < string , string > ) [ 'model' ]
: 'unknown' ) || 'unknown' ,
) ;
2026-03-26 14:45:03 -04:00
const proRequests = apiRequests . filter ( ( r ) = >
2026-04-16 11:20:27 -07:00
( 'model' in ( r . attributes || { } )
? ( r . attributes as unknown as Record < string , string > ) [ 'model' ]
: 'unknown'
) ? . includes ( 'pro' ) ,
2026-03-26 14:45:03 -04:00
) ;
const flashRequests = apiRequests . filter ( ( r ) = >
2026-04-16 11:20:27 -07:00
( 'model' in ( r . attributes || { } )
? ( r . attributes as unknown as Record < string , string > ) [ 'model' ]
: 'unknown'
) ? . includes ( 'flash' ) ,
2026-03-26 14:45:03 -04:00
) ;
expect (
proRequests . length ,
` Expected at least one Pro request. Models used: ${ modelNames . join ( ', ' ) } ` ,
) . toBeGreaterThanOrEqual ( 1 ) ;
expect (
flashRequests . length ,
` Expected at least one Flash request after mode switch. Models used: ${ modelNames . join ( ', ' ) } ` ,
) . toBeGreaterThanOrEqual ( 1 ) ;
} ) ;
2026-02-24 23:56:16 -05:00
} ) ;