| 1 | // @effect-diagnostics nodeBuiltinImport:off - Exercises the real inherited descriptor lifecycle in a subprocess. |
| 2 | import * as NodeChildProcess from "node:child_process"; |
| 3 | import * as NodeURL from "node:url"; |
| 4 | |
| 5 | import { expect, it } from "vite-plus/test"; |
| 6 | |
| 7 | it("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 | }); |