/** * @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", () => { assert.throws( () => cm.writeArtifact({ scope: "project-local", type: "rules", name: "x", content: "x", cwd: REPO, }), (e) => e.code === "EBADSCOPE" ); }); it("rejects a type outside TYPES", () => { assert.throws( () => cm.writeArtifact({ scope: "user", type: "commands", name: "x", content: "x", cwd: REPO }), (e) => e.code === "EBADTYPE" ); }); 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", () => { assert.throws( () => cm.writeArtifact({ scope: "user", type: "memory", name: "AGENTS", content: "x", cwd: REPO, }), (e) => e.code === "EBADNAME" ); }); it("rejects a name passed to config", () => { assert.throws( () => cm.writeArtifact({ scope: "user", type: "config", name: "config", content: "x", cwd: REPO, }), (e) => e.code === "EBADNAME" ); }); 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); }); });