135ea21a61
Fixes the v0.3.4 fresh-box failures: silent CLI under bare-word PATH invocation (SEA argv detection), bootstrap dying on venv creation without an actionable hint (poisoned-partial-venv recovery + apt hint + doctor venv-capability probe + preflight), installer falsely 'verifying' a silent binary, and localhost-only server binding (network mode: 0.0.0.0 + wildcard CORS + hostname-derived API URL — remote browsing zero-config). 38 CLI tests, 3 web tests, 409 ai-service tests green; build/typecheck/lint clean. ---ci--- phase: hotfix milestone: v0.4 status: complete type: hotfix requirements: covered: [REQ-4-001, REQ-4-002, REQ-4-003, REQ-4-004, REQ-4-005] partial: [] ---/ci---
219 lines
8.9 KiB
TypeScript
219 lines
8.9 KiB
TypeScript
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<SpawnResult> => {
|
|
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"));
|
|
});
|