296 lines
9.3 KiB
TypeScript
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")
|
|
})
|
|
})
|