1/**
2 * True `alchemy dev` end-to-end for the Waku site: spawns the REAL CLI
3 * (mirroring examples/aws-dev/test/dev.test.ts), which runs Waku's own
4 * dev server as the local `Website.Server` provider — no Lambda, no
5 * CloudFront, no S3; the only cloud touch is the state store.
6 *
7 * Coverage:
8 * - stack output → `url` is a local dev-server address (port is
9 * whatever the framework bound — never hard-coded)
10 * - SSR env parity → `/` renders the GREETING declared in alchemy.run.ts
11 * (read via waku's `getEnv` in the RSC page)
12 * - routing → `/about` (static route) serves
13 * - static assets → `/hello.txt` from public/
14 * - HOT RELOAD → editing src/pages/index.tsx is served by the dev
15 * server without a redeploy
16 */
17import { afterAll, expect, test } from "bun:test";
18import { DevCli, fetchOk } from "alchemy-test/DevCli";
19import * as fs from "node:fs";
20import * as path from "node:path";
21
22const root = path.resolve(import.meta.dirname, "..");
23// Isolated stage so this suite never fights integ.test.ts (same stack
24// name) over state rows.
25const STAGE = "dev-cli-test";
26const cli = new DevCli({ root, stage: STAGE });
27
28// Hot-reload surface: the SSR index page. The test rewrites it in place
29// with the CLI running, then restores it.
30const pagePath = path.join(root, "src", "pages", "index.tsx");
31const pageSource = fs.readFileSync(pagePath, "utf8");
32const MARKER = "This page is rendered by the server on every request.";
33const MARKER_V2 =
34 "This page is rendered by the server on every request. [dev-v2]";
35
36afterAll(async () => {
37 // Always leave the repo tree clean, even on a mid-reload failure.
38 fs.writeFileSync(pagePath, pageSource);
39
40 await cli.stop();
41 if (!process.env.NO_DESTROY) cli.destroy();
42}, 180_000);
43
44test(
45 "alchemy dev serves the Waku site locally with hot reload",
46 async () => {
47 cli.start();
48
49 const url = await cli.outputUrlWhenReady("url", {
50 tries: 180,
51 delayMs: 1000,
52 });
53
54 // Dev identity: the framework dev server, not CloudFront. The port is
55 // whatever the framework bound — only the URL captured from the CLI's
56 // stdout is authoritative.
57 expect(new URL(url).hostname).toBe("localhost");
58 expect(url).not.toContain("cloudfront.net");
59
60 // SSR env parity: GREETING from alchemy.run.ts reaches the dev server
61 // (read via waku's `getEnv` in the RSC page).
62 const home = await (await fetchOk(url)).text();
63 expect(home).toContain("Hello from Waku on AWS!");
64 expect(home).toContain(MARKER);
65
66 // Static route serves through the dev server.
67 const about = await fetchOk(new URL("/about", url));
68 expect(about.status).toBe(200);
69
70 // Static asset from public/.
71 const hello = await (await fetchOk(new URL("/hello.txt", url))).text();
72 expect(hello).toContain("hello from public/");
73
74 // ── HOT RELOAD: rewrite the index page with the CLI still running —
75 // the framework dev server serves the new markup without a deploy ──
76 fs.writeFileSync(pagePath, pageSource.replace(MARKER, MARKER_V2));
77 await cli.pollUntil(
78 "hot-reloaded page (v2 marker)",
79 async () => {
80 try {
81 const res = await fetch(url);
82 if (!res.ok) return undefined;
83 const html = await res.text();
84 return html.includes("[dev-v2]") ? true : undefined;
85 } catch {
86 return undefined; // mid-reload
87 }
88 },
89 { tries: 120, delayMs: 500 },
90 );
91
92 // Restore — the swap back is itself a second hot reload and leaves
93 // the checked-in tree clean.
94 fs.writeFileSync(pagePath, pageSource);
95 await cli.pollUntil(
96 "restored page (v2 marker gone)",
97 async () => {
98 try {
99 const res = await fetch(url);
100 if (!res.ok) return undefined;
101 const html = await res.text();
102 return html.includes("[dev-v2]") ? undefined : true;
103 } catch {
104 return undefined; // mid-reload
105 }
106 },
107 { tries: 120, delayMs: 500 },
108 );
109 },
110 { timeout: 600_000 },
111);