import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { test } from "bun:test"; import type { CommandResult } from "./command"; import { rootPath } from "./config"; import { hwlabRuntimeLaneSpecForNode } from "./hwlab-node-lanes"; import { evaluateWebProbeMemoryGuard, runWebProbeMemoryGuard, webProbeBrowserEvidenceEnvAssignments, webProbeGuardedLaunchShell, webProbeMemoryStartGuardConfigRef, webProbeScreenshotFullPage } from "./hwlab-node-web-probe-memory-guard"; import { nodeWebProbeScriptRunnerSource } from "./hwlab-node-web-probe-runner-source"; const spec = hwlabRuntimeLaneSpecForNode("v03", "NC01"); const thresholdBytes = 4 * 1024 * 1024 * 1024; function memoryPayload(availableBytes: number, extra: Record = {}): Record { return { ok: true, action: "gc remote memory", providerId: "NC01", metric: "MemAvailable", source: "/proc/meminfo", swapExcluded: true, memory: { availableBytes, availableHuman: "fixture", ...extra }, valuesRedacted: true, }; } function exportedFunctionSource(source: string, name: string): string { const start = source.indexOf(`export function ${name}(`); assert.ok(start >= 0, `missing exported function ${name}`); const nextExport = source.indexOf("\nexport function ", start + 1); const nextFunction = source.indexOf("\nfunction ", start + 1); const next = [nextExport, nextFunction].filter((value) => value >= 0).sort((left, right) => left - right)[0] ?? -1; return source.slice(start, next < 0 ? source.length : next); } test("WebProbe memory threshold is read from the single owning YAML policy", () => { const policy = spec.webProbe?.resourcePolicy?.memoryStartGuard; assert.equal(policy?.thresholdBytes, thresholdBytes); assert.equal(policy?.metric, "MemAvailable"); assert.equal(policy?.source, "/proc/meminfo"); assert.equal(policy?.swapExcluded, true); assert.equal(policy?.manualComparator, "less-than-or-equal"); assert.equal(policy?.sentinelComparator, "less-than"); assert.match(webProbeMemoryStartGuardConfigRef, /resourcePolicy\.memoryStartGuard$/u); }); test("browser evidence limits and viewport screenshots are read from owning YAML", () => { const policy = spec.webProbe?.resourcePolicy?.browserEvidence; assert.deepEqual(policy, { screenshotMode: "viewport", maxNetworkRequestEntries: 200, maxNetworkRequestBytes: 262144, maxNetworkFailureEntries: 100, maxNetworkFailureBytes: 131072, }); const assignments = webProbeBrowserEvidenceEnvAssignments(spec).join(" "); assert.match(assignments, /UNIDESK_WEB_PROBE_SCREENSHOT_MODE='viewport'/u); assert.match(assignments, /UNIDESK_WEB_PROBE_MAX_NETWORK_REQUEST_ENTRIES='200'/u); assert.match(assignments, /UNIDESK_WEB_PROBE_MAX_NETWORK_FAILURE_BYTES='131072'/u); }); test("manual start is blocked at exactly 4 GiB and requires controlled GC then MemAvailable recheck", () => { const guard = evaluateWebProbeMemoryGuard(spec, "manual-start", memoryPayload(thresholdBytes)); const memory = guard.memory as Record; const gcNext = guard.gcNext as Record; assert.equal(guard.status, "blocked"); assert.equal(guard.startBlocked, true); assert.equal(guard.decision, "gc-required"); assert.equal(memory.metric, "MemAvailable"); assert.equal(memory.source, "/proc/meminfo"); assert.equal(memory.swapExcluded, true); assert.equal(memory.comparator, "less-than-or-equal"); assert.equal(memory.thresholdMatched, true); assert.equal(gcNext.requiredBeforeStart, true); assert.match(String(gcNext.planCommand), /gc remote NC01 plan --memory-pressure-only$/u); assert.match(String(gcNext.runCommand), /gc remote NC01 run --confirm --memory-pressure-only$/u); assert.match(String(gcNext.statusCommandTemplate), /gc remote NC01 status --job-id $/u); assert.match(String(gcNext.recheckCommand), /gc remote NC01 memory$/u); assert.equal(gcNext.recheckMetric, "MemAvailable"); assert.deepEqual(gcNext.requiredOrder, ["plan", "run", "status-until-terminal", "memory-recheck"]); assert.deepEqual(gcNext.startRemainsBlockedUntil, ["gc-job-terminal", "MemAvailable-threshold-cleared"]); }); test("sentinel allows exactly 4 GiB but skips one byte below without considering swap", () => { const exact = evaluateWebProbeMemoryGuard(spec, "sentinel-cadence", memoryPayload(thresholdBytes, { swapFreeBytes: 12 * 1024 ** 3 })); const below = evaluateWebProbeMemoryGuard(spec, "sentinel-cadence", memoryPayload(thresholdBytes - 1, { swapFreeBytes: 12 * 1024 ** 3 })); const belowMemory = below.memory as Record; assert.equal(exact.status, "allowed"); assert.equal(exact.startBlocked, false); assert.equal((exact.memory as Record).comparator, "less-than"); assert.equal(below.status, "skipped"); assert.equal(below.startBlocked, true); assert.equal(below.decision, "skip-observer"); assert.equal(belowMemory.availableBytes, thresholdBytes - 1); assert.equal(belowMemory.swapExcluded, true); }); test("memory guard rejects a snapshot that is not the /proc/meminfo MemAvailable contract", () => { const guard = evaluateWebProbeMemoryGuard(spec, "manual-start", { ...memoryPayload(thresholdBytes + 1), source: "cgroup-v2-memory.current", }); assert.equal(guard.status, "blocked"); assert.equal(guard.reason, "memory-snapshot-contract-invalid"); assert.equal(guard.startBlocked, true); }); test("memory guard executes only the controlled read-only remote memory entry", () => { let observedCommand: string[] = []; let observedTimeoutMs = 0; const execute = (command: string[], cwd: string, options: { timeoutMs?: number }): CommandResult => { observedCommand = command; observedTimeoutMs = options.timeoutMs ?? 0; return { command, cwd, exitCode: 0, stdout: JSON.stringify({ ok: true, command: "gc remote NC01 memory", data: memoryPayload(thresholdBytes + 1) }), stderr: "", signal: null, timedOut: false, }; }; const guard = runWebProbeMemoryGuard(spec, "manual-start", execute); assert.deepEqual(observedCommand, ["bun", "scripts/cli.ts", "gc", "remote", "NC01", "memory"]); assert.equal(observedTimeoutMs, 30_000); assert.equal(guard.status, "allowed"); }); test("remote memory collector reads MemAvailable directly and excludes swap", () => { const source = readFileSync(rootPath("scripts/src/gc-remote-runner.py"), "utf8"); const start = source.indexOf("def collect_memory_snapshot():"); const end = source.indexOf("\ndef observe_run_record", start); const collector = source.slice(start, end); assert.match(collector, /source = "\/proc\/meminfo"/u); assert.match(collector, /fields\.get\("MemAvailable"\)/u); assert.match(collector, /"swapExcluded": True/u); assert.doesNotMatch(collector, /SwapFree|SwapTotal|\["free", "-b"\]/u); }); test("manual and sentinel gates return before any observer launch path", () => { const manualSource = readFileSync(rootPath("scripts/src/hwlab-node/web-probe-observe-actions.ts"), "utf8"); const manualStart = manualSource.indexOf("export function runNodeWebProbeObserveStart"); const manualGuard = manualSource.indexOf("runWebProbeMemoryGuard(spec, options.memoryGuardMode)", manualStart); const manualReturn = manualSource.indexOf("startBlocked: true", manualGuard); const manualJob = manualSource.indexOf("const jobId =", manualStart); const manualLaunch = manualSource.indexOf("setsid env", manualStart); assert.ok(manualStart >= 0 && manualGuard > manualStart && manualReturn > manualGuard); assert.ok(manualReturn < manualJob && manualJob < manualLaunch); const sentinelSource = readFileSync(rootPath("scripts/src/hwlab-node-web-sentinel-p5-observe.ts"), "utf8"); const quickVerify = sentinelSource.indexOf("export function runSentinelQuickVerify"); const sentinelGuard = sentinelSource.indexOf("runWebProbeMemoryGuard(state.spec, \"sentinel-cadence\")", quickVerify); const sentinelSkip = sentinelSource.indexOf("return recordSkippedQuickVerify", sentinelGuard); const providerPreflight = sentinelSource.indexOf("runSentinelExactProviderProfilePreflight", quickVerify); const observeStart = sentinelSource.indexOf('"web-probe", "observe", "start"', quickVerify); assert.ok(quickVerify >= 0 && sentinelGuard > quickVerify && sentinelSkip > sentinelGuard); assert.ok(sentinelSkip < providerPreflight && providerPreflight < observeStart); assert.match(sentinelSource.slice(observeStart, observeStart + 800), /--sentinel-cadence/u); const serviceSource = readFileSync(rootPath("scripts/src/hwlab-node-web-sentinel-service.ts"), "utf8"); const plannedStart = serviceSource.indexOf('"bun", "scripts/cli.ts", "web-probe", "observe", "start"'); assert.match(serviceSource.slice(plannedStart, plannedStart + 800), /--sentinel-cadence/u); }); test("all browser-launch entries use the shared preflight and final launch guard", () => { const probeSource = readFileSync(rootPath("scripts/src/hwlab-node/web-probe-observe.ts"), "utf8"); const scriptSource = readFileSync(rootPath("scripts/src/hwlab-node/web-observe-scripts.ts"), "utf8"); const sentinelSource = readFileSync(rootPath("scripts/src/hwlab-node-web-sentinel-p5.ts"), "utf8"); const parserSource = readFileSync(rootPath("scripts/src/hwlab-node/web-probe.ts"), "utf8"); const run = exportedFunctionSource(probeSource, "runNodeWebProbe"); const screenshot = exportedFunctionSource(probeSource, "runNodeWebProbeScreenshot"); const asyncRun = exportedFunctionSource(probeSource, "runNodeWebProbeAsync"); const script = exportedFunctionSource(scriptSource, "runNodeWebProbeScript"); const scriptShell = exportedFunctionSource(scriptSource, "nodeWebProbeScriptRemoteShell"); const dashboard = exportedFunctionSource(sentinelSource, "probeSentinelDashboardBrowser"); const parser = exportedFunctionSource(parserSource, "parseNodeWebProbeOptions"); assert.doesNotMatch(probeSource, /runNodeWebProbeMemoryGuard/u); assert.match(run, /if \(options\.action === "script"\) return runNodeWebProbeScript/u); assert.match(run, /if \(options\.commandTimeoutSeconds > 55\) return runNodeWebProbeAsync/u); assert.match(run, /runWebProbeMemoryGuard\(spec, "manual-start"\)[\s\S]+webProbeGuardedLaunchShell\(spec, "manual-start"/u); assert.match(screenshot, /runWebProbeMemoryGuard\(spec, "manual-start"\)[\s\S]+webProbeGuardedLaunchShell\(spec, "manual-start", webProbeScreenshotRemoteScript/u); assert.match(asyncRun, /runWebProbeMemoryGuard\(spec, "manual-start"\)[\s\S]+webProbeGuardedLaunchShell\(spec, "manual-start"[\s\S]+"structured-output"\)/u); assert.match(script, /runWebProbeMemoryGuard\(spec, "manual-start"\)[\s\S]+nodeWebProbeScriptRemoteShell/u); assert.match(scriptShell, /webProbeGuardedLaunchShell\(spec, "manual-start"/u); assert.match(parser, /if \(actionRaw === "opencode-smoke"\)[\s\S]+action: "script"/u); assert.match(dashboard, /runWebProbeMemoryGuard\(state\.spec, "sentinel-cadence"\)[\s\S]+webProbeGuardedLaunchShell\(state\.spec, "sentinel-cadence"/u); assert.match(dashboard, /UNIDESK_SENTINEL_DASHBOARD_TRIGGER=.*options\.action === "trigger"/u); assert.match(dashboard, /UNIDESK_SENTINEL_DASHBOARD_CAPTURE=.*options\.action === "screenshot"/u); }); test("read-only and control-plane entries stay outside browser launch guard", () => { const dispatchSource = readFileSync(rootPath("scripts/src/hwlab-node/web-probe-observe.ts"), "utf8"); const dispatchStart = dispatchSource.indexOf("export function runNodeWebProbe("); const dispatchEnd = dispatchSource.indexOf("export function runNodeWebProbeScreenshot", dispatchStart); const dispatch = dispatchSource.slice(dispatchStart, dispatchEnd); const readOnlyReturn = dispatch.indexOf('options.action === "observe" && options.observeAction !== "start"'); const firstGuard = dispatch.indexOf("runWebProbeMemoryGuard"); assert.ok(readOnlyReturn >= 0 && firstGuard > readOnlyReturn); assert.match(dispatch.slice(0, readOnlyReturn), /options\.action === "sentinel"/u); assert.match(dispatch.slice(0, readOnlyReturn), /options\.action === "screenshot"/u); const sentinelDispatch = readFileSync(rootPath("scripts/src/hwlab-node-web-sentinel-cicd.ts"), "utf8"); const sentinel = exportedFunctionSource(sentinelDispatch, "runWebProbeSentinelCommand"); assert.doesNotMatch(sentinel, /runWebProbeMemoryGuard|webProbeGuardedLaunchShell/u); assert.match(sentinel, /options\.kind === "dashboard"/u); }); test("generated runner bounds network evidence and defaults screenshots to viewport", () => { const source = nodeWebProbeScriptRunnerSource(); assert.match(source, /maxNetworkRequestEntries/u); assert.match(source, /maxNetworkRequestBytes/u); assert.match(source, /maxNetworkFailureEntries/u); assert.match(source, /maxNetworkFailureBytes/u); assert.match(source, /apiRequestEvidenceDropped/u); assert.match(source, /apiFailureEvidenceDropped/u); assert.doesNotMatch(source, /fullPage: true/u); assert.equal((source.match(/fullPage: screenshotMode !== "viewport"/gu) ?? []).length, 2); const parserSource = exportedFunctionSource(readFileSync(rootPath("scripts/src/hwlab-node/web-probe.ts"), "utf8"), "parseNodeWebProbeOptions"); assert.match(parserSource, /fullPage: args\.includes\("--full-page"\) && !args\.includes\("--no-full-page"\)/u); const screenshotSource = readFileSync(rootPath("scripts/src/hwlab-node/web-probe-observe.ts"), "utf8"); assert.match(screenshotSource, /webProbeScreenshotFullPage\(spec, options\.fullPage\)/u); assert.match(screenshotSource, /UNIDESK_WEB_PROBE_SCREENSHOT_FULL_PAGE === "1"/u); assert.doesNotMatch(screenshotSource, /UNIDESK_WEB_PROBE_SCREENSHOT_FULL_PAGE !== "0"/u); const sentinelSource = readFileSync(rootPath("scripts/src/hwlab-node-web-sentinel-p5.ts"), "utf8"); assert.match(sentinelSource, /webProbeScreenshotFullPage\(state\.spec, options\.fullPage\)/u); assert.match(sentinelSource, /UNIDESK_SENTINEL_DASHBOARD_FULL_PAGE === "1"/u); assert.doesNotMatch(sentinelSource, /UNIDESK_SENTINEL_DASHBOARD_FULL_PAGE !== "0"/u); assert.equal(webProbeScreenshotFullPage(spec, false), false); assert.equal(webProbeScreenshotFullPage({ ...spec, webProbe: { ...spec.webProbe!, resourcePolicy: { ...spec.webProbe!.resourcePolicy!, browserEvidence: { ...spec.webProbe!.resourcePolicy!.browserEvidence!, screenshotMode: "full-page" } } } }, false), true); assert.equal(webProbeScreenshotFullPage(spec, true), true); }); test("final launch guard rechecks MemAvailable under the configured lock", () => { const sync = webProbeGuardedLaunchShell(spec, "manual-start", "node runner.mjs"); const async = webProbeGuardedLaunchShell(spec, "manual-start", "node helper.mjs start", "structured-output"); assert.match(sync, /flock -w '15'/u); assert.match(sync, /MemAvailable:/u); assert.match(sync, /mem_available_bytes.*-le '4294967296'/u); assert.match(sync, /setsid sh -c/u); assert.match(sync, /exec 9>&-/u); assert.equal(sync.includes("playwright"), false); assert.equal(sync.includes("fs.readlinkSync('/proc/' + name + '/exe')"), true); assert.equal(sync.includes("trap 'browser_exit=$?; trap - EXIT TERM INT HUP; cleanup_browser_group"), true); assert.equal(sync.includes('kill -TERM -- "-$browser_job_pid"'), true); assert.equal(sync.includes('kill -0 -- "-$browser_job_pid"'), true); assert.equal(sync.includes('kill -KILL -- "-$browser_job_pid"'), true); assert.equal(async.includes("browserCreated === true"), false); assert.equal(async.includes("data.workerPid, data.jobPid, data.pid"), true); assert.equal(async.includes("[data.browserPid, data.workerPid"), false); assert.equal(async.includes("reportedBrowserPid === null || row.pid === reportedBrowserPid"), true); assert.equal(async.includes("then flock -u 9; exit 43; fi"), true); assert.equal(async.includes("fs.readlinkSync('/proc/' + name + '/exe')"), true); });