



















@@ -15,7 +15,7 @@ import { tmpdir } from "node:os";
1515import { basename, delimiter, join, win32 } from "node:path";
1616import { setTimeout as delay } from "node:timers/promises";
1717import { pathToFileURL } from "node:url";
18-import { beforeAll, describe, expect, it, vi } from "vitest";
18+import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
1919import {
2020extractLastOpenClawVersionFromLog,
2121modelProviderConfigBatchJson,
@@ -50,6 +50,7 @@ import {
5050import { parseArgs as parseWindowsSmokeArgs } from "../../scripts/e2e/parallels/windows-smoke.ts";
5151import { withEnv } from "../../src/test-utils/env.js";
5252import { spawnNodeEvalSync } from "../../src/test-utils/node-process.js";
53+import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";
53545455const WRAPPERS = {
5556linux: "scripts/e2e/parallels-linux-smoke.sh",
@@ -82,6 +83,11 @@ const TS_PATHS = {
8283};
83848485const OS_TS_PATHS = [TS_PATHS.linux, TS_PATHS.macos, TS_PATHS.windows];
86+const tempDirs: string[] = [];
87+88+afterEach(() => {
89+cleanupTempDirs(tempDirs);
90+});
85918692function countNonEmptyLines(value: string): number {
8793let count = 0;
@@ -1300,28 +1306,50 @@ if (isPrlctl) {
13001306expect(result.stdout).toBeTypeOf("string");
13011307});
130213081303-it("does not wait for host commands that trap SIGTERM after a timeout", () => {
1304-const startedAt = Date.now();
1305-const result = run(
1306-process.execPath,
1307-[
1308-"-e",
1309-[
1310-"process.on('SIGTERM', () => {});",
1311-"setTimeout(() => process.exit(77), 700);",
1312-"setInterval(() => {}, 1000);",
1313-].join(""),
1314-],
1315-{
1316-check: false,
1317-quiet: true,
1318-timeoutMs: 50,
1319-},
1320-);
1309+it.runIf(process.platform !== "win32")(
1310+"lets timed host command descendants drain before force kill",
1311+() => {
1312+const tempDir = makeTempDir(tempDirs, "openclaw-parallels-host-command-drain-");
1313+const readyFile = join(tempDir, "ready");
1314+const drainFile = join(tempDir, "drained");
1315+const descendantScript = [
1316+"const { writeFileSync } = require('node:fs');",
1317+"writeFileSync(process.env.READY_FILE, 'ready');",
1318+"process.on('SIGTERM', () => {",
1319+" setTimeout(() => {",
1320+" writeFileSync(process.env.DRAIN_FILE, 'drained');",
1321+" process.exit(0);",
1322+" }, 25);",
1323+"});",
1324+"setInterval(() => {}, 1000);",
1325+].join("\n");
1326+const parentScript = [
1327+"const { spawn } = require('node:child_process');",
1328+`spawn(process.execPath, ['-e', ${JSON.stringify(descendantScript)}], { env: process.env, stdio: 'ignore' });`,
1329+"process.on('SIGTERM', () => process.exit(0));",
1330+"setInterval(() => {}, 1000);",
1331+].join("\n");
132113321322-expect(result.status).toBe(124);
1323-expect(Date.now() - startedAt).toBeLessThan(500);
1324-});
1333+try {
1334+const result = run(process.execPath, ["-e", parentScript], {
1335+check: false,
1336+env: {
1337+ ...process.env,
1338+DRAIN_FILE: drainFile,
1339+READY_FILE: readyFile,
1340+},
1341+quiet: true,
1342+timeoutMs: 1_000,
1343+});
1344+1345+expect(result.status).toBe(124);
1346+expect(existsSync(readyFile)).toBe(true);
1347+expect(readFileSync(drainFile, "utf8")).toBe("drained");
1348+} finally {
1349+cleanupTempDirs(tempDirs);
1350+}
1351+},
1352+);
1325135313261354it.runIf(process.platform !== "win32")("throws checked timed host command timeouts", () => {
13271355expect(() =>
@@ -1424,6 +1452,52 @@ setInterval(() => {}, 1000);
14241452}
14251453});
142614541455+it.runIf(process.platform !== "win32")(
1456+"lets timed streaming host command descendants drain before force kill",
1457+async () => {
1458+const tempDir = makeTempDir(tempDirs, "openclaw-parallels-streaming-host-command-drain-");
1459+const readyFile = join(tempDir, "ready");
1460+const drainFile = join(tempDir, "drained");
1461+const logPath = join(tempDir, "stream.log");
1462+const descendantScript = [
1463+"const { writeFileSync } = require('node:fs');",
1464+"writeFileSync(process.env.READY_FILE, 'ready');",
1465+"process.on('SIGTERM', () => {",
1466+" setTimeout(() => {",
1467+" writeFileSync(process.env.DRAIN_FILE, 'drained');",
1468+" process.exit(0);",
1469+" }, 50);",
1470+"});",
1471+"setInterval(() => {}, 1000);",
1472+].join("\n");
1473+const parentScript = [
1474+"const { spawn } = require('node:child_process');",
1475+`spawn(process.execPath, ['-e', ${JSON.stringify(descendantScript)}], { env: process.env, stdio: 'ignore' });`,
1476+"process.on('SIGTERM', () => process.exit(0));",
1477+"setInterval(() => {}, 1000);",
1478+].join("\n");
1479+1480+try {
1481+const statusPromise = runStreaming(process.execPath, ["-e", parentScript], {
1482+env: {
1483+ ...process.env,
1484+DRAIN_FILE: drainFile,
1485+READY_FILE: readyFile,
1486+},
1487+ logPath,
1488+quiet: true,
1489+timeoutMs: 500,
1490+});
1491+1492+await waitFor(() => existsSync(readyFile), 2_000);
1493+await expect(statusPromise).resolves.toBe(124);
1494+expect(readFileSync(drainFile, "utf8")).toBe("drained");
1495+} finally {
1496+cleanupTempDirs(tempDirs);
1497+}
1498+},
1499+);
1500+14271501it("streams host command logs instead of retaining them in memory", async () => {
14281502const source = readFileSync(TS_PATHS.hostCommand, "utf8");
14291503const runStreamingBlock = source.slice(source.indexOf("export async function runStreaming"));
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。