Files
AgentHub/server/__tests__/claude-home-datadir.test.js
nntrivi2001 c7dbaf2d9f refactor: finish the AgentHub rename and drop the MCP server
Two leftovers from the rebrand, done together because they touch the same
docs and manifests.

**Data directory.** `agent-dashboard` could not simply be renamed in the
source: it names a real directory holding the live SQLite database. Startup
now moves it once, via `migrateLegacyDataDir()` in `server/lib/claude-home.js`
— a same-filesystem rename, so it is instant even at multiple gigabytes and
cannot leave two divergent copies. `getDataDir()` stays PURE and resolves to
the legacy path until the move happens; the first version of this put the
rename inside the getter, which meant `npm run test:server` relocated the
developer's real database as a side effect. The call sits behind
`require.main === module` and ahead of the router requires, because those pull
in db.js, which opens SQLite at module load. A failed rename is silent and the
old directory keeps being used: an unfashionable directory name beats a blank
database beside an unreachable one. Same treatment for the port-discovery file
(`.agent-dashboard.json` → `.agenthub.json`, old one dropped on write).

Not renamed on purpose: the Sagi localStorage keys (renaming silently resets
everyone's prefs) and the PWA manifest `id` (changing it orphans an installed
app).

**MCP server.** 4 tool calls in 46 days across 577,132 events, against 4,556
lines of TypeScript, a 2 MB committed build artifact, and a pre-commit guard
that had already blocked commits twice. Deleted: `mcp/`, the maintainer skills
and `mcp-reviewer` agent, the build check, `plugins/agenthub/.mcp.json`,
`docs/MCP.md`, 12 npm scripts, 5 Makefile targets, and every doc section
describing it.

Kept, because they are unrelated things sharing the name: the Config
Explorer's MCP tab and its API, per-lane Playwright MCP, the agenthub-config
plugin's MCP audit, and the historical `mcp__plugin_agenthub_*` events that
analytics must keep attributing.

Verified: server 1432/1432, client 459/459, tsc clean, prettier clean. Both
directions of the migration proven — a sandbox-HOME test run leaves the legacy
directory untouched, and a real boot moves it with its contents intact.
2026-09-08 09:48:40 +07:00

155 lines
5.7 KiB
JavaScript

