/** * @file Tests for codex-mutate.js: create/edit/delete on Codex config * surfaces (skills, agents, rules, memory, config) across the user * (CODEX_HOME) and project (/.codex or for AGENTS.md) scopes. * @author Nguyễn Ngọc Trí Vĩ */ const fs = require("node:fs"); const path = require("node:path"); const os = require("node:os"); const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); describe("codex-mutate", () => { let TMP; let REPO; let prevCodexHome; let cm; before(() => { prevCodexHome = process.env.CODEX_HOME; TMP = fs.mkdtempSync(path.join(os.tmpdir(), "codex-mut-")); REPO = fs.mkdtempSync(path.join(os.tmpdir(), "codex-mut-repo-")); process.env.CODEX_HOME = TMP; delete require.cache[require.resolve("../lib/codex-home")]; delete require.cache[require.resolve("../lib/codex-mutate")]; cm = require("../lib/codex-mutate"); }); after(() => { if (prevCodexHome === undefined) delete process.env.CODEX_HOME; else process.env.CODEX_HOME = prevCodexHome; fs.rmSync(TMP, { recursive: true, force: true }); fs.rmSync(REPO, { recursive: true, force: true }); }); it("creates a project skill as a directory with SKILL.md inside", () => { const { file, created } = cm.writeArtifact({ scope: "project", type: "skills", name: "release-guard", content: "---\nname: x\n---\n", cwd: REPO, }); assert.equal(created, true); assert.equal(file, path.join(REPO, ".codex", "skills", "release-guard", "SKILL.md")); }); it("backs the old body up before overwriting it", () => { cm.writeArtifact({ scope: "user", type: "rules", name: "default", content: "old", cwd: REPO }); const { backup } = cm.writeArtifact({ scope: "user", type: "rules", name: "default", content: "new", cwd: REPO, }); assert.equal(fs.readFileSync(backup, "utf8"), "old"); assert.equal(fs.readFileSync(path.join(TMP, "rules", "default.rules"), "utf8"), "new"); }); it("refuses a name that climbs out of the root", () => { assert.throws( () => cm.writeArtifact({ scope: "user", type: "agents", name: "../../evil", content: "x", cwd: REPO, }), (e) => e.code === "EBADNAME" ); }); it("writes AGENTS.md at the repo root, not inside .codex", () => { const { file } = cm.writeArtifact({ scope: "project", type: "memory", content: "# rules", cwd: REPO, }); assert.equal(file, path.join(REPO, "AGENTS.md")); }); it("keeps a deleted skill's backup out of the skills tree", () => { cm.writeArtifact({ scope: "user", type: "skills", name: "gone", content: "x", cwd: REPO }); const { backup } = cm.deleteArtifact({ scope: "user", type: "skills", name: "gone", cwd: REPO, }); assert.ok( !backup.includes(path.join(TMP, "skills", "gone")), "a backup must never be discoverable as a skill" ); assert.equal(fs.existsSync(path.join(TMP, "skills", "gone")), false); }); it("writes and reads back a project agent under .codex/agents/.toml", () => { const { file } = cm.writeArtifact({ scope: "project", type: "agents", name: "reviewer", content: 'name = "reviewer"\n', cwd: REPO, }); assert.equal(file, path.join(REPO, ".codex", "agents", "reviewer.toml")); }); it("writes user config.toml under CODEX_HOME and project config.toml under /.codex", () => { const u = cm.writeArtifact({ scope: "user", type: "config", content: 'model = "x"\n', cwd: REPO, }); assert.equal(u.file, path.join(TMP, "config.toml")); const p = cm.writeArtifact({ scope: "project", type: "config", content: 'model = "y"\n', cwd: REPO, }); assert.equal(p.file, path.join(REPO, ".codex", "config.toml")); }); it("rejects a scope that is neither user nor project, and writes nothing", () => { const target = path.join(TMP, "rules", "x.rules"); assert.throws( () => cm.writeArtifact({ scope: "project-local", type: "rules", name: "x", content: "x", cwd: REPO, }), (e) => e.code === "EBADSCOPE" ); assert.equal(fs.existsSync(target), false); }); it("rejects a type outside TYPES, and writes nothing", () => { const target = path.join(TMP, "commands", "x.md"); assert.throws( () => cm.writeArtifact({ scope: "user", type: "commands", name: "x", content: "x", cwd: REPO }), (e) => e.code === "EBADTYPE" ); assert.equal(fs.existsSync(target), false); }); it("refuses a symlinked skill dir that escapes both Codex roots, and writes nothing outside", () => { const outside = fs.mkdtempSync(path.join(os.tmpdir(), "codex-mut-outside-")); const linkPath = path.join(TMP, "skills", "escape-link"); fs.mkdirSync(path.dirname(linkPath), { recursive: true }); fs.symlinkSync(outside, linkPath, "dir"); assert.throws( () => cm.writeArtifact({ scope: "user", type: "skills", name: "escape-link", content: "pwned", cwd: REPO, }), (e) => e.code === "EOUTOFROOT" ); assert.equal(fs.existsSync(path.join(outside, "SKILL.md")), false); fs.rmSync(outside, { recursive: true, force: true }); }); it("follows a symlinked skill dir that points back inside the same root", () => { const realDir = path.join(TMP, "skills", "real-dir"); fs.mkdirSync(realDir, { recursive: true }); const linkPath = path.join(TMP, "skills", "inside-link"); fs.symlinkSync(realDir, linkPath, "dir"); const { file, created } = cm.writeArtifact({ scope: "user", type: "skills", name: "inside-link", content: "fine", cwd: REPO, }); assert.equal(created, true); assert.equal(fs.readFileSync(file, "utf8"), "fine"); assert.equal(fs.readFileSync(path.join(realDir, "SKILL.md"), "utf8"), "fine"); }); it("refuses to back up a symlinked artifact that escapes the roots when deleting it", () => { const outside = fs.mkdtempSync(path.join(os.tmpdir(), "codex-mut-outside-")); fs.writeFileSync(path.join(outside, "secret.rules"), "top secret"); const linkPath = path.join(TMP, "rules", "escape-del.rules"); fs.mkdirSync(path.dirname(linkPath), { recursive: true }); fs.symlinkSync(path.join(outside, "secret.rules"), linkPath, "file"); assert.throws( () => cm.deleteArtifact({ scope: "user", type: "rules", name: "escape-del", cwd: REPO }), (e) => e.code === "EOUTOFROOT" ); assert.equal(fs.existsSync(path.join(outside, "secret.rules")), true); fs.rmSync(outside, { recursive: true, force: true }); }); it("refuses a brand-new artifact whose subdir (not the leaf) is a symlink escaping the root", () => { const outside = fs.mkdtempSync(path.join(os.tmpdir(), "codex-mut-outside-")); const linkPath = path.join(TMP, "agents"); // "agents" doesn't exist yet in this fresh suite of tests for this scope — // remove it first if an earlier test already created it as a real dir. fs.rmSync(linkPath, { recursive: true, force: true }); fs.symlinkSync(outside, linkPath, "dir"); assert.throws( () => cm.writeArtifact({ scope: "user", type: "agents", name: "brand-new", content: "pwned", cwd: REPO, }), (e) => e.code === "EOUTOFROOT" ); assert.equal(fs.existsSync(path.join(outside, "brand-new.toml")), false); fs.rmSync(linkPath, { force: true }); fs.rmSync(outside, { recursive: true, force: true }); }); it("still writes correctly when $CODEX_HOME itself is a symlink (a dotfiles checkout)", () => { const realHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-mut-realhome-")); const linkHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-mut-linkhome-")); fs.rmdirSync(linkHome); // symlinkSync needs the link path itself to not exist fs.symlinkSync(realHome, linkHome, "dir"); const prev = process.env.CODEX_HOME; process.env.CODEX_HOME = linkHome; delete require.cache[require.resolve("../lib/codex-home")]; delete require.cache[require.resolve("../lib/codex-mutate")]; const linkedCm = require("../lib/codex-mutate"); try { const { file, created } = linkedCm.writeArtifact({ scope: "user", type: "rules", name: "via-link", content: "ok", cwd: REPO, }); assert.equal(created, true); assert.equal(fs.readFileSync(file, "utf8"), "ok"); assert.equal( fs.readFileSync(path.join(realHome, "rules", "via-link.rules"), "utf8"), "ok", "the write must land in the real directory the symlinked CODEX_HOME points to" ); } finally { process.env.CODEX_HOME = prev; delete require.cache[require.resolve("../lib/codex-home")]; delete require.cache[require.resolve("../lib/codex-mutate")]; fs.rmSync(linkHome, { force: true }); fs.rmSync(realHome, { recursive: true, force: true }); } }); it("refuses to write (and back up) when /codex-config-backups is a symlink escaping the root", () => { cm.writeArtifact({ scope: "user", type: "rules", name: "toclobber", content: "v1", cwd: REPO }); const outside = fs.mkdtempSync(path.join(os.tmpdir(), "codex-mut-outside-")); const backupsLink = path.join(TMP, "codex-config-backups"); fs.rmSync(backupsLink, { recursive: true, force: true }); fs.symlinkSync(outside, backupsLink, "dir"); try { assert.throws( () => cm.writeArtifact({ scope: "user", type: "rules", name: "toclobber", content: "v2 — attacker wants this in `outside`", cwd: REPO, }), (e) => e.code === "EOUTOFROOT" ); assert.equal(fs.readdirSync(outside).length, 0, "backup must not land outside the root"); } finally { fs.rmSync(backupsLink, { force: true }); fs.rmSync(outside, { recursive: true, force: true }); } }); it("refuses to delete (and back up) when /codex-config-backups is a symlink escaping the root", () => { cm.writeArtifact({ scope: "user", type: "rules", name: "todelete2", content: "keep-me", cwd: REPO, }); const outside = fs.mkdtempSync(path.join(os.tmpdir(), "codex-mut-outside-")); const backupsLink = path.join(TMP, "codex-config-backups"); fs.rmSync(backupsLink, { recursive: true, force: true }); fs.symlinkSync(outside, backupsLink, "dir"); try { assert.throws( () => cm.deleteArtifact({ scope: "user", type: "rules", name: "todelete2", cwd: REPO }), (e) => e.code === "EOUTOFROOT" ); assert.equal(fs.readdirSync(outside).length, 0, "backup must not land outside the root"); assert.equal( fs.readFileSync(path.join(TMP, "rules", "todelete2.rules"), "utf8"), "keep-me", "the original file must survive a refused backup" ); } finally { fs.rmSync(backupsLink, { force: true }); fs.rmSync(outside, { recursive: true, force: true }); } }); for (const badName of ["..", ".", "/etc/passwd", "a/b", "a\0b"]) { it(`rejects the bad name ${JSON.stringify(badName)} with EBADNAME and writes nothing`, () => { const target = path.join(TMP, "agents", `${badName.replace(/[/\0]/g, "_")}.toml`); assert.throws( () => cm.writeArtifact({ scope: "user", type: "agents", name: badName, content: "x", cwd: REPO, }), (e) => e.code === "EBADNAME" ); assert.equal(fs.existsSync(target), false); }); } it("rejects a name passed to memory, and writes nothing", () => { const target = path.join(TMP, "AGENTS.md"); assert.throws( () => cm.writeArtifact({ scope: "user", type: "memory", name: "AGENTS", content: "x", cwd: REPO, }), (e) => e.code === "EBADNAME" ); assert.equal(fs.existsSync(target), false); }); it("rejects a name passed to config, and leaves any existing config.toml untouched", () => { const target = path.join(TMP, "config.toml"); const before = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : null; assert.throws( () => cm.writeArtifact({ scope: "user", type: "config", name: "config", content: "x", cwd: REPO, }), (e) => e.code === "EBADNAME" ); const after = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : null; assert.equal(after, before); }); it("rejects content over the size limit and leaves the existing file untouched", () => { cm.writeArtifact({ scope: "user", type: "rules", name: "toobig", content: "small", cwd: REPO }); const huge = "x".repeat(256 * 1024 + 1); assert.throws( () => cm.writeArtifact({ scope: "user", type: "rules", name: "toobig", content: huge, cwd: REPO, }), (e) => e.code === "ETOOLARGE" ); assert.equal(fs.readFileSync(path.join(TMP, "rules", "toobig.rules"), "utf8"), "small"); }); it("refuses to delete something that does not exist, and creates no backup", () => { const before = fs.existsSync(path.join(TMP, "codex-config-backups", "rules")) ? fs.readdirSync(path.join(TMP, "codex-config-backups", "rules")).length : 0; assert.throws( () => cm.deleteArtifact({ scope: "user", type: "rules", name: "never-existed", cwd: REPO }), (e) => e.code === "ENOTFOUND" ); const after = fs.existsSync(path.join(TMP, "codex-config-backups", "rules")) ? fs.readdirSync(path.join(TMP, "codex-config-backups", "rules")).length : 0; assert.equal(after, before, "no backup should be created for a missing target"); }); it("lists backups for a given scope and type", () => { cm.writeArtifact({ scope: "user", type: "rules", name: "listed", content: "v1", cwd: REPO }); cm.writeArtifact({ scope: "user", type: "rules", name: "listed", content: "v2", cwd: REPO }); const rows = cm.listBackups({ scope: "user", type: "rules" }); assert.ok(rows.length >= 1); assert.equal(rows[0].type, "rules"); assert.equal(rows[0].name.startsWith("listed.rules."), true); }); it("refuses a listBackups scope that is neither user nor project", () => { assert.throws( () => cm.listBackups({ scope: "../../../../tmp" }), (e) => e.code === "EBADSCOPE" ); }); it("refuses a listBackups type that tries to traverse outside the backups root", () => { assert.throws( () => cm.listBackups({ scope: "user", type: "../../../../tmp" }), (e) => e.code === "EBADTYPE" ); }); });