feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard. Lanes: a durable unit of parallel agent work, one per working directory, tracked across session restarts. Managed lanes are git worktrees the dashboard provisions and can reset or remove behind a three-check destroy guard and a counted preflight; adopted lanes are directories you already own and are never destroyable. Pipelines: a lane moves through pipeline stages. A stage the agent declares with evidence renders green; a stage inferred from the tool-event stream renders dashed amber and never counts as done. Detection is forward-only within a 30-minute window, and never writes the declared stage. Workspace: one page at /run with a lane grid, the selected lane's pipeline, and a full Claude console behind a disclosure.
This commit is contained in:
@@ -0,0 +1,457 @@
|
||||
/**
|
||||
* @file Tests for server/lib/worktree.js against a REAL git repository created
|
||||
* in a temp directory. Every behaviour worth testing here is git's own — branch
|
||||
* collisions, what `clean -fd` spares, what `worktree list` reports — so mocking
|
||||
* git would only test our idea of git.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
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.mkdtempSync(path.join(os.tmpdir(), "ccam-wt-"));
|
||||
process.env.LANES_ROOT = path.join(ROOT, "lanes");
|
||||
|
||||
const wt = require("../lib/worktree");
|
||||
|
||||
const SRC = path.join(ROOT, "src-repo");
|
||||
const g = (cwd, ...args) => {
|
||||
// Scrub git hook environment variables (GIT_DIR, GIT_INDEX_FILE, etc.)
|
||||
// so the fixture builder doesn't inherit them from the test harness.
|
||||
const env = { ...process.env };
|
||||
delete env.GIT_DIR;
|
||||
delete env.GIT_WORK_TREE;
|
||||
delete env.GIT_INDEX_FILE;
|
||||
delete env.GIT_COMMON_DIR;
|
||||
delete env.GIT_OBJECT_DIRECTORY;
|
||||
delete env.GIT_ALTERNATE_OBJECT_DIRECTORIES;
|
||||
delete env.GIT_PREFIX;
|
||||
delete env.GIT_NAMESPACE;
|
||||
delete env.GIT_CONFIG_PARAMETERS;
|
||||
env.GIT_TERMINAL_PROMPT = "0";
|
||||
return execFileSync("git", args, { cwd, encoding: "utf8", env });
|
||||
};
|
||||
|
||||
before(() => {
|
||||
fs.mkdirSync(SRC, { recursive: true });
|
||||
g(SRC, "init", "-b", "main");
|
||||
g(SRC, "config", "user.email", "t@example.com");
|
||||
g(SRC, "config", "user.name", "Test");
|
||||
fs.writeFileSync(path.join(SRC, "README.md"), "hello\n");
|
||||
fs.writeFileSync(path.join(SRC, ".gitignore"), "node_modules/\n.env\n");
|
||||
g(SRC, "add", "-A");
|
||||
g(SRC, "commit", "-m", "init");
|
||||
});
|
||||
|
||||
after(() => fs.rmSync(ROOT, { recursive: true, force: true }));
|
||||
|
||||
function laneFor(dir, branch, over = {}) {
|
||||
return {
|
||||
id: 1,
|
||||
kind: "managed",
|
||||
cwd: dir,
|
||||
branch,
|
||||
source_repo: SRC,
|
||||
base_branch: "main",
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
describe("worktree", () => {
|
||||
it("slugifies a title into a safe single segment", () => {
|
||||
assert.equal(wt.slugify("Rename Metric → Rule!"), "rename-metric-rule");
|
||||
assert.equal(wt.slugify(" a//b "), "a-b");
|
||||
assert.ok(wt.slugify("x".repeat(80)).length <= 40);
|
||||
});
|
||||
|
||||
it("resolves the base branch, falling back when origin has none", async () => {
|
||||
assert.equal(await wt.resolveBase(SRC, "main"), "main");
|
||||
assert.equal(await wt.resolveBase(SRC, "does-not-exist"), "main");
|
||||
});
|
||||
|
||||
it("creates a worktree on a new branch and lists it", async () => {
|
||||
const dir = path.join(process.env.LANES_ROOT, "src-repo__alpha");
|
||||
const r = await wt.addWorktree({ sourceRepo: SRC, dir, branch: "feat/alpha", base: "main" });
|
||||
assert.equal(r.created, true);
|
||||
assert.ok(fs.existsSync(path.join(dir, "README.md")));
|
||||
const list = await wt.listWorktrees(SRC);
|
||||
assert.ok(list.some((w) => w.path === dir && w.branch === "feat/alpha"));
|
||||
});
|
||||
|
||||
it("refuses a branch already checked out in another worktree", async () => {
|
||||
const dir2 = path.join(process.env.LANES_ROOT, "src-repo__alpha2");
|
||||
await assert.rejects(
|
||||
() => wt.addWorktree({ sourceRepo: SRC, dir: dir2, branch: "feat/alpha", base: "main" }),
|
||||
(e) => e.code === "EBRANCHBUSY" && typeof e.checkedOutAt === "string"
|
||||
);
|
||||
});
|
||||
|
||||
it("counts dirty, untracked and unpushed work", async () => {
|
||||
const dir = path.join(process.env.LANES_ROOT, "src-repo__alpha");
|
||||
fs.appendFileSync(path.join(dir, "README.md"), "edit\n");
|
||||
fs.writeFileSync(path.join(dir, "scratch.txt"), "untracked\n");
|
||||
fs.mkdirSync(path.join(dir, "node_modules"), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, "node_modules", "dep.js"), "x\n");
|
||||
const s = await wt.statusCounts(dir);
|
||||
assert.equal(s.dirty, 1);
|
||||
assert.equal(s.untracked, 1); // node_modules is ignored, so it does not count
|
||||
assert.match(s.head, /^[0-9a-f]{7,40}$/);
|
||||
// The fixture has no remotes. Given the lane's base branch, unpushedCount
|
||||
// measures base..HEAD — this worktree has committed nothing of its own, so 0.
|
||||
assert.equal(await wt.unpushedCount(dir, "main"), 0);
|
||||
// With no base to measure against (an adopted lane has none), it falls back
|
||||
// to the total commit count, since every commit is then at risk.
|
||||
assert.equal(await wt.unpushedCount(dir), 1);
|
||||
});
|
||||
|
||||
it("reset restores base, drops untracked files, and spares gitignored ones", async () => {
|
||||
const dir = path.join(process.env.LANES_ROOT, "src-repo__alpha");
|
||||
await wt.resetWorktree(laneFor(dir, "feat/alpha"));
|
||||
assert.equal(fs.readFileSync(path.join(dir, "README.md"), "utf8"), "hello\n");
|
||||
assert.equal(fs.existsSync(path.join(dir, "scratch.txt")), false);
|
||||
assert.equal(fs.existsSync(path.join(dir, "node_modules", "dep.js")), true);
|
||||
const s = await wt.statusCounts(dir);
|
||||
assert.equal(s.dirty, 0);
|
||||
// Verify we're on the feature branch, not left on base
|
||||
const branch = g(dir, "rev-parse", "--abbrev-ref", "HEAD").trim();
|
||||
assert.equal(branch, "feat/alpha");
|
||||
});
|
||||
|
||||
it("reset against a bogus base_branch throws ENOBASE and leaves worktree untouched", async () => {
|
||||
// Create a new worktree with a bogus base that will exist as a branch
|
||||
// but we'll change it to non-existent in the lane
|
||||
const dir = path.join(process.env.LANES_ROOT, "src-repo__bogus-test");
|
||||
await wt.addWorktree({ sourceRepo: SRC, dir, branch: "feat/bogus-test", base: "main" });
|
||||
|
||||
// Make a modification to detect if the worktree is mutated
|
||||
fs.writeFileSync(path.join(dir, "test-file.txt"), "test\n");
|
||||
|
||||
// Get the current state before the failed reset
|
||||
const branchBefore = g(dir, "rev-parse", "--abbrev-ref", "HEAD").trim();
|
||||
const statusBefore = wt.statusCounts(dir);
|
||||
|
||||
// Try to reset against a bogus base_branch
|
||||
await assert.rejects(
|
||||
() =>
|
||||
wt.resetWorktree(laneFor(dir, "feat/bogus-test", { base_branch: "non-existent-branch" })),
|
||||
(e) => e.code === "ENOBASE" && e.message.includes("non-existent-branch")
|
||||
);
|
||||
|
||||
// Verify the worktree was not mutated: still on the same branch
|
||||
const branchAfter = g(dir, "rev-parse", "--abbrev-ref", "HEAD").trim();
|
||||
assert.equal(branchAfter, branchBefore);
|
||||
|
||||
// Verify the file still exists (no mutations happened)
|
||||
assert.ok(fs.existsSync(path.join(dir, "test-file.txt")));
|
||||
|
||||
// Clean up
|
||||
await wt.removeWorktree(laneFor(dir, "feat/bogus-test"));
|
||||
});
|
||||
|
||||
it("refuses to destroy an adopted lane, a path outside LANES_ROOT, or a non-worktree", async () => {
|
||||
const dir = path.join(process.env.LANES_ROOT, "src-repo__alpha");
|
||||
await assert.rejects(
|
||||
() => wt.assertDestroyable(laneFor(dir, "feat/alpha", { kind: "adopted" })),
|
||||
(e) => e.code === "ENOTMANAGED"
|
||||
);
|
||||
await assert.rejects(
|
||||
() => wt.assertDestroyable(laneFor("/tmp", "feat/alpha")),
|
||||
(e) => e.code === "EOUTSIDEROOT"
|
||||
);
|
||||
const ghost = path.join(process.env.LANES_ROOT, "src-repo__ghost");
|
||||
fs.mkdirSync(ghost, { recursive: true });
|
||||
await assert.rejects(
|
||||
() => wt.assertDestroyable(laneFor(ghost, "feat/ghost")),
|
||||
(e) => e.code === "ENOTWORKTREE"
|
||||
);
|
||||
});
|
||||
|
||||
// These call removeWorktree DIRECTLY. The route short-circuits adopted lanes
|
||||
// before reaching it, so route-level tests can never pin its guard: deleting
|
||||
// `await assertDestroyable(lane)` from removeWorktree left all 844 server tests
|
||||
// green. Each case asserts the error code AND that nothing was destroyed.
|
||||
describe("removeWorktree refuses what it must not destroy", () => {
|
||||
it("refuses an adopted lane and leaves its directory and files alone", async () => {
|
||||
const dir = path.join(ROOT, "adopted-project");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const file = path.join(dir, "real-work.txt");
|
||||
fs.writeFileSync(file, "the user's own project\n");
|
||||
await assert.rejects(
|
||||
() => wt.removeWorktree(laneFor(dir, "feat/whatever", { kind: "adopted" })),
|
||||
(e) => e.code === "ENOTMANAGED"
|
||||
);
|
||||
assert.equal(fs.existsSync(dir), true);
|
||||
assert.equal(fs.readFileSync(file, "utf8"), "the user's own project\n");
|
||||
});
|
||||
|
||||
it("refuses a cwd outside LANES_ROOT and leaves that directory alone", async () => {
|
||||
const dir = path.join(ROOT, "outside-root");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const file = path.join(dir, "keep.txt");
|
||||
fs.writeFileSync(file, "outside the sandbox\n");
|
||||
await assert.rejects(
|
||||
() => wt.removeWorktree(laneFor(dir, "feat/outside")),
|
||||
(e) => e.code === "EOUTSIDEROOT"
|
||||
);
|
||||
assert.equal(fs.existsSync(dir), true);
|
||||
assert.equal(fs.readFileSync(file, "utf8"), "outside the sandbox\n");
|
||||
});
|
||||
|
||||
it("refuses a real directory inside LANES_ROOT that git does not list as a worktree", async () => {
|
||||
const dir = path.join(process.env.LANES_ROOT, "src-repo__not-a-worktree");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const file = path.join(dir, "keep.txt");
|
||||
fs.writeFileSync(file, "never registered with git\n");
|
||||
await assert.rejects(
|
||||
() => wt.removeWorktree(laneFor(dir, "feat/not-a-worktree")),
|
||||
(e) => e.code === "ENOTWORKTREE"
|
||||
);
|
||||
assert.equal(fs.existsSync(dir), true);
|
||||
assert.equal(fs.readFileSync(file, "utf8"), "never registered with git\n");
|
||||
});
|
||||
});
|
||||
|
||||
it("removes a lane whose worktree was deleted by hand, pruning git's stale record", async () => {
|
||||
// The design promises "the lane reports `missing` and only `remove` is
|
||||
// offered, taking the prune path". Before this, check 2 mapped the vanished
|
||||
// path to EOUTSIDEROOT and the lane could never be removed at all.
|
||||
const dir = path.join(process.env.LANES_ROOT, "src-repo__hand-deleted");
|
||||
await wt.addWorktree({ sourceRepo: SRC, dir, branch: "feat/hand-deleted", base: "main" });
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
assert.ok(
|
||||
(await wt.listWorktrees(SRC)).some((w) => w.path === dir),
|
||||
"git should still list the hand-deleted worktree"
|
||||
);
|
||||
|
||||
await wt.removeWorktree(laneFor(dir, "feat/hand-deleted"));
|
||||
|
||||
assert.equal(
|
||||
(await wt.listWorktrees(SRC)).some((w) => w.path === dir),
|
||||
false
|
||||
);
|
||||
assert.equal(g(SRC, "branch", "--list", "feat/hand-deleted").trim(), "");
|
||||
});
|
||||
|
||||
it("removes a lane git never knew about, when its directory is already gone", async () => {
|
||||
const dir = path.join(process.env.LANES_ROOT, "src-repo__never-existed");
|
||||
assert.equal(fs.existsSync(dir), false);
|
||||
// No throw: there is nothing on disk and nothing in git to clean up.
|
||||
await wt.removeWorktree(laneFor(dir, "feat/never-existed"));
|
||||
});
|
||||
|
||||
it("still refuses the prune path for a missing cwd outside LANES_ROOT", async () => {
|
||||
// The directory is gone, so check 2 cannot realpath it — but it must still
|
||||
// hold, or a lane pointing anywhere could prune a source repo's worktrees.
|
||||
await assert.rejects(
|
||||
() => wt.removeWorktree(laneFor(path.join(ROOT, "gone-and-outside"), "feat/gone")),
|
||||
(e) => e.code === "EOUTSIDEROOT"
|
||||
);
|
||||
});
|
||||
|
||||
it("one prunable sibling worktree does not break reset or remove for other lanes", async () => {
|
||||
// git keeps listing a hand-deleted worktree as `prunable`. realpathSync on
|
||||
// every listed entry threw ENOENT out of assertDestroyable, so a single
|
||||
// stale sibling produced an opaque 500 naming an unrelated directory for
|
||||
// every managed lane in the same repo.
|
||||
const victimDir = path.join(process.env.LANES_ROOT, "src-repo__prunable-victim");
|
||||
await wt.addWorktree({ sourceRepo: SRC, dir: victimDir, branch: "feat/victim", base: "main" });
|
||||
const siblingDir = path.join(process.env.LANES_ROOT, "src-repo__prunable-sibling");
|
||||
await wt.addWorktree({
|
||||
sourceRepo: SRC,
|
||||
dir: siblingDir,
|
||||
branch: "feat/sibling",
|
||||
base: "main",
|
||||
});
|
||||
fs.rmSync(siblingDir, { recursive: true, force: true });
|
||||
assert.ok(
|
||||
(await wt.listWorktrees(SRC)).some((w) => w.path === siblingDir),
|
||||
"git should still list the prunable sibling"
|
||||
);
|
||||
|
||||
await wt.assertDestroyable(laneFor(victimDir, "feat/victim"));
|
||||
await wt.resetWorktree(laneFor(victimDir, "feat/victim"));
|
||||
await wt.removeWorktree(laneFor(victimDir, "feat/victim"));
|
||||
assert.equal(fs.existsSync(victimDir), false);
|
||||
|
||||
// Clean up the stale record so later tests see a tidy list.
|
||||
await wt.removeWorktree(laneFor(siblingDir, "feat/sibling"));
|
||||
});
|
||||
|
||||
it("removes a lane whose worktree's .git pointer is corrupt (unreadable), deregistering it without touching its directory", async () => {
|
||||
// `git worktree remove --force` (even --force --force) refuses outright
|
||||
// when the worktree's OWN .git file fails git's validation — this is what
|
||||
// `unreadable` in the preflight actually is. All three assertDestroyable
|
||||
// checks still pass (the directory exists, is inside LANES_ROOT, and is
|
||||
// still listed by the source repo), so removeWorktree must not get stuck.
|
||||
const dir = path.join(process.env.LANES_ROOT, "src-repo__corrupt");
|
||||
await wt.addWorktree({ sourceRepo: SRC, dir, branch: "feat/corrupt", base: "main" });
|
||||
fs.writeFileSync(path.join(dir, "keep.txt"), "still here\n");
|
||||
fs.writeFileSync(path.join(dir, ".git"), "garbage\n");
|
||||
|
||||
assert.throws(
|
||||
() => g(dir, "status"),
|
||||
"sanity: git itself must refuse to operate inside the corrupt worktree"
|
||||
);
|
||||
|
||||
await wt.removeWorktree(laneFor(dir, "feat/corrupt"));
|
||||
|
||||
assert.equal(
|
||||
(await wt.listWorktrees(SRC)).some((w) => w.path === dir),
|
||||
false,
|
||||
"git should no longer list the corrupt worktree"
|
||||
);
|
||||
assert.equal(g(SRC, "branch", "--list", "feat/corrupt").trim(), "");
|
||||
// The directory and its files were never touched.
|
||||
assert.equal(fs.existsSync(dir), true);
|
||||
assert.equal(fs.readFileSync(path.join(dir, "keep.txt"), "utf8"), "still here\n");
|
||||
});
|
||||
|
||||
it("removes the worktree and its branch, leaving git's list clean", async () => {
|
||||
const dir = path.join(process.env.LANES_ROOT, "src-repo__alpha");
|
||||
await wt.removeWorktree(laneFor(dir, "feat/alpha"));
|
||||
assert.equal(fs.existsSync(dir), false);
|
||||
const list = await wt.listWorktrees(SRC);
|
||||
assert.equal(
|
||||
list.some((w) => w.path === dir),
|
||||
false
|
||||
);
|
||||
const branches = g(SRC, "branch", "--list", "feat/alpha").trim();
|
||||
assert.equal(branches, "");
|
||||
});
|
||||
|
||||
it("never deletes the base branch even if a lane claims it", async () => {
|
||||
const dir = path.join(process.env.LANES_ROOT, "src-repo__beta");
|
||||
await wt.addWorktree({ sourceRepo: SRC, dir, branch: "feat/beta", base: "main" });
|
||||
await wt.removeWorktree(laneFor(dir, "main")); // lane lies about its branch
|
||||
assert.match(g(SRC, "branch", "--list", "main"), /main/);
|
||||
});
|
||||
|
||||
it("scrubs git hook environment variables from child processes", async () => {
|
||||
// Regression test: GIT_DIR, GIT_INDEX_FILE, etc. from git hooks must not
|
||||
// corrupt git operations on worktrees (where .git is a file, not a directory).
|
||||
// This test verifies that addWorktree and statusCounts work even when these
|
||||
// variables are set to bogus values.
|
||||
const oldGitDir = process.env.GIT_DIR;
|
||||
const oldGitIndexFile = process.env.GIT_INDEX_FILE;
|
||||
try {
|
||||
process.env.GIT_DIR = "/nonexistent/.git";
|
||||
process.env.GIT_INDEX_FILE = "/nonexistent/.git/index";
|
||||
|
||||
const dir = path.join(process.env.LANES_ROOT, "src-repo__scrub-test");
|
||||
await wt.addWorktree({ sourceRepo: SRC, dir, branch: "feat/scrub-test", base: "main" });
|
||||
const s = await wt.statusCounts(dir);
|
||||
assert.ok(s.head); // should have a commit hash
|
||||
assert.equal(s.dirty, 0); // should be clean
|
||||
|
||||
// Clean up
|
||||
await wt.removeWorktree(laneFor(dir, "feat/scrub-test"));
|
||||
} finally {
|
||||
// Restore original values
|
||||
if (oldGitDir !== undefined) {
|
||||
process.env.GIT_DIR = oldGitDir;
|
||||
} else {
|
||||
delete process.env.GIT_DIR;
|
||||
}
|
||||
if (oldGitIndexFile !== undefined) {
|
||||
process.env.GIT_INDEX_FILE = oldGitIndexFile;
|
||||
} else {
|
||||
delete process.env.GIT_INDEX_FILE;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("gitFacts", () => {
|
||||
it("reports branch, short head, subject and working-tree counts", async () => {
|
||||
const dir = path.join(ROOT, "facts-repo");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
g(dir, "init", "-b", "feat/facts");
|
||||
g(dir, "config", "user.email", "t@example.com");
|
||||
g(dir, "config", "user.name", "Test");
|
||||
fs.writeFileSync(path.join(dir, "tracked.txt"), "one\n");
|
||||
g(dir, "add", "-A");
|
||||
g(dir, "commit", "-m", "seed the facts fixture");
|
||||
|
||||
// one modified tracked file, one file git has never seen
|
||||
fs.writeFileSync(path.join(dir, "tracked.txt"), "two\n");
|
||||
fs.writeFileSync(path.join(dir, "brand-new.txt"), "x\n");
|
||||
|
||||
const facts = await wt.gitFacts(dir);
|
||||
assert.equal(facts.branch, "feat/facts");
|
||||
assert.equal(facts.head, g(dir, "rev-parse", "--short", "HEAD").trim());
|
||||
assert.equal(facts.subject, "seed the facts fixture");
|
||||
assert.equal(facts.dirty, 1);
|
||||
assert.equal(facts.untracked, 1);
|
||||
});
|
||||
|
||||
it("reports the literal HEAD git gives for a detached checkout", async () => {
|
||||
const dir = path.join(ROOT, "facts-detached");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
g(dir, "init", "-b", "main");
|
||||
g(dir, "config", "user.email", "t@example.com");
|
||||
g(dir, "config", "user.name", "Test");
|
||||
fs.writeFileSync(path.join(dir, "a.txt"), "a\n");
|
||||
g(dir, "add", "-A");
|
||||
g(dir, "commit", "-m", "only commit");
|
||||
g(dir, "checkout", "--detach", "HEAD");
|
||||
|
||||
const facts = await wt.gitFacts(dir);
|
||||
assert.equal(facts.branch, "HEAD");
|
||||
assert.equal(facts.dirty, 0);
|
||||
assert.equal(facts.untracked, 0);
|
||||
});
|
||||
|
||||
it("rejects rather than reporting facts for a directory that is not a repo", async () => {
|
||||
const dir = path.join(ROOT, "facts-plain");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
await assert.rejects(() => wt.gitFacts(dir));
|
||||
});
|
||||
});
|
||||
|
||||
describe("listBranches", () => {
|
||||
it("lists local branches and names the current one", async () => {
|
||||
const dir = path.join(ROOT, "branches-repo");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
g(dir, "init", "-b", "main");
|
||||
g(dir, "config", "user.email", "t@example.com");
|
||||
g(dir, "config", "user.name", "Test");
|
||||
fs.writeFileSync(path.join(dir, "a.txt"), "a\n");
|
||||
g(dir, "add", "-A");
|
||||
g(dir, "commit", "-m", "init");
|
||||
g(dir, "branch", "feat/one");
|
||||
g(dir, "branch", "feat/two");
|
||||
|
||||
const { branches, current } = await wt.listBranches(dir);
|
||||
assert.deepEqual([...branches].sort(), ["feat/one", "feat/two", "main"]);
|
||||
assert.equal(current, "main");
|
||||
});
|
||||
|
||||
it("reports no current branch for a detached HEAD, but still lists branches", async () => {
|
||||
const dir = path.join(ROOT, "branches-detached");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
g(dir, "init", "-b", "main");
|
||||
g(dir, "config", "user.email", "t@example.com");
|
||||
g(dir, "config", "user.name", "Test");
|
||||
fs.writeFileSync(path.join(dir, "a.txt"), "a\n");
|
||||
g(dir, "add", "-A");
|
||||
g(dir, "commit", "-m", "init");
|
||||
g(dir, "checkout", "--detach", "HEAD");
|
||||
|
||||
const { branches, current } = await wt.listBranches(dir);
|
||||
assert.deepEqual(branches, ["main"]);
|
||||
assert.equal(current, null);
|
||||
});
|
||||
|
||||
it("returns an empty branch list for a repo with no commits yet", async () => {
|
||||
const dir = path.join(ROOT, "branches-empty");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
g(dir, "init", "-b", "main");
|
||||
|
||||
const { branches } = await wt.listBranches(dir);
|
||||
assert.deepEqual(branches, []);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user