Files
wizwar6e/deploy/lib/harness.mjs
T
Eric WagonerandClaude Fable 5.1 e1a740119c Credibility pass: the session seams sanded from the clips-and-camera batch
Two blind reviews of everything since the last pass (afa0e17), every
finding checked against the code, no behavior changed: all thirty
scene goldens match without a re-bless and the engine suite is
untouched.

Reel and renderer: the camera's look-down rule is stated once, beside
LOOK_DOWN, instead of twice in the effect; the empty aim branch that
stood where a cutaway used to be is gone (the guard it implied is now
explicit); the pit events and the punch are handled by their own
types, not through "in" casts; smoothstep is one export used by every
tween instead of eleven inline copies; the two floor rings share one
painter; project() takes a Billboard instead of a third hand-typed
copy of its fields; the strides-left figure and the web rim no longer
shadow the reel's steps and the pane's fx; the die card's verdict is
built from events, not by matching an emoji; the workshop asks for the
hover cue by name instead of passing an empty click handler.

Server and engine: one requestBase() for the origin, one slug pattern
in store.ts gating both the clip page and its files, one 404 for both;
LOOPBACK sits above its only caller; doCounteract names what a counter
is played against once; fearCells sits beside its own docblock rather
than between sightedCellsFor and its.

Deploy: chromiumExe, the private server, ffmpeg, and the reel rewind
live in deploy/lib/harness.mjs, shared by the gate, the recorder, and
the card cutter instead of pasted three times; the recorder drops its
duplicate frame counters and names its poster settle; the card uses the
gallery's exact gold; the one-time Sentry URL bootstrap leaves
deploy.sh; the backup comment states the rule rather than the incident.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
2026-09-03 11:55:57 -04:00

57 lines
2.6 KiB
JavaScript

// Shared by the scene gate, the clip recorder, and the card cutter: the
// Playwright chromium they all drive, a private game server on a
// throwaway data dir, ffmpeg with the flags they all want, and the
// reel's rewind. Run from the repo root after `npx vite build` in
// packages/web.
import { execFileSync, spawn } from "node:child_process";
import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
/** The Playwright chromium executable: $WIZWAR_CHROME, else the newest
* build in the ms-playwright cache. */
export function chromiumExe() {
if (process.env.WIZWAR_CHROME) return process.env.WIZWAR_CHROME;
const cache = join(process.env.HOME ?? "", "Library", "Caches", "ms-playwright");
const builds = existsSync(cache)
? readdirSync(cache).filter((d) => /^chromium-\d+$/.test(d)).sort()
: [];
for (const build of builds.reverse()) {
const exe = join(cache, build, "chrome-mac-arm64",
"Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing");
if (existsSync(exe)) return exe;
}
throw new Error("no Playwright chromium found — run: npx playwright-core install chromium (or set WIZWAR_CHROME)");
}
/** A game server on `port` with its own empty data dir. `up()` resolves
* once it answers; `stop()` kills it and removes the dir. */
export function startServer(port, label = "wizwar-harness") {
const dataDir = mkdtempSync(join(tmpdir(), `${label}-`));
const server = spawn("npx", ["tsx", "src/index.ts"], {
cwd: join(import.meta.dirname, "..", "..", "packages", "server"),
env: { ...process.env, PORT: String(port), WIZWAR_DATA_DIR: join(dataDir, "rooms") },
stdio: "ignore",
});
const up = async () => {
for (let i = 0; i < 60; i++) {
try { if ((await fetch(`http://127.0.0.1:${port}/`)).ok) return; } catch { /* not yet */ }
await new Promise((r) => setTimeout(r, 500));
}
throw new Error("server never came up");
};
const stop = () => { server.kill(); rmSync(dataDir, { recursive: true, force: true }); };
return { up, stop };
}
/** ffmpeg, quiet and overwriting. */
export const ff = (...args) => execFileSync("ffmpeg", ["-loglevel", "error", "-y", ...args]);
/** Pause the reel and step it back to its first move. */
export async function rewindReel(page) {
await page.locator("button", { hasText: "❚❚" }).click();
const count = Number((await page.locator(".replay-count").textContent()).match(/of (\d+)/)[1]);
for (let i = 0; i < count; i++) await page.locator("button", { hasText: "◀" }).click();
return count;
}