2025-12-19 17:09:43 -08:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe , it , expect , vi , beforeEach , afterEach } from 'vitest' ;
2026-01-05 15:25:54 -05:00
import { EditTool } from './edit.js' ;
2025-12-19 17:09:43 -08:00
import { WriteFileTool } from './write-file.js' ;
import { WebFetchTool } from './web-fetch.js' ;
import { ToolConfirmationOutcome } from './tools.js' ;
import { ApprovalMode } from '../policy/types.js' ;
import { MessageBusType } from '../confirmation-bus/types.js' ;
import type { MessageBus } from '../confirmation-bus/message-bus.js' ;
import type { Config } from '../config/config.js' ;
import path from 'node:path' ;
2026-01-27 13:17:40 -08:00
import { isSubpath } from '../utils/paths.js' ;
2025-12-19 17:09:43 -08:00
import fs from 'node:fs' ;
import os from 'node:os' ;
// Mock telemetry loggers to avoid failures
vi . mock ( '../telemetry/loggers.js' , ( ) = > ( {
2026-01-05 15:25:54 -05:00
logEditStrategy : vi.fn ( ) ,
logEditCorrectionEvent : vi.fn ( ) ,
2025-12-19 17:09:43 -08:00
logFileOperation : vi.fn ( ) ,
} ) ) ;
describe ( 'Tool Confirmation Policy Updates' , ( ) = > {
let mockConfig : any ;
let mockMessageBus : MessageBus ;
const rootDir = path . join (
os . tmpdir ( ) ,
` gemini-cli-policy-test- ${ Date . now ( ) } ` ,
) ;
beforeEach ( ( ) = > {
if ( ! fs . existsSync ( rootDir ) ) {
fs . mkdirSync ( rootDir , { recursive : true } ) ;
}
mockMessageBus = {
publish : vi.fn ( ) ,
subscribe : vi.fn ( ) ,
unsubscribe : vi.fn ( ) ,
} as unknown as MessageBus ;
mockConfig = {
2026-03-12 18:56:31 -07:00
get config() {
return this ;
} ,
2025-12-19 17:09:43 -08:00
getTargetDir : ( ) = > rootDir ,
getApprovalMode : vi.fn ( ) . mockReturnValue ( ApprovalMode . DEFAULT ) ,
setApprovalMode : vi.fn ( ) ,
getFileSystemService : ( ) = > ( {
readTextFile : vi.fn ( ) . mockImplementation ( ( p ) = > {
if ( fs . existsSync ( p ) ) {
return fs . readFileSync ( p , 'utf8' ) ;
}
return 'existing content' ;
} ) ,
writeTextFile : vi.fn ( ) . mockImplementation ( ( p , c ) = > {
fs . writeFileSync ( p , c ) ;
} ) ,
} ) ,
getFileService : ( ) = > ( { } ) ,
getFileFilteringOptions : ( ) = > ( { } ) ,
getGeminiClient : ( ) = > ( { } ) ,
getBaseLlmClient : ( ) = > ( { } ) ,
2026-01-21 10:53:41 -08:00
getDisableLLMCorrection : ( ) = > true ,
2025-12-19 17:09:43 -08:00
getIdeMode : ( ) = > false ,
2026-02-26 17:50:21 -08:00
getActiveModel : ( ) = > 'test-model' ,
2025-12-19 17:09:43 -08:00
getWorkspaceContext : ( ) = > ( {
isPathWithinWorkspace : ( ) = > true ,
getDirectories : ( ) = > [ rootDir ] ,
} ) ,
2026-02-23 11:50:14 -08:00
getDirectWebFetch : ( ) = > false ,
2026-01-27 13:17:40 -08:00
storage : {
getProjectTempDir : ( ) = > path . join ( os . tmpdir ( ) , 'gemini-cli-temp' ) ,
} ,
isPathAllowed ( this : Config , absolutePath : string ) : boolean {
const workspaceContext = this . getWorkspaceContext ( ) ;
if ( workspaceContext . isPathWithinWorkspace ( absolutePath ) ) {
return true ;
}
const projectTempDir = this . storage . getProjectTempDir ( ) ;
return isSubpath ( path . resolve ( projectTempDir ) , absolutePath ) ;
} ,
validatePathAccess ( this : Config , absolutePath : string ) : string | null {
if ( this . isPathAllowed ( absolutePath ) ) {
return null ;
}
const workspaceDirs = this . getWorkspaceContext ( ) . getDirectories ( ) ;
const projectTempDir = this . storage . getProjectTempDir ( ) ;
return ` Path not in workspace: Attempted path " ${ absolutePath } " resolves outside the allowed workspace directories: ${ workspaceDirs . join ( ', ' ) } or the project temp directory: ${ projectTempDir } ` ;
} ,
2025-12-19 17:09:43 -08:00
} ;
} ) ;
afterEach ( ( ) = > {
if ( fs . existsSync ( rootDir ) ) {
fs . rmSync ( rootDir , { recursive : true , force : true } ) ;
}
vi . restoreAllMocks ( ) ;
} ) ;
const tools = [
{
2026-01-05 15:25:54 -05:00
name : 'EditTool' ,
create : ( config : Config , bus : MessageBus ) = > new EditTool ( config , bus ) ,
2025-12-19 17:09:43 -08:00
params : {
file_path : 'test.txt' ,
instruction : 'change content' ,
old_string : 'existing' ,
new_string : 'new' ,
} ,
} ,
{
name : 'WriteFileTool' ,
create : ( config : Config , bus : MessageBus ) = >
new WriteFileTool ( config , bus ) ,
params : {
file_path : path.join ( rootDir , 'test.txt' ) ,
content : 'new content' ,
} ,
} ,
{
name : 'WebFetchTool' ,
create : ( config : Config , bus : MessageBus ) = >
new WebFetchTool ( config , bus ) ,
params : {
prompt : 'fetch https://example.com' ,
} ,
} ,
] ;
describe . each ( tools ) ( '$name policy updates' , ( { create , params } ) = > {
it . each ( [
{
outcome : ToolConfirmationOutcome.ProceedAlways ,
2026-02-19 12:03:52 -08:00
_shouldPublish : false ,
2025-12-19 17:09:43 -08:00
expectedApprovalMode : ApprovalMode.AUTO_EDIT ,
} ,
{
outcome : ToolConfirmationOutcome.ProceedAlwaysAndSave ,
2026-02-19 12:03:52 -08:00
_shouldPublish : true ,
_persist : true ,
2025-12-19 17:09:43 -08:00
} ,
] ) (
'should handle $outcome correctly' ,
2026-02-19 12:03:52 -08:00
async ( { outcome , expectedApprovalMode } ) = > {
2025-12-19 17:09:43 -08:00
const tool = create ( mockConfig , mockMessageBus ) ;
// For file-based tools, ensure the file exists if needed
if ( params . file_path ) {
const fullPath = path . isAbsolute ( params . file_path )
? params . file_path
: path . join ( rootDir , params . file_path ) ;
fs . writeFileSync ( fullPath , 'existing content' ) ;
}
const invocation = tool . build ( params as any ) ;
// Mock getMessageBusDecision to trigger ASK_USER flow
vi . spyOn ( invocation as any , 'getMessageBusDecision' ) . mockResolvedValue (
'ASK_USER' ,
) ;
const confirmation = await invocation . shouldConfirmExecute (
new AbortController ( ) . signal ,
) ;
expect ( confirmation ) . not . toBe ( false ) ;
if ( confirmation ) {
await confirmation . onConfirm ( outcome ) ;
2026-02-19 12:03:52 -08:00
// Policy updates are no longer published by onConfirm; they are
// handled centrally by the schedulers.
const publishCalls = ( mockMessageBus . publish as any ) . mock . calls ;
const hasUpdatePolicy = publishCalls . some (
( call : any ) = > call [ 0 ] . type === MessageBusType . UPDATE_POLICY ,
) ;
expect ( hasUpdatePolicy ) . toBe ( false ) ;
2025-12-19 17:09:43 -08:00
if ( expectedApprovalMode !== undefined ) {
2026-02-19 12:03:52 -08:00
// expectedApprovalMode in this test (AUTO_EDIT) is now handled
// by updatePolicy in the scheduler, so it should not be called
// here either.
expect ( mockConfig . setApprovalMode ) . not . toHaveBeenCalled ( ) ;
2025-12-19 17:09:43 -08:00
}
}
} ,
) ;
} ) ;
} ) ;