127 lines
4.6 KiB
TypeScript
127 lines
4.6 KiB
TypeScript
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>): 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]);
|
|
});
|
|
});
|