/**
* @file Tests the data-directory resolution in claude-home.js, and the one-time
* migration that carries an existing `~/.claude/agent-dashboard` (the name the
* product used before the AgentHub rebrand) over to `~/.claude/agenthub`.
*
* The stakes are why this is tested at all: that directory holds the live
* SQLite database. Resolving to the new path when the data is still under the
* old one would not lose the data, but it WOULD silently start an empty
* dashboard, which looks exactly like data loss to whoever opens it.
*
* The split matters as much as the migration. `getDataDir()` is a getter that
* gets asked for the path by tests, doctors and `--help`; if it also moved
* directories, every one of those would relocate a multi-gigabyte database as a
* side effect. So it is asserted pure here, and the move lives in its own
* function that only server startup calls.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const os = require("node:os");
const path = require("node:path");
const fs = require("node:fs");
const { describe, it, beforeEach, after } = require("node:test");
const assert = require("node:assert/strict");
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-datadir-"));
after(() => fs.rmSync(SUITE_ROOT, { recursive: true, force: true }));
// claude-home reads CLAUDE_HOME on every call, so each test can point it
// somewhere fresh without reloading the module.
const {
getDataDir,
migrateLegacyDataDir,
getTranscriptSnapshotDir,
} = require("../lib/claude-home");
let home;
let seq = 0;
beforeEach(() => {
seq += 1;
home = path.join(SUITE_ROOT, `home${seq}`, ".claude");
fs.mkdirSync(home, { recursive: true });
process.env.CLAUDE_HOME = home;
delete process.env.DASHBOARD_DATA_DIR;
});
const legacy = () => path.join(home, "agent-dashboard");
const current = () => path.join(home, "agenthub");
/** Put a recognisable database in `dir` so a migration can be proven. */
function seedDataDir(dir, marker) {
fs.mkdirSync(path.join(dir, "transcripts"), { recursive: true });
fs.writeFileSync(path.join(dir, "dashboard.db"), marker);
}
describe("getDataDir", () => {
it("uses the new name on a fresh install", () => {
assert.equal(getDataDir(), current());
});
it("resolves to the legacy directory until it is migrated, and moves nothing", () => {
seedDataDir(legacy(), "the-real-database");
assert.equal(getDataDir(), legacy(), "the data is still there, so that is the answer");
assert.deepEqual(fs.readdirSync(home), ["agent-dashboard"], "resolving must not move anything");
});
it("prefers the new directory once both exist", () => {
seedDataDir(legacy(), "old");
seedDataDir(current(), "new");
assert.equal(getDataDir(), current());
});
it("lets DASHBOARD_DATA_DIR win", () => {
seedDataDir(legacy(), "db");
const pinned = path.join(SUITE_ROOT, `pinned${seq}`);
process.env.DASHBOARD_DATA_DIR = pinned;
assert.equal(getDataDir(), pinned);
});
it("derives the transcript snapshot dir from the migrated location", () => {
seedDataDir(legacy(), "db");
migrateLegacyDataDir();
assert.equal(getTranscriptSnapshotDir(), path.join(current(), "transcripts"));
});
});
describe("migrateLegacyDataDir", () => {
it("moves an existing legacy directory to the new name, keeping its contents", () => {
seedDataDir(legacy(), "the-real-database");
assert.equal(migrateLegacyDataDir(), current());
assert.equal(fs.existsSync(legacy()), false, "the legacy directory should be gone");
assert.equal(
fs.readFileSync(path.join(current(), "dashboard.db"), "utf8"),
"the-real-database"
);
assert.ok(fs.existsSync(path.join(current(), "transcripts")), "nested content must survive");
assert.equal(getDataDir(), current());
});
it("does nothing on a fresh install", () => {
assert.equal(migrateLegacyDataDir(), null);
assert.deepEqual(fs.readdirSync(home), [], "no directory should be created just by migrating");
});
it("migrates once and stays put on later calls", () => {
seedDataDir(legacy(), "db");
migrateLegacyDataDir();
fs.writeFileSync(path.join(current(), "dashboard.db"), "written-after-migration");
assert.equal(migrateLegacyDataDir(), null);
assert.equal(
fs.readFileSync(path.join(current(), "dashboard.db"), "utf8"),
"written-after-migration",
"a second call must not re-migrate over newer data"
);
});
it("leaves both alone when the new directory already exists", () => {
// Nothing sensible to merge here, and picking wrong would hide a database:
// the new one wins and the legacy one is left untouched for the user.
seedDataDir(legacy(), "old");
seedDataDir(current(), "new");
assert.equal(migrateLegacyDataDir(), null);
assert.equal(fs.readFileSync(path.join(current(), "dashboard.db"), "utf8"), "new");
assert.equal(fs.readFileSync(path.join(legacy(), "dashboard.db"), "utf8"), "old");
});
it("keeps using the legacy directory when it cannot be moved", () => {
// A read-only Claude home makes the rename fail. Starting a blank database
// beside 2 GB of unreachable history is the one outcome worth avoiding.
seedDataDir(legacy(), "db");
fs.chmodSync(home, 0o500);
try {
assert.equal(migrateLegacyDataDir(), null, "a failed move must not be reported as done");
assert.equal(getDataDir(), legacy());
} finally {
fs.chmodSync(home, 0o700);
}
});
it("never migrates behind an explicit DASHBOARD_DATA_DIR", () => {
seedDataDir(legacy(), "db");
process.env.DASHBOARD_DATA_DIR = path.join(SUITE_ROOT, `pinned${seq}`);
assert.equal(migrateLegacyDataDir(), null);
assert.ok(fs.existsSync(legacy()), "an explicit data dir must not trigger a migration");
});
});