import { execFile } from "node:child_process"; import { chmod, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { promisify } from "node:util"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; const execFileAsync = promisify(execFile); const TEST_DATABASE_URL = "postgresql://paradigm:paradigm@127.0.0.1:5432/cph_hub_test"; describe("deployment preflight CLI", () => { let root: string; let baseDir: string; let hubDir: string; let persistentDir: string; let marker: string; beforeEach(async () => { root = await mkdtemp(join(tmpdir(), "cph-deployment-preflight-")); baseDir = join(root, "deploy"); hubDir = join(baseDir, "current", "hub"); persistentDir = join(root, "persistent"); marker = join(root, "external-check-ran"); await Promise.all([ mkdir(hubDir, { recursive: true }), mkdir(join(persistentDir, "home"), { recursive: true }), mkdir(join(persistentDir, "state"), { recursive: true }), mkdir(join(persistentDir, "cache"), { recursive: true }), mkdir(join(persistentDir, "workspaces"), { recursive: true }), ]); await Promise.all([ chmod(join(persistentDir, "home"), 0o750), chmod(join(persistentDir, "state"), 0o750), chmod(join(persistentDir, "cache"), 0o750), chmod(join(persistentDir, "workspaces"), 0o750), ]); }); afterEach(async () => { await rm(root, { recursive: true, force: true }); }); it("exits before prerequisite checks when a persistent path overlaps the deployment tree", async () => { const workspaceRoot = join(hubDir, "data", "workspaces"); await mkdir(workspaceRoot, { recursive: true }); const fixture = await createFixture({ workspaceRoot, marker }); const result = await runCli(fixture.args); expect(result.exitCode).toBe(1); expect(result.stderr).toContain("workspace root must not overlap deployment root"); await expect(readFile(marker, "utf8")).rejects.toMatchObject({ code: "ENOENT" }); }); it("returns success only after configuration and external prerequisites pass", async () => { const workspaceRoot = join(persistentDir, "workspaces"); const fixture = await createFixture({ workspaceRoot, marker }); const result = await runCli(fixture.args); expect(result).toMatchObject({ exitCode: 0, stderr: "" }); expect(result.stdout).toContain("[preflight] configuration and prerequisites valid"); expect(await readFile(marker, "utf8")).toContain("cph\npg_isready\n"); }); it("preserves the failed prerequisite exit code and stderr", async () => { const workspaceRoot = join(persistentDir, "workspaces"); const fixture = await createFixture({ workspaceRoot, marker, pgIsReadyScript: "#!/bin/sh\nprintf 'socket permission denied\\n' >&2\nexit 23\n", }); const result = await runCli(fixture.args); expect(result.exitCode).toBe(1); expect(result.stderr).toContain("PostgreSQL is not accepting connections for DATABASE_URL"); expect(result.stderr).toContain("exit=23"); expect(result.stderr).toContain("socket permission denied"); }); it("rejects a database URL that accepts TCP connections but cannot execute a query", async () => { const workspaceRoot = join(persistentDir, "workspaces"); const fixture = await createFixture({ workspaceRoot, marker, databaseUrl: "postgresql://paradigm:paradigm@127.0.0.1:5432/cph_hub_preflight_missing", }); const result = await runCli(fixture.args); expect(result.exitCode).toBe(1); expect(result.stderr).toContain("PostgreSQL authenticated query failed for DATABASE_URL"); }); it("loads the actual Hub dependency graph and queries PostgreSQL in the runtime probe", async () => { const result = await execFileAsync( resolve("node_modules/.bin/tsx"), [resolve("src/deployment/service-runtime-probe.ts")], { env: { ...process.env, DATABASE_URL: TEST_DATABASE_URL, NODE_ENV: "production" }, }, ); expect(result.stderr).toBe(""); expect(result.stdout).toContain("Hub dependencies and PostgreSQL query succeeded"); }); it("probes the built runtime and bubblewrap as the service identity", async () => { const configuredPersistentParent = join(root, "persistent-link"); await symlink(persistentDir, configuredPersistentParent); const workspaceRoot = join(configuredPersistentParent, "workspaces"); const fixture = await createFixture({ workspaceRoot, marker }); const uid = process.getuid?.(); const gid = process.getgid?.(); if (uid === undefined || gid === undefined) throw new Error("service identity probe requires POSIX uid/gid"); const result = await runCli([ ...fixture.args, "--service-uid", String(uid), "--service-gid", String(gid), ]); expect(result).toMatchObject({ exitCode: 0, stderr: "" }); const probeLog = await readFile(marker, "utf8"); expect(probeLog).toContain(workspaceRoot); expect(probeLog).toContain("service-setpriv\n"); expect(probeLog).toContain("service-bwrap\n"); expect(probeLog).toContain("service-prisma\n"); expect(probeLog).toContain("service-database\n"); }); async function createFixture(options: { workspaceRoot: string; marker: string; pgIsReadyScript?: string; databaseUrl?: string; }): Promise<{ args: string[] }> { const binDir = join(root, "bin"); const cphBin = join(binDir, "cph"); const bwrapBin = join(binDir, "bwrap"); const pgIsReadyBin = join(binDir, "pg_isready"); const runuserBin = join(binDir, "runuser"); const setprivBin = join(binDir, "setpriv"); const envFile = join(root, "platform.env"); await Promise.all([ mkdir(binDir, { recursive: true }), mkdir(join(hubDir, "dist"), { recursive: true }), mkdir(join(hubDir, "dist", "deployment"), { recursive: true }), mkdir(join(hubDir, "node_modules", "prisma", "build"), { recursive: true }), mkdir(join(hubDir, "prisma"), { recursive: true }), ]); await Promise.all([ executable(cphBin, `#!/bin/sh\nprintf 'cph\\n' >> '${options.marker}'\nprintf 'cph 0.0.2\\n'\n`), executable(bwrapBin, `#!/bin/sh\nprintf 'service-bwrap\\n' >> '${options.marker}'\nexit 0\n`), executable( runuserBin, `#!/bin/sh\nprintf '%s\\n' "$*" >> '${options.marker}'\n[ "$1" = "-u" ] || exit 91\nshift 2\n[ "$1" = "--" ] || exit 92\nshift\nexec "$@"\n`, ), executable( setprivBin, `#!/bin/sh\n[ "$1" = "--no-new-privs" ] || exit 93\nshift\nprintf 'service-setpriv\\n' >> '${options.marker}'\nexec "$@"\n`, ), executable( pgIsReadyBin, options.pgIsReadyScript ?? `#!/bin/sh\nprintf 'pg_isready\\n' >> '${options.marker}'\nexit 0\n`, ), writeFile(join(hubDir, "dist", "server.js"), "export {};\n"), writeFile( join(hubDir, "dist", "deployment", "service-runtime-probe.js"), `const { appendFileSync } = require("node:fs");\nappendFileSync(${JSON.stringify(options.marker)}, "service-database\\n");\n`, ), writeFile( join(hubDir, "node_modules", "prisma", "build", "index.js"), `const { appendFileSync } = require("node:fs");\nappendFileSync(${JSON.stringify(options.marker)}, "service-prisma\\n");\n`, ), writeFile(join(hubDir, "prisma", "schema.prisma"), "// fixture\n"), ]); await writeFile( envFile, [ "NODE_ENV=production", `DATABASE_URL=${options.databaseUrl ?? TEST_DATABASE_URL}`, "ANTHROPIC_BASE_URL=https://openrouter.ai/api", "ANTHROPIC_AUTH_TOKEN=provider-token", "ANTHROPIC_API_KEY=", "FEISHU_APP_ID=cli_app_id", "FEISHU_APP_SECRET=feishu-app-secret", "FEISHU_BOT_OPEN_ID=ou_bot", `CPH_BIN=${cphBin}`, "HOST=127.0.0.1", "PORT=8788", `HUB_PROJECT_WORKSPACE_ROOT=${options.workspaceRoot}`, "HUB_PUBLIC_BASE_URL=https://hub.example.com", "HUB_SESSION_SECRET=a-production-session-secret-with-32-bytes", "", ].join("\n"), { mode: 0o600 }, ); return { args: [ "--env-file", envFile, "--node-bin", process.execPath, "--runuser-bin", runuserBin, "--setpriv-bin", setprivBin, "--service-user", process.env["USER"] ?? "test-user", "--base-dir", baseDir, "--hub-dir", hubDir, "--service-home", join(persistentDir, "home"), "--state-dir", join(persistentDir, "state"), "--cache-dir", join(persistentDir, "cache"), "--workspace-root", options.workspaceRoot, "--bwrap-bin", bwrapBin, "--pg-isready-bin", pgIsReadyBin, ], }; } }); async function executable(path: string, contents: string): Promise { await writeFile(path, contents); await chmod(path, 0o755); } async function runCli(args: string[]): Promise<{ exitCode: number; stdout: string; stderr: string }> { try { const result = await execFileAsync(resolve("node_modules/.bin/tsx"), [resolve("src/deployment/preflight-cli.ts"), ...args]); return { exitCode: 0, stdout: result.stdout, stderr: result.stderr }; } catch (error) { const failure = error as { code?: number; stdout?: string; stderr?: string }; return { exitCode: failure.code ?? 1, stdout: failure.stdout ?? "", stderr: failure.stderr ?? "" }; } }