package app import "testing" func TestVisibleSessionTreeScopesToFocusedRoot(t *testing.T) { root1 := testChild("c1", "root1", "", StatusRunning) child1 := testChild("c2", "child1", "c1", StatusRunning) root2 := testChild("c3", "root2", "", StatusRunning) child2 := testChild("c4", "child2", "c3", StatusRunning) got := visibleSessionTree([]*Child{root1, child1, root2, child2}, "c4") if len(got) != 2 || got[0].ID != "c3" || got[1].ID != "c4" { t.Fatalf("visible tree = %v, want root2 + child2", childIDs(got)) } } func TestVisibleSessionTreeOmitsExited(t *testing.T) { root := testChild("c1", "root", "", StatusRunning) exitedRoot := testChild("c2", "dead-root", "", StatusExited) runningChild := testChild("c3", "child", "c1", StatusRunning) exitedChild := testChild("c4", "dead-child", "c1", StatusExited) got := visibleSessionTree([]*Child{root, exitedRoot, runningChild, exitedChild}, "c1") if len(got) != 2 || got[0].ID != "c1" || got[1].ID != "c3" { t.Fatalf("visible tree = %v, want running root + running child", childIDs(got)) } } func testChild(id, name, parent string, status ChildStatus) *Child { c := &Child{ID: id, Name: name, ParentID: parent} c.status.Store(&status) return c } func childIDs(cs []*Child) []string { ids := make([]string, 0, len(cs)) for _, c := range cs { ids = append(ids, c.ID) } return ids } func TestNextTabIDWrapsAndSkipsCurrent(t *testing.T) { r1 := testAgent("c1", "root1", "", StatusRunning) r2 := testAgent("c2", "root2", "", StatusRunning) r3 := testAgent("c3", "root3", "", StatusRunning) children := []*Child{r1, r2, r3} if got := nextTabID(children, "c1", +1); got != "c2" { t.Fatalf("next from c1: %q", got) } if got := nextTabID(children, "c1", -1); got != "c3" { t.Fatalf("prev from c1: %q", got) } if got := nextTabID(children, "c3", +1); got != "c1" { t.Fatalf("wrap forward from c3: %q", got) } } func TestNextTabIDFromSubAgentJumpsByRoot(t *testing.T) { r1 := testAgent("c1", "root1", "", StatusRunning) r1Child := testAgent("c2", "child1", "c1", StatusRunning) r2 := testAgent("c3", "root2", "", StatusRunning) children := []*Child{r1, r1Child, r2} // Focus is on a sub-agent of root1; Ctrl+D should jump to root2, // not stay inside root1's sub-tree. if got := nextTabID(children, "c2", +1); got != "c3" { t.Fatalf("next from sub-agent: %q want c3", got) } } func TestNextChildIDCyclesWithinTab(t *testing.T) { r1 := testAgent("c1", "root1", "", StatusRunning) a := testAgent("c2", "a", "c1", StatusRunning) b := testAgent("c3", "b", "c1", StatusRunning) other := testAgent("c4", "other-root", "", StatusRunning) children := []*Child{r1, a, b, other} if got := nextChildID(children, "c1", "c1", +1); got != "c2" { t.Fatalf("root->first child: %q", got) } if got := nextChildID(children, "c2", "c1", +1); got != "c3" { t.Fatalf("a->b: %q", got) } if got := nextChildID(children, "c3", "c1", +1); got != "c1" { t.Fatalf("wrap b->root: %q", got) } if got := nextChildID(children, "c1", "c1", -1); got != "c3" { t.Fatalf("wrap backward root->b: %q", got) } } func TestNextChildIDNoopWhenOnlyOneProcess(t *testing.T) { r := testAgent("c1", "solo", "", StatusRunning) if got := nextChildID([]*Child{r}, "c1", "c1", +1); got != "" { t.Fatalf("expected empty when only one process in tab, got %q", got) } } // testAgent is a testChild wrapper that sets KindAgent — the new // navigation/visibility helpers filter by kind, so tests need explicit // kinds to behave like real agents. func testAgent(id, name, parent string, status ChildStatus) *Child { c := testChild(id, name, parent, status) c.Kind = KindAgent return c } func testProcess(id, name string, status ChildStatus) *Child { c := testChild(id, name, "", status) c.Kind = KindCommand return c } func TestSidebarNavListIncludesProcessesAboveAgentTree(t *testing.T) { p1 := testProcess("p1", "bun", StatusRunning) p2 := testProcess("p2", "queue", StatusRunning) r := testAgent("a1", "claude", "", StatusRunning) sub := testAgent("a2", "sub", "a1", StatusRunning) flat := sidebarNavList([]*Child{p1, p2, r, sub}, "a1") if len(flat) != 4 || flat[0].ID != "p1" || flat[1].ID != "p2" || flat[2].ID != "a1" || flat[3].ID != "a2" { t.Fatalf("flat = %v, want p1 p2 a1 a2", childIDs(flat)) } } func TestSidebarNavListIncludesExitedProcesses(t *testing.T) { p := testProcess("p1", "shell", StatusExited) r := testAgent("a1", "claude", "", StatusRunning) flat := sidebarNavList([]*Child{p, r}, "a1") if len(flat) != 2 || flat[0].ID != "p1" || flat[1].ID != "a1" { t.Fatalf("flat = %v, want exited process then active agent", childIDs(flat)) } } func TestNextChildIDWalksProcessesThenAgentTree(t *testing.T) { p1 := testProcess("p1", "bun", StatusRunning) r := testAgent("a1", "claude", "", StatusRunning) sub := testAgent("a2", "sub", "a1", StatusRunning) children := []*Child{p1, r, sub} // From a process, Ctrl+S walks down into the agent tree. if got := nextChildID(children, "p1", "a1", +1); got != "a1" { t.Fatalf("p1 -> a1: %q", got) } // From the agent root, Ctrl+W walks back up into the process list. if got := nextChildID(children, "a1", "a1", -1); got != "p1" { t.Fatalf("a1 -> p1: %q", got) } } func TestNextChildIDCanEnterSingleExitedProcessFromNoFocus(t *testing.T) { p := testProcess("p1", "shell", StatusExited) if got := nextChildID([]*Child{p}, "", "", +1); got != "p1" { t.Fatalf("empty focus -> exited process: %q want p1", got) } } func TestVisibleAgentTreeExcludesTopLevelCommands(t *testing.T) { p := testProcess("p1", "bun", StatusRunning) r := testAgent("a1", "claude", "", StatusRunning) got := visibleAgentTree([]*Child{p, r}, "a1") if len(got) != 1 || got[0].ID != "a1" { t.Fatalf("agent tree = %v, want only a1", childIDs(got)) } } func TestRunningTopLevelsSkipsCommands(t *testing.T) { p := testProcess("p1", "bun", StatusRunning) r := testAgent("a1", "claude", "", StatusRunning) got := runningTopLevels([]*Child{p, r}) if len(got) != 1 || got[0].ID != "a1" { t.Fatalf("top-levels = %v, want only a1", childIDs(got)) } }