When gt done runs inside a tmux session (e.g., after polecat task completion), calling KillSessionWithProcesses would kill the gt done process itself before it could complete cleanup operations like writing handoff state. Add KillSessionWithProcessesExcluding() function that accepts a list of PIDs to exclude from the kill sequence. Update selfKillSession to pass its own PID, ensuring gt done completes before the session is destroyed. Also fix both Kill*WithProcesses functions to ignore "session not found" errors from KillSession - when we kill all processes in a session, tmux may automatically destroy it before we explicitly call KillSession. Co-authored-by: julianknutsen <julianknutsen@users.noreply.github> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
760 lines
19 KiB
Go
760 lines
19 KiB
Go
package tmux
|
|
|
|
import (
|
|
"os/exec"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func hasTmux() bool {
|
|
_, err := exec.LookPath("tmux")
|
|
return err == nil
|
|
}
|
|
|
|
func TestListSessionsNoServer(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
sessions, err := tm.ListSessions()
|
|
// Should not error even if no server running
|
|
if err != nil {
|
|
t.Fatalf("ListSessions: %v", err)
|
|
}
|
|
// Result may be nil or empty slice
|
|
_ = sessions
|
|
}
|
|
|
|
func TestHasSessionNoServer(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
has, err := tm.HasSession("nonexistent-session-xyz")
|
|
if err != nil {
|
|
t.Fatalf("HasSession: %v", err)
|
|
}
|
|
if has {
|
|
t.Error("expected session to not exist")
|
|
}
|
|
}
|
|
|
|
func TestSessionLifecycle(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
sessionName := "gt-test-session-" + t.Name()
|
|
|
|
// Clean up any existing session
|
|
_ = tm.KillSession(sessionName)
|
|
|
|
// Create session
|
|
if err := tm.NewSession(sessionName, ""); err != nil {
|
|
t.Fatalf("NewSession: %v", err)
|
|
}
|
|
defer func() { _ = tm.KillSession(sessionName) }()
|
|
|
|
// Verify exists
|
|
has, err := tm.HasSession(sessionName)
|
|
if err != nil {
|
|
t.Fatalf("HasSession: %v", err)
|
|
}
|
|
if !has {
|
|
t.Error("expected session to exist after creation")
|
|
}
|
|
|
|
// List should include it
|
|
sessions, err := tm.ListSessions()
|
|
if err != nil {
|
|
t.Fatalf("ListSessions: %v", err)
|
|
}
|
|
found := false
|
|
for _, s := range sessions {
|
|
if s == sessionName {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("session not found in list")
|
|
}
|
|
|
|
// Kill session
|
|
if err := tm.KillSession(sessionName); err != nil {
|
|
t.Fatalf("KillSession: %v", err)
|
|
}
|
|
|
|
// Verify gone
|
|
has, err = tm.HasSession(sessionName)
|
|
if err != nil {
|
|
t.Fatalf("HasSession after kill: %v", err)
|
|
}
|
|
if has {
|
|
t.Error("expected session to not exist after kill")
|
|
}
|
|
}
|
|
|
|
func TestDuplicateSession(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
sessionName := "gt-test-dup-" + t.Name()
|
|
|
|
// Clean up any existing session
|
|
_ = tm.KillSession(sessionName)
|
|
|
|
// Create session
|
|
if err := tm.NewSession(sessionName, ""); err != nil {
|
|
t.Fatalf("NewSession: %v", err)
|
|
}
|
|
defer func() { _ = tm.KillSession(sessionName) }()
|
|
|
|
// Try to create duplicate
|
|
err := tm.NewSession(sessionName, "")
|
|
if err != ErrSessionExists {
|
|
t.Errorf("expected ErrSessionExists, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSendKeysAndCapture(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
sessionName := "gt-test-keys-" + t.Name()
|
|
|
|
// Clean up any existing session
|
|
_ = tm.KillSession(sessionName)
|
|
|
|
// Create session
|
|
if err := tm.NewSession(sessionName, ""); err != nil {
|
|
t.Fatalf("NewSession: %v", err)
|
|
}
|
|
defer func() { _ = tm.KillSession(sessionName) }()
|
|
|
|
// Send echo command
|
|
if err := tm.SendKeys(sessionName, "echo HELLO_TEST_MARKER"); err != nil {
|
|
t.Fatalf("SendKeys: %v", err)
|
|
}
|
|
|
|
// Give it a moment to execute
|
|
// In real tests you'd wait for output, but for basic test we just capture
|
|
output, err := tm.CapturePane(sessionName, 50)
|
|
if err != nil {
|
|
t.Fatalf("CapturePane: %v", err)
|
|
}
|
|
|
|
// Should contain our marker (might not if shell is slow, but usually works)
|
|
if !strings.Contains(output, "echo HELLO_TEST_MARKER") {
|
|
t.Logf("captured output: %s", output)
|
|
// Don't fail, just note - timing issues possible
|
|
}
|
|
}
|
|
|
|
func TestGetSessionInfo(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
sessionName := "gt-test-info-" + t.Name()
|
|
|
|
// Clean up any existing session
|
|
_ = tm.KillSession(sessionName)
|
|
|
|
// Create session
|
|
if err := tm.NewSession(sessionName, ""); err != nil {
|
|
t.Fatalf("NewSession: %v", err)
|
|
}
|
|
defer func() { _ = tm.KillSession(sessionName) }()
|
|
|
|
info, err := tm.GetSessionInfo(sessionName)
|
|
if err != nil {
|
|
t.Fatalf("GetSessionInfo: %v", err)
|
|
}
|
|
|
|
if info.Name != sessionName {
|
|
t.Errorf("Name = %q, want %q", info.Name, sessionName)
|
|
}
|
|
if info.Windows < 1 {
|
|
t.Errorf("Windows = %d, want >= 1", info.Windows)
|
|
}
|
|
}
|
|
|
|
func TestWrapError(t *testing.T) {
|
|
tm := NewTmux()
|
|
|
|
tests := []struct {
|
|
stderr string
|
|
want error
|
|
}{
|
|
{"no server running on /tmp/tmux-...", ErrNoServer},
|
|
{"error connecting to /tmp/tmux-...", ErrNoServer},
|
|
{"no current target", ErrNoServer},
|
|
{"duplicate session: test", ErrSessionExists},
|
|
{"session not found: test", ErrSessionNotFound},
|
|
{"can't find session: test", ErrSessionNotFound},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
err := tm.wrapError(nil, tt.stderr, []string{"test"})
|
|
if err != tt.want {
|
|
t.Errorf("wrapError(%q) = %v, want %v", tt.stderr, err, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnsureSessionFresh_NoExistingSession(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
sessionName := "gt-test-fresh-" + t.Name()
|
|
|
|
// Clean up any existing session
|
|
_ = tm.KillSession(sessionName)
|
|
|
|
// EnsureSessionFresh should create a new session
|
|
if err := tm.EnsureSessionFresh(sessionName, ""); err != nil {
|
|
t.Fatalf("EnsureSessionFresh: %v", err)
|
|
}
|
|
defer func() { _ = tm.KillSession(sessionName) }()
|
|
|
|
// Verify session exists
|
|
has, err := tm.HasSession(sessionName)
|
|
if err != nil {
|
|
t.Fatalf("HasSession: %v", err)
|
|
}
|
|
if !has {
|
|
t.Error("expected session to exist after EnsureSessionFresh")
|
|
}
|
|
}
|
|
|
|
func TestEnsureSessionFresh_ZombieSession(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
sessionName := "gt-test-zombie-" + t.Name()
|
|
|
|
// Clean up any existing session
|
|
_ = tm.KillSession(sessionName)
|
|
|
|
// Create a zombie session (session exists but no Claude/node running)
|
|
// A normal tmux session with bash/zsh is a "zombie" for our purposes
|
|
if err := tm.NewSession(sessionName, ""); err != nil {
|
|
t.Fatalf("NewSession: %v", err)
|
|
}
|
|
defer func() { _ = tm.KillSession(sessionName) }()
|
|
|
|
// Verify it's a zombie (not running Claude/node)
|
|
if tm.IsClaudeRunning(sessionName) {
|
|
t.Skip("session unexpectedly has Claude running - can't test zombie case")
|
|
}
|
|
|
|
// Verify generic agent check also treats it as not running (shell session)
|
|
if tm.IsAgentRunning(sessionName) {
|
|
t.Fatalf("expected IsAgentRunning(%q) to be false for a fresh shell session", sessionName)
|
|
}
|
|
|
|
// EnsureSessionFresh should kill the zombie and create fresh session
|
|
// This should NOT error with "session already exists"
|
|
if err := tm.EnsureSessionFresh(sessionName, ""); err != nil {
|
|
t.Fatalf("EnsureSessionFresh on zombie: %v", err)
|
|
}
|
|
|
|
// Session should still exist
|
|
has, err := tm.HasSession(sessionName)
|
|
if err != nil {
|
|
t.Fatalf("HasSession: %v", err)
|
|
}
|
|
if !has {
|
|
t.Error("expected session to exist after EnsureSessionFresh on zombie")
|
|
}
|
|
}
|
|
|
|
func TestEnsureSessionFresh_IdempotentOnZombie(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
sessionName := "gt-test-idem-" + t.Name()
|
|
|
|
// Clean up any existing session
|
|
_ = tm.KillSession(sessionName)
|
|
|
|
// Call EnsureSessionFresh multiple times - should work each time
|
|
for i := 0; i < 3; i++ {
|
|
if err := tm.EnsureSessionFresh(sessionName, ""); err != nil {
|
|
t.Fatalf("EnsureSessionFresh attempt %d: %v", i+1, err)
|
|
}
|
|
}
|
|
defer func() { _ = tm.KillSession(sessionName) }()
|
|
|
|
// Session should exist
|
|
has, err := tm.HasSession(sessionName)
|
|
if err != nil {
|
|
t.Fatalf("HasSession: %v", err)
|
|
}
|
|
if !has {
|
|
t.Error("expected session to exist after multiple EnsureSessionFresh calls")
|
|
}
|
|
}
|
|
|
|
func TestIsAgentRunning(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
sessionName := "gt-test-agent-" + t.Name()
|
|
|
|
// Clean up any existing session
|
|
_ = tm.KillSession(sessionName)
|
|
|
|
// Create session (will run default shell)
|
|
if err := tm.NewSession(sessionName, ""); err != nil {
|
|
t.Fatalf("NewSession: %v", err)
|
|
}
|
|
defer func() { _ = tm.KillSession(sessionName) }()
|
|
|
|
// Get the current pane command (should be bash/zsh/etc)
|
|
cmd, err := tm.GetPaneCommand(sessionName)
|
|
if err != nil {
|
|
t.Fatalf("GetPaneCommand: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
processNames []string
|
|
wantRunning bool
|
|
}{
|
|
{
|
|
name: "empty process list",
|
|
processNames: []string{},
|
|
wantRunning: false,
|
|
},
|
|
{
|
|
name: "matching shell process",
|
|
processNames: []string{cmd}, // Current shell
|
|
wantRunning: true,
|
|
},
|
|
{
|
|
name: "claude agent (node) - not running",
|
|
processNames: []string{"node"},
|
|
wantRunning: cmd == "node", // Only true if shell happens to be node
|
|
},
|
|
{
|
|
name: "gemini agent - not running",
|
|
processNames: []string{"gemini"},
|
|
wantRunning: cmd == "gemini",
|
|
},
|
|
{
|
|
name: "cursor agent - not running",
|
|
processNames: []string{"cursor-agent"},
|
|
wantRunning: cmd == "cursor-agent",
|
|
},
|
|
{
|
|
name: "multiple process names with match",
|
|
processNames: []string{"nonexistent", cmd, "also-nonexistent"},
|
|
wantRunning: true,
|
|
},
|
|
{
|
|
name: "multiple process names without match",
|
|
processNames: []string{"nonexistent1", "nonexistent2"},
|
|
wantRunning: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tm.IsAgentRunning(sessionName, tt.processNames...)
|
|
if got != tt.wantRunning {
|
|
t.Errorf("IsAgentRunning(%q, %v) = %v, want %v (current cmd: %q)",
|
|
sessionName, tt.processNames, got, tt.wantRunning, cmd)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsAgentRunning_NonexistentSession(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
|
|
// IsAgentRunning on nonexistent session should return false, not error
|
|
got := tm.IsAgentRunning("nonexistent-session-xyz", "node", "gemini", "cursor-agent")
|
|
if got {
|
|
t.Error("IsAgentRunning on nonexistent session should return false")
|
|
}
|
|
}
|
|
|
|
func TestIsClaudeRunning(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
sessionName := "gt-test-claude-" + t.Name()
|
|
|
|
// Clean up any existing session
|
|
_ = tm.KillSession(sessionName)
|
|
|
|
// Create session (will run default shell, not Claude)
|
|
if err := tm.NewSession(sessionName, ""); err != nil {
|
|
t.Fatalf("NewSession: %v", err)
|
|
}
|
|
defer func() { _ = tm.KillSession(sessionName) }()
|
|
|
|
// IsClaudeRunning should be false (shell is running, not node/claude)
|
|
cmd, _ := tm.GetPaneCommand(sessionName)
|
|
wantRunning := cmd == "node" || cmd == "claude"
|
|
|
|
if got := tm.IsClaudeRunning(sessionName); got != wantRunning {
|
|
t.Errorf("IsClaudeRunning() = %v, want %v (pane cmd: %q)", got, wantRunning, cmd)
|
|
}
|
|
}
|
|
|
|
func TestIsClaudeRunning_VersionPattern(t *testing.T) {
|
|
// Test the version pattern regex matching directly
|
|
// Since we can't easily mock the pane command, test the pattern logic
|
|
tests := []struct {
|
|
cmd string
|
|
want bool
|
|
}{
|
|
{"node", true},
|
|
{"claude", true},
|
|
{"2.0.76", true},
|
|
{"1.2.3", true},
|
|
{"10.20.30", true},
|
|
{"bash", false},
|
|
{"zsh", false},
|
|
{"", false},
|
|
{"v2.0.76", false}, // version with 'v' prefix shouldn't match
|
|
{"2.0", false}, // incomplete version
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.cmd, func(t *testing.T) {
|
|
// Check if it matches node/claude directly
|
|
isKnownCmd := tt.cmd == "node" || tt.cmd == "claude"
|
|
// Check version pattern
|
|
matched, _ := regexp.MatchString(`^\d+\.\d+\.\d+`, tt.cmd)
|
|
|
|
got := isKnownCmd || matched
|
|
if got != tt.want {
|
|
t.Errorf("IsClaudeRunning logic for %q = %v, want %v", tt.cmd, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsClaudeRunning_ShellWithNodeChild(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
sessionName := "gt-test-shell-child-" + t.Name()
|
|
|
|
// Clean up any existing session
|
|
_ = tm.KillSession(sessionName)
|
|
|
|
// Create session with "bash -c" running a node process
|
|
// Use a simple node command that runs for a few seconds
|
|
cmd := `node -e "setTimeout(() => {}, 10000)"`
|
|
if err := tm.NewSessionWithCommand(sessionName, "", cmd); err != nil {
|
|
t.Fatalf("NewSessionWithCommand: %v", err)
|
|
}
|
|
defer func() { _ = tm.KillSession(sessionName) }()
|
|
|
|
// Give the node process time to start
|
|
// WaitForCommand waits until NOT running bash/zsh/sh
|
|
shellsToExclude := []string{"bash", "zsh", "sh"}
|
|
err := tm.WaitForCommand(sessionName, shellsToExclude, 2000*1000000) // 2 second timeout
|
|
if err != nil {
|
|
// If we timeout waiting, it means the pane command is still a shell
|
|
// This is the case we're testing - shell with a node child
|
|
paneCmd, _ := tm.GetPaneCommand(sessionName)
|
|
t.Logf("Pane command is %q - testing shell+child detection", paneCmd)
|
|
}
|
|
|
|
// Now test IsClaudeRunning - it should detect node as a child process
|
|
paneCmd, _ := tm.GetPaneCommand(sessionName)
|
|
if paneCmd == "node" {
|
|
// Direct node detection should work
|
|
if !tm.IsClaudeRunning(sessionName) {
|
|
t.Error("IsClaudeRunning should return true when pane command is 'node'")
|
|
}
|
|
} else {
|
|
// Pane is a shell (bash/zsh) with node as child
|
|
// The new child process detection should catch this
|
|
got := tm.IsClaudeRunning(sessionName)
|
|
t.Logf("Pane command: %q, IsClaudeRunning: %v", paneCmd, got)
|
|
// Note: This may or may not detect depending on how tmux runs the command.
|
|
// On some systems, tmux runs the command directly; on others via a shell.
|
|
}
|
|
}
|
|
|
|
func TestHasClaudeChild(t *testing.T) {
|
|
// Test the hasClaudeChild helper function directly
|
|
// This uses the current process as a test subject
|
|
|
|
// Get current process PID as string
|
|
currentPID := "1" // init/launchd - should have children but not claude/node
|
|
|
|
// hasClaudeChild should return false for init (no node/claude children)
|
|
got := hasClaudeChild(currentPID)
|
|
if got {
|
|
t.Logf("hasClaudeChild(%q) = true - init has claude/node child?", currentPID)
|
|
}
|
|
|
|
// Test with a definitely nonexistent PID
|
|
got = hasClaudeChild("999999999")
|
|
if got {
|
|
t.Error("hasClaudeChild should return false for nonexistent PID")
|
|
}
|
|
}
|
|
|
|
func TestGetAllDescendants(t *testing.T) {
|
|
// Test the getAllDescendants helper function
|
|
|
|
// Test with nonexistent PID - should return empty slice
|
|
got := getAllDescendants("999999999")
|
|
if len(got) != 0 {
|
|
t.Errorf("getAllDescendants(nonexistent) = %v, want empty slice", got)
|
|
}
|
|
|
|
// Test with PID 1 (init/launchd) - should find some descendants
|
|
// Note: We can't test exact PIDs, just that the function doesn't panic
|
|
// and returns reasonable results
|
|
descendants := getAllDescendants("1")
|
|
t.Logf("getAllDescendants(\"1\") found %d descendants", len(descendants))
|
|
|
|
// Verify returned PIDs are all numeric strings
|
|
for _, pid := range descendants {
|
|
for _, c := range pid {
|
|
if c < '0' || c > '9' {
|
|
t.Errorf("getAllDescendants returned non-numeric PID: %q", pid)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestKillSessionWithProcesses(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
sessionName := "gt-test-killproc-" + t.Name()
|
|
|
|
// Clean up any existing session
|
|
_ = tm.KillSession(sessionName)
|
|
|
|
// Create session with a long-running process
|
|
cmd := `sleep 300`
|
|
if err := tm.NewSessionWithCommand(sessionName, "", cmd); err != nil {
|
|
t.Fatalf("NewSessionWithCommand: %v", err)
|
|
}
|
|
|
|
// Verify session exists
|
|
has, err := tm.HasSession(sessionName)
|
|
if err != nil {
|
|
t.Fatalf("HasSession: %v", err)
|
|
}
|
|
if !has {
|
|
t.Fatal("expected session to exist after creation")
|
|
}
|
|
|
|
// Kill with processes
|
|
if err := tm.KillSessionWithProcesses(sessionName); err != nil {
|
|
t.Fatalf("KillSessionWithProcesses: %v", err)
|
|
}
|
|
|
|
// Verify session is gone
|
|
has, err = tm.HasSession(sessionName)
|
|
if err != nil {
|
|
t.Fatalf("HasSession after kill: %v", err)
|
|
}
|
|
if has {
|
|
t.Error("expected session to not exist after KillSessionWithProcesses")
|
|
_ = tm.KillSession(sessionName) // cleanup
|
|
}
|
|
}
|
|
|
|
func TestKillSessionWithProcesses_NonexistentSession(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
|
|
// Killing nonexistent session should not panic, just return error or nil
|
|
err := tm.KillSessionWithProcesses("nonexistent-session-xyz-12345")
|
|
// We don't care about the error value, just that it doesn't panic
|
|
_ = err
|
|
}
|
|
|
|
func TestKillSessionWithProcessesExcluding(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
sessionName := "gt-test-killexcl-" + t.Name()
|
|
|
|
// Clean up any existing session
|
|
_ = tm.KillSession(sessionName)
|
|
|
|
// Create session with a long-running process
|
|
cmd := `sleep 300`
|
|
if err := tm.NewSessionWithCommand(sessionName, "", cmd); err != nil {
|
|
t.Fatalf("NewSessionWithCommand: %v", err)
|
|
}
|
|
|
|
// Verify session exists
|
|
has, err := tm.HasSession(sessionName)
|
|
if err != nil {
|
|
t.Fatalf("HasSession: %v", err)
|
|
}
|
|
if !has {
|
|
t.Fatal("expected session to exist after creation")
|
|
}
|
|
|
|
// Kill with empty excludePIDs (should behave like KillSessionWithProcesses)
|
|
if err := tm.KillSessionWithProcessesExcluding(sessionName, nil); err != nil {
|
|
t.Fatalf("KillSessionWithProcessesExcluding: %v", err)
|
|
}
|
|
|
|
// Verify session is gone
|
|
has, err = tm.HasSession(sessionName)
|
|
if err != nil {
|
|
t.Fatalf("HasSession after kill: %v", err)
|
|
}
|
|
if has {
|
|
t.Error("expected session to not exist after KillSessionWithProcessesExcluding")
|
|
_ = tm.KillSession(sessionName) // cleanup
|
|
}
|
|
}
|
|
|
|
func TestKillSessionWithProcessesExcluding_WithExcludePID(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
sessionName := "gt-test-killexcl2-" + t.Name()
|
|
|
|
// Clean up any existing session
|
|
_ = tm.KillSession(sessionName)
|
|
|
|
// Create session with a long-running process
|
|
cmd := `sleep 300`
|
|
if err := tm.NewSessionWithCommand(sessionName, "", cmd); err != nil {
|
|
t.Fatalf("NewSessionWithCommand: %v", err)
|
|
}
|
|
defer func() { _ = tm.KillSession(sessionName) }()
|
|
|
|
// Get the pane PID
|
|
panePID, err := tm.GetPanePID(sessionName)
|
|
if err != nil {
|
|
t.Fatalf("GetPanePID: %v", err)
|
|
}
|
|
if panePID == "" {
|
|
t.Skip("could not get pane PID")
|
|
}
|
|
|
|
// Kill with the pane PID excluded - the function should still kill the session
|
|
// but should not kill the excluded PID before the session is destroyed
|
|
err = tm.KillSessionWithProcessesExcluding(sessionName, []string{panePID})
|
|
if err != nil {
|
|
t.Fatalf("KillSessionWithProcessesExcluding: %v", err)
|
|
}
|
|
|
|
// Session should be gone (the final KillSession always happens)
|
|
has, _ := tm.HasSession(sessionName)
|
|
if has {
|
|
t.Error("expected session to not exist after KillSessionWithProcessesExcluding")
|
|
}
|
|
}
|
|
|
|
func TestKillSessionWithProcessesExcluding_NonexistentSession(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
|
|
// Killing nonexistent session should not panic
|
|
err := tm.KillSessionWithProcessesExcluding("nonexistent-session-xyz-12345", []string{"12345"})
|
|
// We don't care about the error value, just that it doesn't panic
|
|
_ = err
|
|
}
|
|
|
|
func TestSessionSet(t *testing.T) {
|
|
if !hasTmux() {
|
|
t.Skip("tmux not installed")
|
|
}
|
|
|
|
tm := NewTmux()
|
|
sessionName := "gt-test-sessionset-" + t.Name()
|
|
|
|
// Clean up any existing session
|
|
_ = tm.KillSession(sessionName)
|
|
|
|
// Create a test session
|
|
if err := tm.NewSession(sessionName, ""); err != nil {
|
|
t.Fatalf("NewSession: %v", err)
|
|
}
|
|
defer func() { _ = tm.KillSession(sessionName) }()
|
|
|
|
// Get the session set
|
|
set, err := tm.GetSessionSet()
|
|
if err != nil {
|
|
t.Fatalf("GetSessionSet: %v", err)
|
|
}
|
|
|
|
// Test Has() for existing session
|
|
if !set.Has(sessionName) {
|
|
t.Errorf("SessionSet.Has(%q) = false, want true", sessionName)
|
|
}
|
|
|
|
// Test Has() for non-existing session
|
|
if set.Has("nonexistent-session-xyz-12345") {
|
|
t.Error("SessionSet.Has(nonexistent) = true, want false")
|
|
}
|
|
|
|
// Test nil safety
|
|
var nilSet *SessionSet
|
|
if nilSet.Has("anything") {
|
|
t.Error("nil SessionSet.Has() = true, want false")
|
|
}
|
|
|
|
// Test Names() returns the session
|
|
names := set.Names()
|
|
found := false
|
|
for _, n := range names {
|
|
if n == sessionName {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("SessionSet.Names() doesn't contain %q", sessionName)
|
|
}
|
|
}
|