Files
learnharness/scripts/smoke-image-runtime.mjs
T

229 lines
10 KiB
JavaScript
Raw Normal View History

/**
* OFFLINE image runtime gate: exercise the shipped Photon/WASM resize core and
* actual worker, with file modules confined to the standalone payload. No app,
* provider, canvas, or network needed; a tiny synthetic PNG is generated here.
*
* node scripts/smoke-image-runtime.mjs [--standalone <dir>] [--runtime <Node/Electron>]
*/
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import * as fs from "node:fs";
import { createRequire } from "node:module";
import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { Worker } from "node:worker_threads";
import { deflateSync } from "node:zlib";
import { formatProbeError, probeEnv } from "./smoke-demo-runtime.mjs";
export function verifyImageRuntime(standalone, runtime = process.execPath) {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "lh-image-runtime-"));
try {
fs.writeFileSync(path.join(home, "auth.json"), "{}\n", { mode: 0o600 });
const result = spawnSync(runtime, [fileURLToPath(import.meta.url), "--probe", path.resolve(standalone)], {
cwd: home,
env: { ...probeEnv(process.env, home), ELECTRON_RUN_AS_NODE: "1" },
encoding: "utf8", timeout: 30_000, killSignal: "SIGKILL", maxBuffer: 128 * 1024,
});
if (result.status !== 0 || result.error) {
throw new Error(`image runtime probe failed (${result.error?.message ?? result.status}):\n${result.stdout ?? ""}${result.stderr ?? ""}`);
}
assert.match(result.stdout, /Photon in-process and worker resizing passed/, "probe exited without success marker");
process.stdout.write(result.stdout);
} finally { fs.rmSync(home, { recursive: true, force: true }); }
}
// Self-contained so the eval worker can install the SAME guards using only
// builtins, before importing any payload module. Module hooks do not otherwise
// propagate from the dedicated probe process into Worker threads.
function installGuards(require, standalone) {
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const { fileURLToPath } = require("node:url");
const { registerHooks, syncBuiltinESMExports } = require("node:module");
const root = fs.realpathSync(standalone);
const inside = (file) => {
const real = fs.realpathSync(file);
const relative = path.relative(root, real);
assert.ok(relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative),
`dependency escaped standalone payload: ${file}`);
return real;
};
const photonRoot = path.join(root, "node_modules/@silvia-odwyer/photon-node");
const manifest = JSON.parse(fs.readFileSync(inside(path.join(photonRoot, "package.json")), "utf8"));
assert.equal(manifest.version, "0.3.4", "unexpected Photon engine version");
const photonEntry = inside(path.join(photonRoot, "photon_rs.js"));
const photonWasm = inside(path.join(photonRoot, "photon_rs_bg.wasm"));
assert.ok(fs.statSync(photonWasm).size > 0, "empty Photon WASM asset");
let attempts = 0;
const deny = () => { attempts++; throw new Error("OFFLINE image gate: network forbidden"); };
globalThis.fetch = deny;
globalThis.WebSocket = class { constructor() { deny(); } };
for (const [name, methods] of [
["http", ["request", "get"]], ["https", ["request", "get"]],
["net", ["connect", "createConnection"]], ["tls", ["connect"]],
["dgram", ["createSocket"]],
["dns", ["lookup", "resolve", "resolve4", "resolve6", "resolveAny", "reverse"]],
["dns/promises", ["lookup", "resolve", "resolve4", "resolve6", "resolveAny", "reverse"]],
]) {
const module = require(`node:${name}`);
for (const method of methods) module[method] = deny;
}
require("node:net").Socket.prototype.connect = deny;
const loaded = new Set();
const wasmReads = new Set();
const originalRead = fs.readFileSync;
fs.readFileSync = function (file, ...args) {
const filename = file instanceof URL ? fileURLToPath(file) : file;
const wasmFile = typeof filename === "string" && filename.endsWith(".wasm") ? inside(filename) : null;
const bytes = originalRead.call(this, file, ...args);
if (wasmFile) wasmReads.add(wasmFile);
return bytes;
};
syncBuiltinESMExports();
const hooks = registerHooks({
resolve(specifier, context, nextResolve) {
const result = nextResolve(specifier, context);
if (result.url.startsWith("file:")) loaded.add(inside(fileURLToPath(result.url)));
else assert.ok(result.url.startsWith("node:"), `unexpected dependency URL: ${result.url}`);
return result;
},
});
return {
report() {
assert.equal(attempts, 0, "image runtime attempted network access");
assert.ok(loaded.has(photonEntry), "Photon JS was not loaded from the payload");
assert.ok(wasmReads.has(photonWasm), "Photon WASM was not loaded from the payload");
return { modules: loaded.size, wasmFiles: [...wasmReads], networkAttempts: attempts };
},
cleanup() {
hooks.deregister();
fs.readFileSync = originalRead;
syncBuiltinESMExports();
},
};
}
/** Pure Node 2×2 RGBA PNG, with valid PNG CRCs and zlib image data. */
function fixturePng() {
const chunk = (type, data) => {
const buffer = Buffer.alloc(data.length + 12);
buffer.writeUInt32BE(data.length);
buffer.write(type, 4, "ascii");
data.copy(buffer, 8);
let crc = ~0;
for (const byte of buffer.subarray(4, -4)) {
crc ^= byte;
for (let bit = 0; bit < 8; bit++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
}
buffer.writeUInt32BE(~crc >>> 0, buffer.length - 4);
return buffer;
};
const header = Buffer.alloc(13);
header.writeUInt32BE(2, 0);
header.writeUInt32BE(2, 4);
header[8] = 8; // bit depth
header[9] = 6; // RGBA
const pixels = Buffer.from([
0, 255, 0, 0, 255, 0, 255, 0, 255,
0, 0, 0, 255, 255, 255, 255, 255, 255,
]);
return Buffer.concat([Buffer.from("89504e470d0a1a0a", "hex"), chunk("IHDR", header), chunk("IDAT", deflateSync(pixels)), chunk("IEND", Buffer.alloc(0))]);
}
function assertResized(result) {
assert.ok(result, "resize returned null (missing/broken Photon must not silently pass)");
assert.equal(result.originalWidth, 2);
assert.equal(result.originalHeight, 2);
assert.equal(result.width, 1);
assert.equal(result.height, 1);
assert.equal(result.wasResized, true);
assert.equal(result.mimeType, "image/png");
const png = Buffer.from(result.data, "base64");
assert.equal(png.subarray(0, 8).toString("hex"), "89504e470d0a1a0a");
assert.equal(png.readUInt32BE(16), 1);
assert.equal(png.readUInt32BE(20), 1);
}
async function probeWorker(root, inputBytes, options, responseType) {
const readyType = "lh:image-runtime-ready";
const worker = new Worker(`
const { parentPort, workerData } = require("node:worker_threads");
const guards = (${installGuards.toString()})(require, workerData.root);
const post = parentPort.postMessage.bind(parentPort);
parentPort.postMessage = (message, ...args) => {
if (message?.type === workerData.responseType) {
post({ ...message, runtimeProof: guards.report() }, ...args);
} else post(message, ...args);
};
void import(workerData.entry).then(() => post({ type: workerData.readyType }));
`, {
eval: true,
workerData: { root, responseType, readyType, entry: pathToFileURL(path.join(root, "src/agent/image-processing/resize-worker.mjs")).href },
});
try {
return await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("image resize worker timed out")), 10_000);
const fail = (error) => { clearTimeout(timer); reject(error); };
let ready = false;
worker.once("error", fail);
worker.once("exit", (code) => fail(new Error(`image resize worker exited before response: ${code}`)));
worker.on("message", (message) => {
try {
if (message?.type === readyType) {
assert.equal(ready, false, "duplicate worker READY");
ready = true;
worker.postMessage({ inputBytes, mimeType: "image/png", options });
} else if (message?.type === responseType) {
assert.equal(ready, true, "worker replied before its import completed");
assert.equal(message.error, undefined, `worker resize failed: ${message.error}`);
assertResized(message.result);
assert.ok(message.runtimeProof?.modules > 0, "missing worker module-confinement proof");
assert.equal(message.runtimeProof.networkAttempts, 0);
assert.equal(message.runtimeProof.wasmFiles.length, 1);
clearTimeout(timer);
resolve(message.runtimeProof);
}
} catch (error) { fail(error); }
});
});
} finally { await worker.terminate(); }
}
async function probe(standalone) {
const root = fs.realpathSync(standalone);
const guards = installGuards(createRequire(import.meta.url), root);
try {
const entry = pathToFileURL(path.join(root, "src/agent/image-processing/resize-core.mjs"));
const { resizeImageInProcess, IMAGE_RESIZE_WORKER_RESPONSE_TYPE } = await import(entry.href);
assert.equal(typeof resizeImageInProcess, "function");
assert.equal(IMAGE_RESIZE_WORKER_RESPONSE_TYPE, "pi:image-resize-response");
const input = fixturePng();
const options = { maxWidth: 1, maxHeight: 1, maxBytes: 1024 * 1024 };
assertResized(await resizeImageInProcess(input, "image/png", options));
const inProcess = guards.report();
console.log(`[smoke-image-runtime] shipped resize core: PNG 2×2 → 1×1; ${inProcess.modules} modules and Photon WASM confined to ${root}`);
const worker = await probeWorker(root, input, options, IMAGE_RESIZE_WORKER_RESPONSE_TYPE);
guards.report();
console.log(`[smoke-image-runtime] shipped resize worker: READY then tagged PNG 2×2 → 1×1; ${worker.modules} modules and Photon WASM confined to ${root}`);
console.log("[smoke-image-runtime] Photon in-process and worker resizing passed; zero network attempts");
} finally { guards.cleanup(); }
}
async function main() {
const args = process.argv.slice(2);
if (args[0] === "--probe") return probe(args[1]);
const value = (name, fallback) => {
const index = args.indexOf(name);
if (index < 0) return fallback;
assert.ok(args[index + 1] && !args[index + 1].startsWith("--"), `${name} requires a path`);
return path.resolve(args[index + 1]);
};
verifyImageRuntime(value("--standalone", path.resolve(import.meta.dirname, "../.next/standalone")), value("--runtime", process.execPath));
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch((error) => { console.error(`[smoke-image-runtime] FAIL: ${formatProbeError(error)}`); process.exitCode = 1; });
}