

















@@ -1,5 +1,6 @@
1-import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
1+import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
22import path from "node:path";
3+import { setTimeout as sleep } from "node:timers/promises";
34import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
45import { describe, expect, it } from "vitest";
56import {
@@ -10,6 +11,15 @@ import {
1011startMatrixQaOpenClawCli,
1112} from "./scenario-runtime-cli.js";
121314+function isProcessRunning(pid: number): boolean {
15+try {
16+process.kill(pid, 0);
17+return true;
18+} catch {
19+return false;
20+}
21+}
22+1323describe("Matrix QA CLI runtime", () => {
1424it("redacts secret CLI arguments in diagnostic command text", () => {
1525expect(
@@ -176,4 +186,131 @@ describe("Matrix QA CLI runtime", () => {
176186await rm(root, { force: true, recursive: true });
177187}
178188});
189+190+it("kills CLI commands that ignore graceful timeout termination", async () => {
191+const root = await mkdtemp(
192+path.join(resolvePreferredOpenClawTmpDir(), "matrix-qa-cli-timeout-kill-"),
193+);
194+const pidPath = path.join(root, "cli.pid");
195+let childPid: number | undefined;
196+try {
197+await mkdir(path.join(root, "dist"));
198+await writeFile(
199+path.join(root, "dist", "index.mjs"),
200+[
201+"import { writeFileSync } from 'node:fs';",
202+`writeFileSync(${JSON.stringify(pidPath)}, String(process.pid));`,
203+"process.stdout.write('waiting despite graceful shutdown\\n');",
204+"process.on('SIGTERM', () => { process.stdout.write('ignored sigterm\\n'); });",
205+"setInterval(() => {}, 1000);",
206+].join("\n"),
207+);
208+209+await expect(
210+runMatrixQaOpenClawCli({
211+args: ["matrix", "verify", "self"],
212+cwd: root,
213+env: process.env,
214+timeoutMs: 500,
215+}),
216+).rejects.toThrow(/timed out after 500ms/u);
217+218+childPid = Number(await readFile(pidPath, "utf8"));
219+expect(isProcessRunning(childPid)).toBe(false);
220+} finally {
221+if (childPid && isProcessRunning(childPid)) {
222+process.kill(childPid, "SIGKILL");
223+}
224+await rm(root, { force: true, recursive: true });
225+}
226+});
227+228+it("preserves timeout diagnostics when wait attaches after timeout", async () => {
229+const root = await mkdtemp(
230+path.join(resolvePreferredOpenClawTmpDir(), "matrix-qa-cli-late-wait-timeout-"),
231+);
232+const pidPath = path.join(root, "cli.pid");
233+let childPid: number | undefined;
234+try {
235+await mkdir(path.join(root, "dist"));
236+await writeFile(
237+path.join(root, "dist", "index.mjs"),
238+[
239+"import { writeFileSync } from 'node:fs';",
240+`writeFileSync(${JSON.stringify(pidPath)}, String(process.pid));`,
241+"process.stdout.write('late wait timeout marker\\n');",
242+"process.on('SIGTERM', () => {});",
243+"setInterval(() => {}, 1000);",
244+].join("\n"),
245+);
246+247+const session = startMatrixQaOpenClawCli({
248+args: ["matrix", "verify", "self"],
249+cwd: root,
250+env: process.env,
251+timeoutMs: 500,
252+});
253+await sleep(850);
254+255+await expect(session.wait()).rejects.toThrow(/timed out after 500ms/u);
256+await expect(session.wait()).rejects.toThrow(/late wait timeout marker/u);
257+258+childPid = Number(await readFile(pidPath, "utf8"));
259+expect(isProcessRunning(childPid)).toBe(false);
260+} finally {
261+if (childPid && isProcessRunning(childPid)) {
262+process.kill(childPid, "SIGKILL");
263+}
264+await rm(root, { force: true, recursive: true });
265+}
266+});
267+268+it("settles and kills descendants that keep timed-out CLI stdio open", async () => {
269+const root = await mkdtemp(
270+path.join(resolvePreferredOpenClawTmpDir(), "matrix-qa-cli-timeout-tree-"),
271+);
272+const childPidPath = path.join(root, "child.pid");
273+const grandchildPidPath = path.join(root, "grandchild.pid");
274+let childPid: number | undefined;
275+let grandchildPid: number | undefined;
276+try {
277+await mkdir(path.join(root, "dist"));
278+await writeFile(
279+path.join(root, "dist", "index.mjs"),
280+[
281+"import { spawn } from 'node:child_process';",
282+"import { writeFileSync } from 'node:fs';",
283+`writeFileSync(${JSON.stringify(childPidPath)}, String(process.pid));`,
284+"const grandchild = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000);'], { stdio: ['ignore', 'inherit', 'inherit'] });",
285+`writeFileSync(${JSON.stringify(grandchildPidPath)}, String(grandchild.pid));`,
286+"process.stdout.write('spawned persistent descendant\\n');",
287+"process.on('SIGTERM', () => {});",
288+"setInterval(() => {}, 1000);",
289+].join("\n"),
290+);
291+292+await expect(
293+runMatrixQaOpenClawCli({
294+args: ["matrix", "verify", "self"],
295+cwd: root,
296+env: process.env,
297+timeoutMs: 500,
298+}),
299+).rejects.toThrow(/timed out after 500ms/u);
300+301+childPid = Number(await readFile(childPidPath, "utf8"));
302+grandchildPid = Number(await readFile(grandchildPidPath, "utf8"));
303+expect(isProcessRunning(childPid)).toBe(false);
304+if (process.platform !== "win32") {
305+expect(isProcessRunning(grandchildPid)).toBe(false);
306+}
307+} finally {
308+for (const pid of [grandchildPid, childPid]) {
309+if (pid && isProcessRunning(pid)) {
310+process.kill(pid, "SIGKILL");
311+}
312+}
313+await rm(root, { force: true, recursive: true });
314+}
315+});
179316});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。