import { readFileSync, renameSync, rmSync, statSync, utimesSync } from "node:fs"; import { resolve } from "node:path"; const root = process.cwd(); const watchedPath = resolve(root, "scripts/caserun-native-fixtures.ts"); const displacedPath = `${watchedPath}.reload-smoke`; const originalStat = statSync(watchedPath); const port = Number(process.env.HWLAB_CASERUN_RELOAD_SMOKE_PORT || 14316); const baseUrl = `http://127.0.0.1:${port}`; const reloadEvents: Array<{ pid?: number; reason?: string }> = []; let output = ""; let displaced = false; function requireCondition(condition: unknown, message: string): asserts condition { if (!condition) throw new Error(message); } async function waitFor(description: string, condition: () => boolean | Promise, timeoutMs = 5000): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (await condition()) return; await Bun.sleep(50); } throw new Error(`timeout waiting for ${description}`); } async function readOutput(stream: ReadableStream): Promise { const reader = stream.getReader(); const decoder = new TextDecoder(); let pending = ""; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); output += chunk; pending += chunk; const lines = pending.split("\n"); pending = lines.pop() || ""; for (const line of lines) { if (!line.includes('"event":"caserun-native-reload"')) continue; const event = JSON.parse(line) as { pid?: number; reason?: string }; if (!reloadEvents.some((existing) => existing.pid === event.pid)) reloadEvents.push(event); } } const finalChunk = decoder.decode(); output += finalChunk; pending += finalChunk; if (pending.includes('"event":"caserun-native-reload"')) { const event = JSON.parse(pending) as { pid?: number; reason?: string }; if (!reloadEvents.some((existing) => existing.pid === event.pid)) reloadEvents.push(event); } } async function healthAvailable(): Promise { try { const response = await fetch(`${baseUrl}/health`); return response.ok; } catch { return false; } } function childPids(supervisorPid: number): number[] { try { return readFileSync(`/proc/${supervisorPid}/task/${supervisorPid}/children`, "utf8") .trim() .split(/\s+/) .filter(Boolean) .map(Number); } catch { return []; } } rmSync(displacedPath, { force: true }); const supervisor = Bun.spawn([process.execPath, "scripts/caserun-native-supervisor.ts"], { cwd: root, env: { ...process.env, HWLAB_CASERUN_NATIVE_PORT: String(port) }, stdin: "ignore", stdout: "pipe", stderr: "pipe" }); const stdoutTask = readOutput(supervisor.stdout); const stderrTask = readOutput(supervisor.stderr); try { await waitFor("initial native health", healthAvailable); renameSync(watchedPath, displacedPath); displaced = true; await Bun.sleep(300); requireCondition(supervisor.exitCode === null, `supervisor exited during atomic save window: ${supervisor.exitCode}`); renameSync(displacedPath, watchedPath); displaced = false; const restoredAt = new Date(Date.now() + 1000); utimesSync(watchedPath, restoredAt, restoredAt); await waitFor("reload after atomic save", () => reloadEvents.length >= 2); for (let index = 0; index < 5; index += 1) { const changedAt = new Date(Date.now() + (index + 1) * 1000); utimesSync(watchedPath, changedAt, changedAt); await Bun.sleep(120); } await waitFor("final native health", healthAvailable); await Bun.sleep(300); const liveChildren = childPids(supervisor.pid); requireCondition(liveChildren.length === 1, `expected one native server child, found ${liveChildren.join(",") || "none"}`); requireCondition(reloadEvents.length >= 3, `expected repeated reloads, found ${reloadEvents.length}`); requireCondition(!output.includes("EADDRINUSE"), "native reload logged EADDRINUSE"); console.log(JSON.stringify({ ok: true, mode: "native-test", supervisorPid: supervisor.pid, serverPid: liveChildren[0], reloadCount: reloadEvents.length, reloadPids: reloadEvents.map((event) => event.pid), atomicSaveMissingFileTolerated: true, eaddrinuse: false })); } finally { if (displaced) renameSync(displacedPath, watchedPath); rmSync(displacedPath, { force: true }); utimesSync(watchedPath, originalStat.atime, originalStat.mtime); if (supervisor.exitCode === null) supervisor.kill("SIGTERM"); await supervisor.exited; await Promise.all([stdoutTask, stderrTask]); }