/** * @file codex-config.test.js * @description Tests for /api/codex-config — the Codex CLI configuration * explorer. Builds a fake CODEX_HOME and project .codex/ in tmpdir, points * the server at it, and exercises every read/write surface plus * path-containment guards, backups, and the CLI-backed MCP/plugin/ * marketplace routes (via a stubbed `codex` binary on PATH). * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const os = require("node:os"); const http = require("node:http"); const { WebSocket } = require("ws"); const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "codex-config-test-")); const FAKE_HOME = path.join(TMP, "home", ".codex"); const FAKE_PROJECT = path.join(TMP, "project"); const FAKE_PROJECT_CODEX = path.join(FAKE_PROJECT, ".codex"); fs.mkdirSync(path.join(FAKE_HOME, "skills", "demo-skill"), { recursive: true }); fs.mkdirSync(path.join(FAKE_HOME, "agents"), { recursive: true }); fs.mkdirSync(path.join(FAKE_HOME, "rules"), { recursive: true }); fs.mkdirSync(path.join(FAKE_PROJECT_CODEX, "skills", "proj-skill"), { recursive: true }); fs.writeFileSync( path.join(FAKE_HOME, "skills", "demo-skill", "SKILL.md"), `---\nname: demo-skill\ndescription: A demo skill for tests\n---\n\nBody text here.` ); fs.writeFileSync( path.join(FAKE_PROJECT_CODEX, "skills", "proj-skill", "SKILL.md"), `---\nname: proj-skill\n---\n\nProject skill.` ); fs.writeFileSync( path.join(FAKE_HOME, "agents", "demo-agent.toml"), `name = "demo-agent"\nmodel = "gpt-5"\n` ); fs.writeFileSync(path.join(FAKE_HOME, "rules", "default.rules"), "allow"); fs.writeFileSync(path.join(FAKE_HOME, "config.toml"), 'model = "gpt-5"\n'); fs.writeFileSync(path.join(FAKE_PROJECT, "AGENTS.md"), "# Project memory\nHello."); process.env.CODEX_HOME = FAKE_HOME; const TEST_DB = path.join(TMP, "dashboard-test.db"); process.env.DASHBOARD_DB_PATH = TEST_DB; process.env.DASHBOARD_REMOTE_SYNC_MS = "0"; process.env.DASHBOARD_LIVENESS_PROBE = "0"; const { createApp, startServer } = require("../index"); let server; let BASE; function fetchJson(p, opts = {}) { return new Promise((resolve, reject) => { const url = new URL(p, BASE); const headers = { ...(opts.headers || {}) }; let bodyBuf; if (opts.body !== undefined) { bodyBuf = Buffer.from(JSON.stringify(opts.body)); headers["Content-Type"] = "application/json"; headers["Content-Length"] = bodyBuf.length; } const req = http.request( { hostname: url.hostname, port: url.port, path: url.pathname + url.search, method: opts.method || "GET", headers, }, (res) => { const chunks = []; res.on("data", (c) => chunks.push(c)); res.on("end", () => { const body = Buffer.concat(chunks).toString("utf8"); let json; try { json = JSON.parse(body); } catch { json = body; } resolve({ status: res.statusCode, body: json }); }); } ); req.on("error", reject); if (bodyBuf) req.write(bodyBuf); req.end(); }); } function stubCodex(scriptBody) { const bin = path.join(TMP, `bin-${process.hrtime.bigint()}`); fs.mkdirSync(bin, { recursive: true }); const file = path.join(bin, "codex"); fs.writeFileSync(file, `#!/usr/bin/env bash\n${scriptBody}\n`); fs.chmodSync(file, 0o755); const originalPath = process.env.PATH; process.env.PATH = `${bin}${path.delimiter}${originalPath}`; return () => { process.env.PATH = originalPath; }; } const ECHO_ARGV = 'for a in "$@"; do echo "$a"; done'; describe("/api/codex-config", () => { before(async () => { const app = createApp(); server = await startServer(app, 0); BASE = `http://127.0.0.1:${server.address().port}`; }); after(async () => { await new Promise((r) => server.close(r)); try { fs.rmSync(TMP, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); } catch { /* best-effort temp cleanup */ } }); it("overview reports counts and roots", async () => { const { status, body } = await fetchJson( `/api/codex-config/overview?cwd=${encodeURIComponent(FAKE_PROJECT)}` ); assert.equal(status, 200); assert.equal(body.roots.codexHome, FAKE_HOME); assert.equal(body.roots.projectCodexDir, FAKE_PROJECT_CODEX); assert.equal(body.roots.projectRoot, FAKE_PROJECT); assert.equal(body.counts.skills.user, 1); assert.equal(body.counts.skills.project, 1); assert.equal(body.counts.agents.user, 1); assert.equal(body.counts.rules.user, 1); }); it("lists skills for both scopes", async () => { const { status, body } = await fetchJson( `/api/codex-config/skills?cwd=${encodeURIComponent(FAKE_PROJECT)}` ); assert.equal(status, 200); assert.ok(Array.isArray(body.skills)); const demo = body.skills.find((s) => s.name === "demo-skill"); assert.equal(demo.scope, "user"); assert.equal(demo.frontmatter.name, "demo-skill"); const proj = body.skills.find((s) => s.name === "proj-skill"); assert.equal(proj.scope, "project"); }); it("scope=user filters out project items", async () => { const { body } = await fetchJson( `/api/codex-config/skills?scope=user&cwd=${encodeURIComponent(FAKE_PROJECT)}` ); assert.ok(body.skills.every((s) => s.scope === "user")); }); it("agents parses top-level TOML scalars", async () => { const { body } = await fetchJson( `/api/codex-config/agents?cwd=${encodeURIComponent(FAKE_PROJECT)}` ); const a = body.agents.find((x) => x.name === "demo-agent"); assert.equal(a.meta.model, "gpt-5"); }); it("rules returns preview text", async () => { const { body } = await fetchJson( `/api/codex-config/rules?cwd=${encodeURIComponent(FAKE_PROJECT)}` ); const r = body.rules.find((x) => x.name === "default"); assert.match(r.preview, /allow/); }); it("memory returns project AGENTS.md", async () => { const { body } = await fetchJson( `/api/codex-config/memory?cwd=${encodeURIComponent(FAKE_PROJECT)}` ); const proj = body.memory.find((x) => x.scope === "project"); assert.equal(proj.exists, true); assert.match(proj.preview, /Project memory/); }); it("config returns user config.toml content", async () => { const { body } = await fetchJson( `/api/codex-config/config?cwd=${encodeURIComponent(FAKE_PROJECT)}` ); const user = body.config.find((x) => x.scope === "user"); assert.equal(user.exists, true); assert.match(user.content, /gpt-5/); }); it("file endpoint reads inside CODEX_HOME", async () => { const target = path.join(FAKE_HOME, "rules", "default.rules"); const { status, body } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(FAKE_PROJECT)}&path=${encodeURIComponent(target)}` ); assert.equal(status, 200); assert.equal(body.ok, true); assert.equal(body.text, "allow"); }); it("file endpoint blocks paths outside allowed roots", async () => { const outside = path.join(TMP, "evil.rules"); fs.writeFileSync(outside, "secret"); const { status, body } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(FAKE_PROJECT)}&path=${encodeURIComponent(outside)}` ); assert.equal(status, 400); assert.equal(body.error.code, "READ_DENIED"); }); it("file endpoint blocks a ?cwd=-driven escape (path outside the roots ?cwd= computes)", async () => { const otherCwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-config-othercwd-")); const { status, body } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(otherCwd)}&path=${encodeURIComponent( path.join(FAKE_HOME, "..", "..", "etc", "passwd") )}` ); assert.equal(status, 400); assert.equal(body.error.code, "READ_DENIED"); fs.rmSync(otherCwd, { recursive: true, force: true }); }); it("file endpoint follows a leaf symlink whose LOCATION is inside CODEX_HOME (dotfiles setup)", async () => { // Ruling: containment applies to the link's location, not where it // points — the same thing the other four discovery readers already do. const outside = fs.mkdtempSync(path.join(os.tmpdir(), "codex-config-outside-")); fs.writeFileSync(path.join(outside, "secret.rules"), "MY DOTFILES RULE"); const linkPath = path.join(FAKE_HOME, "rules", "leaf-link.rules"); fs.symlinkSync(path.join(outside, "secret.rules"), linkPath, "file"); try { const { status, body } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(FAKE_PROJECT)}&path=${encodeURIComponent(linkPath)}` ); assert.equal(status, 200); assert.equal(body.ok, true); assert.equal(body.text, "MY DOTFILES RULE"); } finally { fs.rmSync(outside, { recursive: true, force: true }); } }); it("file endpoint follows an ancestor symlink whose LOCATION is inside CODEX_HOME", async () => { const outside = fs.mkdtempSync(path.join(os.tmpdir(), "codex-config-outside-")); fs.writeFileSync(path.join(outside, "linked.txt"), "MY DOTFILES ANCESTOR"); const linkDir = path.join(FAKE_HOME, "linked-dir"); fs.symlinkSync(outside, linkDir, "dir"); try { const target = path.join(linkDir, "linked.txt"); const { status, body } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(FAKE_PROJECT)}&path=${encodeURIComponent(target)}` ); assert.equal(status, 200); assert.equal(body.ok, true); assert.equal(body.text, "MY DOTFILES ANCESTOR"); } finally { fs.rmSync(linkDir, { force: true }); fs.rmSync(outside, { recursive: true, force: true }); } }); it("file endpoint 404s on a missing file inside an existing root", async () => { const target = path.join(FAKE_HOME, "rules", "does-not-exist.rules"); const { status, body } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(FAKE_PROJECT)}&path=${encodeURIComponent(target)}` ); assert.equal(status, 404); assert.equal(body.error.code, "NOT_FOUND"); }); it("file endpoint 404s on a ?cwd= that doesn't exist yet, and creates nothing", async () => { const missingCwd = path.join(TMP, `never-existed-${process.hrtime.bigint()}`); const target = path.join(missingCwd, "AGENTS.md"); const { status, body } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(missingCwd)}&path=${encodeURIComponent(target)}` ); assert.equal(status, 404); assert.equal(body.error.code, "NOT_FOUND"); assert.equal(fs.existsSync(missingCwd), false, "GET /file must not create the missing root"); }); it("file endpoint requires a path", async () => { const { status, body } = await fetchJson("/api/codex-config/file"); assert.equal(status, 400); assert.equal(body.error.code, "BAD_PATH"); }); it("PUT /file creates a new rule (no backup, file did not exist)", async () => { const { status, body } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(FAKE_PROJECT)}`, { method: "PUT", body: { scope: "user", type: "rules", name: "fresh", content: "deny" }, } ); assert.equal(status, 200); assert.equal(body.created, true); assert.equal(body.backup, null); assert.equal(fs.readFileSync(body.file, "utf8"), "deny"); }); it("PUT /file overwrites an existing rule and creates a backup, and broadcasts codex_config_changed", async () => { const wsUrl = `ws://127.0.0.1:${server.address().port}/ws`; const ws = new WebSocket(wsUrl); const messages = []; await new Promise((resolve, reject) => { ws.on("open", resolve); ws.on("error", reject); }); ws.on("message", (msg) => { try { messages.push(JSON.parse(msg)); } catch { /* parse error */ } }); const { status, body } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(FAKE_PROJECT)}`, { method: "PUT", body: { scope: "user", type: "rules", name: "default", content: "deny-all" }, } ); assert.equal(status, 200); assert.equal(body.created, false); assert.ok(body.backup, "backup metadata returned"); assert.match(fs.readFileSync(body.file, "utf8"), /deny-all/); await new Promise((resolve) => setTimeout(resolve, 100)); ws.close(); assert.ok( messages.some((m) => m.type === "codex_config_changed"), "PUT /file should broadcast codex_config_changed" ); }); it("PUT /file ignores ?cwd= for project scope — writes to the server's own cwd, not a caller-chosen dir", async () => { const victim = fs.mkdtempSync(path.join(os.tmpdir(), "codex-config-victim-")); const originalCwd = process.cwd(); const serverCwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-config-servercwd-")); process.chdir(serverCwd); try { const { status, body } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(victim)}`, { method: "PUT", body: { scope: "project", type: "memory", content: "PWNED" }, } ); assert.equal(status, 200); assert.equal(fs.existsSync(path.join(victim, "AGENTS.md")), false, "must not write to ?cwd="); assert.equal(body.file, path.join(serverCwd, "AGENTS.md")); assert.equal(fs.readFileSync(body.file, "utf8"), "PWNED"); } finally { process.chdir(originalCwd); fs.rmSync(victim, { recursive: true, force: true }); fs.rmSync(serverCwd, { recursive: true, force: true }); } }); it("DELETE /file ignores ?cwd= for project scope", async () => { const victim = fs.mkdtempSync(path.join(os.tmpdir(), "codex-config-victim-")); fs.writeFileSync(path.join(victim, ".codex-marker"), "x"); // proves this dir wasn't touched const originalCwd = process.cwd(); const serverCwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-config-servercwd-")); fs.mkdirSync(path.join(serverCwd, ".codex", "rules"), { recursive: true }); fs.writeFileSync(path.join(serverCwd, ".codex", "rules", "victim.rules"), "real"); process.chdir(serverCwd); try { const { status } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(victim)}`, { method: "DELETE", body: { scope: "project", type: "rules", name: "victim" }, } ); assert.equal(status, 200); assert.equal(fs.existsSync(path.join(victim, ".codex-marker")), true, "?cwd= dir left alone"); assert.equal(fs.existsSync(path.join(serverCwd, ".codex", "rules", "victim.rules")), false); } finally { process.chdir(originalCwd); fs.rmSync(victim, { recursive: true, force: true }); fs.rmSync(serverCwd, { recursive: true, force: true }); } }); it("PUT /file rejects a write whose name escapes the root with 400, not 500", async () => { const { status, body } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(FAKE_PROJECT)}`, { method: "PUT", body: { scope: "user", type: "agents", name: "../evil", content: "x" }, } ); assert.equal(status, 400); assert.equal(body.error.code, "EBADNAME"); }); it("PUT /file rejects unknown type", async () => { const { status, body } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(FAKE_PROJECT)}`, { method: "PUT", body: { scope: "user", type: "plugins", name: "x", content: "y" }, } ); assert.equal(status, 400); assert.equal(body.error.code, "EBADTYPE"); }); it("PUT /file rejects a name on a fixed-filename type (memory)", async () => { const { status, body } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(FAKE_PROJECT)}`, { method: "PUT", body: { scope: "project", type: "memory", name: "nope", content: "x" }, } ); assert.equal(status, 400); assert.equal(body.error.code, "EBADNAME"); }); it("PUT /file rejects oversize content", async () => { const huge = "x".repeat(256 * 1024 + 1); const { status, body } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(FAKE_PROJECT)}`, { method: "PUT", body: { scope: "user", type: "rules", name: "huge", content: huge }, } ); assert.equal(status, 413); assert.equal(body.error.code, "ETOOLARGE"); }); it("DELETE /file backs up and removes a rule", async () => { fs.writeFileSync(path.join(FAKE_HOME, "rules", "to-delete.rules"), "bye"); const { status, body } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(FAKE_PROJECT)}`, { method: "DELETE", body: { scope: "user", type: "rules", name: "to-delete" }, } ); assert.equal(status, 200); assert.ok(body.backup); assert.equal(fs.existsSync(path.join(FAKE_HOME, "rules", "to-delete.rules")), false); }); it("DELETE /file 404s on a missing item", async () => { const { status, body } = await fetchJson( `/api/codex-config/file?cwd=${encodeURIComponent(FAKE_PROJECT)}`, { method: "DELETE", body: { scope: "user", type: "rules", name: "never-existed" }, } ); assert.equal(status, 404); assert.equal(body.error.code, "ENOTFOUND"); }); it("backups endpoint lists everything we just created", async () => { const { status, body } = await fetchJson( `/api/codex-config/backups?cwd=${encodeURIComponent(FAKE_PROJECT)}` ); assert.equal(status, 200); assert.ok(Array.isArray(body.items)); assert.ok(body.items.length >= 2, `expected >=2 backups, got ${body.items.length}`); }); // ── CLI-backed routes (fake `codex` on PATH) ──────────────────────── it("GET /mcp returns the CLI's JSON output", async () => { const restore = stubCodex(`cat <<'JSON'\n{"servers":[{"name":"docs"}]}\nJSON`); try { const { status, body } = await fetchJson("/api/codex-config/mcp"); assert.equal(status, 200); assert.deepEqual(body.mcp, { servers: [{ name: "docs" }] }); } finally { restore(); } }); it("GET /plugins unwraps the CLI's {installed, available} object into a bare array", async () => { // Real codex-cli 0.149.0 shape: `codex plugin list --json` returns an // object, not an array like `mcp list` — without the unwrap the Plugins // tab silently showed nothing (Array.isArray(res.plugins) was false). const restore = stubCodex( `cat <<'JSON'\n{"installed":[{"name":"demo"}],"available":[{"name":"other"}]}\nJSON` ); try { const { status, body } = await fetchJson("/api/codex-config/plugins"); assert.equal(status, 200); assert.deepEqual(body.plugins, [{ name: "demo" }]); } finally { restore(); } }); it("GET /marketplaces unwraps the CLI's {marketplaces} object into a bare array", async () => { const restore = stubCodex(`cat <<'JSON'\n{"marketplaces":[{"name":"sg"}]}\nJSON`); try { const { status, body } = await fetchJson("/api/codex-config/marketplaces"); assert.equal(status, 200); assert.deepEqual(body.marketplaces, [{ name: "sg" }]); } finally { restore(); } }); it("GET /mcp|/plugins|/marketplaces treat empty stdout as an empty list (fresh-install case), not a 502", async () => { const restore = stubCodex(""); // exits 0, prints nothing — the normal fresh-install state try { for (const [route, key] of [ ["/api/codex-config/mcp", "mcp"], ["/api/codex-config/plugins", "plugins"], ["/api/codex-config/marketplaces", "marketplaces"], ]) { const { status, body } = await fetchJson(route); assert.equal(status, 200, route); assert.deepEqual(body[key], [], route); } } finally { restore(); } }); it("GET /mcp reports unparseable CLI JSON as 502, not a type-confused 200", async () => { const restore = stubCodex('echo "not json"'); try { const { status, body } = await fetchJson("/api/codex-config/mcp"); assert.equal(status, 502); assert.equal(body.error.code, "ECLIBADOUTPUT"); } finally { restore(); } }); it("POST /mcp/add rejects a non-array args with 400, not a 500", async () => { const restore = stubCodex(ECHO_ARGV); try { const { status, body } = await fetchJson("/api/codex-config/mcp/add", { method: "POST", body: { name: "docs", command: "node", args: "oops" }, }); assert.equal(status, 400); assert.equal(body.error.code, "EBADARG"); } finally { restore(); } }); it("POST /mcp/add runs the CLI with the given argv", async () => { const restore = stubCodex(ECHO_ARGV); try { const { status, body } = await fetchJson("/api/codex-config/mcp/add", { method: "POST", body: { name: "docs", url: "https://example.com/mcp" }, }); assert.equal(status, 200); assert.equal(body.ok, true); assert.deepEqual(body.stdout.trim().split("\n"), [ "mcp", "add", "docs", "--url", "https://example.com/mcp", ]); } finally { restore(); } }); it("POST /mcp/remove runs the CLI", async () => { const restore = stubCodex(ECHO_ARGV); try { const { status, body } = await fetchJson("/api/codex-config/mcp/remove", { method: "POST", body: { name: "docs" }, }); assert.equal(status, 200); assert.deepEqual(body.stdout.trim().split("\n"), ["mcp", "remove", "docs"]); } finally { restore(); } }); it("POST /plugins/action validates the action name", async () => { const { status, body } = await fetchJson("/api/codex-config/plugins/action", { method: "POST", body: { spec: "demo", action: "purge" }, }); assert.equal(status, 400); assert.equal(body.error.code, "EBADACTION"); }); it("POST /plugins/action adds through the CLI", async () => { const restore = stubCodex(ECHO_ARGV); try { const { status, body } = await fetchJson("/api/codex-config/plugins/action", { method: "POST", body: { spec: "demo@market", action: "add" }, }); assert.equal(status, 200); assert.deepEqual(body.stdout.trim().split("\n"), ["plugin", "add", "demo@market"]); } finally { restore(); } }); it("POST /marketplaces/add runs the CLI with the pasted url", async () => { const restore = stubCodex(ECHO_ARGV); try { const { status, body } = await fetchJson("/api/codex-config/marketplaces/add", { method: "POST", body: { url: "https://example.com/mp.git" }, }); assert.equal(status, 200); assert.deepEqual(body.stdout.trim().split("\n"), [ "plugin", "marketplace", "add", "https://example.com/mp.git", ]); } finally { restore(); } }); it("POST /marketplaces/add accepts an owner/repo source (not just http(s) urls)", async () => { const restore = stubCodex(ECHO_ARGV); try { const { status, body } = await fetchJson("/api/codex-config/marketplaces/add", { method: "POST", body: { url: "openai/codex" }, }); assert.equal(status, 200); assert.deepEqual(body.stdout.trim().split("\n"), [ "plugin", "marketplace", "add", "openai/codex", ]); } finally { restore(); } }); it("POST /marketplaces/add rejects a source that is none of path/owner-repo/http(s) url", async () => { const { status, body } = await fetchJson("/api/codex-config/marketplaces/add", { method: "POST", body: { url: "not a url" }, }); assert.equal(status, 400); assert.equal(body.error.code, "EBADARG"); }); it("reports a failed CLI call as 422 and keeps the stderr", async () => { const restore = stubCodex('echo "✘ marketplace \\"nope\\" is not registered" >&2\nexit 1'); try { const { status, body } = await fetchJson("/api/codex-config/marketplaces/action", { method: "POST", body: { name: "nope", action: "remove" }, }); assert.equal(status, 422); assert.equal(body.error.code, "ECLIEXIT"); assert.match(body.error.message, /not registered/); } finally { restore(); } }); it("POST /marketplaces/action validates the action name", async () => { const { status, body } = await fetchJson("/api/codex-config/marketplaces/action", { method: "POST", body: { name: "demo-mp", action: "update" }, }); assert.equal(status, 400); assert.equal(body.error.code, "EBADACTION"); }); it("reports a missing `codex` binary as 503 rather than a crash", async () => { const empty = path.join(TMP, `empty-${process.hrtime.bigint()}`); fs.mkdirSync(empty, { recursive: true }); const originalPath = process.env.PATH; process.env.PATH = empty; try { const { status, body } = await fetchJson("/api/codex-config/marketplaces/action", { method: "POST", body: { name: "demo-mp", action: "remove" }, }); assert.equal(status, 503); assert.equal(body.error.code, "ECLINOTFOUND"); } finally { process.env.PATH = originalPath; } }); });