@@ -43,6 +43,7 @@ type TaskTestAccess = {
4343 startTask : ( task ?: string , images ?: string [ ] ) => Promise < void >
4444 resumeTaskFromHistory : ( ) => Promise < void >
4545 presentAssistantMessageSafe : ( ) => void
46+ hasCompleteToolResultsForCurrentTurn : ( ) => boolean
4647 addToClineMessages : ( message : import ( "@roo-code/types" ) . ClineMessage ) => Promise < void >
4748 updateClineMessage : ( message : import ( "@roo-code/types" ) . ClineMessage ) => Promise < void >
4849 saveClineMessages : ( ) => Promise < boolean >
@@ -576,6 +577,93 @@ describe("Cline", () => {
576577 } )
577578
578579 describe ( "native tool-call request isolation" , ( ) => {
580+ it ( "derives readiness only from a closed, idle, fully paired tool turn" , ( ) => {
581+ const task = new Task ( {
582+ provider : mockProvider ,
583+ apiConfiguration : mockApiConfig ,
584+ task : "tool turn readiness test" ,
585+ startTask : false ,
586+ } )
587+ const readiness = ( ) => getTaskTestAccess ( task ) . hasCompleteToolResultsForCurrentTurn ( )
588+ task . assistantMessageContent = [
589+ {
590+ type : "tool_use" ,
591+ id : "call_ready" ,
592+ name : "read_file" ,
593+ params : { } ,
594+ nativeArgs : { path : "README.md" } ,
595+ partial : false ,
596+ } ,
597+ ]
598+ task . userMessageContent = [ { type : "tool_result" , tool_use_id : "call_ready" , content : "finished" } ]
599+
600+ expect ( readiness ( ) ) . toBe ( false )
601+ task . didCompleteReadingStream = true
602+ task . presentAssistantMessageLocked = true
603+ expect ( readiness ( ) ) . toBe ( false )
604+ task . presentAssistantMessageLocked = false
605+ task . assistantMessageContent [ 0 ] . partial = true
606+ expect ( readiness ( ) ) . toBe ( false )
607+ task . assistantMessageContent [ 0 ] . partial = false
608+ task . userMessageContent = [ { type : "tool_result" , tool_use_id : "different_call" , content : "finished" } ]
609+ expect ( readiness ( ) ) . toBe ( false )
610+ task . userMessageContent = [ { type : "tool_result" , tool_use_id : "call_ready" , content : "finished" } ]
611+ expect ( readiness ( ) ) . toBe ( true )
612+ } )
613+
614+ it ( "continues after a complete read_file result when the readiness flag update is lost" , async ( ) => {
615+ const task = new Task ( {
616+ provider : mockProvider ,
617+ apiConfiguration : mockApiConfig ,
618+ task : "read file continuation test" ,
619+ startTask : false ,
620+ } )
621+
622+ vi . spyOn ( task . diffViewProvider , "reset" ) . mockResolvedValue ( undefined )
623+ vi . spyOn ( getTaskTestAccess ( task ) , "safeEnsureModelFetched" ) . mockResolvedValue ( stubModelInfo )
624+ vi . spyOn ( getTaskTestAccess ( task ) , "presentAssistantMessageSafe" ) . mockImplementation ( ( ) => {
625+ const completedRead = task . assistantMessageContent . find (
626+ ( block ) => block . type === "tool_use" && block . name === "read_file" && ! block . partial ,
627+ )
628+ if ( ! completedRead || completedRead . type !== "tool_use" || ! completedRead . id ) return
629+
630+ task . pushToolResultToUserContent ( {
631+ type : "tool_result" ,
632+ tool_use_id : completedRead . id ,
633+ content : "File: README.md\nfinished" ,
634+ } )
635+ // Reproduce the persisted field symptom: the tool result exists and
636+ // presentation is no longer running, but the one-shot readiness latch
637+ // never flips, so the old loop waits forever before the next request.
638+ task . userMessageContentReady = false
639+ } )
640+
641+ const attemptApiRequestSpy = vi
642+ . spyOn ( task , "attemptApiRequest" )
643+ . mockImplementationOnce ( ( ) =>
644+ asyncStreamFrom < ApiStreamChunk > ( [
645+ { type : "tool_call_partial" , index : 0 , id : "call_read" , name : "read_file" } ,
646+ { type : "tool_call_partial" , index : 0 , arguments : '{"path":"README.md"}' } ,
647+ ] ) ,
648+ )
649+ . mockImplementationOnce ( ( ) => {
650+ throw new Error ( "continuation request reached" )
651+ } )
652+
653+ vi . mocked ( pWaitFor ) . mockImplementation ( async ( condition ) => {
654+ if ( ! ( await condition ( ) ) ) {
655+ throw new Error ( "read_file continuation would stall" )
656+ }
657+ } )
658+
659+ try {
660+ await task . recursivelyMakeClineRequests ( [ { type : "text" , text : "read a file, then continue" } ] )
661+ expect ( attemptApiRequestSpy ) . toHaveBeenCalledTimes ( 2 )
662+ } finally {
663+ vi . mocked ( pWaitFor ) . mockImplementation ( async ( ) => { } )
664+ }
665+ } )
666+
579667 it ( "keeps overlapping Task parser state scoped to each request" , async ( ) => {
580668 const firstTask = new Task ( {
581669 provider : mockProvider ,
0 commit comments