@@ -630,6 +630,154 @@ func TestScopeHoldAnyCannotRemoteOperator(t *testing.T) {
630630 require .Nil (t , s2 .holdAnyCannotRemoteOperator ())
631631}
632632
633+ // TestSelectLoopContextCancellation verifies that the select loop in scope
634+ // execution properly exits when the process context is cancelled, preventing
635+ // goroutine leaks.
636+ func TestSelectLoopContextCancellation (t * testing.T ) {
637+ t .Run ("cancel_unblocks_prescope_wait" , func (t * testing.T ) {
638+ ctx , cancel := context .WithCancel (context .Background ())
639+ preScopeChan := make (chan error , 1 )
640+ remoteChan := make (chan notifyMessageResult , 1 )
641+ proc := testutil .NewProcess (t )
642+ proc .BuildPipelineContext (ctx )
643+
644+ done := make (chan error , 1 )
645+ go func () {
646+ preScopeCount := 1
647+ remoteScopeCount := 0
648+ for {
649+ select {
650+ case <- proc .Ctx .Done ():
651+ done <- proc .Ctx .Err ()
652+ return
653+ case e := <- preScopeChan :
654+ if e != nil {
655+ done <- e
656+ return
657+ }
658+ preScopeCount --
659+ case result := <- remoteChan :
660+ result .clean (proc )
661+ if result .err != nil {
662+ done <- result .err
663+ return
664+ }
665+ remoteScopeCount --
666+ }
667+ if preScopeCount == 0 && remoteScopeCount == 0 {
668+ done <- nil
669+ return
670+ }
671+ }
672+ }()
673+
674+ // Cancel context — goroutine must exit promptly
675+ cancel ()
676+
677+ select {
678+ case err := <- done :
679+ require .ErrorIs (t , err , context .Canceled )
680+ case <- time .After (2 * time .Second ):
681+ t .Fatal ("select loop did not exit after context cancellation" )
682+ }
683+ })
684+
685+ t .Run ("cancel_unblocks_remote_wait" , func (t * testing.T ) {
686+ ctx , cancel := context .WithCancel (context .Background ())
687+ preScopeChan := make (chan error , 1 )
688+ remoteChan := make (chan notifyMessageResult , 1 )
689+ proc := testutil .NewProcess (t )
690+ proc .BuildPipelineContext (ctx )
691+
692+ done := make (chan error , 1 )
693+ go func () {
694+ preScopeCount := 0
695+ remoteScopeCount := 1
696+ for {
697+ select {
698+ case <- proc .Ctx .Done ():
699+ done <- proc .Ctx .Err ()
700+ return
701+ case e := <- preScopeChan :
702+ if e != nil {
703+ done <- e
704+ return
705+ }
706+ preScopeCount --
707+ case result := <- remoteChan :
708+ result .clean (proc )
709+ if result .err != nil {
710+ done <- result .err
711+ return
712+ }
713+ remoteScopeCount --
714+ }
715+ if preScopeCount == 0 && remoteScopeCount == 0 {
716+ done <- nil
717+ return
718+ }
719+ }
720+ }()
721+
722+ cancel ()
723+
724+ select {
725+ case err := <- done :
726+ require .ErrorIs (t , err , context .Canceled )
727+ case <- time .After (2 * time .Second ):
728+ t .Fatal ("select loop did not exit after context cancellation" )
729+ }
730+ })
731+
732+ t .Run ("normal_completion_without_cancel" , func (t * testing.T ) {
733+ preScopeChan := make (chan error , 1 )
734+ remoteChan := make (chan notifyMessageResult , 1 )
735+ proc := testutil .NewProcess (t )
736+ proc .BuildPipelineContext (context .Background ())
737+
738+ done := make (chan error , 1 )
739+ go func () {
740+ preScopeCount := 1
741+ remoteScopeCount := 1
742+ for {
743+ select {
744+ case <- proc .Ctx .Done ():
745+ done <- proc .Ctx .Err ()
746+ return
747+ case e := <- preScopeChan :
748+ if e != nil {
749+ done <- e
750+ return
751+ }
752+ preScopeCount --
753+ case result := <- remoteChan :
754+ result .clean (proc )
755+ if result .err != nil {
756+ done <- result .err
757+ return
758+ }
759+ remoteScopeCount --
760+ }
761+ if preScopeCount == 0 && remoteScopeCount == 0 {
762+ done <- nil
763+ return
764+ }
765+ }
766+ }()
767+
768+ // Send successful results
769+ preScopeChan <- nil
770+ remoteChan <- notifyMessageResult {err : nil }
771+
772+ select {
773+ case err := <- done :
774+ require .NoError (t , err )
775+ case <- time .After (2 * time .Second ):
776+ t .Fatal ("select loop did not complete after all results received" )
777+ }
778+ })
779+ }
780+
633781func TestCleanPipelineWitchStartFail (t * testing.T ) {
634782 s := & Scope {
635783 Proc : testutil .NewProcess (t ),
0 commit comments