/** * @file Tests for a session's own worktree (`server/lib/session-worktree.js`): * it is resolved only for a session the workspace OWNS whose cwd git registers * as a worktree of the workspace's repo, and removing it never forces — git * refuses a dirty tree, `branch -d` keeps unmerged work, a lock held by a live * process is refused and a stale one cleared first. * @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 { execFileSync } = require("node:child_process"); const ROOT = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "session-wt-"))); process.env.DASHBOARD_DB_PATH = path.join(ROOT, "t.db"); const REPO = path.join(ROOT, "repo"); // A git hook (the pre-commit suite) exports GIT_DIR / GIT_INDEX_FILE for the // repo being committed; inherited, every git call below would act on THAT repo. const ENV = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith("GIT_"))); const git = (...args) => execFileSync("git", ["-C", REPO, ...args], { encoding: "utf8", env: { ...ENV, GIT_AUTHOR_NAME: "t", GIT_AUTHOR_EMAIL: "t@t", GIT_COMMITTER_NAME: "t", GIT_COMMITTER_EMAIL: "t@t", }, }); let db; let wt; let lane; function addWorktree(name) { git("worktree", "add", "-q", "-b", `worktree-${name}`, `.claude/worktrees/${name}`); return path.join(REPO, ".claude", "worktrees", name); } function session(id, cwd, laneId) { db.prepare( "INSERT INTO sessions (id, name, status, cwd, lane_id, started_at, updated_at) VALUES (?, ?, 'active', ?, ?, '2026-10-02T00:00:00Z', '2026-10-02T00:00:00Z')" ).run(id, id, cwd, laneId); } before(() => { fs.mkdirSync(REPO); git("init", "-q", "-b", "main"); fs.writeFileSync(path.join(REPO, "a.txt"), "a\n"); git("add", "."); git("commit", "-qm", "init"); ({ db } = require("../db")); wt = require("../lib/session-worktree"); const id = db .prepare("INSERT INTO lanes (cwd, tool) VALUES (?, 'claude')") .run(REPO).lastInsertRowid; lane = db.prepare("SELECT * FROM lanes WHERE id = ?").get(id); }); after(() => fs.rmSync(ROOT, { recursive: true, force: true })); describe("sessionWorktree", () => { it("resolves an owned session's registered worktree", async () => { const dir = addWorktree("one"); session("s-one", dir, lane.id); const found = await wt.sessionWorktree(lane, "s-one"); assert.equal(found.path, dir); assert.equal(found.branch, "worktree-one"); }); it("refuses a session it does not own, its own folder, and an unregistered path", async () => { const dir = addWorktree("two"); session("s-other", dir, lane.id + 100); session("s-root", REPO, lane.id); fs.mkdirSync(path.join(REPO, "plain")); session("s-plain", path.join(REPO, "plain"), lane.id); for (const id of ["s-other", "s-root", "s-plain", "s-missing"]) { await assert.rejects(wt.sessionWorktree(lane, id), (e) => e.code === "ENOWORKTREE", id); } }); }); describe("removeSessionWorktree", () => { it("asks for confirmation", async () => { await assert.rejects(wt.removeSessionWorktree(lane, "s-one", {}), (e) => e.code === "ECONFIRM"); }); it("removes a clean worktree and its merged branch", async () => { const out = await wt.removeSessionWorktree(lane, "s-one", { confirm: true }); assert.deepEqual(out, { removed: true, branch: "worktree-one", branchKept: false }); assert.equal(fs.existsSync(path.join(REPO, ".claude/worktrees/one")), false); assert.equal(git("branch", "--list", "worktree-one").trim(), ""); }); it("refuses a dirty worktree and leaves it as it was", async () => { const dir = addWorktree("dirty"); session("s-dirty", dir, lane.id); fs.writeFileSync(path.join(dir, "a.txt"), "changed\n"); await assert.rejects(wt.removeSessionWorktree(lane, "s-dirty", { confirm: true })); assert.equal(fs.readFileSync(path.join(dir, "a.txt"), "utf8"), "changed\n"); }); it("keeps a branch that holds unmerged commits", async () => { const dir = addWorktree("work"); session("s-work", dir, lane.id); fs.writeFileSync(path.join(dir, "b.txt"), "b\n"); execFileSync("git", ["-C", dir, "add", "."], { env: ENV }); execFileSync( "git", ["-C", dir, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "b"], { env: ENV } ); const out = await wt.removeSessionWorktree(lane, "s-work", { confirm: true }); assert.equal(out.branchKept, true); assert.equal(git("branch", "--list", "worktree-work").trim(), "worktree-work"); }); it("refuses a lock held by a live process and clears a stale one", async () => { const dir = addWorktree("locked"); session("s-locked", dir, lane.id); git("worktree", "lock", "--reason", `claude session locked (pid ${process.pid} start 1)`, dir); await assert.rejects( wt.removeSessionWorktree(lane, "s-locked", { confirm: true }), (e) => e.code === "ELOCKED" ); git("worktree", "unlock", dir); // A pid that cannot be alive: past the kernel's pid_max. git("worktree", "lock", "--reason", "claude session locked (pid 2147483646 start 1)", dir); const out = await wt.removeSessionWorktree(lane, "s-locked", { confirm: true }); assert.equal(out.removed, true); }); });