import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs" import { execFileSync } from "child_process" import { tmpdir } from "os" import path from "path" import { describe, expect, test } from "bun:test" const SKILL_PATH = path.join( process.cwd(), "plugins/compound-engineering/skills/ce-update/SKILL.md", ) const SKILL_BODY = readFileSync(SKILL_PATH, "utf8") describe("ce-update SKILL.md", () => { // Regression guard for https://github.com/EveryInc/compound-engineering-plugin/issues/556. // // `CLAUDE_PLUGIN_ROOT` points at the currently-loaded plugin version directory // (e.g. `~/.claude/plugins/cache//compound-engineering/`), // NOT the plugins cache root. Appending `/cache//compound-engineering/` // produces a path that never exists, which caused the cache-probe to fail and // emit `__CE_UPDATE_CACHE_FAILED__` on every healthy install. Has regressed twice. test("does not append a /cache// suffix onto CLAUDE_PLUGIN_ROOT", () => { const antiPattern = /\$\{CLAUDE_PLUGIN_ROOT\}\/cache\// expect( antiPattern.test(SKILL_BODY), "ce-update/SKILL.md reintroduced the ${CLAUDE_PLUGIN_ROOT}/cache/... antipattern — derive the cache dir from dirname \"${CLAUDE_PLUGIN_ROOT}\" instead.", ).toBe(false) }) }) // Regression guard for https://github.com/EveryInc/compound-engineering-plugin/issues/659. // // The marketplace installs plugin contents from `main` HEAD, so the cache // folder basename reflects `plugin.json` at install time — not any release tag. // Comparing the installed folder against the latest GitHub release tag caused // a persistent false-positive "Out of date" whenever `main` was ahead of the // last tag (the normal state between releases), and the prescribed fix // (`claude plugin update ...`) reinstalled the same version, looping forever. // // Rather than grep-testing the literal shell string, this suite extracts the // "Latest upstream version" pre-resolution command and executes it against a // mocked `gh` that returns distinguishable values for `gh api` vs // `gh release list`. The command must report the version from `plugin.json`, // not from release tags. describe("ce-update 'Latest upstream version' pre-resolution command", () => { test("declares a 'Latest upstream version' pre-resolution section", () => { // Fails loudly if the SKILL structure regresses (renamed section, removed // pre-resolution block) so behavioral tests below can rely on it. expect(SKILL_BODY).toMatch(/\*\*Latest upstream version:\*\*\s*\n!`[^`\n]+`/) }) test("returns the version from main's plugin.json, not any release tag", () => { // Chosen so a tag-based fallback would produce a clearly different value // than the plugin.json-based read. Either 1.0.0 or an empty/sentinel // output indicates the command is reading the wrong source. const pluginJsonVersion = "99.0.0" const releaseTagVersion = "1.0.0" const stdout = runUpstreamCommand(extractUpstreamVersionCommand(SKILL_BODY), { pluginJsonVersion, releaseTagVersion, }) expect(stdout).toBe(pluginJsonVersion) }) test("emits __CE_UPDATE_VERSION_FAILED__ when upstream plugin.json cannot be read", () => { // Simulates gh failing entirely (missing auth, offline, rate-limited). // The fallback must produce the sentinel so the skill's decision logic // can stop rather than silently compare against an empty string — a // pipeline-style `|| echo` only catches last-stage failures, and jq on // empty input exits 0 with no output. const stdout = runUpstreamCommand(extractUpstreamVersionCommand(SKILL_BODY), { ghExitCode: 1, }) expect(stdout).toContain("__CE_UPDATE_VERSION_FAILED__") }) }) /** * Extract the shell command between `**Latest upstream version:**` and the * next blank line in SKILL.md. The command sits on the next line wrapped in * backticks with a leading `!` (Claude Code's pre-resolution syntax). */ function extractUpstreamVersionCommand(body: string): string { const match = body.match(/\*\*Latest upstream version:\*\*\s*\n!`([^`\n]+)`/) if (!match) { throw new Error( `Could not extract 'Latest upstream version' pre-resolution command from ${SKILL_PATH}`, ) } return match[1] } type MockOptions = { pluginJsonVersion?: string releaseTagVersion?: string ghExitCode?: number } /** * Run the skill's upstream-version command with a mocked `gh` on PATH. * The mock emits distinct payloads for `gh api` vs `gh release list` so the * test can prove which source the command actually reads from. */ function runUpstreamCommand(command: string, options: MockOptions): string { const { pluginJsonVersion, releaseTagVersion, ghExitCode } = options const mockDir = mkdtempSync(path.join(tmpdir(), "ce-update-gh-")) try { const pluginJsonB64 = pluginJsonVersion ? Buffer.from( JSON.stringify({ name: "compound-engineering", version: pluginJsonVersion }), ).toString("base64") : "" const releaseJson = releaseTagVersion ? JSON.stringify([{ tagName: `compound-engineering-v${releaseTagVersion}` }]) : "[]" // Emulate gh's behaviour without requiring host `jq`: real `gh --jq` uses // gojq embedded in the binary, so neither the skill nor this mock needs // an external jq on PATH. When the skill asks a `--jq` filter that // extracts `.version`, we emit the pre-computed plugin.json version; when // it asks for `.tagName`, we emit the pre-computed release tag. Any other // filter is unexpected and the mock fails loudly so the test doesn't pass // by accident. const ghScript = `#!/bin/bash ${ghExitCode !== undefined ? `exit ${ghExitCode}` : ` subcommand="$1"; shift jq_filter="" while [ $# -gt 0 ]; do case "$1" in --jq) jq_filter="$2"; shift 2 ;; *) shift ;; esac done case "$subcommand" in api) case "$jq_filter" in *'.version'*) printf '%s\\n' '${pluginJsonVersion ?? ""}' ;; '') printf '%s\\n' '{"content":"${pluginJsonB64}"}' ;; *) echo "unexpected --jq filter for gh api: $jq_filter" >&2; exit 2 ;; esac ;; release) # If the skill ever falls back to release-tag lookup, this is what it gets. case "$jq_filter" in *'tagName'*) printf '%s\\n' '${releaseTagVersion ?? ""}' ;; '') printf '%s\\n' '${releaseJson}' ;; *) echo "unexpected --jq filter for gh release: $jq_filter" >&2; exit 2 ;; esac ;; *) exit 1 ;; esac `}` const ghPath = path.join(mockDir, "gh") writeFileSync(ghPath, ghScript) chmodSync(ghPath, 0o755) return execFileSync("bash", ["-c", command], { env: { ...process.env, PATH: `${mockDir}:${process.env.PATH ?? ""}` }, encoding: "utf8", }).trim() } finally { rmSync(mockDir, { recursive: true, force: true }) } }