|
| 1 | +import assert from "node:assert/strict"; |
| 2 | +import { spawn } from "node:child_process"; |
| 3 | +import { access, mkdir, mkdtemp, rm } from "node:fs/promises"; |
| 4 | +import { tmpdir } from "node:os"; |
| 5 | +import { join } from "node:path"; |
| 6 | +import { createServer } from "node:http"; |
| 7 | +import { runProcess } from "../operations/child-process.mjs"; |
| 8 | + |
| 9 | +const repoRoot = process.cwd(); |
| 10 | +const timeoutMs = Number.parseInt(process.env.PROMPT_REFINER_PACKAGE_SMOKE_TIMEOUT_MS || "120000", 10); |
| 11 | +const tempRoot = await mkdtemp(join(tmpdir(), "prompt-refiner-package-smoke-")); |
| 12 | +const packageDir = join(tempRoot, "package"); |
| 13 | +const prefixDir = join(tempRoot, "prefix"); |
| 14 | +const runtimeDir = join(tempRoot, "runtime"); |
| 15 | +const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm"; |
| 16 | +const npmExecPath = process.env.npm_execpath; |
| 17 | +let runtime; |
| 18 | + |
| 19 | +try { |
| 20 | + await mkdir(packageDir, { recursive: true }); |
| 21 | + await mkdir(runtimeDir, { recursive: true }); |
| 22 | + const packOutput = await runNpm(["pack", "--json", "--pack-destination", packageDir]); |
| 23 | + const packed = JSON.parse(packOutput.stdout); |
| 24 | + const tarball = join(packageDir, packed[0].filename); |
| 25 | + |
| 26 | + await runNpm(["install", "--global", "--prefix", prefixDir, "--no-fund", tarball]); |
| 27 | + |
| 28 | + const bin = process.platform === "win32" |
| 29 | + ? join(prefixDir, "gemini-prompt-refiner.cmd") |
| 30 | + : join(prefixDir, "bin", "gemini-prompt-refiner"); |
| 31 | + const installedEntry = join(prefixDir, "node_modules", "gemini-prompt-refiner", "dist", "src", "index.js"); |
| 32 | + await access(bin); |
| 33 | + await access(installedEntry); |
| 34 | + const port = await reservePort(); |
| 35 | + |
| 36 | + runtime = spawn(process.execPath, [installedEntry], { |
| 37 | + cwd: runtimeDir, |
| 38 | + env: { |
| 39 | + ...process.env, |
| 40 | + PORT: String(port), |
| 41 | + PROMPT_REFINER_BACKGROUND: "true", |
| 42 | + }, |
| 43 | + stdio: ["ignore", "pipe", "pipe"], |
| 44 | + windowsHide: true, |
| 45 | + }); |
| 46 | + |
| 47 | + let stdout = ""; |
| 48 | + let stderr = ""; |
| 49 | + runtime.stdout.setEncoding("utf8"); |
| 50 | + runtime.stderr.setEncoding("utf8"); |
| 51 | + runtime.stdout.on("data", chunk => stdout += chunk); |
| 52 | + runtime.stderr.on("data", chunk => stderr += chunk); |
| 53 | + |
| 54 | + await waitForHealth(port, () => `${stdout}\n${stderr}`, timeoutMs); |
| 55 | + console.log(`Package runtime smoke passed: installed ${packed[0].name}-${packed[0].version} and served /api/health on ${port}.`); |
| 56 | +} finally { |
| 57 | + if (runtime && !runtime.killed) { |
| 58 | + runtime.kill("SIGTERM"); |
| 59 | + await waitForClose(runtime, 5_000); |
| 60 | + } |
| 61 | + await rm(tempRoot, { recursive: true, force: true }); |
| 62 | +} |
| 63 | + |
| 64 | +async function waitForClose(child, timeoutMs) { |
| 65 | + if (child.exitCode !== null || child.signalCode !== null) return; |
| 66 | + await Promise.race([ |
| 67 | + new Promise(resolve => child.once("close", resolve)), |
| 68 | + new Promise((resolve) => { |
| 69 | + setTimeout(() => { |
| 70 | + if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL"); |
| 71 | + resolve(); |
| 72 | + }, timeoutMs); |
| 73 | + }), |
| 74 | + ]); |
| 75 | +} |
| 76 | + |
| 77 | +async function reservePort() { |
| 78 | + const server = createServer(); |
| 79 | + await new Promise((resolve, reject) => { |
| 80 | + server.once("error", reject); |
| 81 | + server.listen(0, "127.0.0.1", resolve); |
| 82 | + }); |
| 83 | + const address = server.address(); |
| 84 | + assert.ok(address && typeof address !== "string", "Port reservation failed."); |
| 85 | + const port = address.port; |
| 86 | + await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())); |
| 87 | + return port; |
| 88 | +} |
| 89 | + |
| 90 | +function runNpm(args) { |
| 91 | + return npmExecPath |
| 92 | + ? runProcess(process.execPath, [npmExecPath, ...args], { cwd: repoRoot, timeoutMs }) |
| 93 | + : runProcess(npmCommand, args, { cwd: repoRoot, timeoutMs }); |
| 94 | +} |
| 95 | + |
| 96 | +async function waitForHealth(port, readLogs, deadlineMs) { |
| 97 | + const deadline = Date.now() + deadlineMs; |
| 98 | + let lastError = ""; |
| 99 | + while (Date.now() < deadline) { |
| 100 | + try { |
| 101 | + const response = await fetch(`http://127.0.0.1:${port}/api/health`); |
| 102 | + if (response.ok) { |
| 103 | + const body = await response.json(); |
| 104 | + assert.equal(body.runtime.status, "online"); |
| 105 | + return; |
| 106 | + } |
| 107 | + lastError = `HTTP ${response.status}`; |
| 108 | + } catch (error) { |
| 109 | + lastError = error instanceof Error ? error.message : String(error); |
| 110 | + } |
| 111 | + await new Promise(resolve => setTimeout(resolve, 250)); |
| 112 | + } |
| 113 | + throw new Error(`Package runtime did not become healthy within ${deadlineMs}ms. Last error: ${lastError}\n${readLogs()}`); |
| 114 | +} |
0 commit comments