@@ -387,6 +387,7 @@ describe("History resume delegation - parent metadata transitions", () => {
387387
388388 it ( "reopenParentFromDelegation emits events in correct order: TaskDelegationCompleted → TaskDelegationResumed" , async ( ) => {
389389 const emitSpy = vi . fn ( )
390+ const updateTaskHistory = vi . fn ( ) . mockResolvedValue ( [ ] )
390391
391392 const provider = {
392393 contextProxy : { globalStorageUri : { fsPath : "/tmp" } } ,
@@ -411,7 +412,7 @@ describe("History resume delegation - parent metadata transitions", () => {
411412 overwriteClineMessages : vi . fn ( ) . mockResolvedValue ( undefined ) ,
412413 overwriteApiConversationHistory : vi . fn ( ) . mockResolvedValue ( undefined ) ,
413414 } ) ,
414- updateTaskHistory : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
415+ updateTaskHistory,
415416 } as unknown as ClineProvider
416417
417418 vi . mocked ( readTaskMessages ) . mockResolvedValue ( [ ] )
@@ -433,6 +434,92 @@ describe("History resume delegation - parent metadata transitions", () => {
433434 const resumedIdx = emitSpy . mock . calls . findIndex ( ( c ) => c [ 0 ] === RooCodeEventName . TaskDelegationResumed )
434435 expect ( completedIdx ) . toBeGreaterThanOrEqual ( 0 )
435436 expect ( resumedIdx ) . toBeGreaterThan ( completedIdx )
437+
438+ // RPD-05: verify parent metadata persistence happens before TaskDelegationCompleted emit
439+ const parentUpdateCallIdx = updateTaskHistory . mock . calls . findIndex ( ( call ) => {
440+ const item = call [ 0 ] as { id ?: string ; status ?: string } | undefined
441+ return item ?. id === "p3" && item . status === "active"
442+ } )
443+ expect ( parentUpdateCallIdx ) . toBeGreaterThanOrEqual ( 0 )
444+
445+ const parentUpdateCallOrder = updateTaskHistory . mock . invocationCallOrder [ parentUpdateCallIdx ]
446+ const completedEmitCallOrder = emitSpy . mock . invocationCallOrder [ completedIdx ]
447+ expect ( parentUpdateCallOrder ) . toBeLessThan ( completedEmitCallOrder )
448+ } )
449+
450+ it ( "reopenParentFromDelegation continues when overwrite operations fail and still resumes/emits (RPD-06)" , async ( ) => {
451+ const emitSpy = vi . fn ( )
452+ const parentInstance = {
453+ resumeAfterDelegation : vi . fn ( ) . mockResolvedValue ( undefined ) ,
454+ overwriteClineMessages : vi . fn ( ) . mockRejectedValue ( new Error ( "ui overwrite failed" ) ) ,
455+ overwriteApiConversationHistory : vi . fn ( ) . mockRejectedValue ( new Error ( "api overwrite failed" ) ) ,
456+ }
457+
458+ const provider = {
459+ contextProxy : { globalStorageUri : { fsPath : "/tmp" } } ,
460+ getTaskWithId : vi . fn ( ) . mockImplementation ( async ( id : string ) => {
461+ if ( id === "parent-rpd06" ) {
462+ return {
463+ historyItem : {
464+ id : "parent-rpd06" ,
465+ status : "delegated" ,
466+ awaitingChildId : "child-rpd06" ,
467+ childIds : [ "child-rpd06" ] ,
468+ ts : 800 ,
469+ task : "Parent RPD-06" ,
470+ tokensIn : 0 ,
471+ tokensOut : 0 ,
472+ totalCost : 0 ,
473+ } ,
474+ }
475+ }
476+
477+ return {
478+ historyItem : {
479+ id : "child-rpd06" ,
480+ status : "active" ,
481+ ts : 801 ,
482+ task : "Child RPD-06" ,
483+ tokensIn : 0 ,
484+ tokensOut : 0 ,
485+ totalCost : 0 ,
486+ } ,
487+ }
488+ } ) ,
489+ emit : emitSpy ,
490+ getCurrentTask : vi . fn ( ( ) => ( { taskId : "child-rpd06" } ) ) ,
491+ removeClineFromStack : vi . fn ( ) . mockResolvedValue ( undefined ) ,
492+ createTaskWithHistoryItem : vi . fn ( ) . mockResolvedValue ( parentInstance ) ,
493+ updateTaskHistory : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
494+ } as unknown as ClineProvider
495+
496+ vi . mocked ( readTaskMessages ) . mockResolvedValue ( [ ] )
497+ vi . mocked ( readApiMessages ) . mockResolvedValue ( [ ] )
498+
499+ await expect (
500+ ( ClineProvider . prototype as any ) . reopenParentFromDelegation . call ( provider , {
501+ parentTaskId : "parent-rpd06" ,
502+ childTaskId : "child-rpd06" ,
503+ completionResultSummary : "Subtask finished despite overwrite failures" ,
504+ } ) ,
505+ ) . resolves . toBeUndefined ( )
506+
507+ expect ( parentInstance . overwriteClineMessages ) . toHaveBeenCalledTimes ( 1 )
508+ expect ( parentInstance . overwriteApiConversationHistory ) . toHaveBeenCalledTimes ( 1 )
509+ expect ( parentInstance . resumeAfterDelegation ) . toHaveBeenCalledTimes ( 1 )
510+
511+ expect ( emitSpy ) . toHaveBeenCalledWith (
512+ RooCodeEventName . TaskDelegationCompleted ,
513+ "parent-rpd06" ,
514+ "child-rpd06" ,
515+ "Subtask finished despite overwrite failures" ,
516+ )
517+ expect ( emitSpy ) . toHaveBeenCalledWith ( RooCodeEventName . TaskDelegationResumed , "parent-rpd06" , "child-rpd06" )
518+
519+ const completedIdx = emitSpy . mock . calls . findIndex ( ( c ) => c [ 0 ] === RooCodeEventName . TaskDelegationCompleted )
520+ const resumedIdx = emitSpy . mock . calls . findIndex ( ( c ) => c [ 0 ] === RooCodeEventName . TaskDelegationResumed )
521+ expect ( completedIdx ) . toBeGreaterThanOrEqual ( 0 )
522+ expect ( resumedIdx ) . toBeGreaterThan ( completedIdx )
436523 } )
437524
438525 it ( "reopenParentFromDelegation does NOT emit TaskPaused or TaskUnpaused (new flow only)" , async ( ) => {
@@ -480,6 +567,162 @@ describe("History resume delegation - parent metadata transitions", () => {
480567 expect ( eventNames ) . not . toContain ( RooCodeEventName . TaskSpawned )
481568 } )
482569
570+ it ( "reopenParentFromDelegation skips child close when current task differs and still reopens parent (RPD-02)" , async ( ) => {
571+ const parentInstance = {
572+ resumeAfterDelegation : vi . fn ( ) . mockResolvedValue ( undefined ) ,
573+ overwriteClineMessages : vi . fn ( ) . mockResolvedValue ( undefined ) ,
574+ overwriteApiConversationHistory : vi . fn ( ) . mockResolvedValue ( undefined ) ,
575+ }
576+
577+ const updateTaskHistory = vi . fn ( ) . mockResolvedValue ( [ ] )
578+ const removeClineFromStack = vi . fn ( ) . mockResolvedValue ( undefined )
579+ const createTaskWithHistoryItem = vi . fn ( ) . mockResolvedValue ( parentInstance )
580+
581+ const provider = {
582+ contextProxy : { globalStorageUri : { fsPath : "/tmp" } } ,
583+ getTaskWithId : vi . fn ( ) . mockImplementation ( async ( id : string ) => {
584+ if ( id === "parent-rpd02" ) {
585+ return {
586+ historyItem : {
587+ id : "parent-rpd02" ,
588+ status : "delegated" ,
589+ awaitingChildId : "child-rpd02" ,
590+ childIds : [ "child-rpd02" ] ,
591+ ts : 600 ,
592+ task : "Parent RPD-02" ,
593+ tokensIn : 0 ,
594+ tokensOut : 0 ,
595+ totalCost : 0 ,
596+ } ,
597+ }
598+ }
599+ return {
600+ historyItem : {
601+ id : "child-rpd02" ,
602+ status : "active" ,
603+ ts : 601 ,
604+ task : "Child RPD-02" ,
605+ tokensIn : 0 ,
606+ tokensOut : 0 ,
607+ totalCost : 0 ,
608+ } ,
609+ }
610+ } ) ,
611+ emit : vi . fn ( ) ,
612+ getCurrentTask : vi . fn ( ( ) => ( { taskId : "different-open-task" } ) ) ,
613+ removeClineFromStack,
614+ createTaskWithHistoryItem,
615+ updateTaskHistory,
616+ } as unknown as ClineProvider
617+
618+ vi . mocked ( readTaskMessages ) . mockResolvedValue ( [ ] )
619+ vi . mocked ( readApiMessages ) . mockResolvedValue ( [ ] )
620+
621+ await ( ClineProvider . prototype as any ) . reopenParentFromDelegation . call ( provider , {
622+ parentTaskId : "parent-rpd02" ,
623+ childTaskId : "child-rpd02" ,
624+ completionResultSummary : "Child done without being current" ,
625+ } )
626+
627+ expect ( removeClineFromStack ) . not . toHaveBeenCalled ( )
628+ expect ( updateTaskHistory ) . toHaveBeenCalledWith (
629+ expect . objectContaining ( {
630+ id : "child-rpd02" ,
631+ status : "completed" ,
632+ } ) ,
633+ )
634+ expect ( createTaskWithHistoryItem ) . toHaveBeenCalledWith (
635+ expect . objectContaining ( {
636+ id : "parent-rpd02" ,
637+ status : "active" ,
638+ completedByChildId : "child-rpd02" ,
639+ } ) ,
640+ { startTask : false } ,
641+ )
642+ expect ( parentInstance . resumeAfterDelegation ) . toHaveBeenCalledTimes ( 1 )
643+ } )
644+
645+ it ( "reopenParentFromDelegation logs child status persistence failure and continues reopen flow (RPD-04)" , async ( ) => {
646+ const logSpy = vi . fn ( )
647+ const emitSpy = vi . fn ( )
648+ const parentInstance = {
649+ resumeAfterDelegation : vi . fn ( ) . mockResolvedValue ( undefined ) ,
650+ overwriteClineMessages : vi . fn ( ) . mockResolvedValue ( undefined ) ,
651+ overwriteApiConversationHistory : vi . fn ( ) . mockResolvedValue ( undefined ) ,
652+ }
653+
654+ const updateTaskHistory = vi . fn ( ) . mockImplementation ( async ( historyItem : { id ?: string } ) => {
655+ if ( historyItem . id === "child-rpd04" ) {
656+ throw new Error ( "child status persist failed" )
657+ }
658+ return [ ]
659+ } )
660+
661+ const provider = {
662+ contextProxy : { globalStorageUri : { fsPath : "/tmp" } } ,
663+ getTaskWithId : vi . fn ( ) . mockImplementation ( async ( id : string ) => {
664+ if ( id === "parent-rpd04" ) {
665+ return {
666+ historyItem : {
667+ id : "parent-rpd04" ,
668+ status : "delegated" ,
669+ awaitingChildId : "child-rpd04" ,
670+ childIds : [ "child-rpd04" ] ,
671+ ts : 700 ,
672+ task : "Parent RPD-04" ,
673+ tokensIn : 0 ,
674+ tokensOut : 0 ,
675+ totalCost : 0 ,
676+ } ,
677+ }
678+ }
679+ return {
680+ historyItem : {
681+ id : "child-rpd04" ,
682+ status : "active" ,
683+ ts : 701 ,
684+ task : "Child RPD-04" ,
685+ tokensIn : 0 ,
686+ tokensOut : 0 ,
687+ totalCost : 0 ,
688+ } ,
689+ }
690+ } ) ,
691+ emit : emitSpy ,
692+ log : logSpy ,
693+ getCurrentTask : vi . fn ( ( ) => ( { taskId : "child-rpd04" } ) ) ,
694+ removeClineFromStack : vi . fn ( ) . mockResolvedValue ( undefined ) ,
695+ createTaskWithHistoryItem : vi . fn ( ) . mockResolvedValue ( parentInstance ) ,
696+ updateTaskHistory,
697+ } as unknown as ClineProvider
698+
699+ vi . mocked ( readTaskMessages ) . mockResolvedValue ( [ ] )
700+ vi . mocked ( readApiMessages ) . mockResolvedValue ( [ ] )
701+
702+ await expect (
703+ ( ClineProvider . prototype as any ) . reopenParentFromDelegation . call ( provider , {
704+ parentTaskId : "parent-rpd04" ,
705+ childTaskId : "child-rpd04" ,
706+ completionResultSummary : "Child completion with persistence failure" ,
707+ } ) ,
708+ ) . resolves . toBeUndefined ( )
709+
710+ expect ( logSpy ) . toHaveBeenCalledWith (
711+ expect . stringContaining (
712+ "[reopenParentFromDelegation] Failed to persist child completed status for child-rpd04:" ,
713+ ) ,
714+ )
715+ expect ( updateTaskHistory ) . toHaveBeenCalledWith (
716+ expect . objectContaining ( {
717+ id : "parent-rpd04" ,
718+ status : "active" ,
719+ completedByChildId : "child-rpd04" ,
720+ } ) ,
721+ )
722+ expect ( parentInstance . resumeAfterDelegation ) . toHaveBeenCalledTimes ( 1 )
723+ expect ( emitSpy ) . toHaveBeenCalledWith ( RooCodeEventName . TaskDelegationResumed , "parent-rpd04" , "child-rpd04" )
724+ } )
725+
483726 it ( "handles empty history gracefully when injecting synthetic messages" , async ( ) => {
484727 const provider = {
485728 contextProxy : { globalStorageUri : { fsPath : "/tmp" } } ,
0 commit comments