
























11// Profile Extension Memory tests cover profile extension memory script behavior.
2-import { spawnSync } from "node:child_process";
2+import { spawn, spawnSync } from "node:child_process";
33import { EventEmitter } from "node:events";
4-import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4+import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
55import { tmpdir } from "node:os";
66import path from "node:path";
7+import { pathToFileURL } from "node:url";
78import { describe, expect, it } from "vitest";
89import { parseArgs, runCase } from "../../scripts/profile-extension-memory.mjs";
9101011const SCRIPT_PATH = path.resolve("scripts/profile-extension-memory.mjs");
111213+async function waitForCondition(predicate: () => boolean, timeoutMs = 5_000): Promise<void> {
14+const started = Date.now();
15+while (Date.now() - started < timeoutMs) {
16+if (predicate()) {
17+return;
18+}
19+await new Promise((resolve) => {
20+setTimeout(resolve, 50);
21+});
22+}
23+throw new Error("timed out waiting for condition");
24+}
25+26+function isProcessAlive(pid: number): boolean {
27+try {
28+process.kill(pid, 0);
29+return true;
30+} catch {
31+return false;
32+}
33+}
34+1235function runProfileExtensionMemory(args: string[], cwd = process.cwd()) {
1336return spawnSync(process.execPath, [SCRIPT_PATH, ...args], {
1437 cwd,
1538encoding: "utf8",
1639});
1740}
184142+async function waitForChildExit(
43+child: ReturnType<typeof spawn>,
44+timeoutMs = 8_000,
45+): Promise<{ status: number | null; signal: NodeJS.Signals | null }> {
46+let timer: ReturnType<typeof setTimeout> | undefined;
47+try {
48+return await Promise.race([
49+new Promise<{ status: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {
50+child.once("error", reject);
51+child.once("exit", (status, signal) => resolve({ status, signal }));
52+}),
53+new Promise<never>((_, reject) => {
54+timer = setTimeout(() => reject(new Error("timed out waiting for child exit")), timeoutMs);
55+timer.unref?.();
56+}),
57+]);
58+} finally {
59+if (timer) {
60+clearTimeout(timer);
61+}
62+}
63+}
64+1965describe("scripts/profile-extension-memory", () => {
2066it("prints help without requiring built plugin artifacts", () => {
2167const result = runProfileExtensionMemory(["--help"]);
@@ -174,4 +220,125 @@ describe("scripts/profile-extension-memory", () => {
174220timedOut: false,
175221});
176222});
223+224+it.runIf(process.platform !== "win32")(
225+"cleans timeout descendants before resolving the case",
226+async () => {
227+const root = mkdtempSync(path.join(tmpdir(), "openclaw-extension-memory-timeout-"));
228+const hookPath = path.join(root, "rss-hook.mjs");
229+const descendantPidPath = path.join(root, "descendant.pid");
230+let descendantPid = 0;
231+try {
232+writeFileSync(hookPath, "", "utf8");
233+const descendantScript = [
234+"process.on('SIGTERM', () => {});",
235+"setInterval(() => {}, 1000);",
236+].join("");
237+const body = [
238+"const childProcess = await import('node:child_process');",
239+"const fs = await import('node:fs');",
240+"const descendant = childProcess.spawn(process.execPath, [",
241+" '--input-type=module',",
242+` '--eval', ${JSON.stringify(descendantScript)},`,
243+"], { stdio: 'ignore' });",
244+`fs.writeFileSync(${JSON.stringify(descendantPidPath)}, String(descendant.pid));`,
245+"setInterval(() => {}, 1000);",
246+].join("\n");
247+const resultPromise = runCase({
248+ body,
249+env: process.env,
250+ hookPath,
251+name: "timeout-descendant",
252+repoRoot: root,
253+timeoutMs: 1_000,
254+});
255+256+await waitForCondition(() => existsSync(descendantPidPath));
257+descendantPid = Number.parseInt(readFileSync(descendantPidPath, "utf8"), 10);
258+expect(Number.isInteger(descendantPid)).toBe(true);
259+expect(isProcessAlive(descendantPid)).toBe(true);
260+261+await expect(resultPromise).resolves.toMatchObject({
262+name: "timeout-descendant",
263+signal: "SIGKILL",
264+timedOut: true,
265+});
266+await waitForCondition(() => !isProcessAlive(descendantPid));
267+} finally {
268+if (descendantPid && isProcessAlive(descendantPid)) {
269+process.kill(descendantPid, "SIGKILL");
270+}
271+rmSync(root, { recursive: true, force: true });
272+}
273+},
274+);
275+276+it.runIf(process.platform !== "win32")(
277+"cleans active case descendants on parent signal",
278+async () => {
279+const root = mkdtempSync(path.join(tmpdir(), "openclaw-extension-memory-parent-signal-"));
280+const hookPath = path.join(root, "rss-hook.mjs");
281+const runnerPath = path.join(root, "parent-signal-runner.mjs");
282+const descendantPidPath = path.join(root, "descendant.pid");
283+let descendantPid = 0;
284+try {
285+writeFileSync(hookPath, "", "utf8");
286+const descendantScript = [
287+"process.on('SIGTERM', () => {});",
288+"setInterval(() => {}, 1000);",
289+].join("");
290+const body = [
291+"const childProcess = await import('node:child_process');",
292+"const fs = await import('node:fs');",
293+"const descendant = childProcess.spawn(process.execPath, [",
294+" '--input-type=module',",
295+` '--eval', ${JSON.stringify(descendantScript)},`,
296+"], { stdio: 'ignore' });",
297+`fs.writeFileSync(${JSON.stringify(descendantPidPath)}, String(descendant.pid));`,
298+"setInterval(() => {}, 1000);",
299+].join("\n");
300+writeFileSync(
301+runnerPath,
302+[
303+`const { runCase } = await import(${JSON.stringify(
304+ pathToFileURL(path.resolve("scripts/profile-extension-memory.mjs")).href,
305+ )});`,
306+"void runCase({",
307+` body: ${JSON.stringify(body)},`,
308+" env: process.env,",
309+` hookPath: ${JSON.stringify(hookPath)},`,
310+" name: 'parent-signal-descendant',",
311+` repoRoot: ${JSON.stringify(root)},`,
312+" timeoutMs: 30000,",
313+"});",
314+].join("\n"),
315+"utf8",
316+);
317+const runner = spawn(process.execPath, [runnerPath], {
318+stdio: "ignore",
319+});
320+321+try {
322+await waitForCondition(() => existsSync(descendantPidPath));
323+descendantPid = Number.parseInt(readFileSync(descendantPidPath, "utf8"), 10);
324+expect(Number.isInteger(descendantPid)).toBe(true);
325+expect(isProcessAlive(descendantPid)).toBe(true);
326+327+const runnerExit = waitForChildExit(runner);
328+process.kill(runner.pid!, "SIGTERM");
329+await expect(runnerExit).resolves.toEqual({ status: 143, signal: null });
330+await waitForCondition(() => !isProcessAlive(descendantPid));
331+} finally {
332+if (runner.pid && isProcessAlive(runner.pid)) {
333+process.kill(runner.pid, "SIGKILL");
334+}
335+}
336+} finally {
337+if (descendantPid && isProcessAlive(descendantPid)) {
338+process.kill(descendantPid, "SIGKILL");
339+}
340+rmSync(root, { recursive: true, force: true });
341+}
342+},
343+);
177344});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。