@@ -4,11 +4,144 @@ import { RooCodeEventName, type ClineMessage } from "@roo-code/types"
44
55import { setDefaultSuiteTimeout } from "./test-utils"
66import { waitFor , waitUntilCompleted } from "./utils"
7- import { SUBTASK_CHILD_FOLLOWUP_ANSWER , SUBTASK_PARENT_PROMPT } from "../fixtures/subtasks"
7+ import { SUBTASK_CHILD_FOLLOWUP_ANSWER , SUBTASK_FAST_PARENT_PROMPT , SUBTASK_PARENT_PROMPT } from "../fixtures/subtasks"
88
99suite ( "Roo Code Subtasks" , function ( ) {
1010 setDefaultSuiteTimeout ( this )
1111
12+ test ( "child completing on its first response returns to parent" , async ( ) => {
13+ const api = globalThis . api
14+ const says : Record < string , ClineMessage [ ] > = { }
15+
16+ const messageHandler = ( { taskId, message } : { taskId : string ; message : ClineMessage } ) => {
17+ if ( message . type === "say" && message . partial === false ) {
18+ says [ taskId ] = says [ taskId ] || [ ]
19+ says [ taskId ] . push ( message )
20+ }
21+ }
22+
23+ api . on ( RooCodeEventName . Message , messageHandler )
24+
25+ try {
26+ const parentTaskId = await waitUntilCompleted ( {
27+ api,
28+ start : ( ) =>
29+ api . startNewTask ( {
30+ configuration : {
31+ mode : "ask" ,
32+ alwaysAllowModeSwitch : true ,
33+ alwaysAllowSubtasks : true ,
34+ autoApprovalEnabled : true ,
35+ enableCheckpoints : false ,
36+ } ,
37+ text : SUBTASK_FAST_PARENT_PROMPT ,
38+ } ) ,
39+ } )
40+
41+ assert . ok (
42+ Object . entries ( says ) . some (
43+ ( [ taskId , messages ] ) =>
44+ taskId !== parentTaskId &&
45+ messages . some (
46+ ( { say, text } ) => say === "completion_result" && text ?. trim ( ) === "Fast child completed" ,
47+ ) ,
48+ ) ,
49+ "Immediately-completing child should emit its expected result" ,
50+ )
51+ assert . strictEqual (
52+ says [ parentTaskId ]
53+ ?. filter ( ( { say } ) => say === "completion_result" )
54+ . map ( ( { text } ) => text ?. trim ( ) )
55+ . find ( ( text ) : text is string => ! ! text ) ,
56+ "Fast parent resumed" ,
57+ "Parent should resume after the child completes on its first response" ,
58+ )
59+ } finally {
60+ api . off ( RooCodeEventName . Message , messageHandler )
61+ while ( api . getCurrentTaskStack ( ) . length > 0 ) {
62+ await api . clearCurrentTask ( )
63+ }
64+ }
65+ } )
66+
67+ // Smoke: child completing normally must resume the parent task.
68+ test ( "child task returns to parent after normal completion" , async ( ) => {
69+ const api = globalThis . api
70+ const asks : Record < string , ClineMessage [ ] > = { }
71+ const says : Record < string , ClineMessage [ ] > = { }
72+
73+ const messageHandler = ( { taskId, message } : { taskId : string ; message : ClineMessage } ) => {
74+ if ( message . type === "ask" ) {
75+ asks [ taskId ] = asks [ taskId ] || [ ]
76+ asks [ taskId ] . push ( message )
77+ }
78+ if ( message . type === "say" && message . partial === false ) {
79+ says [ taskId ] = says [ taskId ] || [ ]
80+ says [ taskId ] . push ( message )
81+ }
82+ }
83+
84+ api . on ( RooCodeEventName . Message , messageHandler )
85+
86+ try {
87+ const parentTaskId = await api . startNewTask ( {
88+ configuration : {
89+ mode : "ask" ,
90+ alwaysAllowModeSwitch : true ,
91+ alwaysAllowSubtasks : true ,
92+ autoApprovalEnabled : true ,
93+ enableCheckpoints : false ,
94+ } ,
95+ text : SUBTASK_PARENT_PROMPT ,
96+ } )
97+
98+ // Wait for child to spawn.
99+ let childTaskId : string | undefined
100+ await waitFor ( ( ) => {
101+ const stack = api . getCurrentTaskStack ( )
102+ const current = stack [ stack . length - 1 ]
103+ if ( current && current !== parentTaskId ) {
104+ childTaskId = current
105+ return true
106+ }
107+ return false
108+ } )
109+
110+ // Wait for the child's followup question, then answer so it can complete.
111+ // Register the completion listener before sending the answer to avoid a race.
112+ await waitFor ( ( ) => asks [ childTaskId ! ] ?. some ( ( { ask } ) => ask === "followup" ) ?? false )
113+ await waitUntilCompleted ( {
114+ api,
115+ start : async ( ) => {
116+ await api . sendMessage ( SUBTASK_CHILD_FOLLOWUP_ANSWER )
117+ return parentTaskId
118+ } ,
119+ } )
120+
121+ const parentCompletionText = says [ parentTaskId ]
122+ ?. filter ( ( { say } ) => say === "completion_result" )
123+ . map ( ( { text } ) => text ?. trim ( ) )
124+ . find ( ( t ) : t is string => ! ! t )
125+
126+ assert . strictEqual (
127+ parentCompletionText ,
128+ "Parent task resumed" ,
129+ "Parent should complete with the expected result after child returns" ,
130+ )
131+ } finally {
132+ api . off ( RooCodeEventName . Message , messageHandler )
133+ // Drain the stack so partially-completed tasks don't leak into the next test.
134+ // On the happy path the parent is already gone; on failure both tasks may still be active.
135+ if ( api . getCurrentTaskStack ( ) . length > 0 ) {
136+ await api . clearCurrentTask ( )
137+ }
138+ if ( api . getCurrentTaskStack ( ) . length > 0 ) {
139+ await api . clearCurrentTask ( )
140+ }
141+ await waitFor ( ( ) => api . getCurrentTaskStack ( ) . length === 0 ) . catch ( ( ) => { } )
142+ }
143+ } )
144+
12145 // Race mitigation: skipDelegationRepair prevents removeClineFromStack from
13146 // auto-resuming the parent when the child is cancelled (Race 2).
14147 test ( "parent stays paused after subtask cancellation" , async ( ) => {
0 commit comments