Files
patterm/internal/app/session_test.go

127 lines
3.5 KiB
Go

package app
import (
"syscall"
"testing"
"time"
)
// TestParentExitKillsDescendants verifies that when a parent process
// exits, every still-live process that was spawned under it is signaled
// and dies, recursively through the tree.
func TestParentExitKillsDescendants(t *testing.T) {
sess := NewSession(t.TempDir(), "test")
// Use a tiny pause-then-trap shell so the children survive long
// enough for the parent to die first; SIGTERM should terminate them.
sleepArgv := []string{"sh", "-c", "trap 'exit 0' TERM; sleep 30"}
parent, err := sess.Spawn(SpawnSpec{
Kind: KindTerminal,
Argv: []string{"sh", "-c", "sleep 30"},
}, 80, 24)
if err != nil {
t.Fatalf("spawn parent: %v", err)
}
childA, err := sess.Spawn(SpawnSpec{
Kind: KindCommand,
Argv: sleepArgv,
ParentID: parent.ID,
}, 80, 24)
if err != nil {
t.Fatalf("spawn childA: %v", err)
}
grandchild, err := sess.Spawn(SpawnSpec{
Kind: KindCommand,
Argv: sleepArgv,
ParentID: childA.ID,
}, 80, 24)
if err != nil {
t.Fatalf("spawn grandchild: %v", err)
}
// Wait for everyone to be running.
waitUntilLive(t, parent)
waitUntilLive(t, childA)
waitUntilLive(t, grandchild)
// Kill the parent. Its reapChild should cascade to childA, whose
// reapChild should in turn cascade to grandchild.
if err := parent.signal(syscall.SIGTERM); err != nil {
t.Fatalf("signal parent: %v", err)
}
waitUntilNotLive(t, parent)
waitUntilNotLive(t, childA)
waitUntilNotLive(t, grandchild)
}
// TestSpawnInstallsIdleDetectionBeforePublish guarantees that a child
// spawned with SpawnSpec.IdleDetection has its resolved config visible
// the instant the child appears in s.children — closing the race where
// the classifier could read c.idleDetection before the launcher set it.
func TestSpawnInstallsIdleDetectionBeforePublish(t *testing.T) {
sess := NewSession(t.TempDir(), "test")
want := &resolvedIdleDetection{
strategy: StrategyOSCTitleStability,
idleThresholdMS: 9999,
}
c, err := sess.Spawn(SpawnSpec{
Kind: KindCommand,
Argv: []string{"sh", "-c", "sleep 30"},
IdleDetection: want,
}, 80, 24)
if err != nil {
t.Fatalf("spawn: %v", err)
}
defer func() { _ = c.signal(syscall.SIGTERM) }()
// Read back via the same access path the classifier uses
// (sess.Children) so the test fails if the field is set only
// AFTER the child is published.
var found *Child
for _, ch := range sess.Children() {
if ch.ID == c.ID {
found = ch
break
}
}
if found == nil {
t.Fatalf("spawned child %s not in Children()", c.ID)
}
if found.idleDetection == nil {
t.Fatalf("idleDetection nil after Spawn returned")
}
if found.idleDetection.strategy != StrategyOSCTitleStability {
t.Fatalf("strategy: got %q want %q", found.idleDetection.strategy, StrategyOSCTitleStability)
}
if found.idleDetection.idleThresholdMS != 9999 {
t.Fatalf("threshold: got %d want 9999", found.idleDetection.idleThresholdMS)
}
}
func waitUntilLive(t *testing.T, c *Child) {
t.Helper()
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
if c.IsLive() {
return
}
time.Sleep(20 * time.Millisecond)
}
t.Fatalf("child %s never went live", c.ID)
}
func waitUntilNotLive(t *testing.T, c *Child) {
t.Helper()
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
if !c.IsLive() {
return
}
time.Sleep(20 * time.Millisecond)
}
t.Fatalf("child %s still live after parent died (status=%s)", c.ID, c.Status())
}