



















11// Parallels Smoke Model tests cover parallels smoke model script behavior.
2+import { spawn } from "node:child_process";
23import { EventEmitter } from "node:events";
34import {
45chmodSync,
@@ -189,6 +190,21 @@ async function waitFor(predicate: () => boolean, timeoutMs = 3_000): Promise<voi
189190throw new Error("condition was not met before timeout");
190191}
191192193+async function waitForProcessClose(
194+child: ReturnType<typeof spawn>,
195+timeoutMs = 3_000,
196+): Promise<{ code: number | null; signal: NodeJS.Signals | null }> {
197+return await new Promise((resolve, reject) => {
198+const timer = setTimeout(() => {
199+reject(new Error("child process did not close before timeout"));
200+}, timeoutMs);
201+child.once("close", (code, signal) => {
202+clearTimeout(timer);
203+resolve({ code, signal });
204+});
205+});
206+}
207+192208describe("Parallels smoke model selection", () => {
193209let invalidProviderResult: ReturnType<typeof spawnNodeEvalSync>;
194210let missingProviderKeyResult: ReturnType<typeof spawnNodeEvalSync>;
@@ -1413,6 +1429,78 @@ setInterval(() => {}, 1000);
14131429},
14141430);
141514311432+it.runIf(process.platform !== "win32")(
1433+"reaps externally signaled timed host command descendants",
1434+async () => {
1435+const tempDir = makeTempDir(tempDirs, "openclaw-parallels-host-command-signal-");
1436+const runnerPath = join(tempDir, "runner.mjs");
1437+const readyPath = join(tempDir, "ready");
1438+const grandchildPidPath = join(tempDir, "grandchild.pid");
1439+const hostCommandUrl = pathToFileURL(join(process.cwd(), TS_PATHS.hostCommand)).href;
1440+let runnerPid = 0;
1441+let grandchildPid = 0;
1442+1443+try {
1444+const grandchildScript = [
1445+"const { writeFileSync } = require('node:fs');",
1446+"writeFileSync(process.env.OPENCLAW_TEST_GRANDCHILD_PID, String(process.pid));",
1447+"process.on('SIGTERM', () => {});",
1448+"setInterval(() => {}, 1000);",
1449+].join("\n");
1450+const parentScript = [
1451+"const { spawn } = require('node:child_process');",
1452+"const { writeFileSync } = require('node:fs');",
1453+`spawn(process.execPath, ['-e', ${JSON.stringify(grandchildScript)}], { env: process.env, stdio: 'ignore' });`,
1454+"writeFileSync(process.env.OPENCLAW_TEST_READY_FILE, 'ready');",
1455+"process.on('SIGTERM', () => process.exit(0));",
1456+"setInterval(() => {}, 1000);",
1457+].join("\n");
1458+writeFileSync(
1459+runnerPath,
1460+[
1461+`import { run } from ${JSON.stringify(hostCommandUrl)};`,
1462+`run(process.execPath, ['-e', ${JSON.stringify(parentScript)}], {`,
1463+" check: false,",
1464+" env: {",
1465+" ...process.env,",
1466+` OPENCLAW_TEST_GRANDCHILD_PID: ${JSON.stringify(grandchildPidPath)},`,
1467+` OPENCLAW_TEST_READY_FILE: ${JSON.stringify(readyPath)},`,
1468+" },",
1469+" quiet: true,",
1470+" timeoutMs: 30_000,",
1471+"});",
1472+].join("\n"),
1473+"utf8",
1474+);
1475+1476+const runner = spawn(process.execPath, ["--import", "tsx", runnerPath], {
1477+cwd: process.cwd(),
1478+detached: true,
1479+stdio: "ignore",
1480+});
1481+runnerPid = runner.pid ?? 0;
1482+expect(runnerPid).toBeGreaterThan(0);
1483+await waitFor(() => existsSync(readyPath) && existsSync(grandchildPidPath), 2_000);
1484+grandchildPid = Number.parseInt(readFileSync(grandchildPidPath, "utf8"), 10);
1485+1486+process.kill(-runnerPid, "SIGTERM");
1487+1488+await expect(waitForProcessClose(runner, 3_000)).resolves.toEqual({
1489+code: null,
1490+signal: "SIGTERM",
1491+});
1492+await waitFor(() => !isProcessAlive(grandchildPid), 3_000);
1493+} finally {
1494+if (runnerPid && isProcessAlive(runnerPid)) {
1495+process.kill(-runnerPid, "SIGKILL");
1496+}
1497+if (grandchildPid && isProcessAlive(grandchildPid)) {
1498+process.kill(grandchildPid, "SIGKILL");
1499+}
1500+}
1501+},
1502+);
1503+14161504it.runIf(process.platform !== "win32")("preserves timed host command spawn errors", () => {
14171505expect(() =>
14181506run("openclaw-definitely-missing-host-command", [], {
@@ -1498,6 +1586,78 @@ setInterval(() => {}, 1000);
14981586},
14991587);
150015881589+it.runIf(process.platform !== "win32")(
1590+"reaps externally signaled streaming host command descendants before re-raising",
1591+async () => {
1592+const tempDir = makeTempDir(tempDirs, "openclaw-parallels-streaming-host-command-signal-");
1593+const runnerPath = join(tempDir, "runner.mjs");
1594+const readyPath = join(tempDir, "ready");
1595+const grandchildPidPath = join(tempDir, "grandchild.pid");
1596+const logPath = join(tempDir, "stream.log");
1597+const hostCommandUrl = pathToFileURL(join(process.cwd(), TS_PATHS.hostCommand)).href;
1598+let runnerPid = 0;
1599+let grandchildPid = 0;
1600+1601+try {
1602+const grandchildScript = [
1603+"const { writeFileSync } = require('node:fs');",
1604+"writeFileSync(process.env.OPENCLAW_TEST_GRANDCHILD_PID, String(process.pid));",
1605+"process.on('SIGTERM', () => {});",
1606+"setInterval(() => {}, 1000);",
1607+].join("\n");
1608+const parentScript = [
1609+"const { spawn } = require('node:child_process');",
1610+"const { writeFileSync } = require('node:fs');",
1611+`spawn(process.execPath, ['-e', ${JSON.stringify(grandchildScript)}], { env: process.env, stdio: 'ignore' });`,
1612+"writeFileSync(process.env.OPENCLAW_TEST_READY_FILE, 'ready');",
1613+"process.on('SIGTERM', () => process.exit(0));",
1614+"setInterval(() => {}, 1000);",
1615+].join("\n");
1616+writeFileSync(
1617+runnerPath,
1618+[
1619+`import { runStreaming } from ${JSON.stringify(hostCommandUrl)};`,
1620+`await runStreaming(process.execPath, ['-e', ${JSON.stringify(parentScript)}], {`,
1621+" env: {",
1622+" ...process.env,",
1623+` OPENCLAW_TEST_GRANDCHILD_PID: ${JSON.stringify(grandchildPidPath)},`,
1624+` OPENCLAW_TEST_READY_FILE: ${JSON.stringify(readyPath)},`,
1625+" },",
1626+` logPath: ${JSON.stringify(logPath)},`,
1627+" quiet: true,",
1628+" timeoutMs: 30_000,",
1629+"});",
1630+].join("\n"),
1631+"utf8",
1632+);
1633+1634+const runner = spawn(process.execPath, ["--import", "tsx", runnerPath], {
1635+cwd: process.cwd(),
1636+stdio: "ignore",
1637+});
1638+runnerPid = runner.pid ?? 0;
1639+expect(runnerPid).toBeGreaterThan(0);
1640+await waitFor(() => existsSync(readyPath) && existsSync(grandchildPidPath), 2_000);
1641+grandchildPid = Number.parseInt(readFileSync(grandchildPidPath, "utf8"), 10);
1642+1643+runner.kill("SIGTERM");
1644+1645+await expect(waitForProcessClose(runner, 3_000)).resolves.toEqual({
1646+code: null,
1647+signal: "SIGTERM",
1648+});
1649+await waitFor(() => !isProcessAlive(grandchildPid), 3_000);
1650+} finally {
1651+if (runnerPid && isProcessAlive(runnerPid)) {
1652+process.kill(runnerPid, "SIGKILL");
1653+}
1654+if (grandchildPid && isProcessAlive(grandchildPid)) {
1655+process.kill(grandchildPid, "SIGKILL");
1656+}
1657+}
1658+},
1659+);
1660+15011661it("streams host command logs instead of retaining them in memory", async () => {
15021662const source = readFileSync(TS_PATHS.hostCommand, "utf8");
15031663const runStreamingBlock = source.slice(source.indexOf("export async function runStreaming"));
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。