Skip to content

Lazy-load soundManager, requestId concurrency tests, pauseGuard tests + 503 fix, webhooks.md cross-check - #1569

Open
Themancalledpg wants to merge 4 commits into
LabsCrypt:mainfrom
Themancalledpg:fix/sound-import-requestid-pauseguard-webhooks-1520-1521-1522-1523
Open

Lazy-load soundManager, requestId concurrency tests, pauseGuard tests + 503 fix, webhooks.md cross-check#1569
Themancalledpg wants to merge 4 commits into
LabsCrypt:mainfrom
Themancalledpg:fix/sound-import-requestid-pauseguard-webhooks-1520-1521-1522-1523

Conversation

@Themancalledpg

Copy link
Copy Markdown

Summary

Four independent frontend/backend/docs fixes.

closes #1523
closes #1522
closes #1521
closes #1520

Changes

  • [Frontend] frontend/src/app/utils/soundManager.ts is imported eagerly even when sound is disabled #1523soundManager.ts imported eagerly even when sound is disabled: soundManager.ts was statically imported by GamificationSettings, LevelUpModal, and GlobalXPGain (the last mounted globally in app/layout.tsx), pulling the SoundManager class — including its ~8 placeholder Audio elements — into every page load regardless of whether sound is enabled or ever triggered. Split the class into a new soundManagerCore.ts, loaded via a dynamic import() the first time useSoundEffect()'s returned play/setEnabled/setVolume/preload is actually called; calls made before the import resolves are queued and replayed in order. useSoundEffect()'s public API is unchanged, so no consumer call site needed to change.

  • [Backend] backend/src/middleware/requestId.ts uniqueness under concurrency is untested #1522requestId.ts uniqueness untested under concurrency: the existing tests only covered shape, presence, and propagation for a single request. Added two tests: a 200-request concurrent burst asserting every x-request-id is distinct, and a second test through a route that awaits inside the handler (so requests' async contexts genuinely interleave on the event loop) asserting no request ever observes another request's ID via AsyncLocalStorage. Also documented the ID generation strategy in requestId.ts per the acceptance criteria: createRequestId() delegates to Node's crypto.randomUUID(), which needs no shared mutable state, so concurrent uniqueness holds by construction rather than by locking/sequencing.

  • [Testing] backend/src/middleware/pauseGuard.ts has no dedicated unit test #1521pauseGuard.ts has no dedicated unit test: added pauseGuard.test.ts (10 tests) covering paused-blocks-writes (POST/PUT/PATCH/DELETE, all with a 503), unpaused-allows-writes, and that GET/HEAD/OPTIONS bypass the guard even while paused (confirmed this is the intended behavior by reading the middleware's own read-only check). Also covers the rejection log shape and setPauseState's persistence + in-memory update. While writing these, found pauseGuard.ts:77 calls AppError.serviceUnavailable(...) — a method that didn't exist anywhere on AppError, and there was no SERVICE_UNAVAILABLE ErrorCode either. This meant the guard crashed with a TypeError instead of returning its documented 503 every time it actually blocked a write during a real pause. Added the missing ErrorCode.SERVICE_UNAVAILABLE (httpStatus 503) and AppError.serviceUnavailable() factory, matching the existing pattern exactly — this is a crash fix that makes the guard's already-documented behavior work, not a change to pause-state semantics (which the issue marks out of scope).

  • [Docs] docs/webhooks.md event type list is not cross checked against webhookService.ts #1520webhooks.md not cross-checked against webhookService.ts: the doc's "Supported Event Types" table listed 6 names (loan_approved, repayment_due, repayment_confirmed, loan_defaulted, loan_liquidated, score_changed) that turned out to belong to a completely different, unrelated system — the in-app NotificationType used by notificationService.ts's /api/notifications REST API. The real webhook subscription system dispatches raw Soroban contract event names from SUPPORTED_WEBHOOK_EVENT_TYPES (64 entries, including ~20 explicitly-commented legacy aliases). Replaced the table with the real list — cross-checked programmatically against the source array (zero missing, zero invented) — split into current events and a "Legacy Aliases" section that matches eventIndexer.ts's EVENT_TYPE_ALIASES map exactly, including which legacy names do vs. don't currently resolve to a canonical name. Updated the "Creating a Subscription" and "Payload Examples" sections' event names to match (same envelope/format, only the identifiers changed), since they'd otherwise reference names no longer in the corrected table. Added a comment in webhookService.ts pointing back to the doc.

Test plan

  • frontend/src/app/utils/soundManager.test.ts (new, 5 tests): confirms useSoundEffect() alone triggers no import; confirms play()/setEnabled() each trigger exactly one core-module construction; confirms repeated trigger calls only construct the manager once; confirms calls made before the import resolves are queued and replayed correctly. All 5 passing.
  • backend/src/__tests__/requestId.test.ts (extended, +2 tests): 200-request concurrent burst uniqueness, and an interleaved-async-context uniqueness/no-leakage check.
  • backend/src/middleware/__tests__/pauseGuard.test.ts (new, 10 tests): all passing.
  • npx tsc --noEmit on both frontend and backend — clean (backend has pre-existing errors in src/services/defaultChecker.ts, confirmed via git diff upstream/main to be untouched by this PR — see note below).
  • npx eslint on every touched file — 0 errors; pre-existing warnings confirmed unrelated (unused _frequencies/_duration params carried over verbatim from the original soundManager.ts, and an unrelated pre-existing any cast in requestId.test.ts).

