@@ -65,3 +65,72 @@ func TestCommandContextCancelKillProcessGroup(t *testing.T) {
6565 return grandchild .Signal (syscall .Signal (0 )) != nil
6666 }, 5 * time .Second , 10 * time .Millisecond )
6767}
68+
69+ func TestCommandContextWaitDelayOrphansGrandchild (t * testing.T ) {
70+ ctx , cancel := context .WithCancel (t .Context ())
71+ defer cancel ()
72+
73+ r , w , err := os .Pipe ()
74+ require .NoError (t , err )
75+
76+ // Shell ignores SIGTERM; grandchild inherits SIG_IGN and also survives the group SIGTERM.
77+ cmd := CommandContext (ctx , "sh" , "-c" , "trap '' TERM; sleep 600 & echo $!; wait" ).
78+ WithWaitDelay (1 * time .Second )
79+ cmd .Stdout = w
80+ require .NoError (t , cmd .Start ())
81+ _ = w .Close ()
82+
83+ var grandchildPID int
84+ t .Cleanup (func () {
85+ // Kill by process group — the PGID survives the group leader's death, so this reaches orphaned sleep.
86+ _ = syscall .Kill (- cmd .Process .Pid , syscall .SIGKILL )
87+ _ = cmd .Wait ()
88+ if grandchildPID > 0 {
89+ _ = syscall .Kill (grandchildPID , syscall .SIGKILL )
90+ }
91+ })
92+
93+ scanner := bufio .NewScanner (r )
94+ require .True (t , scanner .Scan (), "expected grandchild PID on stdout" )
95+ grandchildPID , err = strconv .Atoi (strings .TrimSpace (scanner .Text ()))
96+ require .NoError (t , err )
97+ _ = r .Close ()
98+
99+ grandchild , err := os .FindProcess (grandchildPID )
100+ require .NoError (t , err )
101+ require .NoError (t , grandchild .Signal (syscall .Signal (0 )), "grandchild should be alive before cancel" )
102+
103+ cancel ()
104+ _ = cmd .Wait () // blocks ~1s until WaitDelay SIGKILL kills the direct child (shell)
105+
106+ assert .Eventually (t , func () bool {
107+ return grandchild .Signal (syscall .Signal (0 )) != nil
108+ }, 5 * time .Second , 10 * time .Millisecond , "grandchild was orphaned: WaitDelay SIGKILL did not reach the process group" )
109+ }
110+
111+ func TestCommandContextSIGTERMIgnoredHangs (t * testing.T ) {
112+ ctx , cancel := context .WithCancel (t .Context ())
113+ defer cancel ()
114+
115+ cmd := CommandContext (ctx , "sh" , "-c" , "trap '' TERM; sleep 600" ).WithWaitDelay (1 * time .Second )
116+ require .NoError (t , cmd .Start ())
117+
118+ waitDone := make (chan struct {})
119+ go func () {
120+ _ = cmd .Wait ()
121+ close (waitDone )
122+ }()
123+
124+ t .Cleanup (func () {
125+ _ = syscall .Kill (- cmd .Process .Pid , syscall .SIGKILL )
126+ <- waitDone
127+ })
128+
129+ cancel ()
130+
131+ select {
132+ case <- waitDone :
133+ case <- time .After (5 * time .Second ):
134+ t .Fatal ("process did not exit after context cancel: SIGTERM ignored and no SIGKILL escalation occurred" )
135+ }
136+ }
0 commit comments