Files
AgentHub/server/__tests__/codex-config.test.js
T

605 lines
21 KiB
JavaScript

/**
* @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ĩ <vinnt@smartgift.vn>
*/
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 refuses a leaf symlink that escapes CODEX_HOME (no secret leaks)", async () => {
const outside = fs.mkdtempSync(path.join(os.tmpdir(), "codex-config-outside-"));
fs.writeFileSync(path.join(outside, "secret.rules"), "TOP SECRET LEAF");
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, 400);
assert.equal(body.error.code, "READ_DENIED");
assert.doesNotMatch(JSON.stringify(body), /TOP SECRET LEAF/);
} finally {
fs.rmSync(outside, { recursive: true, force: true });
}
});
it("file endpoint refuses an ancestor symlink that escapes CODEX_HOME (no secret leaks)", async () => {
const outside = fs.mkdtempSync(path.join(os.tmpdir(), "codex-config-outside-"));
fs.writeFileSync(path.join(outside, "secret.txt"), "TOP SECRET ANCESTOR");
const linkDir = path.join(FAKE_HOME, "evil-dir");
fs.symlinkSync(outside, linkDir, "dir");
try {
const target = path.join(linkDir, "secret.txt");
const { status, body } = await fetchJson(
`/api/codex-config/file?cwd=${encodeURIComponent(FAKE_PROJECT)}&path=${encodeURIComponent(target)}`
);
assert.equal(status, 400);
assert.equal(body.error.code, "READ_DENIED");
assert.doesNotMatch(JSON.stringify(body), /TOP SECRET ANCESTOR/);
} finally {
fs.rmSync(linkDir, { force: true });
fs.rmSync(outside, { recursive: true, force: true });
}
});
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 /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 rejects a non-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;
}
});
});