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 { 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 { 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 { 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") }) })