| 1 | // @effect-diagnostics nodeBuiltinImport:off - Exercises real Node IPC and process signals. |
| 2 | import * as NodeChildProcess from "node:child_process"; |
| 3 | import * as NodeEvents from "node:events"; |
| 4 | import * as NodeFSP from "node:fs/promises"; |
| 5 | import * as NodeOS from "node:os"; |
| 6 | import * as NodePath from "node:path"; |
| 7 | import { expect, it } from "vite-plus/test"; |
| 8 | |
| 9 | import { legacyCliLauncherScript } from "./legacyCliLauncher.ts"; |
| 10 | |
| 11 | // oxlint-disable-next-line t3code/no-global-process-runtime -- This test launches a real host executable. |
| 12 | const hostPlatform = NodeOS.platform(); |
| 13 | // oxlint-disable-next-line t3code/no-global-process-runtime -- Match the real executable used by the subprocess. |
| 14 | const hostArch = NodeOS.arch(); |
| 15 | |
| 16 | // The fixture executable uses a POSIX shebang. The wrapper itself also runs on Windows. |
| 17 | it.skipIf(hostPlatform === "win32")( |
| 18 | "keeps service IPC, arguments, and termination connected", |
| 19 | async () => { |
| 20 | const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-legacy-launcher-")); |
| 21 | const entry = NodePath.join(root, "node_modules/t3/dist/bin.mjs"); |
| 22 | const executable = NodePath.join( |
| 23 | root, |
| 24 | `node_modules/@t3code/t3-${hostPlatform}-${hostArch}/t3`, |
| 25 | ); |
| 26 | await NodeFSP.mkdir(NodePath.dirname(entry), { recursive: true }); |
| 27 | await NodeFSP.mkdir(NodePath.dirname(executable), { recursive: true }); |
| 28 | await NodeFSP.writeFile( |
| 29 | NodePath.join(NodePath.dirname(executable), "package.json"), |
| 30 | '{"type":"commonjs"}', |
| 31 | ); |
| 32 | await NodeFSP.writeFile(entry, legacyCliLauncherScript()); |
| 33 | await NodeFSP.writeFile( |
| 34 | executable, |
| 35 | `#!${process.execPath} |
| 36 | process.on("SIGTERM", () => process.exit(23)); |
| 37 | process.on("message", message => process.send({ reply: message })); |
| 38 | process.send({ args: process.argv.slice(2) }); |
| 39 | `, |
| 40 | ); |
| 41 | await NodeFSP.chmod(executable, 0o755); |
| 42 | const child = NodeChildProcess.fork(entry, ["serve", "a path with spaces"], { silent: true }); |
| 43 | try { |
| 44 | expect((await NodeEvents.EventEmitter.once(child, "message"))[0]).toEqual({ |
| 45 | args: ["serve", "a path with spaces"], |
| 46 | }); |
| 47 | const reply = NodeEvents.EventEmitter.once(child, "message"); |
| 48 | child.send({ type: "trial-accepted" }); |
| 49 | expect((await reply)[0]).toEqual({ reply: { type: "trial-accepted" } }); |
| 50 | const exit = NodeEvents.EventEmitter.once(child, "exit"); |
| 51 | child.kill("SIGTERM"); |
| 52 | expect(await exit).toEqual([23, null]); |
| 53 | } finally { |
| 54 | if (child.exitCode === null && child.signalCode === null) { |
| 55 | const exit = NodeEvents.EventEmitter.once(child, "exit"); |
| 56 | child.kill("SIGTERM"); |
| 57 | await exit; |
| 58 | } |
| 59 | await NodeFSP.rm(root, { recursive: true, force: true }); |
| 60 | } |
| 61 | }, |
| 62 | ); |