@@ -2,73 +2,95 @@ import * as assert from "assert"
22
33import { RooCodeEventName , type ClineMessage } from "@roo-code/types"
44
5+ import { setDefaultSuiteTimeout } from "./test-utils"
56import { sleep , waitFor , waitUntilCompleted } from "./utils"
7+ import { SUBTASK_CHILD_FOLLOWUP_ANSWER , SUBTASK_CHILD_PROMPT , SUBTASK_PARENT_PROMPT } from "../fixtures/subtasks"
8+
9+ suite ( "Roo Code Subtasks" , function ( ) {
10+ setDefaultSuiteTimeout ( this )
611
7- suite . skip ( "Roo Code Subtasks" , ( ) => {
812 test ( "Should handle subtask cancellation and resumption correctly" , async ( ) => {
913 const api = globalThis . api
10-
14+ const asks : Record < string , ClineMessage [ ] > = { }
1115 const messages : Record < string , ClineMessage [ ] > = { }
16+ const waitForStage = async ( label : string , condition : Parameters < typeof waitFor > [ 0 ] ) => {
17+ try {
18+ await waitFor ( condition )
19+ } catch ( error ) {
20+ const message = error instanceof Error ? error . message : String ( error )
21+ throw new Error ( `${ label } : ${ message } ` )
22+ }
23+ }
24+
25+ const messageHandler = ( { taskId, message } : { taskId : string ; message : ClineMessage } ) => {
26+ if ( message . type === "ask" ) {
27+ asks [ taskId ] = asks [ taskId ] || [ ]
28+ asks [ taskId ] . push ( message )
29+ }
1230
13- api . on ( RooCodeEventName . Message , ( { taskId, message } ) => {
1431 if ( message . type === "say" && message . partial === false ) {
1532 messages [ taskId ] = messages [ taskId ] || [ ]
1633 messages [ taskId ] . push ( message )
1734 }
18- } )
19-
20- const childPrompt = "You are a calculator. Respond only with numbers. What is the square root of 9?"
21-
22- // Start a parent task that will create a subtask.
23- const parentTaskId = await api . startNewTask ( {
24- configuration : {
25- mode : "ask" ,
26- alwaysAllowModeSwitch : true ,
27- alwaysAllowSubtasks : true ,
28- autoApprovalEnabled : true ,
29- enableCheckpoints : false ,
30- } ,
31- text :
32- "You are the parent task. " +
33- `Create a subtask by using the new_task tool with the message '${ childPrompt } '.` +
34- "After creating the subtask, wait for it to complete and then respond 'Parent task resumed'." ,
35- } )
36-
37- let spawnedTaskId : string | undefined = undefined
38-
39- // Wait for the subtask to be spawned and then cancel it.
40- api . on ( RooCodeEventName . TaskSpawned , ( _ , childTaskId ) => ( spawnedTaskId = childTaskId ) )
41- await waitFor ( ( ) => ! ! spawnedTaskId )
42- await sleep ( 1_000 ) // Give the task a chance to start and populate the history.
43- await api . cancelCurrentTask ( )
44-
45- // Wait a bit to ensure any task resumption would have happened.
46- await sleep ( 2_000 )
47-
48- // The parent task should not have resumed yet, so we shouldn't see
49- // "Parent task resumed".
50- assert . ok (
51- messages [ parentTaskId ] ?. find ( ( { type, text } ) => type === "say" && text === "Parent task resumed" ) ===
52- undefined ,
53- "Parent task should not have resumed after subtask cancellation" ,
54- )
55-
56- // Start a new task with the same message as the subtask.
57- const anotherTaskId = await api . startNewTask ( { text : childPrompt } )
58- await waitUntilCompleted ( { api, taskId : anotherTaskId } )
59-
60- // Wait a bit to ensure any task resumption would have happened.
61- await sleep ( 2_000 )
62-
63- // The parent task should still not have resumed.
64- assert . ok (
65- messages [ parentTaskId ] ?. find ( ( { type, text } ) => type === "say" && text === "Parent task resumed" ) ===
66- undefined ,
67- "Parent task should not have resumed after subtask cancellation" ,
68- )
69-
70- // Clean up - cancel all tasks.
71- await api . clearCurrentTask ( )
72- await waitUntilCompleted ( { api, taskId : parentTaskId } )
35+ }
36+
37+ api . on ( RooCodeEventName . Message , messageHandler )
38+
39+ try {
40+ const parentTaskId = await api . startNewTask ( {
41+ configuration : {
42+ mode : "ask" ,
43+ alwaysAllowModeSwitch : true ,
44+ alwaysAllowSubtasks : true ,
45+ autoApprovalEnabled : true ,
46+ enableCheckpoints : false ,
47+ } ,
48+ text : SUBTASK_PARENT_PROMPT ,
49+ } )
50+
51+ let spawnedTaskId : string | undefined
52+ await waitForStage ( "wait for spawned subtask" , ( ) => {
53+ const currentTaskId = api . getCurrentTaskStack ( ) [ 0 ]
54+ if ( currentTaskId && currentTaskId !== parentTaskId ) {
55+ spawnedTaskId = currentTaskId
56+ return true
57+ }
58+ return false
59+ } )
60+ await waitForStage (
61+ "wait for delegated child followup ask" ,
62+ ( ) => asks [ spawnedTaskId ! ] ?. some ( ( { type, ask } ) => type === "ask" && ask === "followup" ) ?? false ,
63+ )
64+
65+ await api . cancelCurrentTask ( )
66+
67+ await sleep ( 2_000 )
68+
69+ assert . ok (
70+ messages [ parentTaskId ] ?. find ( ( { type, text } ) => type === "say" && text === "Parent task resumed" ) ===
71+ undefined ,
72+ "Parent task should not have resumed after subtask cancellation" ,
73+ )
74+
75+ const anotherTaskId = await api . startNewTask ( { text : SUBTASK_CHILD_PROMPT } )
76+ await waitForStage (
77+ "wait for standalone child followup ask" ,
78+ ( ) => asks [ anotherTaskId ] ?. some ( ( { type, ask } ) => type === "ask" && ask === "followup" ) ?? false ,
79+ )
80+ await api . sendMessage ( SUBTASK_CHILD_FOLLOWUP_ANSWER )
81+ await waitUntilCompleted ( { api, taskId : anotherTaskId } )
82+
83+ await sleep ( 2_000 )
84+
85+ assert . ok (
86+ messages [ parentTaskId ] ?. find ( ( { type, text } ) => type === "say" && text === "Parent task resumed" ) ===
87+ undefined ,
88+ "Parent task should not have resumed after subtask cancellation" ,
89+ )
90+
91+ await api . clearCurrentTask ( )
92+ } finally {
93+ api . off ( RooCodeEventName . Message , messageHandler )
94+ }
7395 } )
7496} )
0 commit comments