feat(codex): native plugin install manifests + agents-only converter (#616)
This commit is contained in:
@@ -32,14 +32,24 @@ async function makeFixtureRoot(): Promise<string> {
|
||||
await mkdir(path.join(root, "plugins", "compound-engineering", ".cursor-plugin"), {
|
||||
recursive: true,
|
||||
})
|
||||
await mkdir(path.join(root, "plugins", "compound-engineering", ".codex-plugin"), {
|
||||
recursive: true,
|
||||
})
|
||||
await mkdir(path.join(root, "plugins", "coding-tutor", "skills", "coding-tutor"), {
|
||||
recursive: true,
|
||||
})
|
||||
await mkdir(path.join(root, "plugins", "coding-tutor", ".claude-plugin"), {
|
||||
recursive: true,
|
||||
})
|
||||
await mkdir(path.join(root, "plugins", "coding-tutor", ".cursor-plugin"), {
|
||||
recursive: true,
|
||||
})
|
||||
await mkdir(path.join(root, "plugins", "coding-tutor", ".codex-plugin"), {
|
||||
recursive: true,
|
||||
})
|
||||
await mkdir(path.join(root, ".claude-plugin"), { recursive: true })
|
||||
await mkdir(path.join(root, ".cursor-plugin"), { recursive: true })
|
||||
await mkdir(path.join(root, ".agents", "plugins"), { recursive: true })
|
||||
|
||||
await writeFile(
|
||||
path.join(root, "plugins", "compound-engineering", "agents", "review", "agent.md"),
|
||||
@@ -69,6 +79,38 @@ async function makeFixtureRoot(): Promise<string> {
|
||||
path.join(root, "plugins", "coding-tutor", ".cursor-plugin", "plugin.json"),
|
||||
JSON.stringify({ version: "1.2.1" }, null, 2),
|
||||
)
|
||||
await writeFile(
|
||||
path.join(root, "plugins", "compound-engineering", ".codex-plugin", "plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "compound-engineering",
|
||||
version: "2.42.0",
|
||||
description: "old",
|
||||
skills: "./skills/",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
await writeFile(
|
||||
path.join(root, "plugins", "coding-tutor", ".codex-plugin", "plugin.json"),
|
||||
JSON.stringify(
|
||||
{ name: "coding-tutor", version: "1.2.1", skills: "./skills/" },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
await writeFile(
|
||||
path.join(root, ".agents", "plugins", "marketplace.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "compound-engineering-plugin",
|
||||
plugins: [{ name: "compound-engineering" }, { name: "coding-tutor" }],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
await writeFile(
|
||||
path.join(root, ".claude-plugin", "marketplace.json"),
|
||||
JSON.stringify(
|
||||
@@ -132,4 +174,202 @@ describe("release metadata", () => {
|
||||
expect(changedPaths).toContain(path.join(root, ".claude-plugin", "marketplace.json"))
|
||||
expect(changedPaths).toContain(path.join(root, ".cursor-plugin", "marketplace.json"))
|
||||
})
|
||||
|
||||
test("reports Codex plugin.json version drift without auto-correcting", async () => {
|
||||
const root = await makeFixtureRoot()
|
||||
// Claude is at 2.42.0; fixture Codex is also 2.42.0 — drift Codex to 2.41.0.
|
||||
await writeFile(
|
||||
path.join(root, "plugins", "compound-engineering", ".codex-plugin", "plugin.json"),
|
||||
JSON.stringify(
|
||||
{ name: "compound-engineering", version: "2.41.0", skills: "./skills/" },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
const result = await syncReleaseMetadata({ root, write: true })
|
||||
const codexPath = path.join(root, "plugins", "compound-engineering", ".codex-plugin", "plugin.json")
|
||||
const codexUpdate = result.updates.find((u) => u.path === codexPath)
|
||||
|
||||
expect(codexUpdate).toBeDefined()
|
||||
expect(codexUpdate!.changed).toBe(true)
|
||||
|
||||
// Crucially: write: true did NOT bump the Codex version to match Claude.
|
||||
// release-please owns version writes via extra-files; syncReleaseMetadata detects but does not correct.
|
||||
const afterContents = JSON.parse(await Bun.file(codexPath).text())
|
||||
expect(afterContents.version).toBe("2.41.0")
|
||||
})
|
||||
|
||||
test("rewrites Codex plugin.json description on write when drifted from Claude", async () => {
|
||||
const root = await makeFixtureRoot()
|
||||
// Fixture Claude description is "old"; Codex starts at "old" too. Give Claude a canonical description and drift Codex.
|
||||
await writeFile(
|
||||
path.join(root, "plugins", "compound-engineering", ".claude-plugin", "plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
version: "2.42.0",
|
||||
description: "AI-powered development tools for code review, research, design, and workflow automation.",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
await writeFile(
|
||||
path.join(root, "plugins", "compound-engineering", ".codex-plugin", "plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "compound-engineering",
|
||||
version: "2.42.0",
|
||||
description: "stale codex description",
|
||||
skills: "./skills/",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
const codexPath = path.join(root, "plugins", "compound-engineering", ".codex-plugin", "plugin.json")
|
||||
await syncReleaseMetadata({ root, write: true })
|
||||
|
||||
const afterContents = JSON.parse(await Bun.file(codexPath).text())
|
||||
expect(afterContents.description).toBe(
|
||||
"AI-powered development tools for code review, research, design, and workflow automation.",
|
||||
)
|
||||
})
|
||||
|
||||
test("reports missing Codex manifest as a structural error", async () => {
|
||||
const root = await makeFixtureRoot()
|
||||
await Bun.$`rm ${path.join(root, "plugins", "compound-engineering", ".codex-plugin", "plugin.json")}`.quiet()
|
||||
|
||||
const result = await syncReleaseMetadata({ root, write: false })
|
||||
|
||||
expect(result.errors.some((err) => err.includes(".codex-plugin/plugin.json is missing"))).toBe(true)
|
||||
})
|
||||
|
||||
test("reports Codex plugin.json name mismatch as structural error", async () => {
|
||||
const root = await makeFixtureRoot()
|
||||
await writeFile(
|
||||
path.join(root, "plugins", "compound-engineering", ".codex-plugin", "plugin.json"),
|
||||
JSON.stringify(
|
||||
{ name: "wrong-name", version: "2.42.0", skills: "./skills/" },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
const result = await syncReleaseMetadata({ root, write: false })
|
||||
|
||||
expect(
|
||||
result.errors.some((err) =>
|
||||
err.includes('name "wrong-name" does not match expected "compound-engineering"'),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("reports missing skills field on Codex manifest as structural error", async () => {
|
||||
const root = await makeFixtureRoot()
|
||||
// Drop the `skills` field entirely from the coding-tutor Codex manifest.
|
||||
await writeFile(
|
||||
path.join(root, "plugins", "coding-tutor", ".codex-plugin", "plugin.json"),
|
||||
JSON.stringify({ name: "coding-tutor", version: "1.2.1" }, null, 2),
|
||||
)
|
||||
const result = await syncReleaseMetadata({ root, write: false })
|
||||
|
||||
expect(
|
||||
result.errors.some(
|
||||
(err) =>
|
||||
err.includes("coding-tutor") &&
|
||||
err.includes("missing required field") &&
|
||||
err.includes("skills"),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("reports missing skills directory when Codex manifest declares one", async () => {
|
||||
const root = await makeFixtureRoot()
|
||||
// Remove coding-tutor's skills dir but keep the skills declaration.
|
||||
await Bun.$`rm -rf ${path.join(root, "plugins", "coding-tutor", "skills")}`.quiet()
|
||||
const result = await syncReleaseMetadata({ root, write: false })
|
||||
|
||||
expect(
|
||||
result.errors.some(
|
||||
(err) =>
|
||||
err.includes("coding-tutor") && err.includes("skills:") && err.includes("does not exist"),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("reports Codex marketplace plugin-list mismatch as structural error", async () => {
|
||||
const root = await makeFixtureRoot()
|
||||
// Remove one plugin from Codex marketplace so Claude has a plugin Codex doesn't.
|
||||
await writeFile(
|
||||
path.join(root, ".agents", "plugins", "marketplace.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "compound-engineering-plugin",
|
||||
plugins: [{ name: "compound-engineering" }],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
const result = await syncReleaseMetadata({ root, write: false })
|
||||
|
||||
expect(
|
||||
result.errors.some(
|
||||
(err) => err.includes(".agents/plugins/marketplace.json") && err.includes("does not match"),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("reports Codex marketplace asymmetric extra plugin as structural error", async () => {
|
||||
const root = await makeFixtureRoot()
|
||||
await writeFile(
|
||||
path.join(root, ".agents", "plugins", "marketplace.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "compound-engineering-plugin",
|
||||
plugins: [
|
||||
{ name: "compound-engineering" },
|
||||
{ name: "coding-tutor" },
|
||||
{ name: "rogue-plugin" },
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
const result = await syncReleaseMetadata({ root, write: false })
|
||||
|
||||
expect(
|
||||
result.errors.some(
|
||||
(err) => err.includes(".agents/plugins/marketplace.json") && err.includes("does not match"),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("happy path: fixture with matching Codex manifests produces no Codex errors", async () => {
|
||||
const root = await makeFixtureRoot()
|
||||
// Align Claude <-> Codex versions and descriptions so there's no drift.
|
||||
await writeFile(
|
||||
path.join(root, "plugins", "compound-engineering", ".claude-plugin", "plugin.json"),
|
||||
JSON.stringify({ version: "2.42.0", description: "aligned description" }, null, 2),
|
||||
)
|
||||
await writeFile(
|
||||
path.join(root, "plugins", "compound-engineering", ".codex-plugin", "plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "compound-engineering",
|
||||
version: "2.42.0",
|
||||
description: "aligned description",
|
||||
skills: "./skills/",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
const result = await syncReleaseMetadata({ root, write: false })
|
||||
const codexErrors = result.errors.filter(
|
||||
(err) => err.includes(".codex-plugin") || err.includes(".agents/plugins"),
|
||||
)
|
||||
expect(codexErrors).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user