@@ -150,6 +150,103 @@ suite("Roo Code Subtasks", function () {
150150 }
151151 } )
152152
153+ test ( "delegated child completion persists parent and child history state" , async ( ) => {
154+ const api = globalThis . api
155+ const asks : Record < string , ClineMessage [ ] > = { }
156+ const says : Record < string , ClineMessage [ ] > = { }
157+
158+ let delegationCompletedParentId : string | undefined
159+ let delegationCompletedChildId : string | undefined
160+ let delegationCompletedSummary : string | undefined
161+
162+ const messageHandler = ( { taskId, message } : { taskId : string ; message : ClineMessage } ) => {
163+ if ( message . type === "ask" ) {
164+ asks [ taskId ] = asks [ taskId ] || [ ]
165+ asks [ taskId ] . push ( message )
166+ }
167+ if ( message . type === "say" && message . partial === false ) {
168+ says [ taskId ] = says [ taskId ] || [ ]
169+ says [ taskId ] . push ( message )
170+ }
171+ }
172+
173+ const delegationCompletedHandler = ( parentId : string , childId : string , summary : string ) => {
174+ delegationCompletedParentId = parentId
175+ delegationCompletedChildId = childId
176+ delegationCompletedSummary = summary
177+ }
178+
179+ api . on ( RooCodeEventName . Message , messageHandler )
180+ api . on ( RooCodeEventName . TaskDelegationCompleted , delegationCompletedHandler )
181+
182+ try {
183+ const parentTaskId = await api . startNewTask ( {
184+ configuration : {
185+ mode : "ask" ,
186+ alwaysAllowModeSwitch : true ,
187+ alwaysAllowSubtasks : true ,
188+ autoApprovalEnabled : true ,
189+ enableCheckpoints : false ,
190+ } ,
191+ text : SUBTASK_PARENT_PROMPT ,
192+ } )
193+
194+ let childTaskId : string | undefined
195+ await waitFor ( ( ) => {
196+ const stack = api . getCurrentTaskStack ( )
197+ const current = stack [ stack . length - 1 ]
198+ if ( current && current !== parentTaskId ) {
199+ childTaskId = current
200+ return true
201+ }
202+ return false
203+ } )
204+
205+ await waitFor ( ( ) => asks [ childTaskId ! ] ?. some ( ( { ask } ) => ask === "followup" ) ?? false )
206+
207+ // Send the answer, then wait for TaskDelegationCompleted. That event fires after
208+ // atomicUpdatePair writes the persisted history but before the parent is re-created,
209+ // so it is the right gate for history assertions. waitUntilCompleted alone is not
210+ // sufficient because the resumed parent runs into a mock 404 and never emits
211+ // TaskCompleted in the test environment.
212+ await api . sendMessage ( SUBTASK_CHILD_FOLLOWUP_ANSWER )
213+ await waitFor ( ( ) => delegationCompletedParentId !== undefined )
214+
215+ assert . strictEqual (
216+ delegationCompletedParentId ,
217+ parentTaskId ,
218+ "TaskDelegationCompleted should fire for parent" ,
219+ )
220+ assert . strictEqual ( delegationCompletedChildId , childTaskId , "TaskDelegationCompleted should fire for child" )
221+ assert . strictEqual ( delegationCompletedSummary , "9" , "TaskDelegationCompleted summary should be '9'" )
222+
223+ const parent = await api . getTaskHistoryItem ( parentTaskId )
224+ assert . ok ( parent , "Parent history item should exist" )
225+ assert . strictEqual ( parent . status , "active" , "Parent status should be 'active' after child completes" )
226+ assert . strictEqual ( parent . awaitingChildId , undefined , "Parent awaitingChildId should be cleared" )
227+ assert . strictEqual ( parent . delegatedToId , undefined , "Parent delegatedToId should be cleared" )
228+ assert . strictEqual ( parent . completedByChildId , childTaskId , "Parent completedByChildId should be the child" )
229+ assert . strictEqual ( parent . completionResultSummary , "9" , "Parent completionResultSummary should be '9'" )
230+ assert . ok ( parent . childIds ?. includes ( childTaskId ! ) , "Parent childIds should include the child" )
231+
232+ const child = await api . getTaskHistoryItem ( childTaskId ! )
233+ assert . ok ( child , "Child history item should exist" )
234+ assert . strictEqual ( child . status , "completed" , "Child status should be 'completed'" )
235+ assert . strictEqual ( child . parentTaskId , parentTaskId , "Child parentTaskId should point to parent" )
236+ assert . strictEqual ( child . completionResultSummary , "9" , "Child completionResultSummary should be '9'" )
237+ } finally {
238+ api . off ( RooCodeEventName . Message , messageHandler )
239+ api . off ( RooCodeEventName . TaskDelegationCompleted , delegationCompletedHandler )
240+ if ( api . getCurrentTaskStack ( ) . length > 0 ) {
241+ await api . clearCurrentTask ( )
242+ }
243+ if ( api . getCurrentTaskStack ( ) . length > 0 ) {
244+ await api . clearCurrentTask ( )
245+ }
246+ await waitFor ( ( ) => api . getCurrentTaskStack ( ) . length === 0 ) . catch ( ( ) => { } )
247+ }
248+ } )
249+
153250 // Race mitigation: skipDelegationRepair prevents removeClineFromStack from
154251 // auto-resuming the parent when the child is cancelled (Race 2).
155252 test ( "parent stays paused after subtask cancellation" , async ( ) => {
0 commit comments