























11// Test Install Sh Docker tests cover test install sh docker script behavior.
2-import { spawnSync } from "node:child_process";
2+import { spawn, spawnSync } from "node:child_process";
33import { existsSync, readFileSync } from "node:fs";
44import path from "node:path";
55import { runInNewContext } from "node:vm";
@@ -102,6 +102,30 @@ function runDefaultSmokePlatform(env: Record<string, string>, hostArch: string):
102102return result.stdout;
103103}
104104105+async function waitForCondition(
106+predicate: () => boolean,
107+label: string,
108+timeoutMs = 2_000,
109+): Promise<void> {
110+const deadlineAt = Date.now() + timeoutMs;
111+while (Date.now() < deadlineAt) {
112+if (predicate()) {
113+return;
114+}
115+await new Promise((resolve) => setTimeout(resolve, 25));
116+}
117+throw new Error(`timed out waiting for ${label}`);
118+}
119+120+function isProcessAlive(pid: number): boolean {
121+try {
122+process.kill(pid, 0);
123+return true;
124+} catch {
125+return false;
126+}
127+}
128+105129function extractReadPackTarballFilename(): string {
106130const script = readFileSync(SCRIPT_PATH, "utf8");
107131const match = script.match(/(read_pack_tarball_filename\(\) \{[\s\S]*?\n\})\n\nSMOKE_IMAGE/u);
@@ -812,6 +836,78 @@ describe("bun global install smoke", () => {
812836},
813837);
814838839+it.runIf(process.platform !== "win32")(
840+"cleans Bun global smoke descendants on parent signal",
841+async () => {
842+const tempDir = tempDirs.make("openclaw-bun-global-parent-signal-");
843+const readyPath = path.join(tempDir, "ready");
844+const descendantPidPath = path.join(tempDir, "descendant.pid");
845+let descendantPid = 0;
846+const descendantScript = [
847+"const fs = require('node:fs');",
848+`fs.writeFileSync(${JSON.stringify(descendantPidPath)}, String(process.pid));`,
849+"process.on('SIGTERM', () => {});",
850+"setInterval(() => {}, 1000);",
851+].join("\n");
852+const parentScript = [
853+"const childProcess = require('node:child_process');",
854+"const fs = require('node:fs');",
855+`childProcess.spawn(process.execPath, ["-e", ${JSON.stringify(descendantScript)}], { stdio: "ignore" });`,
856+`fs.writeFileSync(${JSON.stringify(readyPath)}, "ready");`,
857+"process.on('SIGTERM', () => process.exit(0));",
858+"setInterval(() => {}, 1000);",
859+].join("\n");
860+const runner = spawn(
861+process.execPath,
862+[
863+BUN_GLOBAL_ASSERTIONS_PATH,
864+"run-with-timeout",
865+"60000",
866+process.execPath,
867+"-e",
868+parentScript,
869+],
870+{
871+env: {
872+ ...process.env,
873+OPENCLAW_BUN_GLOBAL_SMOKE_TIMEOUT_KILL_GRACE_MS: "100",
874+},
875+stdio: ["ignore", "pipe", "pipe"],
876+},
877+);
878+const runnerExit = new Promise<{ status: number | null; signal: NodeJS.Signals | null }>(
879+(resolve) => {
880+runner.once("exit", (status, signal) => resolve({ status, signal }));
881+},
882+);
883+884+try {
885+await waitForCondition(
886+() => existsSync(readyPath) && existsSync(descendantPidPath),
887+"Bun global smoke descendant readiness",
888+);
889+descendantPid = Number.parseInt(readFileSync(descendantPidPath, "utf8"), 10);
890+expect(Number.isInteger(descendantPid)).toBe(true);
891+expect(isProcessAlive(descendantPid)).toBe(true);
892+893+runner.kill("SIGTERM");
894+895+await expect(runnerExit).resolves.toEqual({ status: 143, signal: null });
896+await waitForCondition(
897+() => !isProcessAlive(descendantPid),
898+"Bun global smoke descendant cleanup",
899+);
900+} finally {
901+if (runner.pid && isProcessAlive(runner.pid)) {
902+process.kill(runner.pid, "SIGKILL");
903+}
904+if (descendantPid && isProcessAlive(descendantPid)) {
905+process.kill(descendantPid, "SIGKILL");
906+}
907+}
908+},
909+);
910+815911it("gates workflow Bun install smoke to scheduled and release-check runs", () => {
816912const workflow = readFileSync(INSTALL_SMOKE_WORKFLOW_PATH, "utf8");
817913const releaseChecks = readFileSync(RELEASE_CHECKS_WORKFLOW_PATH, "utf8");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。