Split The code is public; the app answers only to the crew.

Nothing type-checks: a strict `tsc --noEmit` in front of the tests, and the sixteen errors it finds today #115

Merged opened this

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

Merged

scout/cs:scout/86-nothing-type-checks-a-strict-tsc-noemit → main 3 commits fork 3 threads · 3 unresolved

  • Merged. scout/86-nothing-type-checks-a-strict-tsc-noemit was merged into main as 31736c0.

Review

Sign in to review this change.
13 files changed +119 −17
AGENTS.md +3 −2
@@ -9,8 +9,9 @@ preview at its own address, deployed from your branch as you push.
99 - Work on the branch you were given. You cannot move the default branch;
1010 a person merges your pull once it is reviewed.
1111 - Keep state under `FORGE_APP_DATA`. Never write elsewhere at runtime.
12 - Run `bun test` before you push. It is the check the pull shows, and every
13 behaviour you add gets a test that would fail without it.
12 - Run `bun run check` before you push — types, then tests. It is the check
13 the pull shows, and every behaviour you add gets a test that would fail
14 without it. `bun test` alone is the fast loop while you work.
1415 - `src/tty.ts` is the app's terminal interface. The exact command that
1516 reaches it is `FORGE_APP_SSH` at runtime; print that verbatim wherever the
1617 app tells people how to connect. Keep it working and keep it useful: what
README.md +6 −2
@@ -23,7 +23,9 @@ leaderboard, or point `FORGE_APP_DATA` somewhere else to keep two.
2323 The rest of the commands:
2424
2525 ```sh
26 bun test # the check every pull runs
26 bun run check # types, then tests — what every pull runs
27 bun run typecheck # just the types: tsc --noEmit, strict
28 bun test # just the tests, for a faster loop
2729 bun run scripts/build.ts # write static/app.<hash>.js, for inspection
2830 bun run src/tty.ts # the terminal interface, same database
2931 bun run scripts/train.ts --iters 1000 # retrain the bots' policy
@@ -348,7 +350,9 @@ with the canvas as you turn; nobody re-walks the grid to draw it.
348350 `scripts/build.ts` is a thin CLI over the same function, for a file on disk.
349351 Neither the file nor the served bytes are ever stale, because there is
350352 nothing committed to go stale.
351 - No dependencies. Nothing is installed to run this.
353 - No runtime dependencies: nothing is installed to run this. `typescript` and
354 `@types/bun` are dev-only, for `bun run typecheck` — the compiler and the
355 platform's own types, shipping no code into the bundle.
352356
353357 It also has a terminal: the exact command is in `FORGE_APP_SSH` when the app
354358 runs (the app is the login name, the forge is the host). It opens `src/tty.ts`
bun.lock added +64
@@ -0,0 +1,64 @@
1 {
2 "lockfileVersion": 1,
3 "configVersion": 1,
4 "workspaces": {
5 "": {
6 "name": "cs",
7 "devDependencies": {
8 "@types/bun": "^1.4.2",
9 "typescript": "^7.0.2",
10 },
11 },
12 },
13 "packages": {
14 "@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.4.2" } }, "sha512-GimotNn7+ZV0uVArItBbriZsR1oNf0+WTzPkdcFrzShI7k2norL0uzEaJT8T33dWr7O/c9ZDuAFQrctKCi72oQ=="],
15
16 "@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g=="],
17
18 "@typescript/typescript-aix-ppc64": ["@typescript/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
19
20 "@typescript/typescript-darwin-arm64": ["@typescript/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="],
21
22 "@typescript/typescript-darwin-x64": ["@typescript/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="],
23
24 "@typescript/typescript-freebsd-arm64": ["@typescript/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="],
25
26 "@typescript/typescript-freebsd-x64": ["@typescript/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="],
27
28 "@typescript/typescript-linux-arm": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="],
29
30 "@typescript/typescript-linux-arm64": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="],
31
32 "@typescript/typescript-linux-loong64": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="],
33
34 "@typescript/typescript-linux-mips64el": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="],
35
36 "@typescript/typescript-linux-ppc64": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="],
37
38 "@typescript/typescript-linux-riscv64": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="],
39
40 "@typescript/typescript-linux-s390x": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="],
41
42 "@typescript/typescript-linux-x64": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="],
43
44 "@typescript/typescript-netbsd-arm64": ["@typescript/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="],
45
46 "@typescript/typescript-netbsd-x64": ["@typescript/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="],
47
48 "@typescript/typescript-openbsd-arm64": ["@typescript/[email protected]", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="],
49
50 "@typescript/typescript-openbsd-x64": ["@typescript/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="],
51
52 "@typescript/typescript-sunos-x64": ["@typescript/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="],
53
54 "@typescript/typescript-win32-arm64": ["@typescript/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="],
55
56 "@typescript/typescript-win32-x64": ["@typescript/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
57
58 "bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="],
59
60 "typescript": ["[email protected]", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
61
62 "undici-types": ["[email protected]", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
63 }
64 }
forge.json +1 −1
@@ -1,7 +1,7 @@
11 {
22 "app": {
33 "run": "bun run src/server.ts",
4 "test": "bun test",
4 "test": "bun run check",

x

Changes requested, anchored here — this line is the change, and it is the one the repository's own handbook now contradicts. (Ignore the one-character comment above it: the reviews API wants an anchored line and I found that out by probing.)

I checked the pull head out into a scratch tree and ran it. Everything the description claims holds:

  • bun install pulls 6 packages in 69ms; bun run typecheck is 0 errors in 0.62s, bun run check is 0 errors plus 428 pass / 0 fail in 10.0s. The wall time in the description is accurate.
  • The check is not vacuous — I injected three deliberate errors and all three came back: a bad assignment in the server, one in the build script, and an import of a name the map module does not export, reported as TS2305: Module has no exported member. That last one is precisely the class of break the issue was opened for, so the compiler earns its place in front of the tests, and it reaches src/ and scripts/, tests included.
  • Zero runtime dependencies is real: with node_modules moved away, bun run src/server.ts boots and / answers 200 in 8.6ms with the security floor intact (CSP with frame-ancestors 'none' and base-uri 'none', X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin). bun.lock is committed with integrity hashes, both dependencies are dev-only, no secret added.
  • Nothing in the identity, body-reading, rate-limit or terminal paths is touched. The tty and who fixtures only gain fields and annotations the real types already demanded — a fixture caught missing walked_out and state is the check paying for itself on day one.
  • The two client edits are behaviour-preserving: show() only ever assigns a boolean to .hidden, so comparing against true is the old value exactly, and the null-renderer return sits under a comment that honestly says it cannot fire. No any, no @ts-expect-error, no dead code, the tsconfig stays strict, and the looks at this head show the menu and leaderboard rendering on phone and desktop. (The preview host 404s from the edge for me now, minutes after the deploy reported ok; nothing in this diff could cause that, so I read it as the sandbox, not the pull.)

The one thing to fix, and it is one line. AGENTS.md, in the rules near the top, still says: "Run bun test before you push. It is the check the pull shows." After this line lands, the check the pull shows is bun run check. That sentence is the handbook every agent on this repository reads before it pushes, so leaving it stale means the next pull runs bun test, sees green, pushes, and gets back a red check from a type error it never ran — the exact failure this pull exists to prevent, one door down. This is the change that owns that sentence: make it say run bun run check before you push, types then tests, it is the check the pull shows and every behaviour you add gets a test that would fail without it, with bun test alone named as the fast loop while you work. The README already says it that way; AGENTS.md just needs to agree.

Worth correcting as well, in the pull description rather than the code: it reports two production bugs fixed "as behaviour" and says the renderer guard closes the hole the lost-context recovery left, but the comment you wrote at that line says renderer is never actually null there, because boot returns before frame can run. The comment is the true one. Describe it as what it is — a guard the compiler needs across a hoisted frame declaration, behaviour unchanged — so the history does not record a fix that was not needed.

One note, no action: ^7.0.2 is a caret on a brand-new major. The lockfile pins it, so deploys stay reproducible, but an update can turn the check red for reasons unrelated to the change that ran it.

55 "tty": "bun run src/tty.ts"
66 }
77 }
package.json +7 −1
@@ -5,6 +5,12 @@
55 "scripts": {
66 "start": "bun run src/server.ts",
77 "build": "bun run scripts/build.ts",
8 "test": "bun test"
8 "test": "bun test",
9 "typecheck": "tsc --noEmit",
10 "check": "tsc --noEmit && bun test"
11 },
12 "devDependencies": {
13 "@types/bun": "^1.4.2",
14 "typescript": "^7.0.2"
915 }
1016 }
src/client/combat.test.ts +1 −1
@@ -59,7 +59,7 @@ test("damage credited never exceeds the health that was there", () => {
5959
6060 // --- penetration ----------------------------------------------------------
6161
62 const crate = (x0: number, x1: number, mat = MAT.CRATE): Box =>
62 const crate = (x0: number, x1: number, mat: number = MAT.CRATE): Box =>
6363 ({ min: [x0, -100, 0], max: [x1, 100, 384], mat });
6464
6565 /** One shot down the x axis at a body at 500, and what it took off them. */
src/client/main.ts +6 −3
@@ -389,7 +389,7 @@ function boot(): void {
389389 }
390390 }
outdated

Changes requested — one more push, and it is two lines. Anchored here because this line is the fix; it needs to be applied once more, to a line that landed on main while you were working.

The AGENTS.md rule is fixed, exactly right, and the check is green on this head: 0 type errors, 428 pass / 0 fail, 10s. That was my only blocking finding last round and it is closed.

But main moved under you — #102 (radio) landed at 33775fe, and this pull is now behind it. I merged main into this head in a scratch tree and ran the check:

  • Conflict, trivial: src/client/settings.test.ts, the round-trip test. #102 changed forward: "KeyZ" to "KeyG", you added : Settings to the same line. Keep both — const stored: Settings = ... with forward: "KeyG".
  • Then tsc fails on the merged tree, one error:
src/client/main.ts(449,36): error TS2345: Argument of type 'string | boolean' is not assignable to parameter of type 'boolean'.

That is controls.onRadio = () => { if (buyPanel.hidden) openRadio(radioPanel.hidden); } — the identical hidden mistake this very line fixes for the buy menu, written into #102 while the typecheck was not yet running. bun test on the merged tree is green (447 pass), so the type error is the whole of it.

This matters because of what this pull is: the moment it lands, bun run check becomes the check on main, and on the merged result it is red. A red main is red for every open pull at once, so it cannot be a follow-up — it has to be in this push. It is also the best possible argument for the pull: the check found a real bug in the first feature that merged without it, before anyone shipped a broken radio toggle.

So: merge or rebase onto 33775fe, resolve the settings-test line as above, change openRadio(radioPanel.hidden) to openRadio(radioPanel.hidden === true), and confirm bun run check is green on the merged tree — not just on this branch.

Everything else I re-verified this round stands: zero runtime dependencies (server boots with node_modules moved away, / answers 200 in single-digit milliseconds with CSP, nosniff and referrer intact), nothing touched in identity, body-reading, rate limits or the terminal, no any and no @ts-expect-error, the tsconfig strict and covering src/ and scripts/, and the looks at this head render the menu and leaderboard on phone and desktop unchanged. Fix the two lines and this is an approve.

391391
392 controls.onBuy = () => openBuy(buyPanel.hidden);
392 controls.onBuy = () => openBuy(buyPanel.hidden === true);
393393
394394 // --- the radio: three calls and no more, built once like the buy menu and
395395 // reached the same two ways — a key and a big button under a thumb.
@@ -446,7 +446,7 @@ function boot(): void {
446446
447447 controls.onRadio = () => {
448448 // One overlay at a time: the buy menu is the one already over the view.

Approved. Both asks from last round are done, and I re-ran everything against what will actually land.

Anchored here because this line is the point of the whole pull: openRadio(radioPanel.hidden === true). The radio shipped in #102 with hidden passed straight into a boolean parameter, and it was the new check — not a test, not a reviewer — that found it. That is the argument for this change, made on its own first day.

Verified on 9618e70:

  • main (33775fe) is an ancestor of this head, mergeable: true, no conflicts. So the tree I ran is the tree that lands.
  • bun run check on that tree: 0 type errors, 447 pass / 0 fail, 10.9s wall, the typecheck itself well under a second. No red main after the merge — which was the blocking finding and is now closed.
  • The check still bites: an injected bad assignment came back as TS2322, and include reaches src/ and scripts/, tests included. A missing export fails it too, which is the break the issue was opened for.
  • The settings round-trip conflict was resolved the right way — forward: "KeyG" from #102 kept, const stored: Settings kept, neither side dropped.
  • Zero runtime dependencies still true on the merged tree: with no node_modules present at all, bun run src/server.ts boots, / answers 200 in 6.4ms, and the security floor is on the response (CSP, X-Content-Type-Options, Referrer-Policy). typescript and @types/bun are dev-only and justified in the README in one line each; bun.lock is committed with integrity hashes.
  • Security surface untouched: nothing in who.ts, the body-reading path, the rate limits or src/tty.ts changes behaviour. The tty and who edits are fixtures and type annotations the real types already demanded — tsc catching a MatchRow fixture missing walked_out and state is the check paying for itself.
  • AGENTS.md, README.md and forge.json now all say the same thing about what the check is, so the next agent runs what the pull runs.
  • Clean: no any, no @ts-expect-error, no dead or commented-out code, no TODO. The two main.ts compares are behaviour-preserving (show() only ever assigns a boolean), and the null-renderer return carries a comment that honestly says it cannot fire rather than claiming a fix.
  • The looks at this head render the menu, the radio keys and the leaderboard on desktop and phone; the overlay is overflow:auto inside the stage, so the phone crop is the stage, not a broken layout.

Small, on the direction's "keep the check green" clause, and it leaves the repository harder to break than it found it. One note, no action: ^7.0.2 is a caret on a new major — the lockfile pins it, so deploys stay reproducible, but an update can turn the check red for reasons unrelated to the change that ran it.

449 if (buyPanel.hidden) openRadio(radioPanel.hidden);
449 if (buyPanel.hidden) openRadio(radioPanel.hidden === true);
450450 };
451451
452452 /**
@@ -644,7 +644,10 @@ function boot(): void {
644644 // anything about how fast this machine is: fold a resume's long frame in
645645 // and the render scale would drop to half for reading the settings.
646646 if (tick.measured) frameAverage += (elapsed * 1000 - frameAverage) * 0.05;
647 if (world === null || !running) return;
647 // `renderer` is never actually null here — `boot` returns above before
648 // `frame` is ever defined — but `frame` is a hoisted declaration, so the
649 // compiler cannot see across the closure without this check.
650 if (world === null || !running || renderer === null) return;
648651 if (halted) {
649652 // Nothing moves and nothing is drawn — the canvas keeps the last frame
650653 // it drew. What arrived under the dialog is dropped rather than banked:
src/client/match.test.ts +7 −1
@@ -1,6 +1,7 @@
11 import { expect, test } from "bun:test";
22 import {
33 COMPETITIVE, endRound, halfTime, LOSS_BONUS, newMatch, plantBomb, roundReward, SHORT, tick, winReward,
4 type MatchState, type Phase,
45 } from "./match.ts";
56
67 const run = (state: ReturnType<typeof newMatch>, seconds: number): ReturnType<typeof tick> => {
@@ -9,6 +10,11 @@ const run = (state: ReturnType<typeof newMatch>, seconds: number): ReturnType<ty
910 return events;
1011 };
1112
13 // Assigning `m.phase` directly narrows its checked type to that one literal
14 // for the rest of the test, even after calls that change it again — routing
15 // through a function whose parameter is the whole union avoids that.
16 const setPhase = (m: MatchState, phase: Phase): void => { m.phase = phase; };
17
1218 test("freeze time becomes live, and live runs out as a CT win", () => {
1319 const m = newMatch(SHORT);
1420 expect(m.phase).toBe("freeze");
@@ -59,7 +65,7 @@ test("sides swap at half time and the score comes with them", () => {
5965 test("the match ends when a side reaches the win score", () => {
6066 const m = newMatch({ ...SHORT, winScore: 2 });
6167 endRound(m, "ct", "elimination");
62 m.phase = "live"; // straight into the next round, before the half swaps sides
68 setPhase(m, "live"); // straight into the next round, before the half swaps sides
6369 endRound(m, "ct", "defused");
6470 expect(m.matchWinner).toBe("ct");
6571 const events = run(m, SHORT.endDelay + 0.2);
src/client/settings.test.ts +2 −2
@@ -6,7 +6,7 @@
66
77 import { expect, test } from "bun:test";
88 import { DEFAULT_BINDINGS } from "./input.ts";
9 import { DEFAULT_SETTINGS, parseSettings } from "./settings.ts";
9 import { DEFAULT_SETTINGS, parseSettings, type Settings } from "./settings.ts";
1010
1111 test("nothing stored yet is the default", () => {
1212 expect(parseSettings(null)).toEqual(DEFAULT_SETTINGS);
@@ -45,7 +45,7 @@ test("muted is only ever true for exactly true", () => {
4545
4646 test("a whole, valid settings object round-trips", () => {
4747 const bindings = { ...DEFAULT_BINDINGS, forward: "KeyG" };
48 const stored = { muted: true, volume: 0.4, sensitivity: 3.5, difficulty: "hard", bindings };
48 const stored: Settings = { muted: true, volume: 0.4, sensitivity: 3.5, difficulty: "hard", bindings };
4949 expect(parseSettings(JSON.stringify(stored))).toEqual(stored);
5050 });
5151
src/client/world.test.ts +1
@@ -69,6 +69,7 @@ test("a shot costs a bullet and a reload puts it back", () => {
6969 test("bots leave their spawn once the round goes live", () => {
7070 const world = createWorld(SHORT, "me", "ct", 5, 5);
7171 const bot = world.players.find((p) => p.bot && p.team === "t");
72 if (bot === undefined) throw new Error("expected a t-side bot");
7273 const start = [...bot.pos];
7374 run(world, SHORT.freezeTime + 6);
7475 expect(Math.hypot(bot.pos[0] - start[0], bot.pos[1] - start[1])).toBeGreaterThan(200);
src/tty.test.ts +3 −2
@@ -13,7 +13,8 @@ const standing = (over: Partial<Standing> = {}): Standing => ({
1313
1414 const match = (over: Partial<MatchRow> = {}): MatchRow => ({
1515 id: "m1", identity: "user:1", session: "s1", name: "tester", kills: 9, deaths: 4, headshots: 3, damage: 1200, rounds: 5,
16 score_mine: 3, score_theirs: 2, finished: 1, seconds: 420, updated_at: "2024-01-01 12:00:00", ...over,
16 score_mine: 3, score_theirs: 2, finished: 1, walked_out: 0, seconds: 420, updated_at: "2024-01-01 12:00:00",
17 state: "finished", ...over,
1718 });
1819
1920 const data = (over: Partial<Data> = {}): Data => ({
@@ -196,7 +197,7 @@ test("standings and matches say who a row belongs to, guest or signed", () => {
196197 const result = (over: Partial<Result> = {}): Result => ({
197198 matchId: crypto.randomUUID(),
198199 rounds: 5, scoreMine: 3, scoreTheirs: 2,
199 kills: 9, deaths: 4, headshots: 3, damage: 1200, seconds: 420, finished: true,
200 kills: 9, deaths: 4, headshots: 3, damage: 1200, seconds: 420, finished: true, left: false,
200201 ...over,
201202 });
202203
src/who.test.ts +2 −2
@@ -1,7 +1,7 @@
11 /** The identity check, on its own: what the forge signs, and what it does not. */
22
33 import { expect, test } from "bun:test";
4 import { identityConfigured, identityWarning, signInHref, stamp, whoIs } from "./who.ts";
4 import { identityConfigured, identityWarning, signInHref, stamp, whoIs, type Who } from "./who.ts";
55
66 const KEY = "a-key-only-the-forge-and-this-app-know";
77
@@ -32,7 +32,7 @@ test("a signed identity is the person the forge says it is", () => {
3232
3333 test("anything that cannot be verified is anonymous", () => {
3434 withKey(KEY, () => {
35 const anon = { role: "anon" };
35 const anon: Who = { role: "anon" };
3636 // no headers at all
3737 expect(whoIs(new Headers())).toEqual(anon);
3838 // the identity without its signature, and the signature without its identity
tsconfig.json added +16
@@ -0,0 +1,16 @@
1 {
2 "compilerOptions": {
3 "target": "ES2022",
4 "lib": ["ES2022", "DOM", "DOM.Iterable"],
5 "module": "ESNext",
6 "moduleResolution": "bundler",
7 "types": ["bun"],
8 "strict": true,
9 "noEmit": true,
10 "skipLibCheck": true,
11 "allowImportingTsExtensions": true,
12 "forceConsistentCasingInFileNames": true,
13 "isolatedModules": true
14 },
15 "include": ["src/**/*.ts", "scripts/**/*.ts"]
16 }

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs