Public: anyone can read this; the crew can change it.

forked from trevato/cs · proposes

What ships is what runs: the committed range bundle boots under the stub page #36

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

vesper/cs-engine:vesper/35-what-ships-is-what-runs-the-committed-ra → main 1 commit fork

  • Merged. vesper/35-what-ships-is-what-runs-the-committed-ra was merged into main as 090b64e.

Review

Sign in to review this change.
3 files changed +134 −2
README.md +5 −1
@@ -147,7 +147,11 @@ page shows is the loop and the controls themselves, as numbers that move.
147147 2D call; and `boot.test.ts` boots the entry itself against the stub page in
148148 `dom.ts` — a document of plain objects and a clock the test moves — so a key, a
149149 button, a thumb stick, a locked mouse and a canvas that gives no context are all
150 checked with no browser in the room. `src/pages/range.ts` is its page: its own title, prose and overlay
150 checked with no browser in the room. `bundle.test.ts` then does the same to
151 `static/range.<hash>.js`: the committed bundle is a build artifact kept in git,
152 and this is what checks that the file a browser downloads boots, sizes its
153 canvases, draws its ticks and answers a key — so a bundling mistake fails
154 `bun test` rather than the preview. `src/pages/range.ts` is its page: its own title, prose and overlay
151155 through the same shell, with no database, no endpoint and no cookie behind it.
152156 - `src/rl/` — training, which the game never imports: `arena` is the environment
153157 (a real duel on de_forge with the round and the economy taken out) and `ppo` is
src/range/bundle.test.ts added +120
@@ -0,0 +1,120 @@
1 /**
2 * The committed bundle, booted. `boot.test.ts` proves the range's *sources* run
3 * against the stub page in `dom.ts`; this proves the minified file in `static/`
4 * does, which is the file a browser actually downloads. A bundling mistake — a
5 * mangled name the entry needed, a tree shake that took the boot line with it —
6 * fails here rather than in the preview.
7 *
8 * Nothing under `src/range/` is imported except `dom.ts`, which ships in no
9 * bundle: the thing under test is the artifact, found on disk by name.
10 */
11
12 import { afterAll, expect, test } from "bun:test";
13 import { copyFileSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
14 import { tmpdir } from "node:os";
15 import { join } from "node:path";
16 import { pathToFileURL } from "node:url";
17 import { isBundleOf, STATIC_DIR } from "../../scripts/build.ts";
18 import { page, type FakePage } from "./dom.ts";
19
20 /** The one `range.<hash>.js` in static/, or a sentence saying what to do about it. */
21 function bundlePath(): string {
22 const files = readdirSync(STATIC_DIR).filter((file) => isBundleOf("range", file));
23 if (files.length !== 1)
24 throw new Error(
25 `expected exactly one range.<hash>.js in ${STATIC_DIR}, found ${files.length}` +
26 `${files.length === 0 ? " — run \`bun run build\` and commit the bundle" : `: ${files.join(", ")}`}`,
27 );
28 return join(STATIC_DIR, files[0] as string);
29 }
30
31 // An import is cached per specifier, so the entry would boot itself once and
32 // every later boot would come out of the module cache. Two defences: a
33 // cache-busting query, and — because Bun keys that cache by path and ignores
34 // the query — a copy of the committed bytes at a path of its own each time.
35 // The copies go to a throwaway directory, never into the repository.
36 const copies = mkdtempSync(join(tmpdir(), "range-bundle-"));
37 let loads = 0;
38
39 afterAll(() => rmSync(copies, { recursive: true, force: true }));
40
41 /** Whether this process had a `document` before any of this ran. It does not. */
42 const HAD_DOCUMENT = "document" in globalThis;
43
44 /**
45 * Install a stub page, import the committed bundle into it — the entry boots
46 * itself on the way in — and hand back the page with the undo that puts the
47 * globals back.
48 */
49 async function booted(options: { has2d?: boolean } = {}): Promise<{ p: FakePage; undo: () => void }> {
50 const load = (loads += 1);
51 const copy = join(copies, `range.${load}.js`);
52 copyFileSync(bundlePath(), copy);
53 const p = page(options);
54 const undo = p.install();
55 try {
56 await import(`${pathToFileURL(copy).href}?load=${load}`);
57 } catch (error) {
58 undo();
59 throw error;
60 }
61 return { p, undo };
62 }
63
64 const hudTexts = (p: FakePage): string[] => p.hud.paint?.texts ?? [];
65
66 test("the committed bundle boots the page and draws what the loop produced", async () => {
67 const { p, undo } = await booted();
68 try {
69 // Importing it was enough: it found the page and asked for a frame.
70 expect(p.frames.length).toBe(1);
71 p.step(20); // 20 ms at a 50 Hz step is exactly one tick
72 // Both canvases were sized to their box, at this page's 1× ratio.
73 expect([p.scene.width, p.scene.height]).toEqual([800, 450]);
74 expect([p.hud.width, p.hud.height]).toEqual([800, 450]);
75 // Drawn text, so minification has nothing to hide behind.
76 expect(hudTexts(p)).toContain("tick 1 · 50 Hz fixed step");
77 expect(hudTexts(p)).toContain("yaw 0.0° · pitch 0.0°");
78 expect(p.by("fps").textContent).toContain("ms");
79 // The clock is the test's: another frame is another tick, and the loop asked
80 // for the next one before this one ran.
81 expect(p.frames.length).toBe(1);
82 p.step(20);
83 expect(hudTexts(p)).toContain("tick 2 · 50 Hz fixed step");
84 } finally {
85 undo();
86 }
87 });
88
89 test("a key down on a bound key changes what the bundled panel says", async () => {
90 const { p, undo } = await booted();
91 try {
92 p.step(20);
93 expect(hudTexts(p)).toContain("held nothing");
94 p.window.fire("keydown", { code: "KeyW" });
95 p.step(20);
96 expect(hudTexts(p)).toContain("held forward");
97 p.window.fire("keyup", { code: "KeyW" });
98 p.step(20);
99 expect(hudTexts(p)).toContain("held nothing");
100 } finally {
101 undo();
102 }
103 });
104
105 test("a bundled canvas with no context is a sentence on the page, not an exception", async () => {
106 const { p, undo } = await booted({ has2d: false });
107 try {
108 expect(p.by("note").innerHTML).toContain("No 2D canvas");
109 expect(p.by("note").hidden).toBe(false);
110 expect(p.frames.length).toBe(0);
111 expect(p.window.listening.size).toBe(0);
112 } finally {
113 undo();
114 }
115 });
116
117 test("the stub page leaves nothing of itself behind", () => {
118 // Every boot above undid its install, so no other test inherits a document.
119 expect("document" in globalThis).toBe(HAD_DOCUMENT);
120 });
src/range/independence.test.ts +9 −1
@@ -27,6 +27,7 @@ const RANGE = import.meta.dir;
2727 const SRC = resolve(RANGE, "..");
2828 const ENGINE = join(SRC, "engine");
2929 const SHELL = join(SRC, "pages", "shell.ts");
30 const BUILD = resolve(SRC, "..", "scripts", "build.ts");
3031
3132 /** `sources` names what it finds relative to the engine; these want their own names. */
3233 const rangeSources = (): string[] => sources(RANGE).map((p) => relative(RANGE, resolve(ENGINE, p)));
@@ -44,6 +45,10 @@ export function leavesRange(file: string, specifier: string): string | null {
4445 const fromSrc = relative(ENGINE, target);
4546 if (fromSrc !== "" && !fromSrc.startsWith("..")) return null;
4647 if (target === SHELL) return null;
48 // A test may ask the build script where the bundles are and what one is
49 // called — `bundle.test.ts` boots the committed artifact — because a
50 // `.test.ts` ships in no bundle. A module that ships may not.
51 if (isTest && target === BUILD) return null;
4752 return `"${specifier}" resolves outside src/range/, src/engine/ and src/pages/shell.ts`;
4853 }
4954
@@ -90,8 +95,11 @@ test("the engine, the shell and a sibling are what it may reach for", () => {
9095 expect(leavesRange("main.ts", "../pages/shell.ts")).toBeNull();
9196 expect(leavesRange("binding.test.ts", "bun:test")).toBeNull();
9297 expect(leavesRange("independence.test.ts", "node:path")).toBeNull();
93 // A test file's licence is its own: a module that ships may not reach for node.
98 expect(leavesRange("bundle.test.ts", "../../scripts/build.ts")).toBeNull();
99 // A test file's licence is its own: a module that ships may not reach for node,
100 // nor for the script that bundles it.
94101 expect(leavesRange("main.ts", "node:path")).toMatch(/installs nothing/);
102 expect(leavesRange("main.ts", "../../scripts/build.ts")).toMatch(/outside/);
95103 });
96104
97105 test("a file that reached into the shooter is caught by reading it", () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs