Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
420
test/scripts/run-additional-boundary-checks.test.ts
Normal file
420
test/scripts/run-additional-boundary-checks.test.ts
Normal file
@@ -0,0 +1,420 @@
|
||||
// Run Additional Boundary Checks tests cover run additional boundary checks script behavior.
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
BOUNDARY_CHECKS,
|
||||
createBoundedOutputBuffer,
|
||||
formatCommand,
|
||||
parseCliArgs,
|
||||
parseShardSelection,
|
||||
parseShardSpec,
|
||||
resolveConcurrency,
|
||||
resolvePositiveInteger,
|
||||
runChecks,
|
||||
runSingleCheck,
|
||||
selectChecksForShard,
|
||||
} from "../../scripts/run-additional-boundary-checks.mjs";
|
||||
|
||||
function createOutputBuffer() {
|
||||
const chunks: string[] = [];
|
||||
return {
|
||||
output: {
|
||||
write(chunk: string) {
|
||||
chunks.push(chunk);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
text: () => chunks.join(""),
|
||||
};
|
||||
}
|
||||
|
||||
function runCli(...args: string[]) {
|
||||
return spawnSync(process.execPath, ["scripts/run-additional-boundary-checks.mjs", ...args], {
|
||||
cwd: path.resolve("."),
|
||||
encoding: "utf8",
|
||||
});
|
||||
}
|
||||
|
||||
function isProcessAlive(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isProcessZombie(pid: number): boolean {
|
||||
const result = spawnSync("ps", ["-o", "stat=", "-p", String(pid)], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
return result.status === 0 && result.stdout.trim().startsWith("Z");
|
||||
}
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForFile(filePath: string, timeoutMs: number): Promise<void> {
|
||||
const deadlineAt = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadlineAt) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
return;
|
||||
}
|
||||
await sleep(25);
|
||||
}
|
||||
throw new Error(`timeout waiting for ${filePath}`);
|
||||
}
|
||||
|
||||
async function waitForDead(pid: number, timeoutMs: number): Promise<void> {
|
||||
const deadlineAt = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadlineAt) {
|
||||
if (!isProcessAlive(pid)) {
|
||||
return;
|
||||
}
|
||||
await sleep(25);
|
||||
}
|
||||
throw new Error(`process still alive: ${pid}`);
|
||||
}
|
||||
|
||||
async function waitForNotRunning(pid: number, timeoutMs: number): Promise<void> {
|
||||
const deadlineAt = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadlineAt) {
|
||||
if (!isProcessAlive(pid) || isProcessZombie(pid)) {
|
||||
return;
|
||||
}
|
||||
await sleep(25);
|
||||
}
|
||||
throw new Error(`process still running: ${pid}`);
|
||||
}
|
||||
|
||||
async function waitForChildClose(
|
||||
child: ReturnType<typeof spawn>,
|
||||
timeoutMs: number,
|
||||
): Promise<{ code: number | null; signal: NodeJS.Signals | null }> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("child did not close before timeout"));
|
||||
}, timeoutMs);
|
||||
child.once("close", (code, signal) => {
|
||||
clearTimeout(timeout);
|
||||
resolve({ code, signal });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("run-additional-boundary-checks", () => {
|
||||
it("runs prompt snapshot drift checks in CI", () => {
|
||||
expect(BOUNDARY_CHECKS[0]).toEqual({
|
||||
label: "prompt:snapshots:check",
|
||||
command: "pnpm",
|
||||
args: ["prompt:snapshots:check"],
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes concurrency input", () => {
|
||||
expect(resolveConcurrency("6")).toBe(6);
|
||||
expect(resolveConcurrency(undefined, 2)).toBe(2);
|
||||
expect(() => resolveConcurrency("0")).toThrow("concurrency must be a positive integer; got: 0");
|
||||
expect(() => resolveConcurrency("6x", 2)).toThrow(
|
||||
"concurrency must be a positive integer; got: 6x",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects malformed timeout and output limit integers", () => {
|
||||
expect(resolvePositiveInteger("25", 50, "OPENCLAW_ADDITIONAL_BOUNDARY_TIMEOUT_MS")).toBe(25);
|
||||
expect(resolvePositiveInteger(undefined, 50, "OPENCLAW_ADDITIONAL_BOUNDARY_TIMEOUT_MS")).toBe(
|
||||
50,
|
||||
);
|
||||
expect(() =>
|
||||
resolvePositiveInteger("1000ms", 50, "OPENCLAW_ADDITIONAL_BOUNDARY_TIMEOUT_MS"),
|
||||
).toThrow("OPENCLAW_ADDITIONAL_BOUNDARY_TIMEOUT_MS must be a positive integer; got: 1000ms");
|
||||
expect(() =>
|
||||
resolvePositiveInteger("1e3", 50, "OPENCLAW_ADDITIONAL_BOUNDARY_OUTPUT_MAX_BYTES"),
|
||||
).toThrow("OPENCLAW_ADDITIONAL_BOUNDARY_OUTPUT_MAX_BYTES must be a positive integer; got: 1e3");
|
||||
});
|
||||
|
||||
it("formats command display text", () => {
|
||||
expect(formatCommand({ command: "pnpm", args: ["run", "lint:core"] })).toBe(
|
||||
"pnpm run lint:core",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps only a bounded tail of command output", () => {
|
||||
const output = createBoundedOutputBuffer(12);
|
||||
output.append("first-line\n");
|
||||
output.append("second-line\n");
|
||||
|
||||
expect(output.read()).toBe("[output truncated to last 12 bytes]\nsecond-line\n");
|
||||
});
|
||||
|
||||
it("parses and applies CI shard specs", () => {
|
||||
expect(parseShardSpec("2/4")).toEqual({ count: 4, index: 1, label: "2/4" });
|
||||
expect(parseShardSelection("2/4,3/4")).toEqual([
|
||||
{ count: 4, index: 1, label: "2/4" },
|
||||
{ count: 4, index: 2, label: "3/4" },
|
||||
]);
|
||||
expect(selectChecksForShard(BOUNDARY_CHECKS, "1/4")).toEqual(
|
||||
BOUNDARY_CHECKS.filter((_check, index) => index % 4 === 0),
|
||||
);
|
||||
expect(selectChecksForShard(BOUNDARY_CHECKS, "2/4,3/4")).toEqual(
|
||||
BOUNDARY_CHECKS.filter((_check, index) => index % 4 === 1 || index % 4 === 2),
|
||||
);
|
||||
const shardedLabels = [1, 2, 3, 4].flatMap((index) =>
|
||||
selectChecksForShard(BOUNDARY_CHECKS, `${index}/4`).map((check) => check.label),
|
||||
);
|
||||
expect(shardedLabels.toSorted((a, b) => a.localeCompare(b))).toEqual(
|
||||
BOUNDARY_CHECKS.map((check) => check.label).toSorted((a, b) => a.localeCompare(b)),
|
||||
);
|
||||
expect(new Set(shardedLabels).size).toBe(BOUNDARY_CHECKS.length);
|
||||
expect(() => parseShardSpec("5/4")).toThrow("Invalid shard spec");
|
||||
expect(() => parseShardSpec("9007199254740993/9007199254740994")).toThrow("Invalid shard spec");
|
||||
});
|
||||
|
||||
it("parses CLI help and shard args before running checks", () => {
|
||||
expect(parseCliArgs(["--help"], {})).toEqual({ help: true, shardSpec: "" });
|
||||
expect(parseCliArgs(["--shard", "2/4"], {})).toEqual({ help: false, shardSpec: "2/4" });
|
||||
expect(parseCliArgs(["--shard=3/4"], {})).toEqual({ help: false, shardSpec: "3/4" });
|
||||
expect(parseCliArgs([], { OPENCLAW_ADDITIONAL_BOUNDARY_SHARD: "4/4" })).toEqual({
|
||||
help: false,
|
||||
shardSpec: "4/4",
|
||||
});
|
||||
expect(() => parseCliArgs(["--shard"], {})).toThrow("--shard requires a value");
|
||||
expect(() => parseCliArgs(["--shard", "-h"], {})).toThrow("--shard requires a value");
|
||||
expect(() => parseCliArgs(["--wat"], {})).toThrow("Unknown argument: --wat");
|
||||
});
|
||||
|
||||
it("does not start checks for CLI help or invalid arguments", () => {
|
||||
const help = runCli("--help");
|
||||
expect(help.status).toBe(0);
|
||||
expect(help.stdout).toContain("Usage: node scripts/run-additional-boundary-checks.mjs");
|
||||
expect(help.stdout).not.toContain("::group::");
|
||||
expect(help.stderr).toBe("");
|
||||
|
||||
const unknown = runCli("--wat");
|
||||
expect(unknown.status).toBe(1);
|
||||
expect(unknown.stdout).toBe("");
|
||||
expect(unknown.stderr).toContain("Unknown argument: --wat");
|
||||
expect(unknown.stderr).toContain("Usage: node scripts/run-additional-boundary-checks.mjs");
|
||||
expect(unknown.stderr).not.toContain("::group::");
|
||||
expect(unknown.stderr).not.toContain("pnpm");
|
||||
});
|
||||
|
||||
it("keeps the raw HTTP/2 import guard in source boundary checks", () => {
|
||||
expect(BOUNDARY_CHECKS).toContainEqual({
|
||||
label: "lint:tmp:no-raw-http2-imports",
|
||||
command: "pnpm",
|
||||
args: ["run", "lint:tmp:no-raw-http2-imports"],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the Telegram grammY type import guard in source boundary checks", () => {
|
||||
expect(BOUNDARY_CHECKS).toContainEqual({
|
||||
label: "lint:extensions:telegram-grammy-types",
|
||||
command: "pnpm",
|
||||
args: ["run", "lint:extensions:telegram-grammy-types"],
|
||||
});
|
||||
});
|
||||
|
||||
it("buffers grouped output and reports aggregate failures", async () => {
|
||||
const buffer = createOutputBuffer();
|
||||
const failures = await runChecks(
|
||||
[
|
||||
{
|
||||
label: "passes",
|
||||
command: process.execPath,
|
||||
args: ["-e", "console.log('ok-out')"],
|
||||
},
|
||||
{
|
||||
label: "fails",
|
||||
command: process.execPath,
|
||||
args: ["-e", "console.error('bad-out'); process.exit(7)"],
|
||||
},
|
||||
],
|
||||
{ concurrency: 2, output: buffer.output },
|
||||
);
|
||||
|
||||
const text = buffer.text();
|
||||
expect(failures).toBe(1);
|
||||
expect(text).toContain("::group::passes");
|
||||
expect(text).toContain("ok-out");
|
||||
expect(text).toContain("[ok] passes in ");
|
||||
expect(text).toContain("::group::fails");
|
||||
expect(text).toContain("bad-out");
|
||||
expect(text).toContain("::error title=fails failed::fails failed (exit 7)");
|
||||
expect(text).toContain("Additional boundary check timings:");
|
||||
});
|
||||
|
||||
it("times out hung checks", async () => {
|
||||
const result = await runSingleCheck(
|
||||
{
|
||||
label: "hangs",
|
||||
command: process.execPath,
|
||||
args: ["-e", "setInterval(() => {}, 1000)"],
|
||||
},
|
||||
{
|
||||
checkTimeoutMs: 50,
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
outputMaxBytes: 4096,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.code).toBe(1);
|
||||
expect(result.timedOut).toBe(true);
|
||||
expect(result.output).toContain("timed out after 50ms");
|
||||
});
|
||||
|
||||
it("clamps oversized check timers before scheduling", async () => {
|
||||
const result = await runSingleCheck(
|
||||
{
|
||||
label: "slow-pass",
|
||||
command: process.execPath,
|
||||
args: ["-e", "setTimeout(() => process.exit(0), 25)"],
|
||||
},
|
||||
{
|
||||
checkTimeoutMs: Number.MAX_SAFE_INTEGER,
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
outputMaxBytes: 4096,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.timedOut).toBe(false);
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"waits for timed-out process groups after the wrapper exits",
|
||||
async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-boundary-timeout-"));
|
||||
const childPidPath = path.join(tempDir, "child.pid");
|
||||
let childPid: number | undefined;
|
||||
try {
|
||||
const childScript = [
|
||||
"process.on('SIGTERM', () => {});",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("");
|
||||
const parentScript = [
|
||||
"const { spawn } = require('node:child_process');",
|
||||
"const fs = require('node:fs');",
|
||||
`const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
|
||||
"fs.writeFileSync(process.env.OPENCLAW_TEST_CHILD_PID, String(child.pid));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("");
|
||||
|
||||
const resultPromise = runSingleCheck(
|
||||
{
|
||||
label: "wrapper-exits",
|
||||
command: process.execPath,
|
||||
args: ["-e", parentScript],
|
||||
},
|
||||
{
|
||||
checkTimeoutMs: 100,
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, OPENCLAW_TEST_CHILD_PID: childPidPath },
|
||||
outputMaxBytes: 4096,
|
||||
},
|
||||
);
|
||||
|
||||
await waitForFile(childPidPath, 2000);
|
||||
childPid = Number(fs.readFileSync(childPidPath, "utf8"));
|
||||
const result = await resultPromise;
|
||||
|
||||
expect(result.code).toBe(1);
|
||||
expect(result.timedOut).toBe(true);
|
||||
await waitForDead(childPid, 2000);
|
||||
} finally {
|
||||
if (childPid !== undefined && isProcessAlive(childPid)) {
|
||||
process.kill(childPid, "SIGKILL");
|
||||
}
|
||||
fs.rmSync(tempDir, { force: true, recursive: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"cleans active check descendants on parent signal",
|
||||
async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-boundary-signal-"));
|
||||
const readyPath = path.join(tempDir, "ready");
|
||||
const childPidPath = path.join(tempDir, "child.pid");
|
||||
let childPid: number | undefined;
|
||||
let runner: ReturnType<typeof spawn> | undefined;
|
||||
try {
|
||||
const childScript = [
|
||||
"process.on('SIGTERM', () => {});",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("");
|
||||
const parentScript = [
|
||||
"const { spawn } = require('node:child_process');",
|
||||
"const fs = require('node:fs');",
|
||||
`const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
|
||||
"fs.writeFileSync(process.env.OPENCLAW_TEST_CHILD_PID, String(child.pid));",
|
||||
"fs.writeFileSync(process.env.OPENCLAW_TEST_READY, 'ready');",
|
||||
"process.on('SIGTERM', () => process.exit(0));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("");
|
||||
const runnerScript = `
|
||||
import { runChecks } from ${JSON.stringify(
|
||||
new URL("../../scripts/run-additional-boundary-checks.mjs", import.meta.url).href,
|
||||
)};
|
||||
|
||||
await runChecks(
|
||||
[{
|
||||
label: "parent-signal",
|
||||
command: process.execPath,
|
||||
args: ["-e", ${JSON.stringify(parentScript)}],
|
||||
}],
|
||||
{
|
||||
checkTimeoutMs: 30000,
|
||||
concurrency: 1,
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
output: { write() { return true; } },
|
||||
outputMaxBytes: 4096,
|
||||
},
|
||||
);
|
||||
`;
|
||||
|
||||
runner = spawn(process.execPath, ["--input-type=module", "--eval", runnerScript], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_TEST_CHILD_PID: childPidPath,
|
||||
OPENCLAW_TEST_READY: readyPath,
|
||||
},
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
|
||||
await waitForFile(readyPath, 2000);
|
||||
childPid = Number(fs.readFileSync(childPidPath, "utf8"));
|
||||
expect(Number.isInteger(childPid)).toBe(true);
|
||||
expect(isProcessAlive(childPid)).toBe(true);
|
||||
|
||||
runner.kill("SIGTERM");
|
||||
await sleep(50);
|
||||
runner.kill("SIGTERM");
|
||||
|
||||
await expect(waitForChildClose(runner, 10_000)).resolves.toEqual({
|
||||
code: null,
|
||||
signal: "SIGTERM",
|
||||
});
|
||||
await waitForNotRunning(childPid, 2000);
|
||||
} finally {
|
||||
if (childPid !== undefined && isProcessAlive(childPid)) {
|
||||
process.kill(childPid, "SIGKILL");
|
||||
}
|
||||
if (runner?.pid && isProcessAlive(runner.pid)) {
|
||||
runner.kill("SIGKILL");
|
||||
}
|
||||
fs.rmSync(tempDir, { force: true, recursive: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user