import { describe, expect, it } from "vitest"; import { filterProjects, groupProjects, isStale, sortProjects } from "./projectList.js"; import type { Project } from "./types.js"; const NOW = new Date("2026-08-23T12:00:00Z"); function project(overrides: Partial): Project { return { path: `/home/user/${overrides.name ?? "proj"}`, name: "proj", is_worktree: false, last_activity: undefined, last_command: undefined, ...overrides, } as Project; } describe("isStale", () => { it("marks projects with no known activity stale", () => { expect(isStale(project({ name: "never" }), NOW)).toBe(true); }); it("is fresh within 7 days and stale beyond", () => { const fresh = project({ name: "fresh", last_activity: "2026-08-17T00:00:00Z" }); const stale = project({ name: "stale", last_activity: "2026-08-16T11:59:00Z" }); expect(isStale(fresh, NOW)).toBe(false); expect(isStale(stale, NOW)).toBe(true); }); }); describe("sortProjects", () => { it("sorts newest-activity first", () => { const a = project({ name: "a", last_activity: "2026-08-20T00:00:00Z" }); const b = project({ name: "b", last_activity: "2026-08-22T00:00:00Z" }); expect(sortProjects([a, b], NOW).map((p) => p.name)).toEqual(["b", "a"]); }); it("sorts stale rows after fresh ones, each block newest first", () => { const freshOld = project({ name: "fresh-old", last_activity: "2026-08-18T00:00:00Z" }); const freshNew = project({ name: "fresh-new", last_activity: "2026-08-23T00:00:00Z" }); const staleNew = project({ name: "stale-new", last_activity: "2026-08-10T00:00:00Z" }); const staleNone = project({ name: "stale-none" }); const sorted = sortProjects([staleNew, freshOld, staleNone, freshNew], NOW); expect(sorted.map((p) => p.name)).toEqual(["fresh-new", "fresh-old", "stale-new", "stale-none"]); }); it("does not mutate the input", () => { const a = project({ name: "a", last_activity: "2026-08-01T00:00:00Z" }); const b = project({ name: "b", last_activity: "2026-08-02T00:00:00Z" }); sortProjects([a, b], NOW); expect(a.name).toBe("a"); }); }); describe("groupProjects", () => { it("keeps plain repositories as their own single-entry group", () => { const solo = project({ name: "solo" }); expect(groupProjects(sortProjects([solo], NOW))).toEqual([{ main: solo, worktrees: [] }]); }); it("nests worktrees under their base repository even when fresher than it", () => { const main = project({ name: "maestro" }); const wt = project({ name: "wt", path: "/elsewhere/wt", is_worktree: true, base_repo: main.path, last_activity: "2026-08-23T00:00:00Z", }); const groups = groupProjects(sortProjects([wt, main], NOW)); expect(groups).toHaveLength(1); expect(groups[0].main).toBe(main); expect(groups[0].worktrees).toEqual([wt]); }); it("orders groups by the freshest member and worktrees by activity", () => { const mainA = project({ name: "a", path: "/w/a", last_activity: "2026-08-01T00:00:00Z" }); const wtA = project({ name: "a-wt", path: "/w/a-wt", is_worktree: true, base_repo: "/w/a", last_activity: "2026-08-20T00:00:00Z", }); const mainB = project({ name: "b", path: "/w/b", last_activity: "2026-08-15T00:00:00Z" }); const wtB1 = project({ name: "b-wt1", path: "/w/b-wt1", is_worktree: true, base_repo: "/w/b", last_activity: "2026-08-19T00:00:00Z", }); const wtB2 = project({ name: "b-wt2", path: "/w/b-wt2", is_worktree: true, base_repo: "/w/b", last_activity: "2026-08-21T00:00:00Z", }); const groups = groupProjects(sortProjects([mainA, mainB, wtB1, wtA, wtB2], NOW)); expect(groups.map((g) => g.main.name)).toEqual(["b", "a"]); const gA = groups.find((g) => g.main.name === "a")!; const gB = groups.find((g) => g.main.name === "b")!; expect(gA.worktrees.map((p) => p.name)).toEqual(["a-wt"]); expect(gB.worktrees.map((p) => p.name)).toEqual(["b-wt2", "b-wt1"]); }); }); describe("filterProjects", () => { const maestro = project({ name: "maestro", path: "/home/u/git/maestro" }); const tea = project({ name: "tea", path: "/home/u/work/tea" }); it("matches on name case-insensitively", () => { expect(filterProjects([maestro, tea], "MAES")).toEqual([maestro]); }); it("matches on the full path", () => { expect(filterProjects([maestro, tea], "work")).toEqual([tea]); }); it("returns everything unchanged for an empty query", () => { expect(filterProjects([maestro, tea], " ")).toEqual([maestro, tea]); }); });