nexus / scripts /preview.test.mjs
betterwithage's picture
deploy(hf): sync szl-holdings/nexus@00edebc993282efd5b43b0144b3d3d27ab43e264 derived COPY set
31b3c47 verified
Raw History Blame Contribute Delete
9.21 kB
import assert from "node:assert/strict";
import { test } from "node:test";
import {
looksLikePreviewProcess,
parseListenerInodes,
parsePgid,
parsePid,
parsePreviewArgs,
previewOwners,
stopOutcome,
terminatePids,
} from "./preview.mjs";
test("parsePreviewArgs accepts the two actions", () => {
for (const action of ["stop", "restart"]) {
assert.deepEqual(parsePreviewArgs([action]), { action });
}
});
test("parsePreviewArgs rejects missing, unknown and extra arguments", () => {
assert.match(parsePreviewArgs([]).error, /usage:/);
assert.match(parsePreviewArgs(["reload"]).error, /unknown action: reload/);
assert.match(parsePreviewArgs(["start"]).error, /unknown action: start/);
assert.match(parsePreviewArgs(["restart", "--force"]).error, /unexpected argument: --force/);
});
test("parsePid reads a pidfile and rejects junk", () => {
assert.equal(parsePid("4321\n"), 4321);
assert.equal(parsePid(" 99 "), 99);
assert.equal(parsePid(""), null);
assert.equal(parsePid("not-a-pid"), null);
assert.equal(parsePid("-7"), null);
// pid 1 is the sandbox init, never a preview server.
assert.equal(parsePid("1"), null);
});
test("parsePgid reads the pgrp field past a comm containing spaces", () => {
assert.equal(parsePgid("4321 (node) S 4300 4321 4321 0 -1 4194304 1234"), 4321);
assert.equal(parsePgid("4321 (npm run preview) S 4300 4200 4200 0 -1 0 0"), 4200);
assert.equal(parsePgid("4321 (weird ) name) S 4300 4200 4200 0"), 4200);
assert.equal(parsePgid("garbage"), null);
assert.equal(parsePgid(""), null);
assert.equal(parsePgid(undefined), null);
});
// One /proc/net/tcp row; the socket inode is column 10.
const tcpRow = (sl, local, state, inode) =>
[
` ${sl}:`,
local,
"00000000:0000",
state,
"00000000:00000000",
"00:00000000",
"00000000",
"1000",
"0",
inode,
"1",
"0000",
"100",
].join(" ");
const PROC_NET_TCP = [
" sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode",
tcpRow(0, "0100007F:1F91", "0A", 5551),
tcpRow(1, "0100007F:1F90", "0A", 5552),
tcpRow(2, "0100007F:1F91", "01", 5553),
"",
].join("\n");
test("parseListenerInodes picks LISTEN sockets on the wanted port only", () => {
// 0x1F91 = 8081 (LISTEN), 0x1F90 = 8080, and the third row is ESTABLISHED.
assert.deepEqual(parseListenerInodes(PROC_NET_TCP, 8081), ["5551"]);
assert.deepEqual(parseListenerInodes(PROC_NET_TCP, 8080), ["5552"]);
assert.deepEqual(parseListenerInodes(PROC_NET_TCP, 9999), []);
assert.deepEqual(parseListenerInodes("", 8081), []);
assert.deepEqual(parseListenerInodes(undefined, 8081), []);
});
test("parseListenerInodes reads the tcp6 dump the same way", () => {
const v6 = "00000000000000000000000000000000:1F91";
assert.deepEqual(parseListenerInodes(tcpRow(0, v6, "0A", 7777), 8081), ["7777"]);
});
// /proc/<pid>/cmdline is NUL-separated.
const cmdline = (...argv) => argv.join("\u0000");
test("looksLikePreviewProcess matches the npm wrapper and its vite child", () => {
const npmRun = cmdline("node", "/usr/lib/node_modules/npm/bin/npm-cli.js", "run", "preview");
assert.equal(looksLikePreviewProcess(npmRun), true);
assert.equal(looksLikePreviewProcess(cmdline("npm", "run", "preview")), true);
assert.equal(
looksLikePreviewProcess(cmdline("node", "/ws/node_modules/.bin/vite", "preview")),
true,
);
// `ps -o command=` output is space-separated.
assert.equal(looksLikePreviewProcess("node /ws/node_modules/.bin/vite preview"), true);
});
test("looksLikePreviewProcess spares the sibling scripts and re-used pids", () => {
// The sandbox service runs this one in the same box (CapturePreviewThumbnail).
const thumbnail = cmdline(
"node",
"/opt/app-template/scripts/preview-thumbnail.mjs",
"http://127.0.0.1:8080/",
"/tmp/preview-thumbnail.png",
);
assert.equal(looksLikePreviewProcess(thumbnail), false);
assert.equal(looksLikePreviewProcess(cmdline("node", "scripts/preview.mjs", "stop")), false);
// This tooling's own npm wrappers, which carry no `.mjs` in their cmdline.
const npmCli = "/usr/lib/node_modules/npm/bin/npm-cli.js";
assert.equal(looksLikePreviewProcess(cmdline("node", npmCli, "run", "preview:stop")), false);
assert.equal(looksLikePreviewProcess(cmdline("node", npmCli, "run", "preview:restart")), false);
assert.equal(looksLikePreviewProcess(cmdline("npm", "run", "preview:stop")), false);
const viteBuild = cmdline("vite", "build", "--outDir", "preview-dist");
assert.equal(looksLikePreviewProcess(viteBuild), false);
assert.equal(looksLikePreviewProcess(cmdline("/usr/bin/preview-tool", "--x")), false);
assert.equal(looksLikePreviewProcess(cmdline("sleep", "300")), false);
assert.equal(looksLikePreviewProcess(cmdline("node", "server.mjs")), false);
assert.equal(looksLikePreviewProcess(""), false);
});
test("previewOwners trusts port owners and dedupes the pidfile pid", () => {
const owners = previewOwners({
portPids: [50, 51],
pidFilePid: 50,
cmdlineOf: () => assert.fail("a port owner needs no corroboration"),
});
assert.deepEqual(owners, [50, 51]);
});
test("previewOwners adds a pidfile pid whose command line is still the preview", () => {
const owners = previewOwners({
portPids: [51],
pidFilePid: 50,
cmdlineOf: (pid) => (pid === 50 ? cmdline("node", "npm-cli.js", "run", "preview") : ""),
});
assert.deepEqual(owners, [51, 50]);
});
test("previewOwners drops a stale pidfile pid re-used by another process", () => {
const owners = previewOwners({
portPids: [],
pidFilePid: 50,
cmdlineOf: () => cmdline("sleep", "300"),
});
assert.deepEqual(owners, []);
});
test("previewOwners drops a pidfile pid that no longer exists", () => {
const owners = previewOwners({ portPids: [], pidFilePid: 50, cmdlineOf: () => "" });
assert.deepEqual(owners, []);
});
test("previewOwners on a free port with no pidfile signals nothing", () => {
const owners = previewOwners({ portPids: [], pidFilePid: null, cmdlineOf: () => "" });
assert.deepEqual(owners, []);
});
test("stopOutcome fails when a pid survives or the port is still held", () => {
const stubbornOnly = stopOutcome({
signalled: [50],
stubborn: [50],
after: { pids: [] },
});
assert.equal(stubbornOnly.ok, false);
assert.match(stubbornOnly.error, /still held by pid\(s\) 50/);
const newOwner = stopOutcome({
signalled: [50],
stubborn: [],
after: { pids: [77] },
});
assert.equal(newOwner.ok, false);
assert.match(newOwner.error, /still held by pid\(s\) 77/);
});
test("stopOutcome reports a verified free port", () => {
const stopped = stopOutcome({
signalled: [50],
stubborn: [],
after: { pids: [] },
});
assert.equal(stopped.ok, true);
assert.match(stopped.message, /stopped pid\(s\) 50 — port 8081 is free/);
const idle = stopOutcome({ signalled: [], stubborn: [], after: { pids: [] } });
assert.equal(idle.ok, true);
assert.match(idle.message, /nothing was listening on 8081/);
});
test("stopOutcome fails on a listener whose owner cannot be attributed", () => {
const outcome = stopOutcome({
signalled: [],
stubborn: [],
after: { pids: [], unattributed: true },
});
assert.equal(outcome.ok, false);
assert.match(outcome.error, /port 8081 is held by a process this script cannot see/);
});
function fakeProcesses({ pids, ignoresTerm = [] }) {
const alive = new Set(pids);
const signals = [];
return {
signals,
isAlive: (pid) => alive.has(pid),
kill: (pid, signal) => {
signals.push([pid, signal]);
if (signal === "SIGKILL" || !ignoresTerm.includes(pid)) alive.delete(pid);
},
sleep: async () => {},
};
}
test("terminatePids SIGTERMs live pids and skips dead ones", async () => {
const fake = fakeProcesses({ pids: [11, 12] });
const result = await terminatePids([11, 12, 13], fake);
assert.deepEqual(result, { signalled: [11, 12], killed: [], stubborn: [] });
assert.deepEqual(fake.signals, [
[11, "SIGTERM"],
[12, "SIGTERM"],
]);
});
test("terminatePids escalates to SIGKILL when SIGTERM is ignored", async () => {
const fake = fakeProcesses({ pids: [11, 12], ignoresTerm: [12] });
const result = await terminatePids([11, 12], fake);
assert.deepEqual(result, { signalled: [11, 12], killed: [12], stubborn: [] });
assert.deepEqual(fake.signals, [
[11, "SIGTERM"],
[12, "SIGTERM"],
[12, "SIGKILL"],
]);
});
test("terminatePids reports a pid that survives SIGKILL as stubborn", async () => {
const signals = [];
const result = await terminatePids([11], {
isAlive: () => true,
kill: (pid, signal) => signals.push([pid, signal]),
sleep: async () => {},
});
assert.deepEqual(result, { signalled: [11], killed: [11], stubborn: [11] });
assert.deepEqual(signals, [
[11, "SIGTERM"],
[11, "SIGKILL"],
]);
});
test("terminatePids on a free port signals nothing", async () => {
const fake = fakeProcesses({ pids: [] });
const result = await terminatePids([], fake);
assert.deepEqual(result, { signalled: [], killed: [], stubborn: [] });
assert.deepEqual(fake.signals, []);
});