Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions RUNNING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,14 @@ working instance of X" — read them before inventing a boot path.
scenario source as `test.ts`.
- `cd e2e && bun run serve` builds the viewer and serves the scenario ×
target matrix over HTTP, bound to all interfaces (reachable over the
tailnet). Individual runs are at `#/<target>/<slug>` hash routes — when
handing results to the user, link those directly, not the bare matrix.
tailnet). It prefers port 8901 but walks forward to the next free port if
that's taken (so concurrent worktrees, or a leaked previous viewer, never
wedge each other) — read the printed `e2e viewer → …` URL for the actual
port. `PORT=…` pins a port explicitly and fails loudly if it's busy. The
built SPA is port- and mount-agnostic (relative assets + hash routing), so
whatever port it lands on just works. Individual runs are at
`#/<target>/<slug>` hash routes — when handing results to the user, link
those directly, not the bare matrix.
- `bun e2e/scripts/pr-media.ts e2e/runs/<target>/<slug>` converts a run's
recording to a gif, uploads it to the `e2e-media` branch, and prints
PR-ready markdown.
Expand Down
43 changes: 39 additions & 4 deletions e2e/scripts/serve.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
// Static server for runs/ — the review URL. Supports range requests so the
// session videos seek/stream, gzips text assets, and marks vite's hashed
// /assets/ as immutable so Monaco/React chunks download once, ever.
// `bun e2e/scripts/serve.ts` → http://host:8901
// `bun e2e/scripts/serve.ts` → prints the bound URL (default port 8901, but
// it walks forward to the next free port if that's taken, so two worktrees —
// or a leaked previous viewer — never wedge each other). `PORT=…` pins a port
// explicitly and fails loudly if it's busy (the strictPort rule from
// src/ports.ts). The SPA itself is port- and mount-agnostic (relative assets +
// hash routing), so any port the server lands on just works in the browser.
import { createReadStream, existsSync, statSync } from "node:fs";
import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
import { extname, join, normalize } from "node:path";
import { fileURLToPath } from "node:url";
import { createGzip } from "node:zlib";

const ROOT = fileURLToPath(new URL("../runs/", import.meta.url));
const PORT = Number(process.env.PORT ?? 8901);
// Explicit PORT pins (and fails visibly if busy); otherwise 8901 is just a
// starting preference we walk forward from.
const PINNED = process.env.PORT !== undefined;
const PREFERRED = Number(process.env.PORT ?? 8901);

const MIME: Record<string, string> = {
".html": "text/html; charset=utf-8",
Expand All @@ -27,7 +36,7 @@ const MIME: Record<string, string> = {

const COMPRESSIBLE = new Set([".html", ".js", ".css", ".map", ".svg", ".json", ".ts"]);

createServer((req, res) => {
const server = createServer((req, res) => {
const url = new URL(req.url ?? "/", "http://x");
let path = normalize(decodeURIComponent(url.pathname)).replace(/^([/\\])+/, "");
if (path === "" || path === ".") path = "index.html";
Expand Down Expand Up @@ -78,4 +87,30 @@ createServer((req, res) => {
}
res.writeHead(200, { "content-type": type, "content-length": size, "accept-ranges": "bytes" });
createReadStream(file).pipe(res);
}).listen(PORT, () => console.log(`e2e viewer → http://localhost:${PORT}/`));
});

// Host omitted → bind every interface (reachable over the tailnet). On a busy
// port: a pinned PORT is a hard error (predictable, matches --strictPort); an
// unpinned default walks forward to the next free port instead of crashing.
const MAX_WALK = 50;
const listen = (port: number, attempt = 0): void => {
server.once("error", (err: NodeJS.ErrnoException) => {
if (err.code !== "EADDRINUSE") throw err;
if (PINNED) {
console.error(`e2e viewer: PORT=${port} is in use — free it or pick another port.`);
process.exit(1);
}
if (attempt >= MAX_WALK) {
console.error(`e2e viewer: no free port found in ${PREFERRED}..${PREFERRED + MAX_WALK}.`);
process.exit(1);
}
console.warn(`e2e viewer: port ${port} in use, trying ${port + 1}…`);
listen(port + 1, attempt + 1);
});
server.listen(port, () => {
const actual = (server.address() as AddressInfo).port;
console.log(`e2e viewer → http://localhost:${actual}/`);
});
};

listen(PREFERRED);
Loading