From 523f6274a45237929ca0504b60a65a35b00a5bcd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 13:30:53 +0000 Subject: [PATCH 1/3] Flag silent games in doctor Beacon Bastion, Brightway Park, Ember Command and Odd Orbit shipped with no sound at all while doctor reported nothing. Doctor now fails a game that declares no audio cues and never plays one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RwsemkhoUGyeQkdkKFmUmw --- changes/doctor-silent-game.md | 3 +++ packages/jgengine/src/doctor.test.ts | 7 +++++++ packages/jgengine/src/doctor.ts | 15 +++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 changes/doctor-silent-game.md diff --git a/changes/doctor-silent-game.md b/changes/doctor-silent-game.md new file mode 100644 index 000000000..4d3d64e7c --- /dev/null +++ b/changes/doctor-silent-game.md @@ -0,0 +1,3 @@ +### Added + +- `jgengine doctor` flags a silent game: one that neither declares `defineGame({ audio })` cues nor calls `ctx.game.audio.play`/`loop`. Four reference games shipped without a single sound; the fix text points at procedural `SoundDef` synth patches, which need no asset files. diff --git a/packages/jgengine/src/doctor.test.ts b/packages/jgengine/src/doctor.test.ts index f8590ccf5..a55c39630 100644 --- a/packages/jgengine/src/doctor.test.ts +++ b/packages/jgengine/src/doctor.test.ts @@ -85,6 +85,13 @@ describe("diagnose", () => { expect(failingLabels(dir)).toContain("Tailwind @source covers @jgengine/react and @jgengine/shell"); }); + test("flags a silent game until it declares or plays audio cues", () => { + const dir = scaffold(); + expect(failingLabels(dir)).toContain("game has sound (declares audio cues)"); + writeFileSync(join(dir, "src", "game", "sfx.ts"), 'export const cue = (ctx: { game: { audio: { play(id: string): void } } }) => ctx.game.audio.play("build");\n'); + expect(failingLabels(dir)).not.toContain("game has sound (declares audio cues)"); + }); + test("flags missing editor @source when the F2+E summon is wired (the all-white editor)", () => { const dir = scaffold(); const cssPath = join(dir, "src", "index.css"); diff --git a/packages/jgengine/src/doctor.ts b/packages/jgengine/src/doctor.ts index 6b1adae58..f0e347e62 100644 --- a/packages/jgengine/src/doctor.ts +++ b/packages/jgengine/src/doctor.ts @@ -246,6 +246,11 @@ export function diagnose(dir: string): Finding[] { if (existsSync(modelsPath)) related.push(readFileSync(modelsPath, "utf8")); const assetsPath = join(dir, "src", "game", "assets.ts"); if (existsSync(assetsPath)) related.push(readFileSync(assetsPath, "utf8")); + findings.push({ + ok: /\baudio\s*:/.test(configText) || srcContains(join(dir, "src"), /\baudio\.(play|loop)\(/), + label: "game has sound (declares audio cues)", + fix: 'a silent game reads unfinished — declare cues as procedural SoundDef synth patches (no asset files needed) in defineGame({ audio: { sounds } }) and call ctx.game.audio.play(id) on accepted actions: build, hit, purchase, objective, win/lose', + }); const prototype = assessPrototypeLook(related); findings.push({ ok: !prototype.isPrototype, @@ -274,3 +279,13 @@ export function runDoctor(argv: string[]): number { console.log(failed === 0 ? "\ndoctor: all clear" : `\ndoctor: ${failed} problem(s) found`); return failed === 0 ? 0 : 1; } + +/** True when any script under `folder` matches `pattern`. */ +function srcContains(folder: string, pattern: RegExp): boolean { + if (!existsSync(folder)) return false; + for (const entry of readdirSync(folder, { withFileTypes: true })) { + const path = join(folder, entry.name); + if (entry.isDirectory() ? srcContains(path, pattern) : /\.(tsx?|jsx?)$/.test(entry.name) && pattern.test(readFileSync(path, "utf8"))) return true; + } + return false; +} From 55676f274b20c2f55b84bbe07a5bac9c43e21c0d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 14:18:47 +0000 Subject: [PATCH 2/3] Give the settings boundary browser test a 20 s default timeout [skip changelog] The first test loads the full shell bundle in real Chromium; on a loaded CI runner it passes Bun's 5 s default and the remaining tests cascade. It failed this way on #1995, #1996, #1998 and #1999 while passing on beta and #1997. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RwsemkhoUGyeQkdkKFmUmw --- packages/shell/src/settingsBoundary.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/shell/src/settingsBoundary.test.ts b/packages/shell/src/settingsBoundary.test.ts index 351e95bd9..dda88ac95 100644 --- a/packages/shell/src/settingsBoundary.test.ts +++ b/packages/shell/src/settingsBoundary.test.ts @@ -1,10 +1,14 @@ -import { afterAll, afterEach, beforeAll, beforeEach, expect, test } from "bun:test"; +import { afterAll, afterEach, beforeAll, beforeEach, expect, setDefaultTimeout, test } from "bun:test"; import { execFileSync } from "node:child_process"; import { mkdir, rm, symlink } from "node:fs/promises"; import { resolve } from "node:path"; import { chromium, type Browser, type Page } from "playwright-core"; import { buildBrowserFixture, findChromeExecutable } from "../../../scripts/browser-lib"; +// Real Chromium: the first page load compiles the whole shell bundle, which overruns Bun's 5 s +// default on a loaded CI runner and then cascades into every later test in the file. +setDefaultTimeout(20_000); + const scratch = resolve(import.meta.dir, "../../../.scratch/settings-boundary-test"); let browser: Browser, page: Page, server: ReturnType; beforeAll(async () => { From a9704f0a157a064a461be86e1a5c10b2d5020744 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 14:25:20 +0000 Subject: [PATCH 3/3] Run CI tests with a 20 s default timeout instead of patching one file [skip changelog] After the settings boundary test got 20 s, the same runner timed out hudInputBoundary and the Rapier slope test instead: whichever cold-start browser/WASM suite runs first under load passes Bun's 5 s default. All pass locally. Set the default for the CI test command; tests with their own timeout keep it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RwsemkhoUGyeQkdkKFmUmw --- .github/workflows/ci.yml | 4 +++- packages/shell/src/settingsBoundary.test.ts | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e2cf6000..49c7817d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,7 +84,9 @@ jobs: - run: bun run games:clone env: GAMES_CLONE_TOKEN: ${{ secrets.GAMES_CLONE_TOKEN }} - - run: bun test packages apps/dev scripts + # Browser and physics suites cold-start Chromium/WASM; on a loaded runner the first test in a + # file can pass Bun's 5 s default and cascade through the file. Tests with their own timeout keep it. + - run: bun test --timeout 20000 packages apps/dev scripts web-build: runs-on: ubuntu-latest diff --git a/packages/shell/src/settingsBoundary.test.ts b/packages/shell/src/settingsBoundary.test.ts index dda88ac95..351e95bd9 100644 --- a/packages/shell/src/settingsBoundary.test.ts +++ b/packages/shell/src/settingsBoundary.test.ts @@ -1,14 +1,10 @@ -import { afterAll, afterEach, beforeAll, beforeEach, expect, setDefaultTimeout, test } from "bun:test"; +import { afterAll, afterEach, beforeAll, beforeEach, expect, test } from "bun:test"; import { execFileSync } from "node:child_process"; import { mkdir, rm, symlink } from "node:fs/promises"; import { resolve } from "node:path"; import { chromium, type Browser, type Page } from "playwright-core"; import { buildBrowserFixture, findChromeExecutable } from "../../../scripts/browser-lib"; -// Real Chromium: the first page load compiles the whole shell bundle, which overruns Bun's 5 s -// default on a loaded CI runner and then cascades into every later test in the file. -setDefaultTimeout(20_000); - const scratch = resolve(import.meta.dir, "../../../.scratch/settings-boundary-test"); let browser: Browser, page: Page, server: ReturnType; beforeAll(async () => {