import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdtempSync, writeFileSync, mkdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { doctor } from "../src/commands/doctor.ts"; import { bootstrap } from "../src/commands/bootstrap.ts"; import { verify } from "../src/commands/verify.ts"; import { testCtx } from "./helpers.ts"; import type { SpawnResult } from "../src/lib/spawn.ts"; const r0 = (stdout = ""): SpawnResult => ({ code: 0, signal: null, timedOut: false, stdout, stderr: "" }); const r1 = (): SpawnResult => ({ code: 1, signal: null, timedOut: false, stdout: "", stderr: "" }); const r = (stdout: string): SpawnResult => r0(stdout); test("doctor: all prerequisites present exits 0", async () => { const ctx = testCtx({ spawn: async (cmd, args) => { if (cmd === "which") return r0(); if (cmd === "pnpm" && args[0] === "--version") return r0("10.0.0\n"); if (cmd === "python3" && args[0] === "--version") return r0("Python 3.11.2\n"); if (cmd === "git" && args[0] === "--version") return r0("git version 2.39.2\n"); return r0(); }, }); const code = await doctor([], ctx); assert.equal(code, 0); assert.ok(ctx.out().includes("All checks passed")); }); test("doctor: missing pnpm fails with hint, exit 1", async () => { const ctx = testCtx({ spawn: async (cmd, args) => { if (cmd === "which" && args[0] === "pnpm") return { ...r1(), code: 1 }; if (cmd === "which") return r0(); if (cmd === "python3") return r0("Python 3.11.2\n"); if (cmd === "git") return r0("git version 2.39.2\n"); return r0(); }, }); const code = await doctor([], ctx); assert.equal(code, 1); assert.ok(ctx.err().includes("pnpm not found")); assert.ok(ctx.err().includes("corepack")); }); test("doctor: outdated python3 fails with version comparison", async () => { const ctx = testCtx({ spawn: async (cmd, args) => { if (cmd === "which") return r0(); if (cmd === "python3" && args[0] === "--version") return r0("Python 3.9.0\n"); if (cmd === "pnpm") return r0("10.0.0\n"); if (cmd === "git") return r0("git version 2.39.2\n"); return r0(); }, }); const code = await doctor([], ctx); assert.equal(code, 1); assert.ok(ctx.err().includes("3.9")); }); test("bootstrap: outside a repo fails with clone hint", async () => { const dir = mkdtempSync(join(tmpdir(), "nc-norepo-")); const ctx = testCtx({ cwd: dir }); const code = await bootstrap([], ctx); assert.equal(code, 1); assert.ok(ctx.err().includes("git clone")); }); const doctorPassSpawn = async (cmd: string, args: string[]): Promise => { if (cmd === "pnpm" && args[0] === "--version") return r("10.0.0\n"); if (cmd === "python3" && args[0] === "--version") return r("Python 3.11.2\n"); if (cmd === "git") return r("git version 2.39.2\n"); if (cmd === "python3" && args[1] === "venv") return r0(); return r0(); }; test("bootstrap: step order — preflight, pnpm install, venv bootstrap, env copy", async () => { const dir = mkdtempSync(join(tmpdir(), "nc-repo-")); writeFileSync(join(dir, "pnpm-workspace.yaml"), "packages:\n - apps/*\n"); mkdirSync(join(dir, "apps", "ai-service"), { recursive: true }); writeFileSync(join(dir, "apps", "ai-service", ".env.example"), "AI_PORT=8420\nAI_KEY=\n"); const calls: string[] = []; const ctx = testCtx({ cwd: dir, spawn: async (cmd, args) => { calls.push(`${cmd} ${args.join(" ")}`); return doctorPassSpawn(cmd, args); }, }); const code = await bootstrap([], ctx); assert.equal(code, 0); const installIdx = calls.findIndex((c) => c.startsWith("pnpm install")); const venvIdx = calls.findIndex((c) => c.startsWith("bash scripts/bootstrap.sh")); assert.ok(installIdx >= 0, "pnpm install runs"); assert.ok(venvIdx > installIdx, "venv bootstrap runs after pnpm install"); assert.ok(ctx.exists(join(dir, "apps", "ai-service", ".env"))); assert.ok(ctx.out().includes("bootstrap complete")); }); test("bootstrap: existing .env kept, not overwritten", async () => { const dir = mkdtempSync(join(tmpdir(), "nc-keep-")); writeFileSync(join(dir, "pnpm-workspace.yaml"), "packages:\n - apps/*\n"); const aiDir = join(dir, "apps", "ai-service"); mkdirSync(aiDir, { recursive: true }); writeFileSync(join(aiDir, ".env.example"), "AI_PORT=8420\n"); writeFileSync(join(aiDir, ".env"), "AI_PORT=9999\n"); const ctx = testCtx({ cwd: dir, spawn: doctorPassSpawn }); const code = await bootstrap([], ctx); assert.equal(code, 0); assert.ok(ctx.out().includes("kept as-is")); assert.equal(ctx.readFile(join(aiDir, ".env")), "AI_PORT=9999\n"); }); test("bootstrap: failing pnpm install aborts before venv step", async () => { const dir = mkdtempSync(join(tmpdir(), "nc-fail-")); writeFileSync(join(dir, "pnpm-workspace.yaml"), "packages:\n - apps/*\n"); const calls: string[] = []; const ctx = testCtx({ cwd: dir, spawn: async (cmd, args) => { calls.push(`${cmd} ${args.join(" ")}`); if (cmd === "pnpm" && args[0] === "install") return r1(); return doctorPassSpawn(cmd, args); }, }); const code = await bootstrap([], ctx); assert.equal(code, 1); assert.ok(ctx.err().includes("pnpm install failed"), ctx.err()); assert.equal(calls.filter((c) => c.startsWith("bash scripts/bootstrap.sh")).length, 0, "venv step must not run"); }); test("verify: busy port fails with stop-the-process hint", async () => { const dir = mkdtempSync(join(tmpdir(), "nc-ver-")); writeFileSync(join(dir, "pnpm-workspace.yaml"), "packages:\n - apps/*\n"); const aiDir = join(dir, "apps", "ai-service"); mkdirSync(aiDir, { recursive: true }); mkdirSync(join(aiDir, ".venv", "bin"), { recursive: true }); writeFileSync(join(aiDir, ".venv", "bin", "python3"), "#!/bin/sh\nexit 0\n"); writeFileSync(join(aiDir, ".env"), "AI_PORT=8420\n"); mkdirSync(join(dir, "node_modules", ".bin"), { recursive: true }); writeFileSync(join(dir, "node_modules", ".bin", "turbo"), "#!/bin/sh\n"); const ctx = testCtx({ cwd: dir, spawn: async () => r0(), portFree: async () => false, }); const code = await verify([], ctx); assert.equal(code, 1); assert.ok(ctx.err().includes("port 8420 is busy")); assert.ok(ctx.err().includes("stop the process listening on 8420")); }); test("verify: free port passes and reports AI_PORT from .env", async () => { const dir = mkdtempSync(join(tmpdir(), "nc-ver2-")); writeFileSync(join(dir, "pnpm-workspace.yaml"), "packages:\n - apps/*\n"); const aiDir = join(dir, "apps", "ai-service"); mkdirSync(aiDir, { recursive: true }); mkdirSync(join(aiDir, ".venv", "bin"), { recursive: true }); writeFileSync(join(aiDir, ".venv", "bin", "python3"), "#!/bin/sh\nexit 0\n"); writeFileSync(join(aiDir, ".env"), "AI_PORT=8421\n"); mkdirSync(join(dir, "node_modules", ".bin"), { recursive: true }); writeFileSync(join(dir, "node_modules", ".bin", "turbo"), "#!/bin/sh\n"); const ctx = testCtx({ cwd: dir, spawn: async () => r0(), portFree: async () => true, }); const code = await verify([], ctx); assert.equal(code, 0); assert.ok(ctx.out().includes("port 8421 free")); }); test("bootstrap: preflight failure aborts before pnpm install", async () => { const dir = mkdtempSync(join(tmpdir(), "nc-pre-")); writeFileSync(join(dir, "pnpm-workspace.yaml"), "packages:\n - apps/*\n"); const calls: string[] = []; const ctx = testCtx({ cwd: dir, spawn: async (cmd, args) => { calls.push(`${cmd} ${args.join(" ")}`); if (cmd === "which") return r0(); if (cmd === "pnpm" && args[0] === "--version") return r("10.0.0\n"); if (cmd === "python3" && args[0] === "--version") return r("Python 3.11.2\n"); if (cmd === "git") return r("git version 2.39.2\n"); if (cmd === "python3" && args[1] === "venv") return r1(); if (cmd === "rm") return r0(); return r0(); }, }); const code = await bootstrap([], ctx); assert.equal(code, 1); assert.ok(ctx.err().includes("preflight failed")); assert.equal(calls.filter((c) => c.startsWith("pnpm install")).length, 0, "pnpm install must not run when preflight fails"); }); test("doctor: venv-capability probe failure surfaces apt hint", async () => { const ctx = testCtx({ spawn: async (cmd, args) => { if (cmd === "which") return r0(); if (cmd === "pnpm") return r("10.0.0\n"); if (cmd === "python3" && args[0] === "--version") return r("Python 3.12.3\n"); if (cmd === "git") return r("git version 2.43.0\n"); if (cmd === "node") return r("v24.0.0\n"); if (cmd === "python3" && args[1] === "venv") return r1(); if (cmd === "rm") return r0(); return r0(); }, }); const code = await doctor([], ctx); assert.equal(code, 1); assert.ok(ctx.err().includes("cannot create virtual environments")); assert.ok(ctx.err().includes("apt install python")); assert.ok(ctx.err().includes("-venv")); });