Files
AgentHub/server/__tests__/projects.test.js
T
nntrivi2001 ba2c4e1914 feat(ui): sidebar project tree, status bar and a tabbed workbench for workspaces
The Workspace page's lane strip moves into the sidebar as a project tree:
workspaces grouped by git repository (new GET /api/projects, cached in
memory, POST /api/projects/refresh), untracked worktrees with Adopt, inline
rename, drag/Alt+arrow ordering and a right-click row menu. The selected
workspace is the URL's ?lane=, one shared lane list feeds the tree, the new
bottom status bar and the page. The Changes panel gains an activity bar
(Files / Changes / Sessions, with resume), and split panes get a header.
2026-10-01 16:33:25 +07:00

113 lines
4.3 KiB
JavaScript

/**
* @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ĩ <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 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);
});
});