/** * @file Tests GET/PUT /api/settings/codex-home and setCodexHome: the shared * home-dir guard, persistence to the settings file, the `env_owned` flag, and * a home change from another site refused. Everything lives in a temp dir. * @author Nguyễn Ngọc Trí Vĩ */ const os = require("node:os"); const fs = require("node:fs"); const path = require("node:path"); const http = require("node:http"); const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-codex-home-")); process.env.CLAUDE_HOME = path.join(ROOT, ".claude"); process.env.CODEX_HOME = path.join(ROOT, "codex-a"); process.env.DASHBOARD_DB_PATH = path.join(ROOT, "dashboard.db"); process.env.AGENTHUB_SETTINGS_FILE = path.join(ROOT, "settings.env"); fs.mkdirSync(process.env.CODEX_HOME, { recursive: true }); const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); const express = require("express"); const { envOwned } = require("../lib/settings-env"); const settingsRouter = require("../routes/settings"); let server; let base; function request(method, url, body, extra = {}) { return new Promise((resolve, reject) => { const data = body === undefined ? null : Buffer.from(JSON.stringify(body)); const req = http.request( `${base}${url}`, { method, headers: { ...(data ? { "content-type": "application/json", "content-length": data.length } : {}), ...extra, }, }, (res) => { let text = ""; res.on("data", (c) => (text += c)); res.on("end", () => resolve({ status: res.statusCode, body: JSON.parse(text) })); } ); req.on("error", reject); if (data) req.write(data); req.end(); }); } before(async () => { const app = express(); app.use(express.json()); app.use("/api/settings", settingsRouter); server = http.createServer(app); await new Promise((r) => server.listen(0, "127.0.0.1", r)); base = `http://127.0.0.1:${server.address().port}`; }); after(() => { server.close(); fs.rmSync(ROOT, { recursive: true, force: true }); }); describe("/api/settings/codex-home", () => { it("GET answers the current home and whether the environment owns it", async () => { const res = await request("GET", "/api/settings/codex-home"); assert.equal(res.status, 200); assert.equal(res.body.codex_home, process.env.CODEX_HOME); assert.equal(res.body.env_owned, envOwned.has("CODEX_HOME")); }); it("refuses a path that contains the user's home, changing nothing", async () => { const before = process.env.CODEX_HOME; const res = await request("PUT", "/api/settings/codex-home", { path: "/" }); assert.equal(res.status, 400); assert.equal(res.body.error.code, "EBADHOME"); assert.equal(process.env.CODEX_HOME, before); assert.ok(!fs.existsSync(process.env.AGENTHUB_SETTINGS_FILE)); }); it("refuses a missing path and a non-string body", async () => { for (const body of [{ path: path.join(ROOT, "nope") }, { path: 7 }, {}]) { const res = await request("PUT", "/api/settings/codex-home", body); assert.equal(res.status, 400); assert.equal(res.body.error.code, "INVALID_PATH"); } }); it("switches and persists", async () => { const next = path.join(ROOT, "codex-b"); fs.mkdirSync(next); const res = await request("PUT", "/api/settings/codex-home", { path: next }); assert.equal(res.status, 200); assert.equal(res.body.codex_home, next); assert.equal(process.env.CODEX_HOME, next); assert.match( fs.readFileSync(process.env.AGENTHUB_SETTINGS_FILE, "utf8"), new RegExp(`^CODEX_HOME=${next}$`, "m") ); }); it("refuses a home change asked by another site", async () => { const next = path.join(ROOT, "codex-evil"); fs.mkdirSync(next); const res = await request( "PUT", "/api/settings/codex-home", { path: next }, { Origin: "https://evil.example" } ); assert.equal(res.status, 403); assert.notEqual(process.env.CODEX_HOME, next); }); });