#!/usr/bin/env node // Runs additional architecture and boundary checks with sharding, concurrency, // timeout handling, and grouped CI output. import { spawn } from "node:child_process"; import { performance } from "node:perf_hooks"; const DEFAULT_CHECK_TIMEOUT_MS = 10 * 60 * 1000; const DEFAULT_OUTPUT_MAX_BYTES = 512 * 1024; const TIMEOUT_KILL_GRACE_MS = 5_000; const PROCESS_GROUP_EXIT_POLL_MS = 25; const POST_FORCE_KILL_WAIT_MS = 1_000; const MAX_TIMER_TIMEOUT_MS = 2_147_000_000; /** Ordered list of supplemental boundary checks used by CI sharding. */ export const BOUNDARY_CHECKS = [ ["prompt:snapshots:check", "pnpm", ["prompt:snapshots:check"]], ["plugin-extension-boundary", "pnpm", ["run", "lint:plugins:no-extension-imports"]], ["lint:tmp:no-random-messaging", "pnpm", ["run", "lint:tmp:no-random-messaging"]], ["lint:tmp:channel-agnostic-boundaries", "pnpm", ["run", "lint:tmp:channel-agnostic-boundaries"]], ["lint:tmp:tsgo-core-boundary", "pnpm", ["run", "lint:tmp:tsgo-core-boundary"]], ["lint:tmp:no-raw-channel-fetch", "pnpm", ["run", "lint:tmp:no-raw-channel-fetch"]], ["lint:tmp:no-raw-http2-imports", "pnpm", ["run", "lint:tmp:no-raw-http2-imports"]], ["lint:agent:ingress-owner", "pnpm", ["run", "lint:agent:ingress-owner"]], [ "lint:plugins:no-register-http-handler", "pnpm", ["run", "lint:plugins:no-register-http-handler"], ], [ "lint:plugins:no-monolithic-plugin-sdk-entry-imports", "pnpm", ["run", "lint:plugins:no-monolithic-plugin-sdk-entry-imports"], ], [ "lint:plugins:no-extension-src-imports", "pnpm", ["run", "lint:plugins:no-extension-src-imports"], ], [ "lint:plugins:no-extension-test-core-imports", "pnpm", ["run", "lint:plugins:no-extension-test-core-imports"], ], [ "lint:plugins:plugin-sdk-subpaths-exported", "pnpm", ["run", "lint:plugins:plugin-sdk-subpaths-exported"], ], ["deps:root-ownership:check", "pnpm", ["deps:root-ownership:check"]], ["web-search-provider-boundary", "pnpm", ["run", "lint:web-search-provider-boundaries"]], ["web-fetch-provider-boundary", "pnpm", ["run", "lint:web-fetch-provider-boundaries"]], [ "extension-src-outside-plugin-sdk-boundary", "pnpm", ["run", "lint:extensions:no-src-outside-plugin-sdk"], ], [ "extension-plugin-sdk-internal-boundary", "pnpm", ["run", "lint:extensions:no-plugin-sdk-internal"], ], [ "extension-relative-outside-package-boundary", "pnpm", ["run", "lint:extensions:no-relative-outside-package"], ], [ "lint:extensions:telegram-grammy-types", "pnpm", ["run", "lint:extensions:telegram-grammy-types"], ], ["lint:ui:no-raw-window-open", "pnpm", ["lint:ui:no-raw-window-open"]], ].map(([label, command, args]) => ({ label, command, args })); /** * Resolves the configured boundary-check concurrency. */ export function resolveConcurrency(value, fallback = 4, label = "concurrency") { return resolvePositiveInteger(value, fallback, label); } /** * Parses positive integer CLI/env options with a fallback. */ export function resolvePositiveInteger(value, fallback, label = "value") { if (value === undefined || value === null || value === "") { return fallback; } const text = String(value).trim(); if (!/^\d+$/u.test(text)) { throw new Error(`${label} must be a positive integer; got: ${value}`); } const parsed = Number(text); if (!Number.isSafeInteger(parsed) || parsed < 1) { throw new Error(`${label} must be a positive integer; got: ${value}`); } return parsed; } function resolveTimerTimeoutMs(valueMs) { const value = Number(valueMs); if (!Number.isFinite(value)) { return MAX_TIMER_TIMEOUT_MS; } return Math.min(Math.max(Math.floor(value), 1), MAX_TIMER_TIMEOUT_MS); } /** * Parses one N/TOTAL shard selector into zero-based index form. */ export function parseShardSpec(value) { if (!value) { return null; } const match = String(value).match(/^(\d+)\/(\d+)$/u); if (!match) { throw new Error(`Invalid shard spec '${value}' (expected N/TOTAL)`); } const index = Number(match[1]); const count = Number(match[2]); if ( !Number.isSafeInteger(index) || !Number.isSafeInteger(count) || index < 1 || count < 1 || index > count ) { throw new Error(`Invalid shard spec '${value}' (expected 1 <= N <= TOTAL)`); } return { count, index: index - 1, label: `${index}/${count}` }; } /** * Parses a comma-separated list of N/TOTAL shard selectors. */ export function parseShardSelection(value) { if (!value) { return null; } return String(value) .split(",") .map((part) => part.trim()) .filter(Boolean) .map((part) => { const shard = parseShardSpec(part); if (!shard) { throw new Error(`Invalid shard spec '${value}'`); } return shard; }); } /** * Selects checks whose ordinal belongs to the requested shard set. */ export function selectChecksForShard(checks, shardSpec) { const shards = typeof shardSpec === "string" ? parseShardSelection(shardSpec) : Array.isArray(shardSpec) ? shardSpec : shardSpec ? [shardSpec] : null; if (!shards || shards.length === 0) { return checks; } return checks.filter((_check, index) => shards.some((shard) => index % shard.count === shard.index), ); } /** * Formats a check command for CI group output. */ export function formatCommand({ command, args }) { return [command, ...args].join(" "); } /** * Keeps only the tail of noisy check output so failure logs stay bounded. */ export function createBoundedOutputBuffer(maxBytes = DEFAULT_OUTPUT_MAX_BYTES) { const limit = Math.max(1, maxBytes); const chunks = []; let bytes = 0; let truncated = false; const append = (value) => { const text = String(value); const textBytes = Buffer.byteLength(text); if (textBytes >= limit) { const buffer = Buffer.from(text); const tail = buffer.subarray(buffer.length - limit).toString("utf8"); chunks.splice(0, chunks.length, tail); bytes = Buffer.byteLength(tail); truncated = true; return; } chunks.push(text); bytes += textBytes; while (bytes > limit && chunks.length > 0) { const first = chunks[0]; const firstBytes = Buffer.byteLength(first); const overflow = bytes - limit; if (firstBytes <= overflow) { chunks.shift(); bytes -= firstBytes; truncated = true; continue; } const buffer = Buffer.from(first); const tail = buffer.subarray(overflow).toString("utf8"); chunks[0] = tail; bytes = chunks.reduce((total, chunk) => total + Buffer.byteLength(chunk), 0); truncated = true; } }; return { append, read() { const output = chunks.join(""); return truncated ? `[output truncated to last ${limit} bytes]\n${output}` : output; }, }; } function terminateChild(child, signal) { if (process.platform !== "win32" && child.pid) { try { process.kill(-child.pid, signal); return; } catch {} } child.kill(signal); } function processGroupAlive(child) { if (process.platform === "win32" || !child.pid) { return false; } try { process.kill(-child.pid, 0); return true; } catch (error) { return error?.code === "EPERM"; } } async function waitForProcessGroupExit(child, timeoutMs) { const deadlineAt = Date.now() + timeoutMs; while (Date.now() < deadlineAt) { if (!processGroupAlive(child)) { return true; } await new Promise((resolvePoll) => { setTimeout(resolvePoll, PROCESS_GROUP_EXIT_POLL_MS); }); } return !processGroupAlive(child); } async function finishTerminatedProcessTree(child, timeoutKillGraceMs = TIMEOUT_KILL_GRACE_MS) { if (processGroupAlive(child)) { await waitForProcessGroupExit(child, timeoutKillGraceMs); } if (processGroupAlive(child)) { terminateChild(child, "SIGKILL"); await waitForProcessGroupExit(child, POST_FORCE_KILL_WAIT_MS); } } function terminateActiveChildren(activeChildren, signal) { for (const child of activeChildren) { terminateChild(child, signal); } } function installActiveChildCleanup(activeChildren) { let active = true; let shutdownChildren = []; let shutdownPromise = null; let shutdownForceKillTimer = null; let resolveShutdownForceKill = null; const removeHandlers = () => { for (const [signal, handler] of signalHandlers) { process.off(signal, handler); } process.off("exit", exitHandler); }; const forceKillShutdownChildren = () => { if (shutdownForceKillTimer) { clearTimeout(shutdownForceKillTimer); shutdownForceKillTimer = null; } terminateActiveChildren(shutdownChildren, "SIGKILL"); resolveShutdownForceKill?.(); }; const cleanup = (signal, { waitForExit = false } = {}) => { if (!active) { return shutdownPromise ?? Promise.resolve(); } active = false; shutdownChildren = [...activeChildren]; terminateActiveChildren(shutdownChildren, signal); if (!waitForExit) { return Promise.resolve(); } shutdownPromise = new Promise((resolveForceKill) => { resolveShutdownForceKill = resolveForceKill; // Keep this timer ref'ed: once the leader exits, group liveness can look // gone while descendants are still running and still need the force kill. shutdownForceKillTimer = setTimeout(forceKillShutdownChildren, TIMEOUT_KILL_GRACE_MS); }) .then(() => Promise.all( shutdownChildren.map((child) => waitForProcessGroupExit(child, POST_FORCE_KILL_WAIT_MS)), ), ) .then(() => undefined); return shutdownPromise; }; const signalHandlers = new Map(); const signals = process.platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"]; for (const signal of signals) { const handler = () => { if (shutdownPromise) { forceKillShutdownChildren(); return; } void cleanup(signal, { waitForExit: true }).finally(() => { removeHandlers(); process.kill(process.pid, signal); }); }; signalHandlers.set(signal, handler); process.on(signal, handler); } const exitHandler = () => { void cleanup("SIGTERM"); }; process.once("exit", exitHandler); return () => { if (shutdownPromise) { return; } active = false; removeHandlers(); }; } /** * Runs one boundary check with timeout and process-group termination. */ export function runSingleCheck( check, { activeChildren, checkTimeoutMs = DEFAULT_CHECK_TIMEOUT_MS, cwd, env, outputMaxBytes = DEFAULT_OUTPUT_MAX_BYTES, }, ) { return new Promise((resolve) => { const resolvedCheckTimeoutMs = resolveTimerTimeoutMs(checkTimeoutMs); const startedAt = performance.now(); const child = spawn(check.command, check.args, { cwd, env, detached: process.platform !== "win32", shell: false, stdio: ["ignore", "pipe", "pipe"], }); activeChildren?.add(child); const output = createBoundedOutputBuffer(outputMaxBytes); let settled = false; let timedOut = false; let forceKillTimer = null; const finish = (code, signal) => { if (settled) { return; } settled = true; clearTimeout(timeout); if (forceKillTimer) { clearTimeout(forceKillTimer); } activeChildren?.delete(child); resolve({ check, code: timedOut ? 1 : (code ?? 1), durationMs: Math.round(performance.now() - startedAt), signal, timedOut, output: output.read(), }); }; const finishAfterTimeoutTeardown = async (code, signal) => { await finishTerminatedProcessTree(child, TIMEOUT_KILL_GRACE_MS); finish(code, signal); }; const timeout = setTimeout(() => { timedOut = true; output.append( `\n[boundary-check] ${check.label} timed out after ${formatDuration(resolvedCheckTimeoutMs)}; terminating process group\n`, ); terminateChild(child, "SIGTERM"); forceKillTimer = setTimeout(() => { output.append( `[boundary-check] ${check.label} still running after ${formatDuration(TIMEOUT_KILL_GRACE_MS)}; sending SIGKILL\n`, ); terminateChild(child, "SIGKILL"); }, TIMEOUT_KILL_GRACE_MS); forceKillTimer.unref?.(); }, resolvedCheckTimeoutMs); timeout.unref?.(); child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); child.stdout.on("data", (chunk) => output.append(chunk)); child.stderr.on("data", (chunk) => output.append(chunk)); child.on("error", (error) => { output.append(`${error.stack ?? error.message}\n`); finish(1, null); }); child.on("close", (code, signal) => { if (timedOut) { void finishAfterTimeoutTeardown(code, signal); return; } finish(code, signal); }); }); } function formatDuration(ms) { if (!Number.isFinite(ms)) { return ""; } if (ms < 1000) { return `${ms}ms`; } return `${(ms / 1000).toFixed(1)}s`; } function writeGroupedResult(result, output) { const success = result.code === 0; output.write(`::group::${result.check.label}\n`); output.write(`$ ${formatCommand(result.check)}\n`); if (result.output) { output.write(result.output.endsWith("\n") ? result.output : `${result.output}\n`); } if (success) { output.write(`[ok] ${result.check.label} in ${formatDuration(result.durationMs)}\n`); } else { const suffix = result.timedOut ? " (timeout)" : result.signal ? ` (signal ${result.signal})` : ` (exit ${result.code})`; output.write( `::error title=${result.check.label} failed::${result.check.label} failed${suffix} after ${formatDuration(result.durationMs)}\n`, ); } output.write("::endgroup::\n"); } function writeTimingSummary(results, output) { output.write("Additional boundary check timings:\n"); for (const result of [...results].toSorted((left, right) => right.durationMs - left.durationMs)) { output.write( `${result.check.label.padEnd(48)} ${formatDuration(result.durationMs).padStart(8)}\n`, ); } } /** * Runs boundary checks with bounded concurrency and returns the failure count. */ export async function runChecks( checks = BOUNDARY_CHECKS, { checkTimeoutMs = DEFAULT_CHECK_TIMEOUT_MS, concurrency = 4, cwd = process.cwd(), env = process.env, output = process.stdout, outputMaxBytes = DEFAULT_OUTPUT_MAX_BYTES, } = {}, ) { const results = Array.from({ length: checks.length }); const activeChildren = new Set(); const removeActiveChildCleanup = installActiveChildCleanup(activeChildren); let nextIndex = 0; let active = 0; try { await new Promise((resolve) => { const launch = () => { if (nextIndex >= checks.length && active === 0) { resolve(); return; } while (active < concurrency && nextIndex < checks.length) { const index = nextIndex; const check = checks[nextIndex++]; active += 1; void runSingleCheck(check, { activeChildren, checkTimeoutMs, cwd, env, outputMaxBytes, }) .then((result) => { results[index] = result; }) .finally(() => { active -= 1; launch(); }); } }; launch(); }); } finally { removeActiveChildCleanup(); } let failures = 0; for (const result of results) { writeGroupedResult(result, output); if (result.code !== 0) { failures += 1; } } writeTimingSummary(results, output); return failures; } function usage() { return `Usage: node scripts/run-additional-boundary-checks.mjs [--shard [,]] Runs supplemental architecture and boundary checks with bounded concurrency. Options: --shard Run only checks selected by one or more N/TOTAL shard specs -h, --help Show this help `; } export function parseCliArgs(args, env = process.env) { let shardSpec = env.OPENCLAW_ADDITIONAL_BOUNDARY_SHARD ?? ""; let help = false; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "-h" || arg === "--help") { help = true; continue; } if (arg === "--shard") { const value = args[index + 1]; if (!value || value.startsWith("-")) { throw new Error("--shard requires a value"); } shardSpec = value; index += 1; continue; } if (arg.startsWith("--shard=")) { const value = arg.slice("--shard=".length); if (!value) { throw new Error("--shard requires a value"); } shardSpec = value; continue; } throw new Error(`Unknown argument: ${arg}`); } return { help, shardSpec }; } if (import.meta.url === `file://${process.argv[1]}`) { try { const cliArgs = parseCliArgs(process.argv.slice(2), process.env); if (cliArgs.help) { process.stdout.write(usage()); process.exitCode = 0; } else { const concurrencyRaw = process.env.OPENCLAW_ADDITIONAL_BOUNDARY_CONCURRENCY ?? process.env.OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY; const concurrencyLabel = process.env.OPENCLAW_ADDITIONAL_BOUNDARY_CONCURRENCY === undefined ? "OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY" : "OPENCLAW_ADDITIONAL_BOUNDARY_CONCURRENCY"; const concurrency = resolveConcurrency(concurrencyRaw, 4, concurrencyLabel); const checkTimeoutMs = resolvePositiveInteger( process.env.OPENCLAW_ADDITIONAL_BOUNDARY_TIMEOUT_MS, DEFAULT_CHECK_TIMEOUT_MS, "OPENCLAW_ADDITIONAL_BOUNDARY_TIMEOUT_MS", ); const outputMaxBytes = resolvePositiveInteger( process.env.OPENCLAW_ADDITIONAL_BOUNDARY_OUTPUT_MAX_BYTES, DEFAULT_OUTPUT_MAX_BYTES, "OPENCLAW_ADDITIONAL_BOUNDARY_OUTPUT_MAX_BYTES", ); const shards = parseShardSelection(cliArgs.shardSpec); const checks = selectChecksForShard(BOUNDARY_CHECKS, shards); if (shards) { process.stdout.write( `Running ${checks.length}/${BOUNDARY_CHECKS.length} additional boundary checks (shard ${shards.map((shard) => shard.label).join(",")})\n`, ); } const failures = await runChecks(checks, { checkTimeoutMs, concurrency, outputMaxBytes }); process.exitCode = failures === 0 ? 0 : 1; } } catch (error) { process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n\n${usage()}`); process.exitCode = 1; } }