236e94760f
readTomlMeta's trailing-comment stripper found the first quote character regardless of escaping, so a description containing \"escaped quotes\" before a real comment got silently truncated instead of dropped. The scan now skips a quote preceded by a backslash. The "never walks outside" containment test passed by construction (row.file is always built off a fixed root) and would not fail if the isUnder guard were removed. Replaced it with two tests that actually pin the regression risk: a skill dir and an agent .toml symlinked in from outside CODEX_HOME, asserting the row is discovered and its content read through the link. isUnder stays as a defensive check (matches cc-discovery.js), now commented as such.
172 lines
6.7 KiB
JavaScript
172 lines
6.7 KiB
JavaScript
/**
|
|
* @file Tests for codex-discovery.js: read-only discovery of Codex config
|
|
* surfaces (skills, agents, rules, memory, config) across the user (CODEX_HOME)
|
|
* and project (<cwd>/.codex) scopes.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
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");
|
|
|
|
function write(file, content) {
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, content);
|
|
}
|
|
|
|
describe("codex-discovery", () => {
|
|
let TMP;
|
|
let REPO;
|
|
let prevCodexHome;
|
|
let cx;
|
|
|
|
before(() => {
|
|
prevCodexHome = process.env.CODEX_HOME;
|
|
TMP = fs.mkdtempSync(path.join(os.tmpdir(), "codex-disc-"));
|
|
REPO = fs.mkdtempSync(path.join(os.tmpdir(), "codex-disc-repo-"));
|
|
process.env.CODEX_HOME = TMP;
|
|
delete require.cache[require.resolve("../lib/codex-home")];
|
|
delete require.cache[require.resolve("../lib/codex-discovery")];
|
|
cx = require("../lib/codex-discovery");
|
|
});
|
|
|
|
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("reads a user skill and a project skill into one scoped list", () => {
|
|
write(
|
|
path.join(TMP, "skills", "release-guard", "SKILL.md"),
|
|
"---\nname: release-guard\n---\nbody"
|
|
);
|
|
write(
|
|
path.join(REPO, ".codex", "skills", "onboarding", "SKILL.md"),
|
|
"---\nname: onboarding\n---\nb"
|
|
);
|
|
|
|
const rows = cx.readSkills({ cwd: REPO });
|
|
assert.deepEqual(rows.map((r) => [r.scope, r.name]).sort(), [
|
|
["project", "onboarding"],
|
|
["user", "release-guard"],
|
|
]);
|
|
assert.equal(rows.find((r) => r.name === "release-guard").frontmatter.name, "release-guard");
|
|
});
|
|
|
|
it("pulls an agent's identity out of its TOML without a TOML parser", () => {
|
|
write(
|
|
path.join(REPO, ".codex", "agents", "reviewer.toml"),
|
|
'name = "reviewer"\ndescription = "Read-only review agent."\nmodel = "gpt-5.4"\n' +
|
|
'developer_instructions = """\nmulti\nline\n"""\n'
|
|
);
|
|
|
|
const [agent] = cx.readAgents({ cwd: REPO, scope: "project" });
|
|
assert.equal(agent.meta.name, "reviewer");
|
|
assert.equal(agent.meta.description, "Read-only review agent.");
|
|
assert.equal(agent.meta.model, "gpt-5.4");
|
|
});
|
|
|
|
it("does not mistake a key inside a triple-quoted block for a top-level field", () => {
|
|
const meta = cx.readTomlMeta(
|
|
'name = "implementer"\n' +
|
|
'sandbox_mode = "workspace-write"\n' +
|
|
'developer_instructions = """\n' +
|
|
'model = "not-a-real-field"\n' +
|
|
'"""\n' +
|
|
'model = "gpt-5.3-codex-spark"\n'
|
|
);
|
|
assert.equal(meta.name, "implementer");
|
|
assert.equal(meta.sandbox_mode, "workspace-write");
|
|
assert.equal(meta.model, "gpt-5.3-codex-spark");
|
|
});
|
|
|
|
it("keeps a field with a trailing inline comment, and keeps a '#' that is inside the string", () => {
|
|
const meta = cx.readTomlMeta(
|
|
'model = "gpt-5.4" # default\n' + 'description = "uses # for headings"\n'
|
|
);
|
|
assert.equal(meta.model, "gpt-5.4");
|
|
assert.equal(meta.description, "uses # for headings");
|
|
});
|
|
|
|
it("does not let an escaped quote before a real trailing comment truncate the value", () => {
|
|
const meta = cx.readTomlMeta('description = "say \\"hi\\"" # not a comment\n');
|
|
assert.equal(meta.description, 'say \\"hi\\"');
|
|
});
|
|
|
|
it("reads a rule file's preview", () => {
|
|
write(
|
|
path.join(TMP, "rules", "no-force-push.rules"),
|
|
"never force-push main\nask before deleting"
|
|
);
|
|
const [rule] = cx.readRules({ cwd: REPO, scope: "user" });
|
|
assert.equal(rule.name, "no-force-push");
|
|
assert.match(rule.preview, /never force-push main/);
|
|
});
|
|
|
|
it("reports a missing AGENTS.md as a row that does not exist, not a gap", () => {
|
|
const rows = cx.readMemory({ cwd: REPO });
|
|
const user = rows.find((r) => r.scope === "user");
|
|
assert.equal(user.exists, false, "the UI offers to create it, so the row must be present");
|
|
});
|
|
|
|
it("reads project AGENTS.md from the repo root, not under .codex", () => {
|
|
write(path.join(REPO, "AGENTS.md"), "# Project instructions");
|
|
const rows = cx.readMemory({ cwd: REPO });
|
|
const project = rows.find((r) => r.scope === "project");
|
|
assert.equal(project.exists, true);
|
|
assert.equal(project.file, path.join(REPO, "AGENTS.md"));
|
|
});
|
|
|
|
it("reads config.toml content for both scopes", () => {
|
|
write(path.join(TMP, "config.toml"), 'model = "gpt-5.3-codex-spark"\n');
|
|
const rows = cx.readConfig({ cwd: REPO });
|
|
const user = rows.find((r) => r.scope === "user");
|
|
assert.equal(user.exists, true);
|
|
assert.match(user.content, /gpt-5\.3-codex-spark/);
|
|
const project = rows.find((r) => r.scope === "project");
|
|
assert.equal(project.exists, false);
|
|
});
|
|
|
|
it("follows a skill directory symlinked in from outside CODEX_HOME — a supported install pattern", () => {
|
|
const outside = fs.mkdtempSync(path.join(os.tmpdir(), "codex-disc-outside-"));
|
|
write(path.join(outside, "SKILL.md"), "---\nname: linked\n---\nbody");
|
|
const linkPath = path.join(TMP, "skills", "linked");
|
|
fs.symlinkSync(outside, linkPath, "dir");
|
|
|
|
const [linked] = cx.readSkills({ cwd: REPO, scope: "user" });
|
|
assert.equal(linked.scope, "user");
|
|
assert.equal(linked.name, "linked");
|
|
assert.equal(linked.frontmatter.name, "linked", "frontmatter must be read through the link");
|
|
assert.equal(linked.file, path.join(linkPath, "SKILL.md"));
|
|
fs.rmSync(outside, { recursive: true, force: true });
|
|
});
|
|
|
|
it("follows an agent .toml symlinked in from outside CODEX_HOME", () => {
|
|
const outside = fs.mkdtempSync(path.join(os.tmpdir(), "codex-disc-outside-"));
|
|
const realFile = path.join(outside, "reviewer.toml");
|
|
write(realFile, 'name = "reviewer"\n');
|
|
const linkPath = path.join(TMP, "agents", "reviewer.toml");
|
|
fs.mkdirSync(path.dirname(linkPath), { recursive: true });
|
|
fs.symlinkSync(realFile, linkPath, "file");
|
|
|
|
const [agent] = cx.readAgents({ cwd: REPO, scope: "user" });
|
|
assert.equal(agent.scope, "user");
|
|
assert.equal(agent.name, "reviewer");
|
|
assert.equal(agent.meta.name, "reviewer", "meta must be read through the link");
|
|
assert.equal(agent.file, linkPath);
|
|
fs.rmSync(outside, { recursive: true, force: true });
|
|
});
|
|
|
|
it("summarizes counts and roots in the overview", () => {
|
|
const overview = cx.readOverview({ cwd: REPO });
|
|
assert.equal(overview.roots.projectRoot, REPO);
|
|
assert.ok(overview.counts.skills.user >= 1);
|
|
assert.ok(overview.counts.agents.project >= 1);
|
|
});
|
|
});
|