1import { expect, test } from "bun:test";
2import { spawn } from "node:child_process";
3import {
4 existsSync,
5 mkdtempSync,
6 readFileSync,
7 rmSync,
8 writeFileSync,
9} from "node:fs";
10import { tmpdir } from "node:os";
11import { join, resolve } from "node:path";
12import { pollUntil } from "../src/DevCli.ts";
13
14for (const signal of ["SIGINT", "SIGTERM"] as const) {
15 test(`forwards ${signal} to DevCli and waits for cleanup`, async () => {
16 const root = mkdtempSync(join(tmpdir(), "alchemy-forward-signals-"));
17 const devCli = resolve(import.meta.dirname, "../src/DevCli.ts");
18 writeFileSync(
19 join(root, "cli.ts"),
20 `
21 import { writeFileSync } from "node:fs";
22 process.on(${JSON.stringify(signal)}, () => {
23 setTimeout(() => { writeFileSync("cleaned", "yes"); process.exit(0); }, 200);
24 });
25 writeFileSync("child.pid", String(process.pid));
26 setInterval(() => {}, 1000);
27 `,
28 );
29 writeFileSync(
30 join(root, "owner.test.ts"),
31 `
32 import { afterAll, test } from "bun:test";
33 import { writeFileSync } from "node:fs";
34 import { DevCli } from ${JSON.stringify(devCli)};
35 const cli = new DevCli({ root: process.cwd(), alchemyBin: process.cwd() + "/cli.ts" });
36 afterAll(async () => { writeFileSync("afterAll", "yes"); await cli.stop(); });
37 test("running", async () => { cli.start(); await Bun.sleep(60000); }, 65000);
38 `,
39 );
40 const runner = spawn(process.execPath, ["test", "owner.test.ts"], {
41 cwd: root,
42 detached: true,
43 stdio: "ignore",
44 env: { ...process.env, ALCHEMY_PROFILE: "alchemy" },
45 });
46 const exited = new Promise<number | null>((resolve, reject) => {
47 runner.once("exit", resolve);
48 runner.once("error", reject);
49 });
50 const timeout = setTimeout(() => runner.kill("SIGKILL"), 6000);
51 let childPid: number | undefined;
52 try {
53 await pollUntil(
54 "CLI started",
55 () => (existsSync(join(root, "child.pid")) ? true : undefined),
56 { tries: 80, delayMs: 50 },
57 );
58 childPid = Number(readFileSync(join(root, "child.pid"), "utf8"));
59 process.kill(-runner.pid!, signal);
60 // A launcher can forward the same terminal signal more than once.
61 await Bun.sleep(50);
62 process.kill(-runner.pid!, signal);
63 expect(await exited).toBe(signal === "SIGINT" ? 130 : 143);
64 expect(existsSync(join(root, "cleaned"))).toBe(true);
65 expect(existsSync(join(root, "afterAll"))).toBe(false);
66 expect(() => process.kill(childPid!, 0)).toThrow();
67 } finally {
68 clearTimeout(timeout);
69 runner.kill("SIGKILL");
70 if (childPid !== undefined) {
71 try {
72 process.kill(childPid, "SIGKILL");
73 } catch (error) {
74 if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
75 }
76 }
77 rmSync(root, { recursive: true, force: true });
78 }
79 }, 10000);
80}