Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cli/mutation-scopes.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,19 @@
},
"tools-codex": {
"mutate": "src/contexts/tools/domain/profiles/codex/**/*.ts",
"break": 87
"break": 94
},
"tools-copilot": {
"mutate": "src/contexts/tools/domain/profiles/copilot/**/*.ts",
"break": 80
"break": 93
},
"tools-cursor": {
"mutate": "src/contexts/tools/domain/profiles/cursor/**/*.ts",
"break": 94
},
"tools-opencode": {
"mutate": "src/contexts/tools/domain/profiles/opencode/**/*.ts",
"break": 81
"break": 94
}
},
"excluded": {
Expand Down
60 changes: 60 additions & 0 deletions cli/tests/contexts/tools/domain/profiles/codex.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ describe("codex", () => {
const path = codex.capabilities.skills.buildInstallPath("my-skill");
expect(path).toBe(".agents/skills/aidd-my-skill/SKILL.md");
});

it("keeps a skill folder's name whole, even one ending in .md", () => {
const path = codex.capabilities.skills.buildInstallPath("notes.md/SKILL.md");
expect(path).toBe(".agents/skills/aidd-notes.md/SKILL.md");
});
});

it("names the one config file Codex reads, and where it goes", () => {
Expand Down Expand Up @@ -268,6 +273,21 @@ describe("mergeCodexHooksJson()", () => {
expect(mergeCodexHooksJson(once)).toBe(once);
});

it("finds its own hook inside any group of a user's SessionStart, and adds no second one", () => {
const existing = JSON.stringify(
{
SessionStart: [
{ hooks: [{ type: "command", command: "user-start.sh" }] },
{ hooks: [{ type: "command", command: "user-two.sh" }, AIDD_ENTRY.hooks[0]] },
],
},
null,
2
);

expect(mergeCodexHooksJson(existing)).toBe(existing);
});

it("starts over from a file it cannot read rather than failing the install", () => {
expect(mergeCodexHooksJson("{ not json")).toBe(
JSON.stringify({ SessionStart: [AIDD_ENTRY] }, null, 2)
Expand Down Expand Up @@ -360,6 +380,42 @@ enabled = true
const result = mergeCodexConfigToml(existing, MCP_PAYLOAD);
expect(result).toContain(".agents/skills");
});

it("starts over from a config it cannot parse rather than failing the install", () => {
expect(mergeCodexConfigToml("not = [valid", '[mcp_servers.ctx]\ncommand = "node"\n')).toBe(
'project_doc_max_bytes = 262144\n\n[mcp_servers.ctx]\ncommand = "node"\n\n[features]\nhooks = true\n'
);
});

it("adds only its defaults when the payload names no mcp server", () => {
expect(mergeCodexConfigToml('[mcp_servers.mine]\ncommand = "x"\n', "")).toBe(
'project_doc_max_bytes = 262144\n\n[mcp_servers.mine]\ncommand = "x"\n\n[features]\nhooks = true\n'
);
});

it("raises project_doc_max_bytes to what the payload asks when that is above the floor", () => {
expect(mergeCodexConfigToml("", "project_doc_max_bytes = 500000\n")).toBe(
"project_doc_max_bytes = 500000\n\n[features]\nhooks = true\n"
);
});

it("leaves a user's project_doc_max_bytes already at the floor untouched, whatever the payload asks", () => {
expect(
mergeCodexConfigToml("project_doc_max_bytes = 262144\n", "project_doc_max_bytes = 500000\n")
).toBe("project_doc_max_bytes = 262144\n\n[features]\nhooks = true\n");
});

it("keeps a user's own hooks = false rather than turning hooks on", () => {
expect(mergeCodexConfigToml("[features]\nhooks = false\n", "")).toBe(
"project_doc_max_bytes = 262144\n\n[features]\nhooks = false\n"
);
});

it("turns hooks on beside a user's other features, keeping them", () => {
expect(mergeCodexConfigToml("[features]\nweb_search = true\n", "")).toBe(
"project_doc_max_bytes = 262144\n\n[features]\nweb_search = true\nhooks = true\n"
);
});
});

/**
Expand Down Expand Up @@ -392,6 +448,10 @@ describe("a skill's frontmatter, rewritten for Codex", () => {
expect(stripCodexSkillFrontmatter({ description: "d" })).toEqual({ description: "d" });
});

it("writes no description key for a skill that has none", () => {
expect(stripCodexSkillFrontmatter({ name: "n" })).toStrictEqual({ name: "n" });
});

it("quotes a value whose colon would otherwise make the frontmatter unreadable", () => {
// Not cosmetic: a description containing ": " makes `js-yaml` refuse the source with "bad
// indentation of a mapping entry". Re-serialising with quotes is what makes it parse.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ const HOOKS_JSON = JSON.stringify({
hooks: { Stop: [{ hooks: [{ type: "command", command: "node journal.cjs" }] }] },
});

const NO_CONTENT = {
hasAgents: false,
agentsList: [],
skillsList: [],
hasHooksJson: false,
hasMcpJson: false,
};

function supported(artifact: ArtifactContract): Extract<ArtifactContract, { supported: true }> {
if (!artifact.supported) throw new Error("artifact is declared unsupported");
return artifact;
Expand Down Expand Up @@ -92,6 +100,27 @@ describe("buildCodexContract()", () => {
expect(manifest).toStrictEqual({ name: "aidd-dev" });
});

it("copies an author given as a plain name into the manifest", () => {
const manifest = buildCodexContract().synthesizeManifest?.(
{ name: "aidd-dev", author: "AIDD" },
NO_CONTENT
);

expect(manifest).toStrictEqual({ name: "aidd-dev", author: "AIDD" });
});

it("copies an author given as an object into the manifest", () => {
const manifest = buildCodexContract().synthesizeManifest?.(
{ name: "aidd-dev", author: { name: "AIDD", email: "team@example.test" } },
NO_CONTENT
);

expect(manifest).toStrictEqual({
name: "aidd-dev",
author: { name: "AIDD", email: "team@example.test" },
});
});

it("sources skills, agents, mcp and hooks from the plugin tree, and neither rules nor commands", () => {
const { artifacts } = buildCodexContract();

Expand Down Expand Up @@ -128,6 +157,14 @@ describe("buildCodexContract()", () => {
});
});

it("changes only the trailing .md of a staged agent's name to .toml", () => {
const agents = supported(buildCodexContract().artifacts.agents);

expect(agents.path("aidd-dev", "agents/notes.md-helper.md")).toBe(
"codex-agents/notes.md-helper.toml"
);
});

it("keeps only the three frontmatter fields Codex reads in a skill", () => {
const transform = supported(buildCodexContract().artifacts.skills).transform;

Expand Down Expand Up @@ -259,6 +296,14 @@ describe("buildCodexFlatContract()", () => {
);
});

it("changes only the trailing .md of a flat agent's name to .toml", () => {
const agents = supported(buildCodexFlatContract().artifacts.agents);

expect(agents.path("aidd-dev", "agents/notes.md-helper.md")).toBe(
".codex/agents/aidd-dev-notes.md-helper.toml"
);
});

it("names a flat agent after its plugin, whatever its own frontmatter says", () => {
const transform = supported(buildCodexFlatContract().artifacts.agents).transform;

Expand Down Expand Up @@ -312,4 +357,21 @@ describe("buildCodexFlatContract()", () => {
'project_doc_max_bytes = 262144\n\n[mcp_servers.aidd-dev-context]\ncommand = "node"\n\n[features]\nhooks = true\n',
});
});

it("writes no mcp_servers table when no built plugin ships an mcp server", async () => {
const fs = new InMemoryFileAdapter();

await buildCodexFlatContract().emitConfigArtifact?.(
["aidd-dev"],
"/out",
"/src",
fs,
{ validate: () => undefined },
{ loadConfigAsset: () => ({}), loadSchema: () => ({}) }
);

expect(fs.getFile("/out/.codex/config.toml")).toBe(
"project_doc_max_bytes = 262144\n\n[features]\nhooks = true\n"
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,30 @@ describe("codexAgentMarkdownToToml()", () => {
});
});
});

describe("the name a Codex agent falls back to", () => {
it("ignores an empty frontmatter name and names the agent after its plugin and file", () => {
expect(
codexAgentMarkdownToToml(
"---\nname: ''\ndescription: Plans\n---\nBody.\n",
"aidd-dev",
"planner.md"
)
).toBe(
'name = "aidd-dev-planner"\ndescription = "Plans"\ndeveloper_instructions = "Body.\\n"\n'
);
});

it("drops only the trailing .md from the file it names a flat agent after", () => {
expect(
codexAgentMarkdownToToml(
"---\ndescription: Plans\n---\nBody.\n",
"aidd-dev",
"notes.md-helper.md",
true
)
).toBe(
'name = "aidd-dev-notes.md-helper"\ndescription = "Plans"\ndeveloper_instructions = "Body.\\n"\n'
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { CODEX_ROLLOUT_LOCATION } from "../../../../../../src/contexts/tools/domain/profiles/codex/codex-transcript-location.js";

describe("where Codex keeps a session's rollout", () => {
it("claims no file that is not a rollout, even one named for the session", () => {
expect(
CODEX_ROLLOUT_LOCATION.matches(join("2026", "01", "02", "history-abc.jsonl"), "abc")
).toBe(false);
});
});
Loading