/** * @file Tests setClaudeHome: the shared home-dir guard (a directory that is or * contains the user's home is refused), persistence to the settings file * (never the repo `.env`, which a plugin update wipes), and pinning the data * dir, so a new CLAUDE_HOME can never open an empty database at the next start. * The settings file is a temp path (AGENTHUB_SETTINGS_FILE). * @author Nguyễn Ngọc Trí Vĩ */ const os = require("node:os"); const fs = require("node:fs"); const path = require("node:path"); const { describe, it, before, beforeEach, after } = require("node:test"); const assert = require("node:assert/strict"); const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-claude-home-set-")); const SETTINGS = path.join(ROOT, "settings.env"); const REPO_ENV = path.resolve(__dirname, "..", "..", ".env"); const SAVED = { AGENTHUB_SETTINGS_FILE: process.env.AGENTHUB_SETTINGS_FILE, CLAUDE_HOME: process.env.CLAUDE_HOME, DASHBOARD_DATA_DIR: process.env.DASHBOARD_DATA_DIR, }; process.env.AGENTHUB_SETTINGS_FILE = SETTINGS; const { setClaudeHome, getClaudeHome, getDataDir } = require("../lib/claude-home"); describe("setClaudeHome", () => { let repoEnvBefore; before(() => { repoEnvBefore = fs.existsSync(REPO_ENV) ? fs.readFileSync(REPO_ENV, "utf8") : null; }); beforeEach(() => { fs.rmSync(SETTINGS, { force: true }); process.env.CLAUDE_HOME = fs.mkdtempSync(path.join(ROOT, "start-")); delete process.env.DASHBOARD_DATA_DIR; }); after(() => { for (const [k, v] of Object.entries(SAVED)) { if (v === undefined) delete process.env[k]; else process.env[k] = v; } fs.rmSync(ROOT, { recursive: true, force: true }); }); for (const bad of ["/", os.homedir(), path.dirname(os.homedir()), "~/"]) { it(`refuses ${JSON.stringify(bad)} as too broad`, () => { const current = getClaudeHome(); assert.throws(() => setClaudeHome(bad), { code: "EBADHOME" }); assert.equal(getClaudeHome(), current); assert.ok(!fs.existsSync(SETTINGS)); }); } it("accepts a real directory, persisting it to the settings file only", () => { const next = fs.mkdtempSync(path.join(ROOT, "next-")); assert.equal(setClaudeHome(next), next); assert.equal(getClaudeHome(), next); assert.match(fs.readFileSync(SETTINGS, "utf8"), new RegExp(`^CLAUDE_HOME=${next}$`, "m")); const repoEnvNow = fs.existsSync(REPO_ENV) ? fs.readFileSync(REPO_ENV, "utf8") : null; assert.equal(repoEnvNow, repoEnvBefore, "the repo .env is never written"); }); it("pins the data dir in use before CLAUDE_HOME moves, so the database stays put", () => { const dataDirBefore = getDataDir(); setClaudeHome(fs.mkdtempSync(path.join(ROOT, "next-"))); assert.equal(getDataDir(), dataDirBefore); assert.equal(process.env.DASHBOARD_DATA_DIR, dataDirBefore); assert.match( fs.readFileSync(SETTINGS, "utf8"), new RegExp(`^DASHBOARD_DATA_DIR=${dataDirBefore}$`, "m") ); }); it("leaves an explicit DASHBOARD_DATA_DIR alone", () => { process.env.DASHBOARD_DATA_DIR = path.join(ROOT, "explicit-data"); setClaudeHome(fs.mkdtempSync(path.join(ROOT, "next-"))); assert.equal(process.env.DASHBOARD_DATA_DIR, path.join(ROOT, "explicit-data")); assert.doesNotMatch(fs.readFileSync(SETTINGS, "utf8"), /DASHBOARD_DATA_DIR/); }); });