//go:build !windows && linux package infra import ( "reflect" "testing" ) // fakePPID builds a ppidOf closure from a child->parent map. Unknown PIDs map to // 1 (init), which terminates the ascent without matching any pane. func fakePPID(tree map[int]int) func(int) int { return func(pid int) int { if p, ok := tree[pid]; ok { return p } return 1 } } func TestResolvePaneIDsFrom(t *testing.T) { // Two panes. Pane %3 runs claude directly (pane_pid == claude PID 100). // Pane %7 runs a shell (pane_pid 200) that launched claude as a child (300). tmuxOut := "100 %3\n200 %7\n" tree := map[int]int{ 300: 200, // claude (300) -> shell (200) which is the pane_pid of %7 } got := resolvePaneIDsFrom(tmuxOut, fakePPID(tree), []int{100, 300}) want := map[int]string{ 100: "%3", // direct: pane_pid IS the claude PID 300: "%7", // ascent: claude's parent is the pane_pid } if !reflect.DeepEqual(got, want) { t.Fatalf("resolvePaneIDsFrom = %v, want %v", got, want) } } func TestResolvePaneIDsFromUnresolvable(t *testing.T) { // PID 999 has no pane in its ancestry -> omitted (caller degrades to ""). tmuxOut := "100 %3\n" got := resolvePaneIDsFrom(tmuxOut, fakePPID(map[int]int{}), []int{999}) if len(got) != 0 { t.Fatalf("expected empty map for unresolvable PID, got %v", got) } } func TestResolvePaneIDsFromMalformedLines(t *testing.T) { // Garbage / short / non-numeric lines are skipped without crashing; the one // valid line still resolves. tmuxOut := "\n \nnotapid %9\n42\n100 %3\n" got := resolvePaneIDsFrom(tmuxOut, fakePPID(map[int]int{}), []int{100}) want := map[int]string{100: "%3"} if !reflect.DeepEqual(got, want) { t.Fatalf("resolvePaneIDsFrom (malformed) = %v, want %v", got, want) } } func TestResolvePaneIDsEmptyInputs(t *testing.T) { // Empty socket or no PIDs -> empty map, no tmux call attempted. if got := ResolvePaneIDs("", []int{1, 2}); len(got) != 0 { t.Errorf("empty socket: expected empty map, got %v", got) } if got := ResolvePaneIDs("fleet", nil); len(got) != 0 { t.Errorf("nil pids: expected empty map, got %v", got) } } func TestPaneAncestorBounded(t *testing.T) { // A cycle in the process tree must not hang: the 64-hop bound cuts it. cycle := func(pid int) int { if pid == 500 { return 501 } return 500 // 501 -> 500 -> 501 ... never reaches a pane } if id, ok := paneAncestor(500, map[int]string{100: "%3"}, cycle); ok { t.Fatalf("expected no resolution for cyclic tree, got %q", id) } }