@@ -10,15 +10,33 @@ vi.mock("../../tools/validateToolUse", () => ({
1010 validateToolUse : vi . fn ( ) ,
1111 isValidToolName : vi . fn ( ( ) => true ) ,
1212} ) )
13+ vi . mock ( "@roo-code/core" , ( ) => ( {
14+ customToolRegistry : {
15+ get : vi . fn ( ( ) => undefined ) ,
16+ has : vi . fn ( ( ) => false ) ,
17+ } ,
18+ ConsecutiveMistakeError : class ConsecutiveMistakeError extends Error {
19+ constructor ( message : string ) {
20+ super ( message )
21+ }
22+ } ,
23+ } ) )
1324vi . mock ( "@roo-code/telemetry" , ( ) => ( {
1425 TelemetryService : {
1526 instance : {
1627 captureToolUsage : vi . fn ( ) ,
1728 captureConsecutiveMistakeError : vi . fn ( ) ,
1829 captureEvent : vi . fn ( ) ,
30+ captureException : vi . fn ( ) ,
1931 } ,
2032 } ,
2133} ) )
34+ vi . mock ( "../../i18n" , ( ) => ( {
35+ t : vi . fn ( ( key : string , params ?: Record < string , unknown > ) => {
36+ if ( key === "tools:unknownToolError" ) return `Unknown tool ${ params ?. toolName } `
37+ return key
38+ } ) ,
39+ } ) )
2240
2341function createMockTask ( ) {
2442 const mockTask : any = {
@@ -34,6 +52,8 @@ function createMockTask() {
3452 didRejectTool : false ,
3553 didAlreadyUseTool : false ,
3654 consecutiveMistakeCount : 0 ,
55+ consecutiveMistakeLimit : 3 ,
56+ apiConfiguration : { apiProvider : "test-provider" } ,
3757 clineMessages : [ ] ,
3858 api : {
3959 getModel : ( ) => ( { id : "test-model" , info : { } } ) ,
@@ -74,8 +94,11 @@ function createMockTask() {
7494describe ( "presentAssistantMessage - Error Interception Integration" , ( ) => {
7595 let mockTask : ReturnType < typeof createMockTask >
7696
77- beforeEach ( ( ) => {
97+ beforeEach ( async ( ) => {
7898 mockTask = createMockTask ( )
99+ // Reset validateToolUse mock to prevent cross-test contamination from mockImplementationOnce
100+ const { validateToolUse } = await import ( "../../tools/validateToolUse" )
101+ ; ( validateToolUse as any ) . mockReset ( )
79102 } )
80103
81104 describe ( "XML_NATIVE_DUAL_PROTOCOL detection" , ( ) => {
@@ -449,4 +472,155 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
449472 )
450473 } )
451474 } )
475+
476+ describe ( "validation error classification (validateToolUse catch)" , ( ) => {
477+ it ( "classifies 'not allowed in' as modeRestriction and pushes tool_result" , async ( ) => {
478+ const { validateToolUse } = await import ( "../../tools/validateToolUse" )
479+ ; ( validateToolUse as any ) . mockImplementationOnce ( ( ) => {
480+ throw new Error ( "Tool 'read_file' is not allowed in 'ask' mode." )
481+ } )
482+
483+ const toolCallId = "call_mode_restriction"
484+ mockTask . assistantMessageContent = [
485+ {
486+ type : "tool_use" ,
487+ id : toolCallId ,
488+ name : "read_file" ,
489+ params : { path : "x.txt" } ,
490+ partial : false ,
491+ } ,
492+ ]
493+
494+ await presentAssistantMessage ( mockTask )
495+
496+ const toolResult = mockTask . userMessageContent . find (
497+ ( item : any ) => item . type === "tool_result" && item . tool_use_id === toolCallId ,
498+ )
499+ expect ( toolResult ) . toBeDefined ( )
500+ expect ( toolResult . is_error ) . toBe ( true )
501+ expect ( mockTask . consecutiveMistakeCount ) . toBe ( 1 )
502+ expect ( mockTask . didAlreadyUseTool ) . toBe ( false )
503+ } )
504+
505+ it ( "classifies 'Unknown tool' as unknownTool and pushes tool_result" , async ( ) => {
506+ const { validateToolUse } = await import ( "../../tools/validateToolUse" )
507+ ; ( validateToolUse as any ) . mockImplementationOnce ( ( ) => {
508+ throw new Error ( "Unknown tool 'fake_tool_xyz'" )
509+ } )
510+
511+ const toolCallId = "call_unknown_tool_validation"
512+ mockTask . assistantMessageContent = [
513+ {
514+ type : "tool_use" ,
515+ id : toolCallId ,
516+ name : "fake_tool_xyz" ,
517+ params : { } ,
518+ partial : false ,
519+ } ,
520+ ]
521+
522+ await presentAssistantMessage ( mockTask )
523+
524+ const toolResult = mockTask . userMessageContent . find (
525+ ( item : any ) => item . type === "tool_result" && item . tool_use_id === toolCallId ,
526+ )
527+ expect ( toolResult ) . toBeDefined ( )
528+ expect ( toolResult . is_error ) . toBe ( true )
529+ expect ( mockTask . consecutiveMistakeCount ) . toBe ( 1 )
530+ } )
531+
532+ it ( "classifies 'File restriction' as fileRestriction and pushes tool_result" , async ( ) => {
533+ const { validateToolUse } = await import ( "../../tools/validateToolUse" )
534+ ; ( validateToolUse as any ) . mockImplementationOnce ( ( ) => {
535+ throw new Error ( "File restriction: cannot edit .git files" )
536+ } )
537+
538+ const toolCallId = "call_file_restriction"
539+ mockTask . assistantMessageContent = [
540+ {
541+ type : "tool_use" ,
542+ id : toolCallId ,
543+ name : "write_to_file" ,
544+ params : { path : ".git/config" , content : "bad" } ,
545+ partial : false ,
546+ } ,
547+ ]
548+
549+ await presentAssistantMessage ( mockTask )
550+
551+ const toolResult = mockTask . userMessageContent . find (
552+ ( item : any ) => item . type === "tool_result" && item . tool_use_id === toolCallId ,
553+ )
554+ expect ( toolResult ) . toBeDefined ( )
555+ expect ( toolResult . is_error ) . toBe ( true )
556+ } )
557+ } )
558+
559+ describe ( "tool repetition detection" , ( ) => {
560+ it ( "pushes guided error and asks user when repetition is detected" , async ( ) => {
561+ const toolCallId = "call_repetition"
562+ mockTask . assistantMessageContent = [
563+ {
564+ type : "tool_use" ,
565+ id : toolCallId ,
566+ name : "read_file" ,
567+ params : { path : "x.txt" } ,
568+ nativeArgs : { path : "x.txt" } ,
569+ partial : false ,
570+ } ,
571+ ]
572+
573+ // Mock repetition detector to block execution
574+ mockTask . toolRepetitionDetector . check . mockReturnValue ( {
575+ allowExecution : false ,
576+ askUser : {
577+ messageKey : "tool_repetition_limit" ,
578+ messageDetail : "Tool {toolName} has been repeated too many times." ,
579+ } ,
580+ } )
581+
582+ // Mock user response
583+ mockTask . ask . mockResolvedValue ( { response : "yesButtonClicked" } )
584+
585+ await presentAssistantMessage ( mockTask )
586+
587+ // Should have called ask for user confirmation
588+ expect ( mockTask . ask ) . toHaveBeenCalledWith (
589+ "tool_repetition_limit" ,
590+ expect . stringContaining ( "read_file" ) ,
591+ )
592+ // Should NOT have set didAlreadyUseTool (the tool was blocked)
593+ expect ( mockTask . didAlreadyUseTool ) . toBe ( false )
594+ } )
595+ } )
596+
597+ describe ( "unknown tool handling" , ( ) => {
598+ it ( "pushes guided tool_result for unknown tool and does not set didAlreadyUseTool" , async ( ) => {
599+ const toolCallId = "call_unknown_tool_handler"
600+ mockTask . assistantMessageContent = [
601+ {
602+ type : "tool_use" ,
603+ id : toolCallId ,
604+ name : "completely_unknown_tool_abc" ,
605+ params : { } ,
606+ nativeArgs : { } ,
607+ partial : false ,
608+ } ,
609+ ]
610+
611+ await presentAssistantMessage ( mockTask )
612+
613+ const toolResult = mockTask . userMessageContent . find (
614+ ( item : any ) => item . type === "tool_result" && item . tool_use_id === toolCallId ,
615+ )
616+ expect ( toolResult ) . toBeDefined ( )
617+ expect ( toolResult . is_error ) . toBe ( true )
618+ expect ( mockTask . consecutiveMistakeCount ) . toBe ( 1 )
619+ expect ( mockTask . recordToolError ) . toHaveBeenCalledWith (
620+ "completely_unknown_tool_abc" ,
621+ expect . stringContaining ( "Unknown tool" ) ,
622+ )
623+ expect ( mockTask . didAlreadyUseTool ) . toBe ( false )
624+ } )
625+ } )
452626} )
0 commit comments