/** * @file Tests for server/lib/projects.js and GET /api/projects: a lane's * project root from a main worktree, a linked worktree, a subdirectory and a * non-repo; grouping lanes per repo with the Folders pseudo-project last; and * matching each worktree to the lane that sits on it. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const http = require("node:http"); const { execFileSync } = require("node:child_process"); const ROOT = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "projects-"))); process.env.DASHBOARD_DB_PATH = path.join(ROOT, "t.db"); process.env.DASHBOARD_REMOTE_SYNC_MS = "0"; process.env.DASHBOARD_LIVENESS_PROBE = "0"; process.env.GIT_CONFIG_NOSYSTEM = "1"; // Inside a git hook (the pre-commit suite run) git exports GIT_DIR, // GIT_INDEX_FILE and friends; the fixture repos below must not inherit them. for (const k of Object.keys(process.env)) { if (k.startsWith("GIT_") && k !== "GIT_CONFIG_NOSYSTEM") delete process.env[k]; } const { createApp } = require("../index"); const lanesLib = require("../lib/lanes"); const { projectRootFor, clearProjectCaches } = require("../lib/projects"); const repo = path.join(ROOT, "zrepo"); const linked = path.join(ROOT, "linked"); const claudeWt = path.join(repo, ".claude", "worktrees", "wf-1"); const plain = path.join(ROOT, "plain"); let app; let base; function git(cwd, ...args) { execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", ...args], { cwd, stdio: "ignore", }); } before(async () => { fs.mkdirSync(path.join(repo, "sub"), { recursive: true }); fs.mkdirSync(plain); git(repo, "init", "-q", "-b", "main"); fs.writeFileSync(path.join(repo, "a.txt"), "a"); git(repo, "add", "."); git(repo, "commit", "-q", "-m", "init"); git(repo, "worktree", "add", "-q", "-b", "feat", linked); git(repo, "worktree", "add", "-q", "-b", "wf-1", claudeWt); app = http.createServer(createApp()); await new Promise((r) => app.listen(0, "127.0.0.1", r)); base = `http://127.0.0.1:${app.address().port}`; }); after(async () => { await new Promise((r) => app.close(r)); }); describe("projectRootFor", () => { it("resolves a main worktree, a linked one and a subdir to the main root", async () => { assert.equal(await projectRootFor(repo), repo); assert.equal(await projectRootFor(linked), repo); assert.equal(await projectRootFor(path.join(repo, "sub")), repo); }); it("answers null outside a repo", async () => { assert.equal(await projectRootFor(plain), null); }); }); describe("GET /api/projects", () => { it("groups lanes per repo, Folders last, and marks untracked worktrees", async () => { const main = lanesLib.createLane({ cwd: repo }); const sub = lanesLib.createLane({ cwd: path.join(repo, "sub") }); const folder = lanesLib.createLane({ cwd: plain }); clearProjectCaches(); const res = await fetch(`${base}/api/projects`); assert.equal(res.status, 200); const { projects } = await res.json(); assert.deepEqual( projects.map((p) => [p.root, p.name, p.workspaces]), [ [repo, "zrepo", [main.id, sub.id]], [null, "Folders", [folder.id]], ] ); const wt = Object.fromEntries(projects[0].worktrees.map((w) => [w.path, w])); assert.deepEqual(wt[repo], { path: repo, branch: "main", main: true, workspace_id: main.id }); // A lane on a SUBDIRECTORY marks no worktree; nobody sits on `linked`. assert.equal(wt[linked].workspace_id, null); assert.equal(wt[linked].source, undefined); assert.equal(wt[claudeWt].source, "claude"); }); it("matches a worktree to a lane adopted on it after the list was cached", async () => { const adopted = lanesLib.createLane({ cwd: linked }); const { projects } = await (await fetch(`${base}/api/projects`)).json(); const wt = projects[0].worktrees.find((w) => w.path === linked); assert.equal(wt.workspace_id, adopted.id); }); it("refresh refuses a cross-origin POST", async () => { const res = await fetch(`${base}/api/projects/refresh`, { method: "POST", headers: { Origin: "http://evil.test" }, }); assert.equal(res.status, 403); }); });