The browser app had no tests and no way to run any: package.json shipped dev/build/preview/typecheck, and CI installed Node and pnpm but never ran anything with them. Its densest logic was module-private, so even with a runner none of it was reachable. Extracts four pure modules out of DOM-heavy ones — tree.ts (directory grouping), keys.ts (terminal escape encoding), paths.ts (completion arithmetic), html.ts (one escaper, replacing four copies). Extraction rather than `export` because attach.ts imports xterm's CSS at module level, which a node test cannot load. CatalogStore takes an injectable socket factory, scheduler and refetch, so the reducer and the backoff schedule run without a server. snapshot() now returns a copy: it returned the live object while events mutated it in place, so listeners could not diff and every event rebuilt the whole tree. 36 tests across 6 files, and a `web` CI job running typecheck, test and build. vitest is pinned to 2.x — 4.x requires vite >= 6 and this is vite 5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
98 lines
3.0 KiB
TypeScript
98 lines
3.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { buildTree, 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("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");
|
|
});
|
|
|
|
it("filters on both session name and directory", () => {
|
|
const sessions = [session("a", "/home/n/alpha", "claude"), session("b", "/home/n/beta", "bash")];
|
|
|
|
const byDir = buildTree(sessions, "/home/n", "alpha") as TreeFolder;
|
|
expect(names(byDir)).toEqual(["~/alpha"]);
|
|
|
|
const byCommand = buildTree(sessions, "/home/n", "claude") as TreeFolder;
|
|
expect(names(byCommand)).toEqual(["~/alpha"]);
|
|
|
|
const noMatch = buildTree(sessions, "/home/n", "zzz") as TreeFolder;
|
|
expect(noMatch.children).toEqual([]);
|
|
});
|
|
|
|
it("ignores case and surrounding whitespace in the filter", () => {
|
|
const root = buildTree([session("a", "/home/n/Alpha")], "/home/n", " ALPHA ") as TreeFolder;
|
|
expect(root.children).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|