

























1+import { spawn } from "node:child_process";
12import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
23import { readFile } from "node:fs/promises";
34import { tmpdir } from "node:os";
@@ -53,6 +54,24 @@ async function waitForDead(pid: number, timeoutMs: number): Promise<void> {
5354throw new Error(`process still alive: ${pid}`);
5455}
555657+async function waitForExit(
58+child: ReturnType<typeof spawn>,
59+timeoutMs: number,
60+): Promise<{ code: number | null; signal: NodeJS.Signals | null }> {
61+if (child.exitCode !== null || child.signalCode !== null) {
62+return { code: child.exitCode, signal: child.signalCode as NodeJS.Signals | null };
63+}
64+return await new Promise((resolve, reject) => {
65+const timer = setTimeout(() => {
66+reject(new Error(`process did not exit within ${timeoutMs}ms`));
67+}, timeoutMs);
68+child.once("exit", (code, signal) => {
69+clearTimeout(timer);
70+resolve({ code, signal });
71+});
72+});
73+}
74+5675afterEach(() => {
5776for (const dir of tempDirs.splice(0)) {
5877rmSync(dir, { force: true, recursive: true });
@@ -147,43 +166,155 @@ setInterval(() => {}, 1000);
147166},
148167);
149168169+it.runIf(process.platform !== "win32")("kills timed-out child process groups", async () => {
170+const dir = makeTempDir("openclaw-telegram-credential-tree-timeout-");
171+const childPidPath = path.join(dir, "child.pid");
172+let childPid: number | undefined;
173+174+try {
175+const childScript = "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);";
176+const parentScript = [
177+"const { spawn } = require('node:child_process');",
178+"const fs = require('node:fs');",
179+`const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
180+`fs.writeFileSync(${JSON.stringify(childPidPath)}, String(child.pid));`,
181+"setInterval(() => {}, 1000);",
182+].join("");
183+184+const runPromise = runCommand(process.execPath, ["-e", parentScript], dir, {
185+timeoutKillGraceMs: 25,
186+timeoutMs: 100,
187+});
188+await waitForFile(childPidPath, 2_000);
189+childPid = Number.parseInt(readFileSync(childPidPath, "utf8"), 10);
190+191+await expect(runPromise).rejects.toMatchObject({
192+code: "ETIMEDOUT",
193+message: expect.stringContaining("timed out after 100ms"),
194+});
195+await waitForDead(childPid, 2_000);
196+} finally {
197+if (childPid !== undefined && isProcessAlive(childPid)) {
198+process.kill(childPid, "SIGKILL");
199+}
200+}
201+});
202+150203it.runIf(process.platform !== "win32")(
151-"kills timed-out child process groups",
204+"exits promptly after forwarded SIGTERM children exit cleanly",
152205async () => {
153-const dir = makeTempDir("openclaw-telegram-credential-tree-timeout-");
206+const dir = makeTempDir("openclaw-telegram-credential-signal-");
207+const runnerPath = path.join(dir, "runner.mjs");
208+const readyPath = path.join(dir, "ready.txt");
154209const childPidPath = path.join(dir, "child.pid");
210+const ioModuleUrl = new URL(
211+"../../scripts/e2e/telegram-user-credential-io.ts",
212+import.meta.url,
213+).href;
214+const childScript = [
215+"const fs = require('node:fs');",
216+`fs.writeFileSync(${JSON.stringify(childPidPath)}, String(process.pid));`,
217+`fs.writeFileSync(${JSON.stringify(readyPath)}, 'ready');`,
218+"process.on('SIGTERM', () => process.exit(0));",
219+"setInterval(() => {}, 1000);",
220+].join("");
221+writeFileSync(
222+runnerPath,
223+[
224+`import { runCommand } from ${JSON.stringify(ioModuleUrl)};`,
225+`await runCommand(process.execPath, ['-e', ${JSON.stringify(childScript)}], undefined, { timeoutMs: 30_000 });`,
226+"",
227+].join("\n"),
228+"utf8",
229+);
230+const runner = spawn(process.execPath, ["--import", "tsx", runnerPath], {
231+env: process.env,
232+stdio: ["ignore", "pipe", "pipe"],
233+});
155234let childPid: number | undefined;
156-157235try {
158-const childScript = "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);";
159-const parentScript = [
160-"const { spawn } = require('node:child_process');",
161-"const fs = require('node:fs');",
162-`const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
163-`fs.writeFileSync(${JSON.stringify(childPidPath)}, String(child.pid));`,
164-"setInterval(() => {}, 1000);",
165-].join("");
166-167-const runPromise = runCommand(process.execPath, ["-e", parentScript], dir, {
168-timeoutKillGraceMs: 25,
169-timeoutMs: 100,
170-});
171-await waitForFile(childPidPath, 2_000);
236+await waitForFile(readyPath, 2_000);
172237childPid = Number.parseInt(readFileSync(childPidPath, "utf8"), 10);
238+const startedAt = Date.now();
239+runner.kill("SIGTERM");
240+const exit = await waitForExit(runner, 2_000);
173241174-await expect(runPromise).rejects.toMatchObject({
175-code: "ETIMEDOUT",
176-message: expect.stringContaining("timed out after 100ms"),
177-});
242+expect(exit).toEqual({ code: 143, signal: null });
243+expect(Date.now() - startedAt).toBeLessThan(1_500);
178244await waitForDead(childPid, 2_000);
179245} finally {
246+if (runner.exitCode === null && runner.signalCode === null) {
247+runner.kill("SIGKILL");
248+}
180249if (childPid !== undefined && isProcessAlive(childPid)) {
181250process.kill(childPid, "SIGKILL");
182251}
183252}
184253},
185254);
186255256+it.runIf(process.platform !== "win32")(
257+"keeps the forwarded signal force-kill armed while grandchildren survive",
258+async () => {
259+const dir = makeTempDir("openclaw-telegram-credential-grandchild-signal-");
260+const runnerPath = path.join(dir, "runner.mjs");
261+const readyPath = path.join(dir, "ready.txt");
262+const grandchildPidPath = path.join(dir, "grandchild.pid");
263+const ioModuleUrl = new URL(
264+"../../scripts/e2e/telegram-user-credential-io.ts",
265+import.meta.url,
266+).href;
267+const grandchildScript = [
268+"const fs = require('node:fs');",
269+`fs.writeFileSync(${JSON.stringify(grandchildPidPath)}, String(process.pid));`,
270+"process.on('SIGTERM', () => {});",
271+"setInterval(() => {}, 1000);",
272+].join("");
273+const parentScript = [
274+"const { spawn } = require('node:child_process');",
275+"const fs = require('node:fs');",
276+`const grandchild = spawn(process.execPath, ['-e', ${JSON.stringify(grandchildScript)}], { stdio: 'ignore' });`,
277+`fs.writeFileSync(${JSON.stringify(readyPath)}, String(grandchild.pid));`,
278+"process.on('SIGTERM', () => process.exit(0));",
279+"setInterval(() => {}, 1000);",
280+].join("");
281+writeFileSync(
282+runnerPath,
283+[
284+`import { runCommand } from ${JSON.stringify(ioModuleUrl)};`,
285+`await runCommand(process.execPath, ['-e', ${JSON.stringify(parentScript)}], undefined, { timeoutMs: 30_000 });`,
286+"",
287+].join("\n"),
288+"utf8",
289+);
290+const runner = spawn(process.execPath, ["--import", "tsx", runnerPath], {
291+env: {
292+ ...process.env,
293+OPENCLAW_QA_CREDENTIAL_KILL_GRACE_MS: "100",
294+},
295+stdio: ["ignore", "pipe", "pipe"],
296+});
297+let grandchildPid: number | undefined;
298+try {
299+await waitForFile(readyPath, 2_000);
300+await waitForFile(grandchildPidPath, 2_000);
301+grandchildPid = Number.parseInt(readFileSync(grandchildPidPath, "utf8"), 10);
302+runner.kill("SIGTERM");
303+const exit = await waitForExit(runner, 2_000);
304+305+expect(exit).toEqual({ code: 143, signal: null });
306+await waitForDead(grandchildPid, 2_000);
307+} finally {
308+if (runner.exitCode === null && runner.signalCode === null) {
309+runner.kill("SIGKILL");
310+}
311+if (grandchildPid !== undefined && isProcessAlive(grandchildPid)) {
312+process.kill(grandchildPid, "SIGKILL");
313+}
314+}
315+},
316+);
317+187318it("aborts broker fetches that never return", async () => {
188319let signal: AbortSignal | undefined;
189320await expect(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。