quill/val Public Public: anyone can read this; nobody can change it.
forked from benjivers/val
workbenchthis machine
Repair main: two green branches merged into a red one
Pulls #13 (the spectator view) and #15 (the renderer) were each green, and git merged them without a conflict into a main whose tests fail — so the deploy gate refused twice and production kept serving the build from before the renderer work. Nobody was playing what we thought they were playing. Textually clean, semantically not: - client.test.ts. #13 added its spectator test in the middle of #15's seated test, so that test's tail — the retina case, the blit, the readout — landed on a client that has no seat, no world and nothing to draw. It asserted 40+ fills against a frame that is one rectangle and six lines of text, which is exactly what the watching view should be. The seated test is whole again and the spectator test stands on its own. - server.test.ts. The /join test needs a seat, and a playing seat is rationed per address deliberately; earlier tests in the file take that seat and never open a socket to close, so nothing frees it. It now releases the shared address first, which is the test's own problem and not the rule's. Nothing in src/ changed: this was two test files describing each other's world. 257 pass, three runs clean.
2 files
changed +40
−32
src/client.test.ts +32 −32
| @@ -364,38 +364,6 @@ test("the bundled client connects, paints the HUD and draws a frame", async () = | ||
| 364 | 364 | expect(seat.x).toBe(startedAt.x); |
| 365 | 365 | expect(seat.y).toBe(startedAt.y); |
| 366 | 366 | arena.startArena(); |
| 367 | }, 15000); | |
| 368 | ||
| 369 | test("a snapshot with no seat in it draws the watching view instead of throwing", async () => { | |
| 370 | // The shape no other test covers: score, round, clock and feed, and no `me` | |
| 371 | // and no players at all. The client used to reach straight into `snap.me` | |
| 372 | // on the way in, so a second tab's first frame threw and the page stopped | |
| 373 | // drawing. This is the server's own spectator snapshot, handed to the | |
| 374 | // bundled client the way the socket hands it over. | |
| 375 | const { spectatorSnapshot } = await import("./arena.ts"); | |
| 376 | const deliver = (why: "address" | "cookie") => | |
| 377 | openedSocket!.onmessage!({ data: spectatorSnapshot(why) } as MessageEvent); | |
| 378 | ||
| 379 | drawn.length = 0; | |
| 380 | deliver("address"); | |
| 381 | runFrame(); | |
| 382 | ||
| 383 | // It says what the match is doing, and why this connection is watching. | |
| 384 | expect(String(nodes.get("phase")!.textContent)).toContain("round"); | |
| 385 | expect(String(nodes.get("hp")!.textContent)).toBe("watching"); | |
| 386 | expect(String(nodes.get("prompt")!.textContent)).toContain("address"); | |
| 387 | // Nothing on screen that would need a position to fill in. | |
| 388 | expect((nodes.get("kit")!.children as unknown[]).length).toBe(0); | |
| 389 | expect((nodes.get("roster")!.children as unknown[]).length).toBe(0); | |
| 390 | expect((nodes.get("atkboard")!.children as unknown[]).length).toBe(0); | |
| 391 | // And the canvas is words on a panel, not a map: one rectangle, no tiles. | |
| 392 | expect(drawn.filter((op) => op.startsWith("fillRect")).length).toBe(1); | |
| 393 | expect(drawn.filter((op) => op.startsWith("fillText")).length).toBeGreaterThan(3); | |
| 394 | ||
| 395 | // The other reason reads differently, and the frame after it still draws. | |
| 396 | deliver("cookie"); | |
| 397 | runFrame(); | |
| 398 | expect(String(nodes.get("prompt")!.textContent)).toContain("cookie"); | |
| 399 | 367 | |
| 400 | 368 | // Back to the first-person view for the last of it, and a frame of it to |
| 401 | 369 | // measure: the one before is the one that rebuilds anything cached. |
| @@ -442,4 +410,36 @@ test("a snapshot with no seat in it draws the watching view instead of throwing" | ||
| 442 | 410 | // connection rather than left holding a seat for the rest of the suite. |
| 443 | 411 | openedSocket?.close(); |
| 444 | 412 | await Bun.sleep(50); |
| 413 | }, 15000); | |
| 414 | ||
| 415 | test("a snapshot with no seat in it draws the watching view instead of throwing", async () => { | |
| 416 | // The shape no other test covers: score, round, clock and feed, and no `me` | |
| 417 | // and no players at all. The client used to reach straight into `snap.me` | |
| 418 | // on the way in, so a second tab's first frame threw and the page stopped | |
| 419 | // drawing. This is the server's own spectator snapshot, handed to the | |
| 420 | // bundled client the way the socket hands it over. | |
| 421 | const { spectatorSnapshot } = await import("./arena.ts"); | |
| 422 | const deliver = (why: "address" | "cookie") => | |
| 423 | openedSocket!.onmessage!({ data: spectatorSnapshot(why) } as MessageEvent); | |
| 424 | ||
| 425 | drawn.length = 0; | |
| 426 | deliver("address"); | |
| 427 | runFrame(); | |
| 428 | ||
| 429 | // It says what the match is doing, and why this connection is watching. | |
| 430 | expect(String(nodes.get("phase")!.textContent)).toContain("round"); | |
| 431 | expect(String(nodes.get("hp")!.textContent)).toBe("watching"); | |
| 432 | expect(String(nodes.get("prompt")!.textContent)).toContain("address"); | |
| 433 | // Nothing on screen that would need a position to fill in. | |
| 434 | expect((nodes.get("kit")!.children as unknown[]).length).toBe(0); | |
| 435 | expect((nodes.get("roster")!.children as unknown[]).length).toBe(0); | |
| 436 | expect((nodes.get("atkboard")!.children as unknown[]).length).toBe(0); | |
| 437 | // And the canvas is words on a panel, not a map: one rectangle, no tiles. | |
| 438 | expect(drawn.filter((op) => op.startsWith("fillRect")).length).toBe(1); | |
| 439 | expect(drawn.filter((op) => op.startsWith("fillText")).length).toBeGreaterThan(3); | |
| 440 | ||
| 441 | // The other reason reads differently, and the frame after it still draws. | |
| 442 | deliver("cookie"); | |
| 443 | runFrame(); | |
| 444 | expect(String(nodes.get("prompt")!.textContent)).toContain("cookie"); | |
| 445 | 445 | }); |
src/server.test.ts +8
| @@ -35,6 +35,14 @@ test("the page hands out a session cookie and the script is named by content", a | ||
| 35 | 35 | }); |
| 36 | 36 | |
| 37 | 37 | test("a name is taken from a same-origin form and sanitised", async () => { |
| 38 | // Every test in this file reaches the server from the same address, and a | |
| 39 | // playing seat is rationed per address on purpose. Earlier tests here take | |
| 40 | // that seat and never open a socket to close, so nothing frees it: without | |
| 41 | // this line the join below is correctly refused and the assertion measures | |
| 42 | // the leftovers of another test rather than this one's behaviour. | |
| 43 | releaseAddress("127.0.0.1"); | |
| 44 | releaseAddress("::1"); | |
| 45 | releaseAddress("::ffff:127.0.0.1"); | |
| 38 | 46 | const form = new FormData(); |
| 39 | 47 | form.set("name", ' Wren<script>" '); |
| 40 | 48 | const posted = await fetch(base + "/join", { method: "POST", body: form, redirect: "manual" }); |