Files
pikasTech-HWLAB/web/hwlab-cloud-web/scripts/caserun-native-reload-smoke.ts
T

130 lines
4.5 KiB
TypeScript

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<boolean>, timeoutMs = 5000): Promise<void> {
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<Uint8Array>): Promise<void> {
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<boolean> {
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]);
}