@@ -14,6 +14,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/
1414import { IAccessibilityService } from '../../../../../../platform/accessibility/common/accessibility.js' ;
1515import { TestAccessibilityService } from '../../../../../../platform/accessibility/test/common/testAccessibilityService.js' ;
1616import { AccessibilitySignal , IAccessibilitySignalService } from '../../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js' ;
17+ import { ICommandService } from '../../../../../../platform/commands/common/commands.js' ;
1718import { ConfigurationTarget , IConfigurationChangeEvent } from '../../../../../../platform/configuration/common/configuration.js' ;
1819import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js' ;
1920import { ContextKeyService } from '../../../../../../platform/contextkey/browser/contextKeyService.js' ;
@@ -147,6 +148,7 @@ interface TestToolsServiceOptions {
147148 accessibilitySignalService ?: Partial < IAccessibilitySignalService > ;
148149 telemetryService ?: Partial < ITelemetryService > ;
149150 hooksExecutionService ?: MockHooksExecutionService ;
151+ commandService ?: Partial < ICommandService > ;
150152 /** Called after configurationService is created but before the service is instantiated */
151153 configureServices ?: ( config : TestConfigurationService ) => void ;
152154}
@@ -181,6 +183,9 @@ function createTestToolsService(store: ReturnType<typeof ensureNoDisposablesAreL
181183 if ( options ?. telemetryService ) {
182184 instaService . stub ( ITelemetryService , options . telemetryService ) ;
183185 }
186+ if ( options ?. commandService ) {
187+ instaService . stub ( ICommandService , options . commandService as ICommandService ) ;
188+ }
184189
185190 const service = store . add ( instaService . createInstance ( LanguageModelToolsService ) ) ;
186191 return { configurationService, chatService, service, contextKeyService } ;
@@ -4037,6 +4042,152 @@ suite('LanguageModelToolsService', () => {
40374042 assert . strictEqual ( result . content [ 0 ] . kind , 'text' ) ;
40384043 assert . strictEqual ( ( result . content [ 0 ] as IToolResultTextPart ) . value , 'success' ) ;
40394044 } ) ;
4045+
4046+ test ( 'when hook returns updatedInput, tool is invoked with replaced parameters' , async ( ) => {
4047+ let receivedParameters : Record < string , any > | undefined ;
4048+ mockHooksService . preToolUseHookResult = {
4049+ output : undefined ,
4050+ success : true ,
4051+ permissionDecision : 'allow' ,
4052+ updatedInput : { safeCommand : 'echo hello' } ,
4053+ } ;
4054+
4055+ const tool = registerToolForTest ( hookService , store , 'hookUpdatedInputTool' , {
4056+ invoke : async ( dto ) => {
4057+ receivedParameters = dto . parameters ;
4058+ return { content : [ { kind : 'text' , value : 'done' } ] } ;
4059+ } ,
4060+ prepareToolInvocation : async ( ) => ( {
4061+ confirmationMessages : {
4062+ title : 'Confirm?' ,
4063+ message : 'Confirm action' ,
4064+ allowAutoConfirm : true
4065+ }
4066+ } )
4067+ } ) ;
4068+
4069+ stubGetSession ( hookChatService , 'hook-test-updated-input' , { requestId : 'req1' } ) ;
4070+
4071+ await hookService . invokeTool (
4072+ tool . makeDto ( { originalCommand : 'rm -rf /' } , { sessionId : 'hook-test-updated-input' } ) ,
4073+ async ( ) => 0 ,
4074+ CancellationToken . None
4075+ ) ;
4076+
4077+ assert . deepStrictEqual ( receivedParameters , { safeCommand : 'echo hello' } ) ;
4078+ } ) ;
4079+
4080+ test ( 'when hook returns updatedInput that fails schema validation, original parameters are kept' , async ( ) => {
4081+ const mockCommandService = {
4082+ executeCommand : async ( commandId : string ) => {
4083+ if ( commandId === 'json.validate' ) {
4084+ return [ { message : 'Missing required property "command"' , range : [ { line : 0 , character : 0 } , { line : 0 , character : 1 } ] , severity : 'Error' } ] ;
4085+ }
4086+ return undefined ;
4087+ }
4088+ } ;
4089+
4090+ const mockHooks = new MockHooksExecutionService ( ) ;
4091+ const setup = createTestToolsService ( store , {
4092+ hooksExecutionService : mockHooks ,
4093+ commandService : mockCommandService as ICommandService ,
4094+ } ) ;
4095+
4096+ let receivedParameters : Record < string , any > | undefined ;
4097+ mockHooks . preToolUseHookResult = {
4098+ output : undefined ,
4099+ success : true ,
4100+ permissionDecision : 'allow' ,
4101+ updatedInput : { invalidField : 'wrong' } ,
4102+ } ;
4103+
4104+ const tool = registerToolForTest ( setup . service , store , 'hookValidationFailTool' , {
4105+ invoke : async ( dto ) => {
4106+ receivedParameters = dto . parameters ;
4107+ return { content : [ { kind : 'text' , value : 'done' } ] } ;
4108+ } ,
4109+ prepareToolInvocation : async ( ) => ( {
4110+ confirmationMessages : {
4111+ title : 'Confirm?' ,
4112+ message : 'Confirm action' ,
4113+ allowAutoConfirm : true
4114+ }
4115+ } )
4116+ } , {
4117+ inputSchema : {
4118+ type : 'object' ,
4119+ properties : { command : { type : 'string' } } ,
4120+ required : [ 'command' ] ,
4121+ }
4122+ } ) ;
4123+
4124+ stubGetSession ( setup . chatService , 'hook-test-validation-fail' , { requestId : 'req1' } ) ;
4125+
4126+ await setup . service . invokeTool (
4127+ tool . makeDto ( { command : 'original' } , { sessionId : 'hook-test-validation-fail' } ) ,
4128+ async ( ) => 0 ,
4129+ CancellationToken . None
4130+ ) ;
4131+
4132+ // Original parameters should be kept since validation failed
4133+ assert . deepStrictEqual ( receivedParameters , { command : 'original' } ) ;
4134+ } ) ;
4135+
4136+ test ( 'when hook returns updatedInput that passes schema validation, parameters are replaced' , async ( ) => {
4137+ const mockCommandService = {
4138+ executeCommand : async ( commandId : string ) => {
4139+ if ( commandId === 'json.validate' ) {
4140+ return [ ] ; // no diagnostics = valid
4141+ }
4142+ return undefined ;
4143+ }
4144+ } ;
4145+
4146+ const mockHooks = new MockHooksExecutionService ( ) ;
4147+ const setup = createTestToolsService ( store , {
4148+ hooksExecutionService : mockHooks ,
4149+ commandService : mockCommandService as ICommandService ,
4150+ } ) ;
4151+
4152+ let receivedParameters : Record < string , any > | undefined ;
4153+ mockHooks . preToolUseHookResult = {
4154+ output : undefined ,
4155+ success : true ,
4156+ permissionDecision : 'allow' ,
4157+ updatedInput : { command : 'safe-command' } ,
4158+ } ;
4159+
4160+ const tool = registerToolForTest ( setup . service , store , 'hookValidationPassTool' , {
4161+ invoke : async ( dto ) => {
4162+ receivedParameters = dto . parameters ;
4163+ return { content : [ { kind : 'text' , value : 'done' } ] } ;
4164+ } ,
4165+ prepareToolInvocation : async ( ) => ( {
4166+ confirmationMessages : {
4167+ title : 'Confirm?' ,
4168+ message : 'Confirm action' ,
4169+ allowAutoConfirm : true
4170+ }
4171+ } )
4172+ } , {
4173+ inputSchema : {
4174+ type : 'object' ,
4175+ properties : { command : { type : 'string' } } ,
4176+ required : [ 'command' ] ,
4177+ }
4178+ } ) ;
4179+
4180+ stubGetSession ( setup . chatService , 'hook-test-validation-pass' , { requestId : 'req1' } ) ;
4181+
4182+ await setup . service . invokeTool (
4183+ tool . makeDto ( { command : 'original' } , { sessionId : 'hook-test-validation-pass' } ) ,
4184+ async ( ) => 0 ,
4185+ CancellationToken . None
4186+ ) ;
4187+
4188+ // Updated parameters should be applied since validation passed
4189+ assert . deepStrictEqual ( receivedParameters , { command : 'safe-command' } ) ;
4190+ } ) ;
40404191 } ) ;
40414192
40424193 suite ( 'postToolUse hooks' , ( ) => {
0 commit comments