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

356 lines
12 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import { promises as fs } from "fs"
import os from "os"
import path from "path"
const resolvePort = path.join(
import.meta.dir,
"..",
"..",
"plugins",
"compound-engineering",
"skills",
"ce-polish-beta",
"scripts",
"resolve-port.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-resolve-port-"))
await runCommand(["git", "init", "-b", "main"], root)
return root
}
async function writeJson(filePath: string, data: unknown): Promise<void> {
await fs.mkdir(path.dirname(filePath), { recursive: true })
await fs.writeFile(filePath, JSON.stringify(data, null, 2))
}
async function touch(filePath: string, content = ""): Promise<void> {
await fs.mkdir(path.dirname(filePath), { recursive: true })
await fs.writeFile(filePath, content)
}
describe("resolve-port.sh", () => {
// Explicit override
test("--port 8080 returns 8080", async () => {
const repo = await initRepo()
const result = await runCommand(["bash", resolvePort, repo, "--port", "8080"], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("8080")
})
// Framework config probes
test("next.config.js with port: 4000 returns 4000", async () => {
const repo = await initRepo()
await touch(path.join(repo, "next.config.js"), `module.exports = { server: { port: 4000 } }`)
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("4000")
})
test("next.config.ts with server: { port: 4000 } returns 4000", async () => {
const repo = await initRepo()
await touch(
path.join(repo, "next.config.ts"),
`export default { server: { port: 4000 } }`,
)
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("4000")
})
test("vite.config.ts with server: { port: 8888 } returns 8888", async () => {
const repo = await initRepo()
await touch(
path.join(repo, "vite.config.ts"),
`export default { server: { port: 8888 } }`,
)
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("8888")
})
// Rails
test("config/puma.rb with port 3001 returns 3001 (with --type rails)", async () => {
const repo = await initRepo()
await touch(path.join(repo, "config", "puma.rb"), `port 3001\n`)
const result = await runCommand(["bash", resolvePort, repo, "--type", "rails"], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("3001")
})
test("multiline next.config.js with port on its own line returns port", async () => {
const repo = await initRepo()
await touch(
path.join(repo, "next.config.js"),
["module.exports = {", " server: {", " port: 3000", " }", "}"].join("\n"),
)
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("3000")
})
// Procfile
test("Procfile.dev web line with -p 4567 returns 4567", async () => {
const repo = await initRepo()
await touch(path.join(repo, "Procfile.dev"), "web: bundle exec puma -p 4567\n")
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("4567")
})
test("Procfile.dev web line with compact -p3000 returns 3000", async () => {
const repo = await initRepo()
await touch(path.join(repo, "Procfile.dev"), "web: rails s -p3000\n")
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("3000")
})
// docker-compose
test('docker-compose.yml with ports: ["9000:9000"] returns 9000', async () => {
const repo = await initRepo()
await touch(
path.join(repo, "docker-compose.yml"),
[
"version: '3'",
"services:",
" web:",
" image: myapp",
" ports:",
' - "9000:9000"',
].join("\n") + "\n",
)
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("9000")
})
// package.json
test("package.json dev script with --port 4000 returns 4000", async () => {
const repo = await initRepo()
await writeJson(path.join(repo, "package.json"), {
scripts: {
dev: "next dev --port 4000",
},
})
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("4000")
})
// .env parsing
test(".env PORT=3001 returns 3001", async () => {
const repo = await initRepo()
await touch(path.join(repo, ".env"), "PORT=3001\n")
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("3001")
})
test('.env PORT="3001" returns 3001 (quotes stripped)', async () => {
const repo = await initRepo()
await touch(path.join(repo, ".env"), 'PORT="3001"\n')
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("3001")
})
test(".env PORT='3001' returns 3001 (single quotes stripped)", async () => {
const repo = await initRepo()
await touch(path.join(repo, ".env"), "PORT='3001'\n")
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("3001")
})
test(".env PORT=3001 # dev only returns 3001 (comment stripped)", async () => {
const repo = await initRepo()
await touch(path.join(repo, ".env"), "PORT=3001 # dev only\n")
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("3001")
})
test('.env PORT="3001" # quoted+commented returns 3001', async () => {
const repo = await initRepo()
await touch(path.join(repo, ".env"), 'PORT="3001" # quoted and commented\n')
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("3001")
})
// .env override order
test(".env.local PORT=4000 + .env PORT=3000 -> .env.local wins", async () => {
const repo = await initRepo()
await touch(path.join(repo, ".env.local"), "PORT=4000\n")
await touch(path.join(repo, ".env"), "PORT=3000\n")
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("4000")
})
test(".env.development PORT=4000 + .env PORT=3000 -> .env.development wins", async () => {
const repo = await initRepo()
await touch(path.join(repo, ".env.development"), "PORT=4000\n")
await touch(path.join(repo, ".env"), "PORT=3000\n")
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("4000")
})
test(".env.local PORT=4000 + .env.development PORT=5000 -> .env.local wins", async () => {
const repo = await initRepo()
await touch(path.join(repo, ".env.local"), "PORT=4000\n")
await touch(path.join(repo, ".env.development"), "PORT=5000\n")
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("4000")
})
// Priority: framework config beats .env
test("next.config.js port: 3000 + .env.local PORT=4000 -> framework config wins", async () => {
const repo = await initRepo()
await touch(path.join(repo, "next.config.js"), `module.exports = { server: { port: 3000 } }`)
await touch(path.join(repo, ".env.local"), "PORT=4000\n")
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("3000")
})
test("multiple probes hit -- framework config wins over .env", async () => {
const repo = await initRepo()
await touch(
path.join(repo, "vite.config.ts"),
`export default { server: { port: 7777 } }`,
)
await touch(path.join(repo, ".env"), "PORT=9999\n")
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("7777")
})
// Defaults
test("no probe matches, --type next -> 3000", async () => {
const repo = await initRepo()
const result = await runCommand(["bash", resolvePort, repo, "--type", "next"], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("3000")
})
test("no probe matches, --type vite -> 5173", async () => {
const repo = await initRepo()
const result = await runCommand(["bash", resolvePort, repo, "--type", "vite"], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("5173")
})
test("no probe matches, --type astro -> 4321", async () => {
const repo = await initRepo()
const result = await runCommand(["bash", resolvePort, repo, "--type", "astro"], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("4321")
})
test("no probe matches, --type sveltekit -> 5173", async () => {
const repo = await initRepo()
const result = await runCommand(["bash", resolvePort, repo, "--type", "sveltekit"], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("5173")
})
test("no probe matches, no --type -> 3000", async () => {
const repo = await initRepo()
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("3000")
})
// Error / fallthrough
test("malformed docker-compose.yml -> probe misses, falls through", async () => {
const repo = await initRepo()
await touch(path.join(repo, "docker-compose.yml"), "this is not yaml at all\n")
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("3000")
})
test("next.config.js with computed port: getPort() -> regex misses, falls through to default", async () => {
const repo = await initRepo()
await touch(
path.join(repo, "next.config.js"),
`module.exports = { server: { port: getPort() } }`,
)
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("3000")
})
test('next.config.js with "port: process.env.PORT || 3000" -> probe rejects, falls through', async () => {
const repo = await initRepo()
await touch(
path.join(repo, "next.config.js"),
`module.exports = { server: { port: process.env.PORT || 3000 } }`,
)
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
// The regex should NOT match "port: process.env.PORT || 3000" because it
// contains non-numeric content. Falls through to default.
expect(result.stdout.trim()).toBe("3000")
})
test("positional path doesn't exist -> stderr ERROR: + exit 1", async () => {
const repo = await initRepo()
const result = await runCommand(
["bash", resolvePort, path.join(repo, "nonexistent")],
repo,
)
expect(result.exitCode).toBe(1)
expect(result.stderr).toContain("ERROR:")
})
// Regression: AGENTS.md/CLAUDE.md NOT scanned
test("AGENTS.md mentioning port 8443 -> ignored (returns default 3000)", async () => {
const repo = await initRepo()
await touch(
path.join(repo, "AGENTS.md"),
"# Instructions\n\nThe dev server runs on port 8443.\n",
)
const result = await runCommand(["bash", resolvePort, repo], repo)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe("3000")
})
})