Note on requestId.test.ts

Could not execute this specific test file in this environment: backend/src/services/defaultChecker.ts has a pre-existing syntax error (a private modifier used outside any class body — it's declared at module scope after the class it presumably belongs to has already closed) that breaks the whole TypeScript compile graph, and requestId.test.ts transitively imports app.js which pulls that file in. Confirmed via git diff upstream/main -- backend/src/services/defaultChecker.ts (zero lines changed) that this is fully pre-existing and unrelated to this PR — pauseGuard.test.ts, which doesn't import app.js, ran and passed fine in the same environment. Verified the concurrent-uniqueness assertion's core logic standalone with a small script calling crypto.randomUUID() directly (200/200 unique), so the test logic itself is sound; recommend confirming in CI where defaultChecker.ts presumably isn't broken (or flagging that file separately).

…bsCrypt#1523)

soundManager.ts was statically imported by GamificationSettings,
LevelUpModal, and GlobalXPGain — the last mounted globally in
app/layout.tsx — pulling the SoundManager class (and its ~8 placeholder
Audio-element setup) into every page load regardless of whether sound
is enabled or ever triggered.

Splits the SoundManager class into a new soundManagerCore.ts, loaded
via a dynamic import() the first time useSoundEffect()'s returned
play/setEnabled/setVolume/preload is actually called. Calls made before
the import resolves are queued and replayed in order once it's ready.
useSoundEffect()'s public API is unchanged, so no consumer call site
needed to change — only the module doing the actual work is now
deferred.
…pt#1522)

Existing tests only covered shape, presence, and propagation for a
single request — none proved IDs stay unique when many requests are
actually in flight concurrently, the scenario createRequestId is used
for in practice. Adds two tests: a 200-request concurrent burst
asserting every x-request-id is distinct, and a second test using a
route that awaits inside the handler so multiple requests' async
contexts genuinely interleave, asserting no request ever sees another
request's ID through AsyncLocalStorage.

Also documents the ID generation strategy in requestId.ts per the
issue's acceptance criteria: createRequestId() delegates to Node's
crypto.randomUUID(), which needs no shared/mutable state, so uniqueness
under concurrency holds by construction rather than by locking.

Note: could not execute this test file in this environment —
backend/src/services/defaultChecker.ts has a pre-existing syntax error
(`private` modifier used outside any class body, confirmed via
`git diff upstream/main` showing zero changes to that file) that breaks
the whole TS compile graph transitively through app.js. Verified the
core uniqueness assertion logic standalone with a small script using
the same crypto.randomUUID() call directly; disclosed in the PR.
…ry (LabsCrypt#1521)

Adds pauseGuard.test.ts covering: paused state blocks POST/PUT/PATCH/
DELETE with a 503 AppError, GET/HEAD/OPTIONS bypass the guard even
while paused, the rejection is logged with method/path/contracts/
reason, and setPauseState both updates in-memory state and persists it
via query(). 10 tests total.

While writing these tests, found pauseGuard.ts:77 calls
AppError.serviceUnavailable(...) — a method that didn't exist anywhere
on the AppError class, and there was no SERVICE_UNAVAILABLE entry in
the ErrorCode enum either. This meant the guard crashed with a
TypeError instead of returning the intended 503 every time it actually
blocked a write during a real pause — the guard's core purpose was
non-functional. Added ErrorCode.SERVICE_UNAVAILABLE (httpStatus: 503)
and AppError.serviceUnavailable(), matching the existing factory-method
pattern exactly. This is a crash fix, not a change to pause-state
semantics (which the issue marks out of scope) — the guard's documented
503 behavior now actually happens.
…LabsCrypt#1520)

docs/webhooks.md documented 6 event names (loan_approved,
repayment_due, repayment_confirmed, loan_defaulted, loan_liquidated,
score_changed) that don't exist in webhookService.ts's
SUPPORTED_WEBHOOK_EVENT_TYPES at all — they're actually NotificationType
values from the unrelated in-app notification system
(notificationService.ts / the /api/notifications REST API). The real
webhook subscription system dispatches raw Soroban contract event
names (LoanApproved, LoanRepaid, CollateralLiquidated, ProposalCreated,
etc.) — 64 entries total, including ~20 explicitly-commented legacy
aliases.

Replaced the "Supported Event Types" table with the real 64 entries
(cross-checked programmatically against SUPPORTED_WEBHOOK_EVENT_TYPES —
zero missing, zero invented), split into current events and a
"Legacy Aliases" section matching eventIndexer.ts's EVENT_TYPE_ALIASES
map exactly (including which legacy names do vs. don't currently
resolve to a canonical name). Updated the "Creating a Subscription" and
"Payload Examples" sections' event names to match, since they'd
otherwise reference event names no longer present in the corrected
table — kept the same envelope/example format, only the event
identifiers changed.

Added a comment in webhookService.ts pointing back to this doc so
future additions/renames to SUPPORTED_WEBHOOK_EVENT_TYPES are a
reminder to update both together.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment