import { describe, expect, it } from "vitest"; import type { Command } from "../../commands.js"; import type { Session } from "../../types.js"; import { barGroups, barRows, commandBarCandidates, dedupeCommands, directoryBarCandidates, directoryCandidate, highlightSegments, orderSessionsForBar, parseBarQuery, sessionActionCandidates, sessionBarCandidates, sessionCandidate, } from "./model.js"; function session(id: string, over: Partial = {}): Session { return { id, cwd: "/home/me/dev/api", command: "bash", env: {}, metadata: { name: id }, status: "Running", created_at: "2026-01-01T00:00:00Z", ...over, }; } function command(over: Partial = {}): Command { return { id: "a", label: "A command", group: "Test", keys: ["a"], scope: "global", run: () => {}, ...over, }; } describe("empty-query ordering", () => { it("sorts most recently focused first", () => { const older = session("older", { last_focused_at: "2026-01-01T10:00:00Z" }); const newer = session("newer", { last_focused_at: "2026-01-01T11:00:00Z" }); expect(orderSessionsForBar([older, newer]).map((s) => s.id)).toEqual(["newer", "older"]); }); it("sorts a never-focused session after every focused one, newest created first", () => { const focused = session("focused", { last_focused_at: "2026-01-01T09:00:00Z" }); const old = session("old", { created_at: "2026-01-01T00:00:00Z" }); const fresh = session("fresh", { created_at: "2026-01-02T00:00:00Z" }); expect(orderSessionsForBar([old, fresh, focused]).map((s) => s.id)).toEqual([ "focused", "fresh", "old", ]); }); it("sorts dead sessions after live ones regardless of recency", () => { const dead = session("dead", { status: "Dead", last_focused_at: "2026-01-01T12:00:00Z" }); const live = session("live", { last_focused_at: "2026-01-01T10:00:00Z" }); expect(orderSessionsForBar([dead, live]).map((s) => s.id)).toEqual(["live", "dead"]); }); it("treats an empty last_focused_at as never focused", () => { const blank = session("blank", { last_focused_at: "" }); const focused = session("focused", { last_focused_at: "2026-01-01T10:00:00Z" }); expect(orderSessionsForBar([blank, focused]).map((s) => s.id)).toEqual(["focused", "blank"]); }); }); describe("session candidates", () => { it("carries the candidate table's fields, with the directory abbreviated", () => { const s = session("api", { metadata: { name: "api" }, current_cwd: "/home/me/dev/api", command: "claude --resume", }); expect(sessionCandidate(s, "/home/me").fields).toEqual({ name: "api", directory: "~/dev/api", command: "claude --resume", tool: "claude", }); }); it("leaves the tool out when the program has no mark, and groups a worktree by its repo", () => { const s = session("edit", { metadata: {}, command: "hx src/", worktree: { path: "/home/me/dev/api/wt/x", base_repo: "/home/me/dev/api" }, }); const candidate = sessionCandidate(s, "/home/me"); expect(candidate.fields.tool).toBeUndefined(); expect(candidate.fields.directory).toBe("~/dev/api"); // No `metadata.name`: the sidebar's fallback is the command line. expect(candidate.fields.name).toBe("hx src/"); }); it("builds candidates in the empty-query order", () => { const a = session("a", { last_focused_at: "2026-01-01T10:00:00Z" }); const b = session("b", { last_focused_at: "2026-01-01T11:00:00Z" }); expect(sessionBarCandidates([a, b]).map((c) => c.session.id)).toEqual(["b", "a"]); }); }); describe("rows and grouping", () => { it("an empty query keeps every candidate in input order", () => { const candidates = sessionBarCandidates([session("a"), session("b")]); expect(barRows("", candidates).map((r) => r.score)).toEqual([0, 0]); }); it("groups by kind without reordering the ranking", () => { const rows = barRows("kill", [ ...sessionBarCandidates([session("killer-app", { metadata: { name: "killer-app" } })]), ...commandBarCandidates([command({ label: "Kill selected session" })]), ]); const groups = barGroups(rows); expect(groups.map((g) => g.title)).toEqual(["Sessions", "Commands"]); expect(groups.flatMap((g) => g.results)).toEqual(rows); }); }); describe("parsing the query line", () => { it("reads a sigil only in first position", () => { expect(parseBarQuery(">help")).toEqual({ sigil: ">", query: "help" }); expect(parseBarQuery("~dev")).toEqual({ sigil: "~", query: "dev" }); }); it("leaves a sigil character elsewhere in the query literal", () => { expect(parseBarQuery("~dev/~scratch")).toEqual({ sigil: "~", query: "dev/~scratch" }); expect(parseBarQuery("a>b")).toEqual({ sigil: null, query: "a>b" }); }); it("returns to the mixed list when the sigil is backspaced away", () => { expect(parseBarQuery("")).toEqual({ sigil: null, query: "" }); }); }); describe("command candidates", () => { it("dedupes by action, not by chord", () => { const run = () => {}; const cmds = [ command({ id: "one", label: "Do it", keys: ["j"], run }), command({ id: "two", label: "Do it", keys: ["down"], run }), ]; expect(commandBarCandidates(cmds)).toHaveLength(1); expect(dedupeCommands(cmds)).toHaveLength(1); }); it("matches on the label only, never on the chord", () => { const candidates = commandBarCandidates([command({ label: "Show help", keys: ["ctrl-q"] })]); expect(barRows("ctrlq", candidates)).toEqual([]); expect(barRows("show", candidates)).toHaveLength(1); }); }); describe("directory candidates", () => { it("abbreviates the path it shows and keeps the one it spawns in", () => { const candidate = directoryCandidate("/home/me/dev/api", "/home/me"); expect(candidate.fields.path).toBe("~/dev/api"); expect(candidate.path).toBe("/home/me/dev/api"); }); it("expands a typed `~` and dedupes by the directory spawned in", () => { const candidates = directoryBarCandidates( ["~/dev/api", "/home/me/dev/api", "", "/srv/x"], "/home/me", ); expect(candidates.map((c) => c.path)).toEqual(["/home/me/dev/api", "/srv/x"]); }); }); describe("the action page's candidates", () => { it("offers no action beyond the three", () => { expect(sessionActionCandidates().map((c) => c.action.id)).toEqual([ "kill", "rename", "spawn-similar", ]); }); }); describe("highlight segments", () => { it("splits the text on the matcher's ranges", () => { expect(highlightSegments("web-search", [[0, 1], [4, 5]])).toEqual([ { text: "w", hit: true }, { text: "eb-", hit: false }, { text: "s", hit: true }, { text: "earch", hit: false }, ]); }); });