Files
claude-engineering-plugin/tests/plugin-path.test.ts

296 lines
9.3 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import { promises as fs } from "fs"
import path from "path"
import os from "os"
async function exists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath)
return true
} catch {
return false
}
}
async function runGit(args: string[], cwd: string, env?: NodeJS.ProcessEnv): Promise<void> {
const proc = Bun.spawn(["git", ...args], {
cwd,
stdout: "pipe",
stderr: "pipe",
env: env ?? process.env,
})
const exitCode = await proc.exited
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`git ${args.join(" ")} failed (exit ${exitCode}).\nstderr: ${stderr}`)
}
}
const gitEnv = {
...process.env,
GIT_AUTHOR_NAME: "Test",
GIT_AUTHOR_EMAIL: "test@example.com",
GIT_COMMITTER_NAME: "Test",
GIT_COMMITTER_EMAIL: "test@example.com",
}
const projectRoot = path.join(import.meta.dir, "..")
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
async function createTestRepo(): Promise<string> {
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "plugin-path-repo-"))
const pluginRoot = path.join(repoRoot, "plugins", "compound-engineering")
await fs.mkdir(path.dirname(pluginRoot), { recursive: true })
await fs.cp(fixtureRoot, pluginRoot, { recursive: true })
await runGit(["init", "-b", "main"], repoRoot, gitEnv)
await runGit(["add", "."], repoRoot, gitEnv)
await runGit(["commit", "-m", "initial"], repoRoot, gitEnv)
return repoRoot
}
describe("plugin-path", () => {
test("clones a branch to a stable cache path", async () => {
const repoRoot = await createTestRepo()
await runGit(["checkout", "-b", "feat/test-branch"], repoRoot, gitEnv)
await runGit(["checkout", "main"], repoRoot, gitEnv)
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "plugin-path-home-"))
const proc = Bun.spawn([
"bun",
"run",
path.join(projectRoot, "src", "index.ts"),
"plugin-path",
"compound-engineering",
"--branch",
"feat/test-branch",
], {
cwd: projectRoot,
stdout: "pipe",
stderr: "pipe",
env: {
...gitEnv,
HOME: tempHome,
COMPOUND_PLUGIN_GITHUB_SOURCE: repoRoot,
},
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
const cacheDir = path.join(tempHome, ".cache", "compound-engineering", "branches", "compound-engineering-feat~test-branch")
const pluginDir = path.join(cacheDir, "plugins", "compound-engineering")
expect(stderr).toContain("claude --plugin-dir")
expect(stdout.trim()).toBe(pluginDir)
expect(await exists(path.join(pluginDir, ".claude-plugin", "plugin.json"))).toBe(true)
})
test("sanitizes branch names with slashes into stable directory names", async () => {
const repoRoot = await createTestRepo()
await runGit(["checkout", "-b", "feat/deep/nested/branch"], repoRoot, gitEnv)
await runGit(["checkout", "main"], repoRoot, gitEnv)
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "plugin-path-sanitize-"))
const proc = Bun.spawn([
"bun",
"run",
path.join(projectRoot, "src", "index.ts"),
"plugin-path",
"compound-engineering",
"--branch",
"feat/deep/nested/branch",
], {
cwd: projectRoot,
stdout: "pipe",
stderr: "pipe",
env: {
...gitEnv,
HOME: tempHome,
COMPOUND_PLUGIN_GITHUB_SOURCE: repoRoot,
},
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("compound-engineering-feat~deep~nested~branch")
expect(stderr).toContain("claude --plugin-dir")
})
test("updates existing checkout on re-run", async () => {
const repoRoot = await createTestRepo()
await runGit(["checkout", "-b", "feat/update-test"], repoRoot, gitEnv)
// Add a marker file on the branch
const markerPath = path.join(repoRoot, "plugins", "compound-engineering", "MARKER.txt")
await fs.writeFile(markerPath, "v1")
await runGit(["add", "."], repoRoot, gitEnv)
await runGit(["commit", "-m", "add marker v1"], repoRoot, gitEnv)
await runGit(["checkout", "main"], repoRoot, gitEnv)
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "plugin-path-update-"))
const cacheDir = path.join(tempHome, ".cache", "compound-engineering", "branches", "compound-engineering-feat~update-test")
const runPluginPath = async () => {
const proc = Bun.spawn([
"bun",
"run",
path.join(projectRoot, "src", "index.ts"),
"plugin-path",
"compound-engineering",
"--branch",
"feat/update-test",
], {
cwd: projectRoot,
stdout: "pipe",
stderr: "pipe",
env: {
...gitEnv,
HOME: tempHome,
COMPOUND_PLUGIN_GITHUB_SOURCE: repoRoot,
},
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
return { stdout, stderr }
}
// First run: clone
const first = await runPluginPath()
expect(first.stderr).toContain("Cloning")
const cachedMarker = path.join(cacheDir, "plugins", "compound-engineering", "MARKER.txt")
expect(await fs.readFile(cachedMarker, "utf-8")).toBe("v1")
// Push a new commit to the branch
await runGit(["checkout", "feat/update-test"], repoRoot, gitEnv)
await fs.writeFile(markerPath, "v2")
await runGit(["add", "."], repoRoot, gitEnv)
await runGit(["commit", "-m", "update marker to v2"], repoRoot, gitEnv)
await runGit(["checkout", "main"], repoRoot, gitEnv)
// Second run: update
const second = await runPluginPath()
expect(second.stderr).toContain("Updating")
expect(await fs.readFile(cachedMarker, "utf-8")).toBe("v2")
})
test("fails with a clear error for a nonexistent branch", async () => {
const repoRoot = await createTestRepo()
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "plugin-path-noexist-"))
const proc = Bun.spawn([
"bun",
"run",
path.join(projectRoot, "src", "index.ts"),
"plugin-path",
"compound-engineering",
"--branch",
"does-not-exist",
], {
cwd: projectRoot,
stdout: "pipe",
stderr: "pipe",
env: {
...gitEnv,
HOME: tempHome,
COMPOUND_PLUGIN_GITHUB_SOURCE: repoRoot,
},
})
const exitCode = await proc.exited
expect(exitCode).not.toBe(0)
})
test("produces distinct cache paths for branches that differ only by slash placement", async () => {
const repoRoot = await createTestRepo()
await runGit(["checkout", "-b", "feat/foo-bar"], repoRoot, gitEnv)
await runGit(["checkout", "main"], repoRoot, gitEnv)
await runGit(["checkout", "-b", "feat-foo/bar"], repoRoot, gitEnv)
await runGit(["checkout", "main"], repoRoot, gitEnv)
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "plugin-path-collision-"))
const runForBranch = async (branch: string) => {
const proc = Bun.spawn([
"bun",
"run",
path.join(projectRoot, "src", "index.ts"),
"plugin-path",
"compound-engineering",
"--branch",
branch,
], {
cwd: projectRoot,
stdout: "pipe",
stderr: "pipe",
env: {
...gitEnv,
HOME: tempHome,
COMPOUND_PLUGIN_GITHUB_SOURCE: repoRoot,
},
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed for branch '${branch}' (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
return stdout.trim()
}
const path1 = await runForBranch("feat/foo-bar")
const path2 = await runForBranch("feat-foo/bar")
expect(path1).not.toBe(path2)
expect(path1).toContain("feat~foo-bar")
expect(path2).toContain("feat-foo~bar")
})
test("fails when plugin name does not exist in the repo", async () => {
const repoRoot = await createTestRepo()
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "plugin-path-noplugin-"))
const proc = Bun.spawn([
"bun",
"run",
path.join(projectRoot, "src", "index.ts"),
"plugin-path",
"nonexistent-plugin",
"--branch",
"main",
], {
cwd: projectRoot,
stdout: "pipe",
stderr: "pipe",
env: {
...gitEnv,
HOME: tempHome,
COMPOUND_PLUGIN_GITHUB_SOURCE: repoRoot,
},
})
const exitCode = await proc.exited
const stderr = await new Response(proc.stderr).text()
expect(exitCode).not.toBe(0)
expect(stderr).toContain("Plugin directory not found")
})
})