import { mkdtemp, mkdir, writeFile } from "fs/promises" import os from "os" import path from "path" import { afterEach, describe, expect, test } from "bun:test" import { buildCompoundEngineeringDescription, getCompoundEngineeringCounts, syncReleaseMetadata, } from "../src/release/metadata" const tempRoots: string[] = [] afterEach(async () => { for (const root of tempRoots.splice(0, tempRoots.length)) { await Bun.$`rm -rf ${root}`.quiet() } }) async function makeFixtureRoot(): Promise { const root = await mkdtemp(path.join(os.tmpdir(), "release-metadata-")) tempRoots.push(root) await mkdir(path.join(root, "agents", "review"), { recursive: true }) await mkdir(path.join(root, "skills", "ce-plan"), { 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, ".codex-plugin"), { recursive: true }) await mkdir(path.join(root, ".kimi-plugin"), { recursive: true }) await mkdir(path.join(root, ".grok-plugin"), { recursive: true }) await mkdir(path.join(root, ".devin-plugin"), { recursive: true }) await mkdir(path.join(root, ".agents", "plugins"), { recursive: true }) await writeFile( path.join(root, "agents", "review", "agent.md"), "# Review Agent\n", ) await writeFile( path.join(root, "skills", "ce-plan", "SKILL.md"), "# ce-plan\n", ) await writeFile( path.join(root, ".mcp.json"), JSON.stringify({ mcpServers: { context7: { command: "ctx7" } } }, null, 2), ) await writeFile( path.join(root, "package.json"), JSON.stringify({ version: "2.42.0" }, null, 2), ) await writeFile( path.join(root, ".claude-plugin", "plugin.json"), JSON.stringify({ version: "2.42.0", description: "old" }, null, 2), ) await writeFile( path.join(root, ".cursor-plugin", "plugin.json"), JSON.stringify({ version: "2.33.0", description: "old" }, null, 2), ) await writeFile( path.join(root, ".codex-plugin", "plugin.json"), JSON.stringify( { name: "compound-engineering", version: "2.42.0", description: "old", skills: "./skills/", }, null, 2, ), ) await writeFile( path.join(root, ".kimi-plugin", "plugin.json"), JSON.stringify( { name: "compound-engineering", version: "2.42.0", description: "old", skills: "./skills/", }, null, 2, ), ) await writeFile( path.join(root, ".devin-plugin", "plugin.json"), JSON.stringify( { name: "compound-engineering", version: "2.42.0", description: "old", }, null, 2, ), ) await writeFile( path.join(root, "plugin.json"), JSON.stringify({ name: "compound-engineering", version: "2.42.0" }, null, 2), ) await writeFile( path.join(root, ".grok-plugin", "plugin.json"), JSON.stringify( { name: "compound-engineering", version: "2.42.0", description: "old", skills: "./skills/", }, null, 2, ), ) await writeFile( path.join(root, ".grok-plugin", "marketplace.json"), JSON.stringify( { name: "compound-engineering", owner: { name: "Kieran Klaassen and Trevin Chow" }, plugins: [ { name: "compound-engineering", source: { source: "url", url: "https://github.com/EveryInc/compound-engineering-plugin.git", }, }, ], }, null, 2, ), ) await mkdir(path.join(root, ".agy"), { recursive: true }) await writeFile( path.join(root, ".agents", "plugins", "marketplace.json"), JSON.stringify( { name: "compound-engineering-plugin", plugins: [{ name: "compound-engineering" }], }, null, 2, ), ) await writeFile( path.join(root, ".kimi-plugin", "marketplace.json"), JSON.stringify( { version: "2", plugins: [ { id: "compound-engineering", displayName: "Compound Engineering", source: "https://github.com/EveryInc/compound-engineering-plugin", }, ], }, null, 2, ), ) await writeFile( path.join(root, ".claude-plugin", "marketplace.json"), JSON.stringify( { metadata: { version: "1.0.0", description: "marketplace" }, plugins: [ { name: "compound-engineering", version: "2.41.0", description: "old" }, ], }, null, 2, ), ) await writeFile( path.join(root, ".cursor-plugin", "marketplace.json"), JSON.stringify( { metadata: { version: "1.0.0", description: "marketplace" }, plugins: [ { name: "compound-engineering", version: "2.41.0", description: "old" }, ], }, null, 2, ), ) return root } describe("release metadata", () => { test("reports current compound-engineering counts from the repo", async () => { const counts = await getCompoundEngineeringCounts(process.cwd()) expect(counts).toEqual({ agents: 0, skills: 30, mcpServers: 0, }) }) test("builds a stable compound-engineering manifest description", async () => { const description = await buildCompoundEngineeringDescription(process.cwd()) expect(description).toBe( "Brainstorm, plan, debug, review, and compound learnings with AI agents", ) }) test("detects cross-surface version drift even without explicit override versions", async () => { const root = await makeFixtureRoot() const result = await syncReleaseMetadata({ root, write: false }) const changedPaths = result.updates.filter((update) => update.changed).map((update) => update.path) expect(changedPaths).toContain(path.join(root, ".cursor-plugin", "plugin.json")) 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, ".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, ".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("reports Kimi plugin.json version drift without auto-correcting", async () => { const root = await makeFixtureRoot() await writeFile( path.join(root, ".kimi-plugin", "plugin.json"), JSON.stringify( { name: "compound-engineering", version: "2.41.0", skills: "./skills/" }, null, 2, ), ) const result = await syncReleaseMetadata({ root, write: true }) const kimiPath = path.join(root, ".kimi-plugin", "plugin.json") const kimiUpdate = result.updates.find((u) => u.path === kimiPath) expect(kimiUpdate).toBeDefined() expect(kimiUpdate!.changed).toBe(true) const afterContents = JSON.parse(await Bun.file(kimiPath).text()) expect(afterContents.version).toBe("2.41.0") }) test("reports Devin plugin.json version drift without auto-correcting", async () => { const root = await makeFixtureRoot() await writeFile( path.join(root, ".devin-plugin", "plugin.json"), JSON.stringify( { name: "compound-engineering", version: "2.41.0" }, null, 2, ), ) const result = await syncReleaseMetadata({ root, write: true }) const devinPath = path.join(root, ".devin-plugin", "plugin.json") const devinUpdate = result.updates.find((u) => u.path === devinPath) expect(devinUpdate).toBeDefined() expect(devinUpdate!.changed).toBe(true) const afterContents = JSON.parse(await Bun.file(devinPath).text()) expect(afterContents.version).toBe("2.41.0") }) test("reports Grok plugin.json version drift without auto-correcting", async () => { const root = await makeFixtureRoot() await writeFile( path.join(root, ".grok-plugin", "plugin.json"), JSON.stringify( { name: "compound-engineering", version: "2.41.0", skills: "./skills/" }, null, 2, ), ) const result = await syncReleaseMetadata({ root, write: true }) const grokPath = path.join(root, ".grok-plugin", "plugin.json") const grokUpdate = result.updates.find((u) => u.path === grokPath) expect(grokUpdate).toBeDefined() expect(grokUpdate!.changed).toBe(true) const afterContents = JSON.parse(await Bun.file(grokPath).text()) expect(afterContents.version).toBe("2.41.0") }) test("reports missing Grok manifest as a structural error", async () => { const root = await makeFixtureRoot() await Bun.$`rm ${path.join(root, ".grok-plugin", "plugin.json")}`.quiet() const result = await syncReleaseMetadata({ root, write: false }) expect(result.errors.some((err) => err.includes(".grok-plugin/plugin.json is missing"))).toBe(true) }) test("reports self-referential Grok marketplace source as a structural error", async () => { const root = await makeFixtureRoot() await writeFile( path.join(root, ".grok-plugin", "marketplace.json"), JSON.stringify( { name: "compound-engineering", owner: { name: "Kieran Klaassen and Trevin Chow" }, plugins: [ { name: "compound-engineering", source: { type: "local", path: "." } }, ], }, null, 2, ), ) const result = await syncReleaseMetadata({ root, write: false }) expect( result.errors.some( (err) => err.includes(".grok-plugin/marketplace.json") && err.includes("self-referential"), ), ).toBe(true) }) test("reports package.json version drift without auto-correcting", async () => { const root = await makeFixtureRoot() await writeFile( path.join(root, "package.json"), JSON.stringify({ version: "2.41.0" }, null, 2), ) const result = await syncReleaseMetadata({ root, write: true }) const packagePath = path.join(root, "package.json") const packageUpdate = result.updates.find((u) => u.path === packagePath) expect(packageUpdate).toBeDefined() expect(packageUpdate!.changed).toBe(true) const afterContents = JSON.parse(await Bun.file(packagePath).text()) expect(afterContents.version).toBe("2.41.0") }) test("reports Antigravity plugin.json version drift without auto-correcting", async () => { const root = await makeFixtureRoot() await writeFile( path.join(root, "plugin.json"), JSON.stringify({ name: "compound-engineering", version: "2.41.0" }, null, 2), ) const result = await syncReleaseMetadata({ root, write: true }) const antigravityPath = path.join(root, "plugin.json") const antigravityUpdate = result.updates.find((u) => u.path === antigravityPath) expect(antigravityUpdate).toBeDefined() expect(antigravityUpdate!.changed).toBe(true) const afterContents = JSON.parse(await Bun.file(antigravityPath).text()) expect(afterContents.version).toBe("2.41.0") }) test("reports missing Antigravity plugin.json as a structural error", async () => { const root = await makeFixtureRoot() await Bun.$`rm ${path.join(root, "plugin.json")}`.quiet() const result = await syncReleaseMetadata({ root, write: false }) expect(result.errors.some((err) => err.includes("plugin.json is missing"))).toBe(true) }) 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, ".claude-plugin", "plugin.json"), JSON.stringify( { version: "2.42.0", description: "Brainstorm, plan, debug, review, and compound learnings with AI agents", }, null, 2, ), ) await writeFile( path.join(root, ".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, ".codex-plugin", "plugin.json") await syncReleaseMetadata({ root, write: true }) const afterContents = JSON.parse(await Bun.file(codexPath).text()) expect(afterContents.description).toBe( "Brainstorm, plan, debug, review, and compound learnings with AI agents", ) }) test("reports missing Codex manifest as a structural error", async () => { const root = await makeFixtureRoot() await Bun.$`rm ${path.join(root, ".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 missing Kimi manifest as a structural error", async () => { const root = await makeFixtureRoot() await Bun.$`rm ${path.join(root, ".kimi-plugin", "plugin.json")}`.quiet() const result = await syncReleaseMetadata({ root, write: false }) expect(result.errors.some((err) => err.includes(".kimi-plugin/plugin.json is missing"))).toBe(true) }) test("reports missing Devin manifest as a structural error", async () => { const root = await makeFixtureRoot() await Bun.$`rm ${path.join(root, ".devin-plugin", "plugin.json")}`.quiet() const result = await syncReleaseMetadata({ root, write: false }) expect(result.errors.some((err) => err.includes(".devin-plugin/plugin.json is missing"))).toBe(true) // The missing manifest short-circuits (Codex semantics): exactly one // unchanged update entry, never a drift entry for a nonexistent file. const devinPath = path.join(root, ".devin-plugin", "plugin.json") expect(result.updates.filter((u) => u.path === devinPath)).toEqual([ { path: devinPath, changed: false }, ]) }) test("reports Devin plugin.json name mismatch as structural error", async () => { const root = await makeFixtureRoot() await writeFile( path.join(root, ".devin-plugin", "plugin.json"), JSON.stringify({ name: "wrong-name", version: "2.42.0" }, null, 2), ) const result = await syncReleaseMetadata({ root, write: false }) expect( result.errors.some( (err) => err.includes(".devin-plugin/plugin.json") && err.includes('name "wrong-name" does not match expected "compound-engineering"'), ), ).toBe(true) }) test("rewrites Devin plugin.json description on write when drifted from Claude", async () => { const root = await makeFixtureRoot() await writeFile( path.join(root, ".claude-plugin", "plugin.json"), JSON.stringify( { version: "2.42.0", description: "Brainstorm, plan, debug, review, and compound learnings with AI agents", }, null, 2, ), ) await writeFile( path.join(root, ".devin-plugin", "plugin.json"), JSON.stringify( { name: "compound-engineering", version: "2.42.0", description: "stale devin description", }, null, 2, ), ) const devinPath = path.join(root, ".devin-plugin", "plugin.json") await syncReleaseMetadata({ root, write: true }) const afterContents = JSON.parse(await Bun.file(devinPath).text()) expect(afterContents.description).toBe( "Brainstorm, plan, debug, review, and compound learnings with AI agents", ) }) test("reports Codex plugin.json name mismatch as structural error", async () => { const root = await makeFixtureRoot() await writeFile( path.join(root, ".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 compound-engineering Codex manifest. await writeFile( path.join(root, ".codex-plugin", "plugin.json"), JSON.stringify({ name: "compound-engineering", version: "2.42.0" }, null, 2), ) const result = await syncReleaseMetadata({ root, write: false }) expect( result.errors.some( (err) => err.includes("compound-engineering") && err.includes("missing required field") && err.includes("skills"), ), ).toBe(true) }) test("reports missing skills field on Kimi manifest as structural error", async () => { const root = await makeFixtureRoot() await writeFile( path.join(root, ".kimi-plugin", "plugin.json"), JSON.stringify({ name: "compound-engineering", version: "2.42.0" }, null, 2), ) const result = await syncReleaseMetadata({ root, write: false }) expect( result.errors.some( (err) => err.includes(".kimi-plugin/plugin.json") && 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 compound-engineering's skills dir but keep the skills declaration. await Bun.$`rm -rf ${path.join(root, "skills")}`.quiet() const result = await syncReleaseMetadata({ root, write: false }) expect( result.errors.some( (err) => err.includes(".codex-plugin/plugin.json") && 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() // Empty the 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: [], }, 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: "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("reports Codex marketplace root-local plugin source 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", source: { source: "local", path: "./", }, }, ], }, null, 2, ), ) const result = await syncReleaseMetadata({ root, write: false }) expect( result.errors.some( (err) => err.includes(".agents/plugins/marketplace.json") && err.includes("compound-engineering") && err.includes('source.path "./"'), ), ).toBe(true) }) test("reports Kimi marketplace plugin-list mismatch as structural error", async () => { const root = await makeFixtureRoot() await writeFile( path.join(root, ".kimi-plugin", "marketplace.json"), JSON.stringify( { version: "2", plugins: [], }, null, 2, ), ) const result = await syncReleaseMetadata({ root, write: false }) expect( result.errors.some( (err) => err.includes(".kimi-plugin/marketplace.json") && err.includes("does not match"), ), ).toBe(true) }) test("reports Kimi marketplace schema version mismatch as structural error", async () => { const root = await makeFixtureRoot() await writeFile( path.join(root, ".kimi-plugin", "marketplace.json"), JSON.stringify( { version: "1", plugins: [{ id: "compound-engineering" }], }, null, 2, ), ) const result = await syncReleaseMetadata({ root, write: false }) expect( result.errors.some( (err) => err.includes(".kimi-plugin/marketplace.json") && err.includes('schema version "2"'), ), ).toBe(true) }) test("reports Kimi marketplace root-local plugin source as structural error", async () => { const root = await makeFixtureRoot() await writeFile( path.join(root, ".kimi-plugin", "marketplace.json"), JSON.stringify( { version: "2", plugins: [{ id: "compound-engineering", source: "./" }], }, null, 2, ), ) const result = await syncReleaseMetadata({ root, write: false }) expect( result.errors.some( (err) => err.includes(".kimi-plugin/marketplace.json") && err.includes("compound-engineering") && err.includes('source "./"'), ), ).toBe(true) }) test("reports Kimi marketplace missing plugin source as structural error", async () => { const root = await makeFixtureRoot() await writeFile( path.join(root, ".kimi-plugin", "marketplace.json"), JSON.stringify( { version: "2", plugins: [{ id: "compound-engineering" }], }, null, 2, ), ) const result = await syncReleaseMetadata({ root, write: false }) expect( result.errors.some( (err) => err.includes(".kimi-plugin/marketplace.json") && err.includes("compound-engineering") && err.includes("missing required field") && err.includes("source"), ), ).toBe(true) }) test("happy path: fixture with matching native manifests produces no native plugin errors", async () => { const root = await makeFixtureRoot() // Align Claude <-> Codex versions and descriptions so there's no drift. await writeFile( path.join(root, ".claude-plugin", "plugin.json"), JSON.stringify({ version: "2.42.0", description: "aligned description" }, null, 2), ) await writeFile( path.join(root, ".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 nativePluginErrors = result.errors.filter( (err) => err.includes(".codex-plugin") || err.includes(".agents/plugins") || err.includes(".kimi-plugin") || err.includes(".devin-plugin"), ) expect(nativePluginErrors).toEqual([]) }) })