This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/web/src/tree.test.ts
T
Miguel Palhas 8269bcabcc feat(maestro): sort dormant sessions after live ones in the sidebar
docs/SIDEBAR.md: a dormant session takes the lowest precedence in the
sidebar tree, so a rebooted host's live work never sorts under a pile
of dormant cards.
2026-09-04 09:19:03 +01:00

176 lines
6.0 KiB
TypeScript

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"]);
});
});