
























11// Check Extension Package Tsc Boundary tests cover check extension package tsc boundary script behavior.
2+import { spawn } from "node:child_process";
23import { EventEmitter } from "node:events";
34import fs from "node:fs";
45import os from "node:os";
@@ -46,6 +47,43 @@ function createMockPipe() {
4647return pipe;
4748}
484950+function isProcessAlive(pid: number): boolean {
51+try {
52+process.kill(pid, 0);
53+return true;
54+} catch {
55+return false;
56+}
57+}
58+59+async function sleep(ms: number): Promise<void> {
60+await new Promise((resolve) => {
61+setTimeout(resolve, ms);
62+});
63+}
64+65+async function waitForFile(filePath: string, timeoutMs: number): Promise<void> {
66+const deadlineAt = Date.now() + timeoutMs;
67+while (Date.now() < deadlineAt) {
68+if (fs.existsSync(filePath)) {
69+return;
70+}
71+await sleep(25);
72+}
73+throw new Error(`timeout waiting for ${filePath}`);
74+}
75+76+async function waitForDead(pid: number, timeoutMs: number): Promise<void> {
77+const deadlineAt = Date.now() + timeoutMs;
78+while (Date.now() < deadlineAt) {
79+if (!isProcessAlive(pid)) {
80+return;
81+}
82+await sleep(25);
83+}
84+throw new Error(`process still alive: ${pid}`);
85+}
86+4987afterEach(() => {
5088for (const rootDir of tempRoots) {
5189fs.rmSync(rootDir, { force: true, recursive: true });
@@ -423,6 +461,7 @@ describe("check-extension-package-tsc-boundary", () => {
423461424462it("hard-kills timed out async node steps", async () => {
425463const processSignals: Array<[number, NodeJS.Signals | number | undefined]> = [];
464+let processGroupAlive = true;
426465const child = new EventEmitter() as EventEmitter & {
427466kill: (signal?: NodeJS.Signals | number) => boolean;
428467pid: number;
@@ -445,6 +484,13 @@ describe("check-extension-package-tsc-boundary", () => {
445484return child;
446485},
447486killProcess(pid: number, signal?: NodeJS.Signals | number) {
487+if (signal === "SIGKILL") {
488+processGroupAlive = false;
489+}
490+if (signal === 0 && !processGroupAlive) {
491+processSignals.push([pid, signal]);
492+throw Object.assign(new Error("gone"), { code: "ESRCH" });
493+}
448494processSignals.push([pid, signal]);
449495return true;
450496},
@@ -457,7 +503,10 @@ describe("check-extension-package-tsc-boundary", () => {
457503(error: unknown) => error,
458504);
459505460-expect(processSignals).toEqual([[-1234, "SIGKILL"]]);
506+expect(processSignals).toEqual([
507+[-1234, "SIGKILL"],
508+[-1234, 0],
509+]);
461510expect(failure).toBeInstanceOf(Error);
462511if (!(failure instanceof Error)) {
463512throw new Error("expected timeout failure to reject with an Error");
@@ -466,6 +515,57 @@ describe("check-extension-package-tsc-boundary", () => {
466515expect((failure as { kind?: unknown }).kind).toBe("timeout");
467516});
468517518+it.skipIf(process.platform === "win32")(
519+"waits for timed-out async node step process groups",
520+async () => {
521+const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-extension-tsc-timeout-"));
522+tempRoots.add(root);
523+const childPidPath = path.join(root, "child.pid");
524+let childPid = 0;
525+const childScript = [
526+"process.on('SIGTERM', () => {});",
527+"setInterval(() => {}, 1000);",
528+].join("");
529+const parentScript = [
530+"const { spawn } = require('node:child_process');",
531+"const fs = require('node:fs');",
532+`const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
533+`fs.writeFileSync(${JSON.stringify(childPidPath)}, String(child.pid));`,
534+"setInterval(() => {}, 1000);",
535+].join("");
536+537+try {
538+const failurePromise = runNodeStepAsync(
539+"hung-step-group",
540+["--eval", parentScript],
541+100,
542+{
543+spawnImpl(command: string, args: string[], options: unknown) {
544+return spawn(command, args, options as Parameters<typeof spawn>[2]);
545+},
546+},
547+).then(
548+() => {
549+throw new Error("expected hung-step-group to time out");
550+},
551+(error: unknown) => error,
552+);
553+554+await waitForFile(childPidPath, 2_000);
555+childPid = Number.parseInt(fs.readFileSync(childPidPath, "utf8"), 10);
556+expect(isProcessAlive(childPid)).toBe(true);
557+558+const failure = await failurePromise;
559+expect(failure).toBeInstanceOf(Error);
560+await waitForDead(childPid, 2_000);
561+} finally {
562+if (childPid && isProcessAlive(childPid)) {
563+process.kill(childPid, "SIGKILL");
564+}
565+}
566+},
567+);
568+469569it("aborts concurrent sibling steps after the first failure", async () => {
470570const startedAt = Date.now();
471571const slowStepTimeoutMs = 60_000;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。