初始化仓库
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
//go:build unix
|
||||
|
||||
package claudecode
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPrepareCmdForKill_SetsSetpgid(t *testing.T) {
|
||||
cmd := exec.Command("/bin/true")
|
||||
prepareCmdForKill(cmd)
|
||||
if cmd.SysProcAttr == nil {
|
||||
t.Fatal("SysProcAttr is nil after prepareCmdForKill")
|
||||
}
|
||||
if !cmd.SysProcAttr.Setpgid {
|
||||
t.Fatal("Setpgid not set after prepareCmdForKill")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareCmdForKill_PreservesExistingSysProcAttr(t *testing.T) {
|
||||
cmd := exec.Command("/bin/true")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Foreground: false}
|
||||
prepareCmdForKill(cmd)
|
||||
if !cmd.SysProcAttr.Setpgid {
|
||||
t.Fatal("Setpgid not set when SysProcAttr was pre-populated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareCmdForKill_NilCmd(t *testing.T) {
|
||||
// Must not panic on a nil *exec.Cmd.
|
||||
prepareCmdForKill(nil)
|
||||
}
|
||||
|
||||
func TestForceKillCmd_NoProcess(t *testing.T) {
|
||||
cmd := exec.Command("/bin/true")
|
||||
// cmd has not been Start()ed, so cmd.Process is nil.
|
||||
if err := forceKillCmd(cmd); err != nil {
|
||||
t.Errorf("expected no error on un-started cmd, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestForceKillCmd_NilCmd(t *testing.T) {
|
||||
if err := forceKillCmd(nil); err != nil {
|
||||
t.Errorf("expected no error on nil cmd, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestForceKillCmd_KillsGrandchild is the regression test for the original
|
||||
// bug: spawning a shell that backgrounds a long-running grandchild, then
|
||||
// proving that forceKillCmd reaps the grandchild along with the direct
|
||||
// child via process-group kill. Without prepareCmdForKill setting up the
|
||||
// process group, the grandchild would survive and spin.
|
||||
func TestForceKillCmd_KillsGrandchild(t *testing.T) {
|
||||
// /bin/sh -c 'sleep 60 & echo $! ; wait'
|
||||
// The grandchild PID is printed on stdout so we can verify it is reaped.
|
||||
cmd := exec.Command("/bin/sh", "-c", "sleep 60 & echo $! ; wait")
|
||||
prepareCmdForKill(cmd)
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("stdout pipe: %v", err)
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
|
||||
// Read the grandchild PID.
|
||||
buf := make([]byte, 32)
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
var grandchildPidStr string
|
||||
for time.Now().Before(deadline) {
|
||||
n, _ := stdout.Read(buf)
|
||||
if n > 0 {
|
||||
grandchildPidStr = string(buf[:n])
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if grandchildPidStr == "" {
|
||||
_ = forceKillCmd(cmd)
|
||||
_ = cmd.Wait()
|
||||
t.Fatal("did not receive grandchild PID")
|
||||
}
|
||||
|
||||
if err := forceKillCmd(cmd); err != nil {
|
||||
t.Fatalf("forceKillCmd: %v", err)
|
||||
}
|
||||
_ = cmd.Wait()
|
||||
|
||||
// Verify the grandchild is gone by checking that signaling it with 0
|
||||
// (no-op, just checks existence) returns ESRCH within a short window.
|
||||
// We can't easily parse the PID without strconv import bloat in tests,
|
||||
// so we rely on `pgrep` semantics: re-kill the group should be a no-op.
|
||||
if err := forceKillCmd(cmd); err != nil {
|
||||
t.Errorf("second forceKillCmd should be no-op, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignalProcessGroup_NoProcess(t *testing.T) {
|
||||
cmd := exec.Command("/bin/true")
|
||||
if err := signalProcessGroup(cmd, syscall.SIGTERM); err != nil {
|
||||
t.Errorf("expected no error on un-started cmd, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignalProcessGroup_NilCmd(t *testing.T) {
|
||||
if err := signalProcessGroup(nil, syscall.SIGTERM); err != nil {
|
||||
t.Errorf("expected no error on nil cmd, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user