Files
claude-engineering-plugin/tests/skills/ce-polish-beta-project-type.test.ts
2026-04-16 17:55:10 -05:00

341 lines
14 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import { promises as fs } from "fs"
import os from "os"
import path from "path"
const detectProjectType = path.join(
import.meta.dir,
"..",
"..",
"plugins",
"compound-engineering",
"skills",
"ce-polish-beta",
"scripts",
"detect-project-type.sh",
)
const gitEnv = {
...process.env,
GIT_AUTHOR_NAME: "Test",
GIT_AUTHOR_EMAIL: "test@example.com",
GIT_COMMITTER_NAME: "Test",
GIT_COMMITTER_EMAIL: "test@example.com",
}
type RunResult = {
exitCode: number
stdout: string
stderr: string
}
async function runCommand(cmd: string[], cwd: string): Promise<RunResult> {
const proc = Bun.spawn(cmd, {
cwd,
env: gitEnv,
stderr: "pipe",
stdout: "pipe",
})
const [exitCode, stdout, stderr] = await Promise.all([
proc.exited,
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
])
return { exitCode, stdout, stderr }
}
async function initRepo(): Promise<string> {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "ce-polish-projtype-"))
await runCommand(["git", "init", "-b", "main"], root)
return root
}
async function touch(filePath: string, content = ""): Promise<void> {
await fs.mkdir(path.dirname(filePath), { recursive: true })
await fs.writeFile(filePath, content)
}
// ── New framework root detection ────────────────────────────────────────────
describe("detect-project-type.sh — new signatures", () => {
test("nuxt.config.ts at root -> 'nuxt'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "nuxt.config.ts"), "export default {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("nuxt")
})
test("nuxt.config.mjs at root -> 'nuxt'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "nuxt.config.mjs"), "export default {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("nuxt")
})
test("astro.config.mjs at root -> 'astro'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "astro.config.mjs"), "export default {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("astro")
})
test("astro.config.ts at root -> 'astro'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "astro.config.ts"), "export default {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("astro")
})
test("remix.config.js at root -> 'remix'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "remix.config.js"), "module.exports = {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("remix")
})
test("remix.config.ts at root -> 'remix'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "remix.config.ts"), "export default {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("remix")
})
test("svelte.config.js at root -> 'sveltekit'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "svelte.config.js"), "export default {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("sveltekit")
})
test("svelte.config.mjs at root -> 'sveltekit'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "svelte.config.mjs"), "export default {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("sveltekit")
})
})
// ── Monorepo probe ──────────────────────────────────────────────────────────
describe("detect-project-type.sh — monorepo probe", () => {
// Single hit in monorepo
test("apps/web/next.config.js (no root signature) -> 'next@apps/web'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "apps", "web", "next.config.js"), "module.exports = {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("next@apps/web")
})
test("packages/frontend/vite.config.ts (no root signature) -> 'vite@packages/frontend'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "packages", "frontend", "vite.config.ts"), "export default {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("vite@packages/frontend")
})
test("apps/site/nuxt.config.ts (no root signature) -> 'nuxt@apps/site'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "apps", "site", "nuxt.config.ts"), "export default {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("nuxt@apps/site")
})
test("apps/docs/astro.config.mjs (no root signature) -> 'astro@apps/docs'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "apps", "docs", "astro.config.mjs"), "export default {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("astro@apps/docs")
})
// Multiple hits in monorepo
test("multiple next apps in monorepo -> starts with 'multiple:' and contains both", async () => {
const repo = await initRepo()
await touch(path.join(repo, "apps", "web", "next.config.js"), "module.exports = {}\n")
await touch(path.join(repo, "apps", "admin", "next.config.js"), "module.exports = {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
const output = result.stdout.trim()
expect(output.startsWith("multiple:")).toBe(true)
expect(output).toContain("next@apps/web")
expect(output).toContain("next@apps/admin")
})
test("next + rails in monorepo -> starts with 'multiple:' and contains both types", async () => {
const repo = await initRepo()
await touch(path.join(repo, "apps", "web", "next.config.js"), "module.exports = {}\n")
await touch(path.join(repo, "apps", "api", "Gemfile"), "source 'x'\n")
await touch(path.join(repo, "apps", "api", "bin", "dev"), "#!/usr/bin/env bash\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
const output = result.stdout.trim()
expect(output.startsWith("multiple:")).toBe(true)
expect(output).toContain("next@apps/web")
expect(output).toContain("rails@apps/api")
})
// Exclusion list
test("node_modules/next/examples/next.config.js (no root signature) -> 'unknown'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "node_modules", "next", "examples", "next.config.js"), "module.exports = {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("unknown")
})
test("fixtures/sample/next.config.js (no root signature) -> 'unknown'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "fixtures", "sample", "next.config.js"), "module.exports = {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("unknown")
})
// Depth cap
test("depth 4 is too deep -> 'unknown'", async () => {
const repo = await initRepo()
await touch(
path.join(repo, "projects", "app", "web", "client", "next.config.js"),
"module.exports = {}\n",
)
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("unknown")
})
test("depth 2 (apps/web) is within limit -> detected", async () => {
const repo = await initRepo()
await touch(path.join(repo, "apps", "web", "next.config.js"), "module.exports = {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("next@apps/web")
})
test("depth 3 (services/api/server) is exactly at limit -> detected", async () => {
const repo = await initRepo()
await touch(
path.join(repo, "services", "api", "server", "vite.config.ts"),
"export default {}\n",
)
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("vite@services/api/server")
})
// Root wins over monorepo probe
test("rails at root + next inside apps/web -> 'rails' (root wins)", async () => {
const repo = await initRepo()
await touch(path.join(repo, "bin", "dev"), "#!/usr/bin/env bash\n")
await touch(path.join(repo, "Gemfile"), "source 'x'\n")
await touch(path.join(repo, "apps", "web", "next.config.js"), "module.exports = {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("rails")
})
test("next at root + vite inside packages/ui -> 'next' (root wins)", async () => {
const repo = await initRepo()
await touch(path.join(repo, "next.config.js"), "module.exports = {}\n")
await touch(path.join(repo, "packages", "ui", "vite.config.ts"), "export default {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("next")
})
// Still unknown
test("only README.md, no signatures anywhere -> 'unknown'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "README.md"), "# nothing\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("unknown")
})
// Monorepo probe at depth 1
test("apps/web/ with next.config.js directly in it -> 'next@apps/web'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "apps", "web", "next.config.js"), "module.exports = {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("next@apps/web")
})
})
// ── Regressions ─────────────────────────────────────────────────────────────
describe("detect-project-type.sh — regressions", () => {
test("bin/dev + Gemfile at root -> 'rails'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "bin", "dev"), "#!/usr/bin/env bash\n")
await touch(path.join(repo, "Gemfile"), "source 'https://rubygems.org'\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("rails")
})
test("next.config.mjs at root -> 'next'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "next.config.mjs"), "export default {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("next")
})
test("vite.config.ts at root -> 'vite'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "vite.config.ts"), "export default {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("vite")
})
test("Procfile.dev without bin/dev -> 'procfile'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "Procfile.dev"), "web: node server.js\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("procfile")
})
test("Rails (bin/dev+Gemfile) + Procfile.dev -> 'rails' (rails wins, not multiple)", async () => {
const repo = await initRepo()
await touch(path.join(repo, "bin", "dev"), "#!/usr/bin/env bash\n")
await touch(path.join(repo, "Gemfile"), "source 'x'\n")
await touch(path.join(repo, "Procfile.dev"), "web: bin/rails s\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("rails")
})
test("Rails + Next at root -> 'multiple'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "bin", "dev"), "#!/usr/bin/env bash\n")
await touch(path.join(repo, "Gemfile"), "source 'x'\n")
await touch(path.join(repo, "next.config.mjs"), "export default {}\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("multiple")
})
test("No signatures -> 'unknown'", async () => {
const repo = await initRepo()
await touch(path.join(repo, "README.md"), "# nothing\n")
const result = await runCommand(["bash", detectProjectType], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("unknown")
})
})