@@ -569,6 +569,117 @@ describe("ClineProvider", () => {
569569 expect ( stackSizeBeforeAbort - stackSizeAfterAbort ) . toBe ( 1 )
570570 } )
571571
572+ describe ( "clearTask message handler" , ( ) => {
573+ beforeEach ( async ( ) => {
574+ await provider . resolveWebviewView ( mockWebviewView )
575+ } )
576+
577+ test ( "calls clearTask when there is no parent task" , async ( ) => {
578+ // Setup a single task without parent
579+ const mockCline = new Task ( defaultTaskOptions )
580+ // No need to set parentTask - it's undefined by default
581+
582+ // Mock the provider methods
583+ const clearTaskSpy = vi . spyOn ( provider , "clearTask" ) . mockResolvedValue ( undefined )
584+ const finishSubTaskSpy = vi . spyOn ( provider , "finishSubTask" ) . mockResolvedValue ( undefined )
585+ const postStateToWebviewSpy = vi . spyOn ( provider , "postStateToWebview" ) . mockResolvedValue ( undefined )
586+
587+ // Add task to stack
588+ await provider . addClineToStack ( mockCline )
589+
590+ // Get the message handler
591+ const messageHandler = ( mockWebviewView . webview . onDidReceiveMessage as any ) . mock . calls [ 0 ] [ 0 ]
592+
593+ // Trigger clearTask message
594+ await messageHandler ( { type : "clearTask" } )
595+
596+ // Verify clearTask was called (not finishSubTask)
597+ expect ( clearTaskSpy ) . toHaveBeenCalled ( )
598+ expect ( finishSubTaskSpy ) . not . toHaveBeenCalled ( )
599+ expect ( postStateToWebviewSpy ) . toHaveBeenCalled ( )
600+ } )
601+
602+ test ( "calls finishSubTask when there is a parent task" , async ( ) => {
603+ // Setup parent and child tasks
604+ const parentTask = new Task ( defaultTaskOptions )
605+ const childTask = new Task ( defaultTaskOptions )
606+
607+ // Set up parent-child relationship by setting the parentTask property
608+ // The mock allows us to set properties directly
609+ ; ( childTask as any ) . parentTask = parentTask
610+ ; ( childTask as any ) . rootTask = parentTask
611+
612+ // Mock the provider methods
613+ const clearTaskSpy = vi . spyOn ( provider , "clearTask" ) . mockResolvedValue ( undefined )
614+ const finishSubTaskSpy = vi . spyOn ( provider , "finishSubTask" ) . mockResolvedValue ( undefined )
615+ const postStateToWebviewSpy = vi . spyOn ( provider , "postStateToWebview" ) . mockResolvedValue ( undefined )
616+
617+ // Add both tasks to stack (parent first, then child)
618+ await provider . addClineToStack ( parentTask )
619+ await provider . addClineToStack ( childTask )
620+
621+ // Get the message handler
622+ const messageHandler = ( mockWebviewView . webview . onDidReceiveMessage as any ) . mock . calls [ 0 ] [ 0 ]
623+
624+ // Trigger clearTask message
625+ await messageHandler ( { type : "clearTask" } )
626+
627+ // Verify finishSubTask was called (not clearTask)
628+ expect ( finishSubTaskSpy ) . toHaveBeenCalledWith ( expect . stringContaining ( "canceled" ) )
629+ expect ( clearTaskSpy ) . not . toHaveBeenCalled ( )
630+ expect ( postStateToWebviewSpy ) . toHaveBeenCalled ( )
631+ } )
632+
633+ test ( "handles case when no current task exists" , async ( ) => {
634+ // Don't add any tasks to the stack
635+
636+ // Mock the provider methods
637+ const clearTaskSpy = vi . spyOn ( provider , "clearTask" ) . mockResolvedValue ( undefined )
638+ const finishSubTaskSpy = vi . spyOn ( provider , "finishSubTask" ) . mockResolvedValue ( undefined )
639+ const postStateToWebviewSpy = vi . spyOn ( provider , "postStateToWebview" ) . mockResolvedValue ( undefined )
640+
641+ // Get the message handler
642+ const messageHandler = ( mockWebviewView . webview . onDidReceiveMessage as any ) . mock . calls [ 0 ] [ 0 ]
643+
644+ // Trigger clearTask message
645+ await messageHandler ( { type : "clearTask" } )
646+
647+ // When there's no current task, clearTask is still called (it handles the no-task case internally)
648+ expect ( clearTaskSpy ) . toHaveBeenCalled ( )
649+ expect ( finishSubTaskSpy ) . not . toHaveBeenCalled ( )
650+ // State should still be posted
651+ expect ( postStateToWebviewSpy ) . toHaveBeenCalled ( )
652+ } )
653+
654+ test ( "correctly identifies subtask scenario for issue #4602" , async ( ) => {
655+ // This test specifically validates the fix for issue #4602
656+ // where canceling during API retry was incorrectly treating a single task as a subtask
657+
658+ const mockCline = new Task ( defaultTaskOptions )
659+ // No parent task by default - no need to explicitly set
660+
661+ // Mock the provider methods
662+ const clearTaskSpy = vi . spyOn ( provider , "clearTask" ) . mockResolvedValue ( undefined )
663+ const finishSubTaskSpy = vi . spyOn ( provider , "finishSubTask" ) . mockResolvedValue ( undefined )
664+
665+ // Add only one task to stack
666+ await provider . addClineToStack ( mockCline )
667+
668+ // Verify stack size is 1
669+ expect ( provider . getClineStackSize ( ) ) . toBe ( 1 )
670+
671+ // Get the message handler
672+ const messageHandler = ( mockWebviewView . webview . onDidReceiveMessage as any ) . mock . calls [ 0 ] [ 0 ]
673+
674+ // Trigger clearTask message (simulating cancel during API retry)
675+ await messageHandler ( { type : "clearTask" } )
676+
677+ // The fix ensures clearTask is called, not finishSubTask
678+ expect ( clearTaskSpy ) . toHaveBeenCalled ( )
679+ expect ( finishSubTaskSpy ) . not . toHaveBeenCalled ( )
680+ } )
681+ } )
682+
572683 test ( "addClineToStack adds multiple Cline instances to the stack" , async ( ) => {
573684 // Setup Cline instance with auto-mock from the top of the file
574685 const mockCline1 = new Task ( defaultTaskOptions ) // Create a new mocked instance
0 commit comments