import { describe, expect, it } from "vitest"; import { buildTree, compareNodes, mergeLinear, type TreeFolder, type TreeNode } from "./tree.js"; import type { Session } from "./types.js"; function session(id: string, cwd: string, command = "bash"): Session { return { id, cwd, command, env: {}, metadata: {}, status: "Running", created_at: "2026-08-22T00:00:00Z", } as Session; } /** Folder names at one level, in render order. */ function names(node: TreeNode): string[] { if (node.kind === "leaf") return []; return node.children.map((c) => (c.kind === "folder" ? c.name : "leaf")); } describe("buildTree", () => { it("groups sessions under their directory", () => { const root = buildTree( [session("a", "/home/n/work"), session("b", "/home/n/work")], "/home/n", ) as TreeFolder; expect(names(root)).toEqual(["~/work"]); const folder = root.children[0] as TreeFolder; expect(folder.children).toHaveLength(2); }); it("collapses a linear chain into one folder", () => { const root = buildTree([session("a", "/home/n/git/naps62/maestro")], "/home/n") as TreeFolder; expect(names(root)).toEqual(["~/git/naps62/maestro"]); }); it("folds the sole root folder into each of its own children when it branches", () => { const root = buildTree( [session("a", "/home/n/subvisual/bullish"), session("b", "/home/n/tea/maestro")], "/home/n", ) as TreeFolder; expect(names(root).sort()).toEqual(["~/subvisual/bullish", "~/tea/maestro"]); }); it("stops collapsing where a directory branches", () => { const root = buildTree( [session("a", "/home/n/git/one"), session("b", "/home/n/git/two")], "/home/n", ) as TreeFolder; expect(names(root)).toEqual(["~/git"]); const git = root.children[0] as TreeFolder; expect(names(git).sort()).toEqual(["one", "two"]); }); it("sorts folders before leaves, then by name", () => { const root = buildTree( [session("loose", ""), session("nested", "/home/n/work")], "/home/n", ) as TreeFolder; expect(root.children[0].kind).toBe("folder"); expect(root.children[1].kind).toBe("leaf"); }); // #168, docs/SIDEBAR.md: a dormant session takes the lowest precedence in // the ordering, so live work always sorts above it, even a name that would // otherwise sort first. it("sorts a dormant session after live ones in the same folder, name notwithstanding", () => { const dormant = { ...session("aaa-dormant", "/home/n/work"), status: "Dormant" } as Session; const live = session("zzz-live", "/home/n/work"); const root = buildTree([dormant, live], "/home/n") as TreeFolder; const folder = root.children[0] as TreeFolder; const ids = folder.children.map((c) => (c.kind === "leaf" ? c.session.id : c.name)); expect(ids).toEqual(["zzz-live", "aaa-dormant"]); }); it("still sorts dormant sessions by name against each other", () => { const a = { ...session("a", "/home/n/work"), status: "Dormant", metadata: { name: "a" }, } as Session; const b = { ...session("b", "/home/n/work"), status: "Dormant", metadata: { name: "b" }, } as Session; const root = buildTree([b, a], "/home/n") as TreeFolder; const folder = root.children[0] as TreeFolder; const ids = folder.children.map((c) => (c.kind === "leaf" ? c.session.id : c.name)); expect(ids).toEqual(["a", "b"]); }); it("exposes the leaf/name precedence directly via compareNodes", () => { const dormant: TreeNode = { kind: "leaf", session: { ...session("a", "/x"), status: "Dormant" } }; const live: TreeNode = { kind: "leaf", session: session("b", "/x") }; expect(compareNodes(dormant, live)).toBeGreaterThan(0); expect(compareNodes(live, dormant)).toBeLessThan(0); }); }); describe("mergeLinear", () => { it("keeps the deepest path so collapse state survives growth", () => { const folder: TreeFolder = { kind: "folder", name: "a", path: "a", children: [{ kind: "folder", name: "b", path: "a/b", children: [] }], }; const merged = mergeLinear(folder); expect(merged.name).toBe("a/b"); expect(merged.path).toBe("a/b"); }); }); describe("worktree sessions group under their repository (#99)", () => { /** A session the daemon spawned into a worktree of `repo`. */ function worktreeSession(id: string, repo: string, wt: string): Session { return { ...session(id, wt), worktree: { path: wt, base_repo: repo } } as Session; } /** Top-level folder names, after linear chains are collapsed. */ function topFolders(root: TreeFolder): string[] { return root.children.filter((c) => c.kind === "folder").map((c) => (c as TreeFolder).name); } it("nests under the repo, not the worktree directory", () => { const root = buildTree( [ worktreeSession( "a", "/home/n/tea/agent-skills", "/home/n/tea/agent-skills/worktrees/largo-clarinet", ), ], "/home/n", ) as TreeFolder; expect(topFolders(root)).toEqual(["~/tea/agent-skills"]); const repo = root.children[0] as TreeFolder; expect(repo.children).toHaveLength(1); expect(repo.children[0].kind).toBe("leaf"); }); it("puts a repo's plain and worktree sessions side by side", () => { const root = buildTree( [ session("plain", "/home/n/tea/agent-skills"), worktreeSession( "wt", "/home/n/tea/agent-skills", "/home/n/tea/agent-skills/worktrees/largo-clarinet", ), ], "/home/n", ) as TreeFolder; expect(topFolders(root)).toEqual(["~/tea/agent-skills"]); const repo = root.children[0] as TreeFolder; expect(repo.children).toHaveLength(2); expect(repo.children.every((c) => c.kind === "leaf")).toBe(true); }); it("leaves a session without a worktree where it is", () => { const root = buildTree([session("a", "/home/n/tea/arr")], "/home/n") as TreeFolder; expect(topFolders(root)).toEqual(["~/tea/arr"]); }); });