1// @effect-diagnostics nodeBuiltinImport:off - Exercises the real inherited descriptor lifecycle in a subprocess.
2import * as NodeChildProcess from "node:child_process";
3import * as NodeURL from "node:url";
4
5import { expect, it } from "vite-plus/test";
6
7it("receives desktop messages and exits while the parent keeps both input pipes open", async () => {
8 const child = NodeChildProcess.spawn(
9 process.execPath,
10 [NodeURL.fileURLToPath(new URL("./testing/DesktopPipeLifecycle.fixture.ts", import.meta.url))],
11 { stdio: ["ignore", "pipe", "pipe", "pipe", "pipe", "pipe", "pipe"] },
12 );
13 // A failed exit must not leave a test process running. This never fires on success.
14 // @effect-diagnostics-next-line globalTimers:off -- Bounds the native subprocess on failure; success waits for process exit.
15 const watchdog = setTimeout(() => child.kill("SIGKILL"), 8_000);
16 let output = "";
17 let errors = "";
18 let verified = false;
19 child.stderr?.on("data", (chunk: Buffer) => {
20 errors += chunk.toString();
21 });
22 const write = (fd: number, value: Record<string, unknown>) => {
23 const stream = child.stdio[fd];
24 if (!stream || !("write" in stream)) throw new Error(`Missing pipe ${fd}`);
25 stream.write(`${JSON.stringify(value)}\n`);
26 };
27 const key = { threadId: "thread-1", tabId: "tab-1" };
28 child.stdout?.on("data", (chunk: Buffer) => {
29 output += chunk.toString();
30 let end: number;
31 while ((end = output.indexOf("\n")) >= 0) {
32 const line = output.slice(0, end);
33 output = output.slice(end + 1);
34 if (line === "ready") {
35 write(3, { type: "attached", ...key });
36 write(5, { version: 1, type: "desktopTelemetryHello", electronPid: process.pid });
37 } else if (line === "attached") {
38 write(3, { type: "detached", ...key });
39 } else if (line === "verified") {
40 verified = true;
41 }
42 }
43 });
44 try {
45 const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
46 (resolve, reject) => {
47 child.once("error", reject);
48 child.once("exit", (code, signal) => resolve({ code, signal }));
49 },
50 );
51 expect(errors).not.toContain("Error");
52 expect(verified).toBe(true);
53 expect(result).toEqual({ code: 0, signal: null });
54 } finally {
55 clearTimeout(watchdog);
56 if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
57 for (const stream of child.stdio) stream?.destroy();
58 }
59});