11import { Anthropic } from "@anthropic-ai/sdk"
22import { TelemetryService } from "@roo-code/telemetry"
3- import { validateAndFixToolResultIds , ToolResultIdMismatchError } from "../validateToolResultIds"
3+ import {
4+ validateAndFixToolResultIds ,
5+ ToolResultIdMismatchError ,
6+ MissingToolResultError ,
7+ } from "../validateToolResultIds"
48
59// Mock TelemetryService
610vi . mock ( "@roo-code/telemetry" , ( ) => ( {
@@ -394,7 +398,7 @@ describe("validateAndFixToolResultIds", () => {
394398 } )
395399
396400 describe ( "when there are more tool_uses than tool_results" , ( ) => {
397- it ( "should fix the available tool_results" , ( ) => {
401+ it ( "should fix the available tool_results and add missing ones " , ( ) => {
398402 const assistantMessage : Anthropic . MessageParam = {
399403 role : "assistant" ,
400404 content : [
@@ -426,15 +430,174 @@ describe("validateAndFixToolResultIds", () => {
426430
427431 const result = validateAndFixToolResultIds ( userMessage , [ assistantMessage ] )
428432
433+ expect ( Array . isArray ( result . content ) ) . toBe ( true )
434+ const resultContent = result . content as Anthropic . ToolResultBlockParam [ ]
435+ // Should now have 2 tool_results: one fixed and one added for the missing tool_use
436+ expect ( resultContent . length ) . toBe ( 2 )
437+ // The missing tool_result is prepended
438+ expect ( resultContent [ 0 ] . tool_use_id ) . toBe ( "tool-2" )
439+ expect ( resultContent [ 0 ] . content ) . toBe ( "Tool execution was interrupted before completion." )
440+ // The original is fixed
441+ expect ( resultContent [ 1 ] . tool_use_id ) . toBe ( "tool-1" )
442+ } )
443+ } )
444+
445+ describe ( "when tool_results are completely missing" , ( ) => {
446+ it ( "should add missing tool_result for single tool_use" , ( ) => {
447+ const assistantMessage : Anthropic . MessageParam = {
448+ role : "assistant" ,
449+ content : [
450+ {
451+ type : "tool_use" ,
452+ id : "tool-123" ,
453+ name : "read_file" ,
454+ input : { path : "test.txt" } ,
455+ } ,
456+ ] ,
457+ }
458+
459+ const userMessage : Anthropic . MessageParam = {
460+ role : "user" ,
461+ content : [
462+ {
463+ type : "text" ,
464+ text : "Some user message without tool results" ,
465+ } ,
466+ ] ,
467+ }
468+
469+ const result = validateAndFixToolResultIds ( userMessage , [ assistantMessage ] )
470+
471+ expect ( Array . isArray ( result . content ) ) . toBe ( true )
472+ const resultContent = result . content as Array < Anthropic . ToolResultBlockParam | Anthropic . TextBlockParam >
473+ expect ( resultContent . length ) . toBe ( 2 )
474+ // Missing tool_result should be prepended
475+ expect ( resultContent [ 0 ] . type ) . toBe ( "tool_result" )
476+ expect ( ( resultContent [ 0 ] as Anthropic . ToolResultBlockParam ) . tool_use_id ) . toBe ( "tool-123" )
477+ expect ( ( resultContent [ 0 ] as Anthropic . ToolResultBlockParam ) . content ) . toBe (
478+ "Tool execution was interrupted before completion." ,
479+ )
480+ // Original text block should be preserved
481+ expect ( resultContent [ 1 ] . type ) . toBe ( "text" )
482+ } )
483+
484+ it ( "should add missing tool_results for multiple tool_uses" , ( ) => {
485+ const assistantMessage : Anthropic . MessageParam = {
486+ role : "assistant" ,
487+ content : [
488+ {
489+ type : "tool_use" ,
490+ id : "tool-1" ,
491+ name : "read_file" ,
492+ input : { path : "a.txt" } ,
493+ } ,
494+ {
495+ type : "tool_use" ,
496+ id : "tool-2" ,
497+ name : "write_to_file" ,
498+ input : { path : "b.txt" , content : "test" } ,
499+ } ,
500+ ] ,
501+ }
502+
503+ const userMessage : Anthropic . MessageParam = {
504+ role : "user" ,
505+ content : [
506+ {
507+ type : "text" ,
508+ text : "User message" ,
509+ } ,
510+ ] ,
511+ }
512+
513+ const result = validateAndFixToolResultIds ( userMessage , [ assistantMessage ] )
514+
515+ expect ( Array . isArray ( result . content ) ) . toBe ( true )
516+ const resultContent = result . content as Array < Anthropic . ToolResultBlockParam | Anthropic . TextBlockParam >
517+ expect ( resultContent . length ) . toBe ( 3 )
518+ // Both missing tool_results should be prepended
519+ expect ( resultContent [ 0 ] . type ) . toBe ( "tool_result" )
520+ expect ( ( resultContent [ 0 ] as Anthropic . ToolResultBlockParam ) . tool_use_id ) . toBe ( "tool-1" )
521+ expect ( resultContent [ 1 ] . type ) . toBe ( "tool_result" )
522+ expect ( ( resultContent [ 1 ] as Anthropic . ToolResultBlockParam ) . tool_use_id ) . toBe ( "tool-2" )
523+ // Original text should be preserved
524+ expect ( resultContent [ 2 ] . type ) . toBe ( "text" )
525+ } )
526+
527+ it ( "should add only the missing tool_results when some exist" , ( ) => {
528+ const assistantMessage : Anthropic . MessageParam = {
529+ role : "assistant" ,
530+ content : [
531+ {
532+ type : "tool_use" ,
533+ id : "tool-1" ,
534+ name : "read_file" ,
535+ input : { path : "a.txt" } ,
536+ } ,
537+ {
538+ type : "tool_use" ,
539+ id : "tool-2" ,
540+ name : "write_to_file" ,
541+ input : { path : "b.txt" , content : "test" } ,
542+ } ,
543+ ] ,
544+ }
545+
546+ const userMessage : Anthropic . MessageParam = {
547+ role : "user" ,
548+ content : [
549+ {
550+ type : "tool_result" ,
551+ tool_use_id : "tool-1" ,
552+ content : "Content for tool 1" ,
553+ } ,
554+ ] ,
555+ }
556+
557+ const result = validateAndFixToolResultIds ( userMessage , [ assistantMessage ] )
558+
559+ expect ( Array . isArray ( result . content ) ) . toBe ( true )
560+ const resultContent = result . content as Anthropic . ToolResultBlockParam [ ]
561+ expect ( resultContent . length ) . toBe ( 2 )
562+ // Missing tool_result for tool-2 should be prepended
563+ expect ( resultContent [ 0 ] . tool_use_id ) . toBe ( "tool-2" )
564+ expect ( resultContent [ 0 ] . content ) . toBe ( "Tool execution was interrupted before completion." )
565+ // Existing tool_result should be preserved
566+ expect ( resultContent [ 1 ] . tool_use_id ) . toBe ( "tool-1" )
567+ expect ( resultContent [ 1 ] . content ) . toBe ( "Content for tool 1" )
568+ } )
569+
570+ it ( "should handle empty user content array by adding all missing tool_results" , ( ) => {
571+ const assistantMessage : Anthropic . MessageParam = {
572+ role : "assistant" ,
573+ content : [
574+ {
575+ type : "tool_use" ,
576+ id : "tool-1" ,
577+ name : "read_file" ,
578+ input : { path : "test.txt" } ,
579+ } ,
580+ ] ,
581+ }
582+
583+ const userMessage : Anthropic . MessageParam = {
584+ role : "user" ,
585+ content : [ ] ,
586+ }
587+
588+ const result = validateAndFixToolResultIds ( userMessage , [ assistantMessage ] )
589+
429590 expect ( Array . isArray ( result . content ) ) . toBe ( true )
430591 const resultContent = result . content as Anthropic . ToolResultBlockParam [ ]
431592 expect ( resultContent . length ) . toBe ( 1 )
593+ expect ( resultContent [ 0 ] . type ) . toBe ( "tool_result" )
432594 expect ( resultContent [ 0 ] . tool_use_id ) . toBe ( "tool-1" )
595+ expect ( resultContent [ 0 ] . content ) . toBe ( "Tool execution was interrupted before completion." )
433596 } )
434597 } )
435598
436599 describe ( "telemetry" , ( ) => {
437- it ( "should call captureException when there is a mismatch" , ( ) => {
600+ it ( "should call captureException for both missing and mismatch when there is a mismatch" , ( ) => {
438601 const assistantMessage : Anthropic . MessageParam = {
439602 role : "assistant" ,
440603 content : [
@@ -460,7 +623,17 @@ describe("validateAndFixToolResultIds", () => {
460623
461624 validateAndFixToolResultIds ( userMessage , [ assistantMessage ] )
462625
463- expect ( TelemetryService . instance . captureException ) . toHaveBeenCalledTimes ( 1 )
626+ // A mismatch also triggers missing detection since the wrong-id doesn't match any tool_use
627+ expect ( TelemetryService . instance . captureException ) . toHaveBeenCalledTimes ( 2 )
628+ expect ( TelemetryService . instance . captureException ) . toHaveBeenCalledWith (
629+ expect . any ( MissingToolResultError ) ,
630+ expect . objectContaining ( {
631+ missingToolUseIds : [ "correct-id" ] ,
632+ existingToolResultIds : [ "wrong-id" ] ,
633+ toolUseCount : 1 ,
634+ toolResultCount : 1 ,
635+ } ) ,
636+ )
464637 expect ( TelemetryService . instance . captureException ) . toHaveBeenCalledWith (
465638 expect . any ( ToolResultIdMismatchError ) ,
466639 expect . objectContaining ( {
@@ -516,4 +689,132 @@ describe("validateAndFixToolResultIds", () => {
516689 expect ( error . toolUseIds ) . toEqual ( [ "use-1" , "use-2" ] )
517690 } )
518691 } )
692+
693+ describe ( "MissingToolResultError" , ( ) => {
694+ it ( "should create error with correct properties" , ( ) => {
695+ const error = new MissingToolResultError (
696+ "Missing tool results detected" ,
697+ [ "tool-1" , "tool-2" ] ,
698+ [ "existing-result-1" ] ,
699+ )
700+
701+ expect ( error . name ) . toBe ( "MissingToolResultError" )
702+ expect ( error . message ) . toBe ( "Missing tool results detected" )
703+ expect ( error . missingToolUseIds ) . toEqual ( [ "tool-1" , "tool-2" ] )
704+ expect ( error . existingToolResultIds ) . toEqual ( [ "existing-result-1" ] )
705+ } )
706+ } )
707+
708+ describe ( "telemetry for missing tool_results" , ( ) => {
709+ it ( "should call captureException when tool_results are missing" , ( ) => {
710+ const assistantMessage : Anthropic . MessageParam = {
711+ role : "assistant" ,
712+ content : [
713+ {
714+ type : "tool_use" ,
715+ id : "tool-123" ,
716+ name : "read_file" ,
717+ input : { path : "test.txt" } ,
718+ } ,
719+ ] ,
720+ }
721+
722+ const userMessage : Anthropic . MessageParam = {
723+ role : "user" ,
724+ content : [
725+ {
726+ type : "text" ,
727+ text : "No tool results here" ,
728+ } ,
729+ ] ,
730+ }
731+
732+ validateAndFixToolResultIds ( userMessage , [ assistantMessage ] )
733+
734+ expect ( TelemetryService . instance . captureException ) . toHaveBeenCalledTimes ( 1 )
735+ expect ( TelemetryService . instance . captureException ) . toHaveBeenCalledWith (
736+ expect . any ( MissingToolResultError ) ,
737+ expect . objectContaining ( {
738+ missingToolUseIds : [ "tool-123" ] ,
739+ existingToolResultIds : [ ] ,
740+ toolUseCount : 1 ,
741+ toolResultCount : 0 ,
742+ } ) ,
743+ )
744+ } )
745+
746+ it ( "should call captureException twice when both mismatch and missing occur" , ( ) => {
747+ const assistantMessage : Anthropic . MessageParam = {
748+ role : "assistant" ,
749+ content : [
750+ {
751+ type : "tool_use" ,
752+ id : "tool-1" ,
753+ name : "read_file" ,
754+ input : { path : "a.txt" } ,
755+ } ,
756+ {
757+ type : "tool_use" ,
758+ id : "tool-2" ,
759+ name : "read_file" ,
760+ input : { path : "b.txt" } ,
761+ } ,
762+ ] ,
763+ }
764+
765+ const userMessage : Anthropic . MessageParam = {
766+ role : "user" ,
767+ content : [
768+ {
769+ type : "tool_result" ,
770+ tool_use_id : "wrong-id" , // Wrong ID (mismatch)
771+ content : "Content" ,
772+ } ,
773+ // Missing tool_result for tool-2
774+ ] ,
775+ }
776+
777+ validateAndFixToolResultIds ( userMessage , [ assistantMessage ] )
778+
779+ // Should be called twice: once for missing, once for mismatch
780+ expect ( TelemetryService . instance . captureException ) . toHaveBeenCalledTimes ( 2 )
781+ expect ( TelemetryService . instance . captureException ) . toHaveBeenCalledWith (
782+ expect . any ( MissingToolResultError ) ,
783+ expect . any ( Object ) ,
784+ )
785+ expect ( TelemetryService . instance . captureException ) . toHaveBeenCalledWith (
786+ expect . any ( ToolResultIdMismatchError ) ,
787+ expect . any ( Object ) ,
788+ )
789+ } )
790+
791+ it ( "should not call captureException for missing when all tool_results exist" , ( ) => {
792+ const assistantMessage : Anthropic . MessageParam = {
793+ role : "assistant" ,
794+ content : [
795+ {
796+ type : "tool_use" ,
797+ id : "tool-123" ,
798+ name : "read_file" ,
799+ input : { path : "test.txt" } ,
800+ } ,
801+ ] ,
802+ }
803+
804+ const userMessage : Anthropic . MessageParam = {
805+ role : "user" ,
806+ content : [
807+ {
808+ type : "tool_result" ,
809+ tool_use_id : "tool-123" ,
810+ content : "Content" ,
811+ } ,
812+ ] ,
813+ }
814+
815+ validateAndFixToolResultIds ( userMessage , [ assistantMessage ] )
816+
817+ expect ( TelemetryService . instance . captureException ) . not . toHaveBeenCalled ( )
818+ } )
819+ } )
519820} )
0 commit comments