import test from "node:test"; import assert from "node:assert/strict"; import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import http from "node:http"; import { setTimeout as sleep } from "node:timers/promises"; function randomPort(): number { return 20_000 + Math.floor(Math.random() * 10_000); } function httpGet( port: number, path: string, ): Promise<{ statusCode: number; body: string; headers: http.IncomingHttpHeaders }> { return new Promise((resolve, reject) => { const req = http.request( { hostname: "127.0.0.1", port, path, method: "GET", timeout: 4_000, }, (res) => { const chunks: Buffer[] = []; res.on("data", (chunk) => { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }); res.on("end", () => { resolve({ statusCode: res.statusCode ?? 0, body: Buffer.concat(chunks).toString("utf8"), headers: res.headers, }); }); }, ); req.on("timeout", () => { req.destroy(new Error(`timeout GET ${path}`)); }); req.on("error", reject); req.end(); }); } async function waitForStartup(child: ChildProcessWithoutNullStreams, port: number, timeoutMs: number): Promise { const started = Date.now(); while (Date.now() - started < timeoutMs) { if (child.exitCode !== null) { throw new Error(`exporter exited early with code ${child.exitCode}`); } try { const res = await httpGet(port, "/healthz"); if ([200, 503].includes(res.statusCode)) return; } catch { // server not up yet } await sleep(125); } throw new Error("timeout waiting for exporter startup"); } test("exporter serves healthz and metrics without crashing", async (t) => { const port = randomPort(); const child = spawn( "pnpm", ["exec", "tsx", "src/index.ts"], { cwd: process.cwd(), env: { ...process.env, RELAYS: "wss://relay.damus.io,wss://nos.lol", WRITE_CHECK_ENABLED: "false", PROBE_TIMEOUT_SECONDS: "2", PROBE_INTERVAL_SECONDS: "3", PORT: String(port), LOG_LEVEL: "error", }, stdio: ["ignore", "pipe", "pipe"], }, ); let stderr = ""; child.stderr.on("data", (d: Buffer) => { stderr += d.toString("utf8"); }); t.after(() => { if (child.exitCode === null) { child.kill("SIGTERM"); } }); await waitForStartup(child, port, 8000); const health = await httpGet(port, "/healthz"); assert.ok([200, 503].includes(health.statusCode), `unexpected /healthz status ${health.statusCode}`); assert.match(health.body, /"status": "(ok|degraded)"/); const metrics = await httpGet(port, "/metrics"); assert.equal(metrics.statusCode, 200); assert.match(metrics.body, /nostr_relay_up/); assert.match(metrics.body, /nostr_relay_open_ok/); assert.match(metrics.body, /nostr_relay_probe_runs_total/); assert.match(metrics.body, /process_cpu_user_seconds_total|nodejs_eventloop_lag_seconds/); await sleep(3500); assert.equal(child.exitCode, null, `exporter crashed unexpectedly: ${stderr}`); });