



























11// Write Cli Startup Metadata tests cover write cli startup metadata script behavior.
2+import { spawn } from "node:child_process";
23import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
34import path from "node:path";
5+import { pathToFileURL } from "node:url";
46import { describe, expect, it } from "vitest";
57import { __testing, writeCliStartupMetadata } from "../../scripts/write-cli-startup-metadata.ts";
68import { createScriptTestHarness } from "./test-helpers.js";
@@ -69,6 +71,21 @@ async function waitForProcessExit(pid: number, timeoutMs = 1_000): Promise<void>
6971throw new Error(`process ${pid} was still alive after ${timeoutMs}ms`);
7072}
717374+async function waitForChildClose(
75+child: ReturnType<typeof spawn>,
76+timeoutMs = 2_000,
77+): Promise<{ code: number | null; signal: NodeJS.Signals | null }> {
78+return await new Promise((resolve, reject) => {
79+const timeout = setTimeout(() => {
80+reject(new Error("child did not close before timeout"));
81+}, timeoutMs);
82+child.once("close", (code, signal) => {
83+clearTimeout(timeout);
84+resolve({ code, signal });
85+});
86+});
87+}
88+7289describe("write-cli-startup-metadata", () => {
7390const { createTempDir } = createScriptTestHarness();
7491@@ -137,6 +154,120 @@ describe("write-cli-startup-metadata", () => {
137154},
138155);
139156157+it.runIf(process.platform !== "win32")(
158+"waits for all command help descendants before re-raising parent signals",
159+async () => {
160+const tempRoot = createTempDir("openclaw-startup-metadata-signal-");
161+const fastCommandPath = path.join(tempRoot, "fast-command.mjs");
162+const fastReadyPath = path.join(tempRoot, "fast-ready");
163+const commandPath = path.join(tempRoot, "command.mjs");
164+const runnerPath = path.join(tempRoot, "runner.mjs");
165+const grandchildPidPath = path.join(tempRoot, "grandchild.pid");
166+const grandchildScript = [
167+"process.on('SIGTERM', () => {});",
168+"setInterval(() => {}, 1000);",
169+].join("\n");
170+writeFixtureFile(
171+tempRoot,
172+"fast-command.mjs",
173+[
174+"import { writeFileSync } from 'node:fs';",
175+`writeFileSync(${JSON.stringify(fastReadyPath)}, "ready");`,
176+"process.on('SIGTERM', () => process.exit(0));",
177+"setInterval(() => {}, 1000);",
178+].join("\n"),
179+);
180+writeFixtureFile(
181+tempRoot,
182+"command.mjs",
183+[
184+"import { spawn } from 'node:child_process';",
185+"import { writeFileSync } from 'node:fs';",
186+`const grandchild = spawn(process.execPath, ["--eval", ${JSON.stringify(
187+ grandchildScript,
188+ )}], { stdio: "ignore" });`,
189+`writeFileSync(${JSON.stringify(grandchildPidPath)}, String(grandchild.pid));`,
190+"process.on('SIGTERM', () => process.exit(0));",
191+"setInterval(() => {}, 1000);",
192+].join("\n"),
193+);
194+writeFixtureFile(
195+tempRoot,
196+"runner.mjs",
197+[
198+`const { __testing } = await import(${JSON.stringify(
199+ pathToFileURL(path.resolve("scripts/write-cli-startup-metadata.ts")).href,
200+ )});`,
201+"void __testing.spawnText(",
202+` [${JSON.stringify(fastCommandPath)}],`,
203+" {",
204+` cwd: ${JSON.stringify(tempRoot)},`,
205+" env: process.env,",
206+" failureMessage: 'fast render failed',",
207+" killGraceMs: 100,",
208+" maxOutputBytes: 1024,",
209+" timeoutMs: 30_000,",
210+" },",
211+").catch(() => undefined);",
212+"void __testing.spawnText(",
213+` [${JSON.stringify(commandPath)}],`,
214+" {",
215+` cwd: ${JSON.stringify(tempRoot)},`,
216+" env: process.env,",
217+" failureMessage: 'render failed',",
218+" killGraceMs: 100,",
219+" maxOutputBytes: 1024,",
220+" timeoutMs: 30_000,",
221+" },",
222+").catch(() => undefined);",
223+].join("\n"),
224+);
225+226+const runner = spawn(process.execPath, ["--import", "tsx", runnerPath], {
227+cwd: process.cwd(),
228+stdio: "ignore",
229+});
230+let grandchildPid = 0;
231+232+try {
233+const deadline = Date.now() + 1_000;
234+while (Date.now() < deadline) {
235+try {
236+grandchildPid = Number(readFileSync(grandchildPidPath, "utf8"));
237+} catch {}
238+let fastReady = false;
239+try {
240+fastReady = readFileSync(fastReadyPath, "utf8") === "ready";
241+} catch {}
242+if (fastReady && grandchildPid > 0 && processIsAlive(grandchildPid)) {
243+break;
244+}
245+await new Promise((resolve) => {
246+setTimeout(resolve, 10);
247+});
248+}
249+expect(readFileSync(fastReadyPath, "utf8")).toBe("ready");
250+expect(grandchildPid).toBeGreaterThan(0);
251+expect(processIsAlive(grandchildPid)).toBe(true);
252+253+runner.kill("SIGTERM");
254+255+await expect(waitForChildClose(runner)).resolves.toEqual({
256+code: null,
257+signal: "SIGTERM",
258+});
259+await waitForProcessExit(grandchildPid, 2_000);
260+} finally {
261+if (runner.pid && processIsAlive(runner.pid)) {
262+runner.kill("SIGKILL");
263+}
264+if (grandchildPid > 0 && processIsAlive(grandchildPid)) {
265+process.kill(grandchildPid, "SIGKILL");
266+}
267+}
268+},
269+);
270+140271it("writes startup metadata with populated root help text when dist falls back to source rendering", async () => {
141272const tempRoot = createTempDir("openclaw-startup-metadata-");
142273const distDir = path.join(tempRoot, "dist");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。