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:
463
test/scripts/run-with-env.test.ts
Normal file
463
test/scripts/run-with-env.test.ts
Normal file
@@ -0,0 +1,463 @@
|
||||
// Run With Env tests cover run with env script behavior.
|
||||
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
isRunWithEnvHelpRequest,
|
||||
parseRunWithEnvArgs,
|
||||
resolveForceKillDelayMs,
|
||||
resolveSpawnCommand,
|
||||
signalRunWithEnvChild,
|
||||
} from "../../scripts/run-with-env.mjs";
|
||||
|
||||
const taskkillPath = path.win32.join("C:\\Windows", "System32", "taskkill.exe");
|
||||
|
||||
function restoreEnvValue(key: string, value: string | undefined): void {
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
return;
|
||||
}
|
||||
process.env[key] = value;
|
||||
}
|
||||
|
||||
function withDefaultWindowsSystemRoot(run: () => void): void {
|
||||
const originalSystemRoot = process.env.SystemRoot;
|
||||
const originalWindir = process.env.WINDIR;
|
||||
try {
|
||||
process.env.SystemRoot = "C:\\Windows";
|
||||
delete process.env.WINDIR;
|
||||
run();
|
||||
} finally {
|
||||
restoreEnvValue("SystemRoot", originalSystemRoot);
|
||||
restoreEnvValue("WINDIR", originalWindir);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitFor(predicate: () => boolean, label: string, timeoutMs = 3_000): Promise<void> {
|
||||
const startedAt = Date.now();
|
||||
while (!predicate()) {
|
||||
if (Date.now() - startedAt > timeoutMs) {
|
||||
throw new Error(`timed out waiting for ${label}`);
|
||||
}
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 25);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForExit(
|
||||
child: ChildProcess,
|
||||
timeoutMs = 3_000,
|
||||
): Promise<{ code: number | null; signal: NodeJS.Signals | null }> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
child.kill("SIGKILL");
|
||||
reject(new Error("timed out waiting for child exit"));
|
||||
}, timeoutMs);
|
||||
child.once("exit", (code, signal) => {
|
||||
clearTimeout(timer);
|
||||
resolve({ code, signal });
|
||||
});
|
||||
child.once("error", (error) => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isProcessAlive(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
describe("run-with-env", () => {
|
||||
it("parses leading env assignments before the command separator", () => {
|
||||
expect(
|
||||
parseRunWithEnvArgs([
|
||||
"OPENCLAW_GATEWAY_PROJECT_SHARDS=1",
|
||||
"EMPTY=",
|
||||
"--",
|
||||
"node",
|
||||
"scripts/run-vitest.mjs",
|
||||
"run",
|
||||
]),
|
||||
).toEqual({
|
||||
env: {
|
||||
OPENCLAW_GATEWAY_PROJECT_SHARDS: "1",
|
||||
EMPTY: "",
|
||||
},
|
||||
command: "node",
|
||||
args: ["scripts/run-vitest.mjs", "run"],
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects missing command separators", () => {
|
||||
expect(() => parseRunWithEnvArgs(["OPENCLAW_GATEWAY_PROJECT_SHARDS=1", "node"])).toThrow(
|
||||
/Usage:/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("prints wrapper help without spawning a command", () => {
|
||||
const result = spawnSync(process.execPath, ["scripts/run-with-env.mjs", "--help"], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain("Usage: node scripts/run-with-env.mjs");
|
||||
expect(result.stderr).toBe("");
|
||||
});
|
||||
|
||||
it("keeps command help passthrough after the separator", () => {
|
||||
expect(
|
||||
isRunWithEnvHelpRequest(["OPENCLAW_GATEWAY_PROJECT_SHARDS=1", "--", "node", "--help"]),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects malformed assignments before spawning", () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/run-with-env.mjs",
|
||||
"1INVALID=value",
|
||||
"--",
|
||||
"node",
|
||||
"-e",
|
||||
"process.stdout.write('spawned')",
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain("invalid environment assignment");
|
||||
});
|
||||
|
||||
it("uses the current Node executable for node commands", () => {
|
||||
expect(resolveSpawnCommand("node", ["scripts/run-vitest.mjs"], "node.exe")).toEqual({
|
||||
command: "node.exe",
|
||||
args: ["scripts/run-vitest.mjs"],
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects malformed force-kill grace configuration before spawning", () => {
|
||||
expect(resolveForceKillDelayMs({})).toBe(5_000);
|
||||
expect(resolveForceKillDelayMs({ OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS: "250" })).toBe(250);
|
||||
expect(
|
||||
resolveForceKillDelayMs({
|
||||
OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS: String(MAX_TIMER_TIMEOUT_MS + 1),
|
||||
}),
|
||||
).toBe(MAX_TIMER_TIMEOUT_MS);
|
||||
for (const value of ["0", "-1", "1e3", "100ms"]) {
|
||||
expect(() => resolveForceKillDelayMs({ OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS: value })).toThrow(
|
||||
"OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS must be a positive integer",
|
||||
);
|
||||
}
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/run-with-env.mjs",
|
||||
"OPENCLAW_RUN_WITH_ENV_SIGNAL_TEST=1",
|
||||
"--",
|
||||
"node",
|
||||
"-e",
|
||||
"process.stdout.write('spawned')",
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS: "100ms" },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain(
|
||||
"OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS must be a positive integer",
|
||||
);
|
||||
});
|
||||
|
||||
it("signals Windows wrapped command trees with taskkill", () => {
|
||||
withDefaultWindowsSystemRoot(() => {
|
||||
const child = {
|
||||
kill: vi.fn(),
|
||||
pid: 12345,
|
||||
};
|
||||
const runTaskkill = vi.fn(() => ({ error: undefined, status: 0 }));
|
||||
|
||||
signalRunWithEnvChild(child, "SIGTERM", {
|
||||
platform: "win32",
|
||||
runTaskkill,
|
||||
});
|
||||
expect(runTaskkill).toHaveBeenNthCalledWith(1, taskkillPath, ["/PID", "12345", "/T"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
|
||||
signalRunWithEnvChild(child, "SIGKILL", {
|
||||
platform: "win32",
|
||||
runTaskkill,
|
||||
});
|
||||
expect(runTaskkill).toHaveBeenNthCalledWith(2, taskkillPath, ["/PID", "12345", "/T", "/F"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
expect(child.kill).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("force-kills Windows wrapped command trees when graceful taskkill fails", () => {
|
||||
withDefaultWindowsSystemRoot(() => {
|
||||
const child = {
|
||||
kill: vi.fn(),
|
||||
pid: 12345,
|
||||
};
|
||||
const runTaskkill = vi
|
||||
.fn()
|
||||
.mockReturnValueOnce({ error: undefined, status: 1 })
|
||||
.mockReturnValueOnce({ error: undefined, status: 0 });
|
||||
|
||||
signalRunWithEnvChild(child, "SIGTERM", {
|
||||
platform: "win32",
|
||||
runTaskkill,
|
||||
});
|
||||
|
||||
expect(runTaskkill).toHaveBeenNthCalledWith(1, taskkillPath, ["/PID", "12345", "/T"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
expect(runTaskkill).toHaveBeenNthCalledWith(2, taskkillPath, ["/PID", "12345", "/T", "/F"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
expect(child.kill).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32").each(["SIGTERM", "SIGHUP", "SIGINT"] as const)(
|
||||
"forwards parent %s to the wrapped command",
|
||||
async (signal) => {
|
||||
const tempDir = mkdtempSync(path.join(tmpdir(), "openclaw-run-with-env-signals-"));
|
||||
const readyFile = path.join(tempDir, "ready");
|
||||
const signaledFile = path.join(tempDir, "signaled");
|
||||
const handlerLines = ["SIGTERM", "SIGHUP", "SIGINT"].flatMap((handledSignal) => [
|
||||
`process.on('${handledSignal}', () => {`,
|
||||
` fs.writeFileSync(process.env.SIGNALED_FILE, '${handledSignal}');`,
|
||||
" setTimeout(() => process.exit(0), 25);",
|
||||
"});",
|
||||
]);
|
||||
const childScript = [
|
||||
"const fs = require('node:fs');",
|
||||
...handlerLines,
|
||||
"fs.writeFileSync(process.env.READY_FILE, 'ready');",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("\n");
|
||||
|
||||
const wrapper = spawn(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/run-with-env.mjs",
|
||||
`READY_FILE=${readyFile}`,
|
||||
`SIGNALED_FILE=${signaledFile}`,
|
||||
"--",
|
||||
"node",
|
||||
"-e",
|
||||
childScript,
|
||||
],
|
||||
{ cwd: process.cwd(), stdio: "ignore" },
|
||||
);
|
||||
|
||||
try {
|
||||
await waitFor(() => existsSync(readyFile), "wrapped command readiness");
|
||||
wrapper.kill(signal);
|
||||
|
||||
const exit = await waitForExit(wrapper);
|
||||
expect(exit).toEqual({ code: null, signal });
|
||||
expect(readFileSync(signaledFile, "utf8")).toBe(signal);
|
||||
} finally {
|
||||
wrapper.kill("SIGKILL");
|
||||
rmSync(tempDir, { force: true, recursive: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"cleans up wrapped command descendants on wrapper shutdown",
|
||||
async () => {
|
||||
const tempDir = mkdtempSync(path.join(tmpdir(), "openclaw-run-with-env-descendants-"));
|
||||
const readyFile = path.join(tempDir, "ready");
|
||||
const grandchildReadyFile = path.join(tempDir, "grandchild-ready");
|
||||
const grandchildPidFile = path.join(tempDir, "grandchild-pid");
|
||||
const grandchildScript = [
|
||||
"const fs = require('node:fs');",
|
||||
"process.on('SIGTERM', () => {});",
|
||||
"process.on('SIGHUP', () => {});",
|
||||
"fs.writeFileSync(process.env.GRANDCHILD_READY_FILE, 'ready');",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("\n");
|
||||
const childScript = [
|
||||
"const { spawn } = require('node:child_process');",
|
||||
"const fs = require('node:fs');",
|
||||
`const grandchild = spawn(process.execPath, ['-e', ${JSON.stringify(grandchildScript)}], { stdio: 'ignore' });`,
|
||||
"fs.writeFileSync(process.env.GRANDCHILD_PID_FILE, String(grandchild.pid));",
|
||||
"fs.writeFileSync(process.env.READY_FILE, 'ready');",
|
||||
"process.on('SIGTERM', () => process.exit(0));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("\n");
|
||||
const wrapper = spawn(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/run-with-env.mjs",
|
||||
`READY_FILE=${readyFile}`,
|
||||
`GRANDCHILD_READY_FILE=${grandchildReadyFile}`,
|
||||
`GRANDCHILD_PID_FILE=${grandchildPidFile}`,
|
||||
"--",
|
||||
"node",
|
||||
"-e",
|
||||
childScript,
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS: "200" },
|
||||
stdio: "ignore",
|
||||
},
|
||||
);
|
||||
let grandchildPid = 0;
|
||||
|
||||
try {
|
||||
await waitFor(() => existsSync(readyFile), "wrapped command readiness");
|
||||
await waitFor(
|
||||
() => existsSync(grandchildReadyFile),
|
||||
"wrapped command descendant readiness",
|
||||
);
|
||||
grandchildPid = Number(readFileSync(grandchildPidFile, "utf8"));
|
||||
expect(grandchildPid).toBeGreaterThan(0);
|
||||
expect(isProcessAlive(grandchildPid)).toBe(true);
|
||||
|
||||
wrapper.kill("SIGTERM");
|
||||
const exit = await waitForExit(wrapper, 3_000);
|
||||
expect(exit).toEqual({ code: null, signal: "SIGTERM" });
|
||||
await waitFor(
|
||||
() => !isProcessAlive(grandchildPid),
|
||||
"wrapped command descendant cleanup",
|
||||
5_000,
|
||||
);
|
||||
} finally {
|
||||
wrapper.kill("SIGKILL");
|
||||
if (grandchildPid > 0 && isProcessAlive(grandchildPid)) {
|
||||
process.kill(grandchildPid, "SIGKILL");
|
||||
}
|
||||
rmSync(tempDir, { force: true, recursive: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"lets wrapped command descendants finish during the shutdown grace period",
|
||||
async () => {
|
||||
const tempDir = mkdtempSync(path.join(tmpdir(), "openclaw-run-with-env-grace-"));
|
||||
const readyFile = path.join(tempDir, "ready");
|
||||
const gracefulFile = path.join(tempDir, "graceful");
|
||||
const grandchildReadyFile = path.join(tempDir, "grandchild-ready");
|
||||
const grandchildScript = [
|
||||
"const fs = require('node:fs');",
|
||||
"fs.writeFileSync(process.env.GRANDCHILD_READY_FILE, 'ready');",
|
||||
"process.on('SIGTERM', () => {",
|
||||
" setTimeout(() => {",
|
||||
" fs.writeFileSync(process.env.GRACEFUL_FILE, 'done');",
|
||||
" process.exit(0);",
|
||||
" }, 75);",
|
||||
"});",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("\n");
|
||||
const childScript = [
|
||||
"const { spawn } = require('node:child_process');",
|
||||
"const fs = require('node:fs');",
|
||||
`spawn(process.execPath, ['-e', ${JSON.stringify(grandchildScript)}], { stdio: 'ignore' });`,
|
||||
"fs.writeFileSync(process.env.READY_FILE, 'ready');",
|
||||
"process.on('SIGTERM', () => process.exit(0));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("\n");
|
||||
const wrapper = spawn(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/run-with-env.mjs",
|
||||
`READY_FILE=${readyFile}`,
|
||||
`GRACEFUL_FILE=${gracefulFile}`,
|
||||
`GRANDCHILD_READY_FILE=${grandchildReadyFile}`,
|
||||
"--",
|
||||
"node",
|
||||
"-e",
|
||||
childScript,
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_RUN_WITH_ENV_FORCE_KILL_MS: String(MAX_TIMER_TIMEOUT_MS + 1),
|
||||
},
|
||||
stdio: "ignore",
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await waitFor(() => existsSync(readyFile), "wrapped command readiness");
|
||||
await waitFor(
|
||||
() => existsSync(grandchildReadyFile),
|
||||
"wrapped command descendant readiness",
|
||||
);
|
||||
wrapper.kill("SIGTERM");
|
||||
|
||||
const exit = await waitForExit(wrapper, 3_000);
|
||||
expect(exit).toEqual({ code: null, signal: "SIGTERM" });
|
||||
expect(readFileSync(gracefulFile, "utf8")).toBe("done");
|
||||
} finally {
|
||||
wrapper.kill("SIGKILL");
|
||||
rmSync(tempDir, { force: true, recursive: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")("preserves wrapped command signal exits", () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/run-with-env.mjs",
|
||||
"OPENCLAW_RUN_WITH_ENV_SIGNAL_TEST=1",
|
||||
"--",
|
||||
"node",
|
||||
"-e",
|
||||
"process.kill(process.pid, 'SIGTERM')",
|
||||
],
|
||||
{ cwd: process.cwd(), encoding: "utf8" },
|
||||
);
|
||||
|
||||
expect(result.status).toBeNull();
|
||||
expect(result.signal).toBe("SIGTERM");
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")("preserves wrapped command force-kill exits", () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/run-with-env.mjs",
|
||||
"OPENCLAW_RUN_WITH_ENV_SIGNAL_TEST=1",
|
||||
"--",
|
||||
"node",
|
||||
"-e",
|
||||
"process.kill(process.pid, 'SIGKILL')",
|
||||
],
|
||||
{ cwd: process.cwd(), encoding: "utf8" },
|
||||
);
|
||||
|
||||
expect(result.status).toBeNull();
|
||||
expect(result.signal).toBe("SIGKILL");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user