Skip to content

fix(autoplay): block Spanish gospel tracks when Last.fm is not linked - #820

Merged
LucasSantana-Dev merged 10 commits into
release/v2.10.0from
fix/autoplay-spanish-gospel-filter
May 10, 2026
Merged

LucasSantana-Dev merged 10 commits into
release/v2.10.0from
fix/autoplay-spanish-gospel-filter

Conversation

@LucasSantana-Dev

@LucasSantana-Dev LucasSantana-Dev commented May 7, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Root cause: collectBroadFallbackCandidates was the only candidate collection path that called calculateRecommendationScore with no genreContext, completely bypassing the cross-locale Spanish veto. Additionally, when Last.fm is not linked the artist-tag fetcher always returned [], silencing the veto for all paths — including Spanish gospel artists like Marcos Witt, Alex Zurdo, and Christine D'Clario whose names carry no Spanish diacritics.

  • Three-layer fix:

    1. artistTagCache.ts: createArtistTagFetcher now accepts an optional spotifyFallback. When Last.fm returns [], the fetcher calls the Spotify genres API and caches the result, enabling the veto on genre strings like 'latin worship', 'musica cristiana', 'latin gospel'.
    2. replenisher.ts: Fetches a Spotify token once early in _replenishQueue and threads it through createArtistTagFetcher, so every candidate collector in the pass (seed loop, Spotify recs, Last.fm, broad fallback) benefits automatically.
    3. candidateFallback.ts: collectBroadFallbackCandidates now accepts and uses a genreContext parameter (including getArtistTags), closing the last unprotected path.
  • languageHeuristics.ts: Added 'latin worship', 'ccm en español', 'spanish ccm' to SPANISH_GENRE_MARKERS and 11 gospel-specific Spanish tokens (eres, nuestro/a, siervo/a, digno, fuego, cielos) to SPANISH_DISTINCT_TOKENS.

Test plan

  • 3342 bot tests pass (no regressions)
  • 4 new tests in candidateFallback.spec.ts covering: tag passthrough to scorer, genreContext override, -Infinity hard-reject for Spanish gospel tracks, full genreContext propagation
  • artistTagCache.ts updated tests pass (Spotify fallback coalescing, Promise.resolve normalization)
  • Sonar preflight: S5852 PASS, S5144 PASS, no new files without specs

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Spotify integration as a fallback to improve artist genre discovery during queue replenishment when Last.fm data is unavailable.
  • Bug Fixes

    • Improved error messages for SoundCloud streaming failures with better context.
    • Enhanced Last.fm session expiration detection and handling for more reliable authentication.
  • Tests

    • Expanded test coverage for error handling, artist tag caching, Spotify fallback behavior, and queue replenishment flows.

Review Change Stack

Greptile Summary

  • Implements a three-layer fix for Spanish gospel track bleed-through in autoplay: artistTagCache gains a spotifyFallback path when Last.fm returns [], replenisher fetches a Spotify token once per pass and threads it through all collectors, and collectBroadFallbackCandidates now receives a full genreContext.
  • lastFmApi.ts adds LastFmSessionExpiredError (thrown on error-code 9 in signedPost), getTrackMetadata for scrobble enrichment, and parseArtists for splitting featured artists — but updateNowPlaying was updated to use parseArtists while scrobble still uses normalizeLastFmArtist, creating a P1 artist-string mismatch for featured-artist tracks.
  • Test coverage is thorough: new specs for parseArtists, LastFmSessionExpiredError, Spotify fallback coalescing, and the extra getValidAccessToken mock in the multi-user blend integration test.

Confidence Score: 4/5

Safe to merge with one fix: scrobble must use parseArtists to stay consistent with the updated updateNowPlaying.

A single isolated P1 — scrobble still sends the raw normalizeLastFmArtist string while updateNowPlaying now sends parseArtists().primary. For any track whose artist field contains a feat./& separator, the two Last.fm calls will submit different artist values, breaking track matching in user play history. No P0s; no security concerns; all other changes are well-structured and well-tested.

packages/bot/src/lastfm/lastFmApi.ts — scrobble artist normalization needs updating to match updateNowPlaying.

Important Files Changed

Filename Overview
packages/bot/src/lastfm/lastFmApi.ts Adds LastFmSessionExpiredError, LastFmTrackMetadata, parseArtists, getTrackMetadata; throws typed error on error-code 9 in signedPost; updateNowPlaying now uses parseArtists while scrobble still uses normalizeLastFmArtist, creating a P1 mismatch for featured-artist strings.
packages/bot/src/utils/music/autoplay/artistTagCache.ts Adds optional spotifyFallback to createArtistTagFetcher; fallback is called and cached when Last.fm returns [], with proper error handling and non-array guard.
packages/bot/src/utils/music/autoplay/replenisher.ts Fetches Spotify token once per pass and passes it as spotifyFallback to createArtistTagFetcher, propagating genre-aware vetoing to all candidate collectors including the broad fallback path.
packages/bot/src/lastfm/lastFmApi.spec.ts Adds tests for parseArtists (covering separators, whitespace, empty string) and LastFmSessionExpiredError construction; all cases look correct.
packages/bot/src/utils/music/autoplay/artistTagCache.spec.ts Comprehensive rewrite of tests covering caching, coalescing, Spotify fallback triggering/bypassing, error handling, and null-safety guards for the fallback result.
packages/bot/src/utils/music/queueManipulation.spec.ts Adds an extra mockResolvedValueOnce(null) for the new getValidAccessToken call introduced in _replenishQueue for the Spotify artist-tag fallback token.

Sequence Diagram

sequenceDiagram
    participant R as replenisher
    participant SLS as spotifyLinkService
    participant ATF as createArtistTagFetcher
    participant LFM as getArtistTopTags (Last.fm)
    participant SPT as getArtistGenres (Spotify)

    R->>SLS: getValidAccessToken(userId)
    SLS-->>R: spotifyToken (or null)
    R->>ATF: createArtistTagFetcher(spotifyFallback?)
    Note over ATF: per-pass in-memory cache

    R->>ATF: getArtistTags(artist)
    ATF->>LFM: getArtistTopTags(artist)
    LFM-->>ATF: tags[]

    alt "tags.length === 0 && spotifyFallback"
        ATF->>SPT: getArtistGenres(token, artist)
        SPT-->>ATF: genres[]
        ATF-->>R: genres[] (cached)
    else tags present
        ATF-->>R: tags[] (cached)
    end

    R->>R: "candidateGenreContext = { getArtistTags, currentTrackTags, sessionGenreFamilies }"
    R->>R: collectRecommendationCandidates(... candidateGenreContext)
    R->>R: collectLastFmCandidates(... candidateGenreContext)
    R->>R: collectBroadFallbackCandidates(... candidateGenreContext)
Loading

Comments Outside Diff (1)

  1. packages/bot/src/lastfm/lastFmApi.ts, line 220 (link)

    P1 scrobble artist diverges from updateNowPlaying after parseArtists refactor

    updateNowPlaying now calls parseArtists(artist).primary, stripping feat./ft./& collaborators from the artist field. scrobble still calls normalizeLastFmArtist(artist), which only strips comma/slash-delimited prefixes. For an artist string like "Drake feat. Rihanna", updateNowPlaying sends artist = "Drake" while scrobble sends artist = "Drake feat. Rihanna". Last.fm matches track.updateNowPlaying and track.scrobble entries by artist+track; mismatched artist strings break that matching and can result in unscrobbled tracks or duplicate/orphaned play history.

    Fix in Claude Code Fix in Codex

Fix All in Claude Code Fix All in Codex

Reviews (4): Last reviewed commit: "Merge branch 'release/v2.10.0' into fix/..." | Re-trigger Greptile

@vercel

vercel Bot commented May 7, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lucky Ready Ready Preview, Comment May 10, 2026 1:04am

Request Review

@coderabbitai

coderabbitai Bot commented May 7, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR extends Last.fm error handling with a typed session-expiration error, adds optional Spotify-fallback support to artist tag fetching (used when Last.fm returns empty results), integrates token-driven Spotify genre lookup into queue replenishment, and hardens SoundCloud stream error reporting. All changes include comprehensive test coverage.

Changes

Last.fm Session Error & Spotify-Fallback Artist Tag Caching

Layer / File(s) Summary
Last.fm Error Type
packages/bot/src/lastfm/lastFmApi.ts, packages/bot/src/lastfm/lastFmApi.spec.ts
Introduces LastFmSessionExpiredError class with default message for error code 9; tests cover constructor behavior and error instance detection.
Last.fm Error Detection & Response Handling
packages/bot/src/lastfm/lastFmApi.ts, packages/bot/src/lastfm/lastFmApi.spec.ts
signedPost throws LastFmSessionExpiredError when API response contains error code 9; isLastFmInvalidSessionError recognizes the new error type; tests verify detection.
Artist Tag Fetcher Signature & Fallback Support
packages/bot/src/utils/music/autoplay/artistTagCache.ts
createArtistTagFetcher signature updated to accept optional spotifyFallback(artist) => Promise<string[]> callback.
Artist Tag Fetcher Fallback Logic
packages/bot/src/utils/music/autoplay/artistTagCache.ts, packages/bot/src/utils/music/autoplay/artistTagCache.spec.ts
Fetcher swallows Last.fm errors; when tags are empty and spotifyFallback exists, invokes fallback (also error-swallowed); normalizes non-array fallback results to []; tests cover all scenarios and caching behavior.
Replenisher Spotify Token & Tag Fetcher Wiring
packages/bot/src/utils/music/autoplay/replenisher.ts, packages/bot/src/utils/music/autoplay/replenisher.spec.ts
Derives Spotify token from requestedBy.id via spotifyLinkService.getValidAccessToken; passes Spotify-backed getArtistGenres(spotifyToken, artist) callback to createArtistTagFetcher for genre-aware candidate selection; tests verify token availability and callback wiring.
SoundCloud Stream Error Wrapping
packages/bot/src/handlers/player/soundcloudMatcher.ts
Wraps playdl.stream() in try/catch; on failure, throws formatted error message with track name and underlying error; attaches original error as cause.
Test Infrastructure & Mock Updates
packages/bot/src/handlers/player/streamBridge.spec.ts, packages/bot/src/utils/music/queueManipulation.spec.ts
Adds afterEach hook to reset Jest timers after yt-dlp lifecycle tests; updates mock token sequence in energy/valence test to account for artist-tag-fetcher token call.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • LucasSantana-Dev/Lucky#817: Modifies the same artistTagCache.ts and replenisher.ts autoplay code paths for tag fetching and genre-aware scoring.
  • LucasSantana-Dev/Lucky#815: Also wraps playdl.stream() in try/catch for SoundCloud error handling with contextual error messages.
  • LucasSantana-Dev/Lucky#574: Introduces the spotifyLinkService and access-token functionality that this PR integrates into replenisher.ts.

Suggested labels

size/xl, feature/autoplay, feature/error-handling

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix(autoplay): block Spanish gospel tracks when Last.fm is not linked' accurately summarizes the main objective—preventing Spanish gospel tracks in autoplay when Last.fm is unavailable. However, it doesn't capture the broader architectural changes (Spotify fallback, genreContext propagation) that enable the fix.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/autoplay-spanish-gospel-filter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread packages/bot/src/lastfm/lastFmApi.ts Outdated
Comment thread packages/bot/src/lastfm/lastFmApi.ts Outdated
Comment thread packages/bot/src/utils/music/languageHeuristics.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/bot/src/handlers/player/soundcloudMatcher.ts`:
- Around line 39-43: The catch block in soundcloudMatcher's stream creation
constructs an Error using (err as Error).message which becomes "undefined" for
non-Error throws; update the throw in the catch to build the message from a safe
conversion (e.g., use err instanceof Error ? err.message : String(err)) so the
thrown Error for SoundCloud stream creation includes meaningful text for
match.name, and keep the original err as the cause when rethrowing; locate the
catch around playdl.stream usage in soundcloudMatcher.ts and replace the unsafe
(err as Error).message usage with the safe conversion.

In `@packages/bot/src/handlers/player/streamBridge.spec.ts`:
- Around line 177-187: The test enables fake timers with jest.useFakeTimers()
but restores them only at the end of the test, which can leak if the assertion
fails; move the restoration into a shared cleanup (e.g., an afterEach) so
jest.useRealTimers() always runs; locate uses of
jest.useFakeTimers()/jest.useRealTimers() in the streamViaYtDlp spec (test
"kills proc and rejects on timeout") and add an afterEach that calls
jest.useRealTimers() (and any necessary timer cleanup) to guarantee real timers
are restored even on test failures.
🪄 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: CHILL

Plan: Pro

Run ID: 82ff024d-5379-4cd3-b6b2-2783df99ec5d

📥 Commits

Reviewing files that changed from the base of the PR and between 85777be and 19f02d9.

📒 Files selected for processing (13)
  • packages/bot/src/functions/music/commands/autoplay/queueHandlers.ts
  • packages/bot/src/handlers/player/playerFactory.ts
  • packages/bot/src/handlers/player/soundcloudMatcher.spec.ts
  • packages/bot/src/handlers/player/soundcloudMatcher.ts
  • packages/bot/src/handlers/player/streamBridge.spec.ts
  • packages/bot/src/handlers/player/streamBridge.ts
  • packages/bot/src/lastfm/lastFmApi.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
  • packages/bot/src/utils/music/candidateFallback.ts
  • packages/bot/src/utils/music/languageHeuristics.ts
  • packages/bot/src/utils/music/queueManipulation.spec.ts
👮 Files not reviewed due to content moderation or server errors (8)
  • packages/bot/src/utils/music/languageHeuristics.ts
  • packages/bot/src/lastfm/lastFmApi.ts
  • packages/bot/src/functions/music/commands/autoplay/queueHandlers.ts
  • packages/bot/src/utils/music/queueManipulation.spec.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/candidateFallback.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Quality Gates
  • GitHub Check: SonarCloud Scan
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use the isPrisma*Error() helper functions to check for specific Prisma error types (e.g., isPrismaForeignKeyError, isPrismaUniqueConstraintError) instead of manually checking error codes
Use Prisma's $transaction() method to ensure database operations are atomic and avoid partial updates when multiple related tables are modified
Always use select or include in Prisma queries to explicitly specify which fields to return, avoiding unnecessary data transfer
For Redis operations, use connection pooling and implement exponential backoff retry logic for transient failures
Always use logAndRethrow() or logAndSwallow() utilities when handling errors to ensure errors are logged with context before propagating or suppressing

Files:

  • packages/bot/src/functions/music/commands/autoplay/queueHandlers.ts
  • packages/bot/src/handlers/player/soundcloudMatcher.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/languageHeuristics.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
  • packages/bot/src/utils/music/queueManipulation.spec.ts
  • packages/bot/src/handlers/player/playerFactory.ts
  • packages/bot/src/utils/music/candidateFallback.ts
  • packages/bot/src/handlers/player/streamBridge.ts
  • packages/bot/src/lastfm/lastFmApi.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
  • packages/bot/src/handlers/player/streamBridge.spec.ts
  • packages/bot/src/handlers/player/soundcloudMatcher.spec.ts
packages/{bot,backend,shared}/src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/{bot,backend,shared}/src/**/*.ts: For feature toggles, check both global and guild-specific toggles using FeatureToggleService.isEnabledForGuild() rather than checking them separately
Use branded types (e.g., GuildId, UserId, ChannelId) for Discord IDs throughout the codebase to prevent type-level ID confusion

Files:

  • packages/bot/src/functions/music/commands/autoplay/queueHandlers.ts
  • packages/bot/src/handlers/player/soundcloudMatcher.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/languageHeuristics.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
  • packages/bot/src/utils/music/queueManipulation.spec.ts
  • packages/bot/src/handlers/player/playerFactory.ts
  • packages/bot/src/utils/music/candidateFallback.ts
  • packages/bot/src/handlers/player/streamBridge.ts
  • packages/bot/src/lastfm/lastFmApi.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
  • packages/bot/src/handlers/player/streamBridge.spec.ts
  • packages/bot/src/handlers/player/soundcloudMatcher.spec.ts
packages/bot/src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

When building Discord embeds, use EmbedBuilderService.createTemplate() or EmbedBuilderService.getTemplate() instead of constructing embeds directly

Files:

  • packages/bot/src/functions/music/commands/autoplay/queueHandlers.ts
  • packages/bot/src/handlers/player/soundcloudMatcher.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/languageHeuristics.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
  • packages/bot/src/utils/music/queueManipulation.spec.ts
  • packages/bot/src/handlers/player/playerFactory.ts
  • packages/bot/src/utils/music/candidateFallback.ts
  • packages/bot/src/handlers/player/streamBridge.ts
  • packages/bot/src/lastfm/lastFmApi.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
  • packages/bot/src/handlers/player/streamBridge.spec.ts
  • packages/bot/src/handlers/player/soundcloudMatcher.spec.ts
packages/bot/src/handlers/player/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use MusicSessionSnapshotService to save and restore queue state, enabling users to resume playback after disconnections

Files:

  • packages/bot/src/handlers/player/soundcloudMatcher.ts
  • packages/bot/src/handlers/player/playerFactory.ts
  • packages/bot/src/handlers/player/streamBridge.ts
  • packages/bot/src/handlers/player/streamBridge.spec.ts
  • packages/bot/src/handlers/player/soundcloudMatcher.spec.ts
packages/bot/src/utils/music/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Implement error recovery in music queue operations by using rescueQueue() when tracks fail to load or dependencies are unavailable

Files:

  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/languageHeuristics.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
  • packages/bot/src/utils/music/queueManipulation.spec.ts
  • packages/bot/src/utils/music/candidateFallback.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
packages/bot/src/utils/music/autoplay/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

For autoplay candidate selection, use genre-family-based penalties via getGenreFamilies() to avoid monotonous genre clustering

Files:

  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
packages/bot/src/{spotify,utils/music}/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Extract Spotify track IDs using extractSpotifyTrackId() before passing to Spotify API calls to prevent malformed requests

Files:

  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/languageHeuristics.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
  • packages/bot/src/utils/music/queueManipulation.spec.ts
  • packages/bot/src/utils/music/candidateFallback.ts
  • packages/bot/src/utils/music/candidateFallback.spec.ts
🔇 Additional comments (4)
packages/bot/src/handlers/player/playerFactory.ts (1)

37-58: LGTM — extractor initialization order correctly updated.

The header comment accurately documents the new sequence, and initPlayDlSoundCloud() correctly runs before loadYoutubeExtractor so the SoundCloud token is available before the bridge is needed.

packages/bot/src/handlers/player/streamBridge.ts (1)

84-90: LGTM — proc.kill() correctly placed before reject in the error handler.

Setting settled = true before calling proc.kill() ensures the subsequent close event is correctly short-circuited by the if (settled) return guard, preventing a double-reject.

packages/bot/src/handlers/player/soundcloudMatcher.spec.ts (1)

1-228: Well-structured test suite with thorough coverage of all changed paths.

Boundary tests for the 75% token threshold, the ±30 s duration window (including exact-boundary acceptance), and the new error-wrapping behavior are all correctly validated.

packages/bot/src/handlers/player/streamBridge.spec.ts (1)

217-327: ⚡ Quick win

The Jest configuration already has clearMocks: true enabled globally.

The packages/bot/jest.config.cjs (which applies to this test file) explicitly sets clearMocks: true, resetMocks: true, and restoreMocks: true (lines 46–48). Mock calls are automatically cleared before each test, so assertions using toHaveBeenCalledTimes() and mock.calls[n] are not fragile, and the test order-dependency concern does not apply here.

			> Likely an incorrect or invalid review comment.

Comment thread packages/bot/src/handlers/player/soundcloudMatcher.ts
Comment thread packages/bot/src/handlers/player/streamBridge.spec.ts
LucasSantana-Dev and others added 2 commits May 8, 2026 16:05
When Last.fm is unlinked the artist-tag fetcher returned [] for every
artist, silently bypassing the cross-locale Spanish veto.  Spanish
gospel artists like Marcos Witt, Alex Zurdo, and Christine D'Clario
carry no Spanish diacritics in their names, so the text-only heuristic
also missed them.

Three-layer fix:
• artistTagCache: accept an optional Spotify genre fallback so the veto
  fires on Spotify genre strings ('latin worship', 'musica cristiana',
  'latin gospel') even without Last.fm
• replenisher: fetch a single Spotify token early and thread it through
  createArtistTagFetcher, covering every candidate collector in the pass
• candidateFallback: wire genreContext (including getArtistTags) into
  collectBroadFallbackCandidates, the only path that previously received
  no genre context at all
• languageHeuristics: add 'latin worship', 'ccm en español',
  'spanish ccm' to SPANISH_GENRE_MARKERS and 11 gospel-specific tokens
  to SPANISH_DISTINCT_TOKENS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add comprehensive test suite for artistTagCache utility, covering cache behavior,
Spotify fallback logic, and concurrent request coalescing.

Add tests for new lastFmApi functions: parseArtists (splits featured artists from
primary artist) and LastFmSessionExpiredError (error class for expired session keys).

Suppress SonarCloud hotspot for internal Last.fm API key in GET request URLs
(not user-controlled data) with NOSONAR comments.

- artistTagCache.spec.ts: 13 test cases covering cache mechanics, fallback behavior
- lastFmApi tests: 12 new tests for parseArtists, 3 for LastFmSessionExpiredError
- All 246 affected tests pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@LucasSantana-Dev
LucasSantana-Dev force-pushed the fix/autoplay-spanish-gospel-filter branch from e6f2e43 to da6e091 Compare May 8, 2026 19:07
@github-actions github-actions Bot removed the size/xl label May 8, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 8, 2026
Comment thread packages/bot/src/lastfm/lastFmApi.ts
…agFetcher

When getValidAccessToken returns a token, the code passes a Spotify
genre-fetching function to createArtistTagFetcher instead of undefined.
Add a test that verifies this branch is exercised to satisfy
SonarCloud new-code coverage requirement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LucasSantana-Dev has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

…er spec

The coverage test triggers the Spotify path which calls getUserSpotifySeeds,
which calls spotifyLinkService.getByDiscordId. Without this in the mock the
test throws TypeError at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LucasSantana-Dev has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

…edBy on mock track

The test for createArtistTagFetcher Spotify path requires currentTrack.requestedBy.id
to be truthy so the token fetch branch is entered.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LucasSantana-Dev has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

Adds 9 tests covering success path, cache hit, !response.ok,
data.error, missing track, empty artist/title, network error,
and metadata without album — pushing new code coverage above 80%.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LucasSantana-Dev has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

Resolved conflicts in lastFmApi.ts/spec.ts after release branch absorbed
PR #821 (canonical metadata + S5852-safe parseArtists + getTrackMetadata).

Strategy: take release branch's superset version of lastFmApi.ts (it has
S5852-safe bounded-repetition regex, full caching, scrobble improvements,
in-flight dedup), then patch back the LastFmSessionExpiredError class
+ error-9 trap from this PR (unique value not in #821).

Spec: take release branch's expanded test coverage, add back
LastFmSessionExpiredError test block + import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LucasSantana-Dev has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

…ExpiredError

The new LastFmSessionExpiredError class is thrown by signedPost on
error code 9, but its default message ('Last.fm session key has
expired (error code 9)') doesn't match isLastFmInvalidSessionError's
patterns ('error':9 regex, ' 9 - ' substring, 'invalid session key'
substring), so the typed error was being silently dropped by the
detector — exactly the path callers rely on for re-auth.

Add an instanceof short-circuit at the top of isLastFmInvalidSessionError.
Add a regression test covering both the default message and a
custom-message variant.

Addresses Greptile feedback on PR #820.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LucasSantana-Dev has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

1. soundcloudMatcher.ts: use type-safe error message extraction
   ((err as Error).message produces 'undefined' for non-Error throws;
   use 'err instanceof Error ? err.message : String(err)' instead)

2. streamBridge.spec.ts: protect against fake-timer leak on assertion
   failure by moving jest.useRealTimers() into afterEach inside the
   process-lifecycle describe block

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LucasSantana-Dev has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
packages/bot/src/utils/music/autoplay/artistTagCache.spec.ts (2)

147-160: ⚡ Quick win

Add case-insensitive caching test for Spotify fallback.

The existing caching test for fallback results calls fetcher('Artist') twice with identical casing. The Last.fm path has a dedicated test (lines 48-62) verifying case-insensitive caching (e.g., "Daft Punk" vs "daft punk"). Consider adding a similar test to verify the Spotify fallback results are cached case-insensitively.

🧪 Suggested test case
     it('caches fallback results same as Last.fm results', async () => {
         getArtistTopTags.mockResolvedValue([])
         const spotifyFallback = jest
             .fn<(artist: string) => Promise<string[]>>()
             .mockResolvedValue(['genre1', 'genre2'])
 
         const fetcher = createArtistTagFetcher(spotifyFallback)
 
-        const result1 = await fetcher('Artist')
-        const result2 = await fetcher('Artist')
+        const result1 = await fetcher('Artist Name')
+        const result2 = await fetcher('artist name')
+        const result3 = await fetcher('ARTIST NAME')
 
         expect(spotifyFallback).toHaveBeenCalledTimes(1)
         expect(result1).toEqual(result2)
+        expect(result2).toEqual(result3)
     })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bot/src/utils/music/autoplay/artistTagCache.spec.ts` around lines
147 - 160, Add a case-insensitive caching test for the Spotify fallback path:
using the existing mocks (getArtistTopTags and spotifyFallback) set
getArtistTopTags.mockResolvedValue([]) and
spotifyFallback.mockResolvedValue([...]), create the fetcher via
createArtistTagFetcher(spotifyFallback), call fetcher('Artist') then
fetcher('artist'), and assert spotifyFallback was called only once and both
results are equal; reference createArtistTagFetcher, spotifyFallback,
getArtistTopTags, and fetcher to locate where to add the test.

12-12: 💤 Low value

Consider using dynamic import for consistency.

The mock is accessed via require() after ES6 imports at the top. While this pattern works with Jest, consider using dynamic import() in the beforeEach block for consistency with the module imports.

♻️ Alternative approach using async beforeEach
-const { getArtistTopTags } = require('../../../lastfm')
-
 describe('createArtistTagFetcher', () => {
+    let getArtistTopTags: jest.Mock
+    
     beforeEach(() => {
+        const lastfm = require('../../../lastfm')
+        getArtistTopTags = lastfm.getArtistTopTags
         jest.clearAllMocks()
     })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bot/src/utils/music/autoplay/artistTagCache.spec.ts` at line 12, The
test currently uses require('../../../lastfm') to get getArtistTopTags after ES
module imports; switch to a dynamic import inside the beforeEach so the module
is loaded asynchronously and consistently with other ES6 imports: replace the
require call with await import('../../../lastfm') inside the beforeEach and
extract getArtistTopTags from the imported module (or reassign the module
variable), ensuring any mocks are applied before the module is used in tests
(refer to getArtistTopTags and beforeEach).
packages/bot/src/utils/music/autoplay/replenisher.spec.ts (2)

20-20: 💤 Low value

Verify if getByDiscordId mock is needed.

The getByDiscordId method is added to the spotifyLinkService mock but is never called in any test. Confirm whether this is preparation for future test cases or can be removed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bot/src/utils/music/autoplay/replenisher.spec.ts` at line 20, The
mock property getByDiscordId on the spotifyLinkService mock in
replenisher.spec.ts is not used by any test; either remove getByDiscordId from
the spotifyLinkService mock object to keep the test surface minimal, or if
future tests will call it, add a focused test that uses
spotifyLinkService.getByDiscordId (mock its return value via jest.fn()) and
assert expected behavior—update the spotifyLinkService mock declaration and any
related beforeEach/arrange code accordingly.

338-340: ⚡ Quick win

Consider strengthening the assertion.

The test only verifies that createArtistTagFetcher is called with expect.any(Function), which doesn't confirm the function is the correct Spotify genre fallback. Consider either:

  1. Capturing and invoking the function argument to verify it calls the Spotify API
  2. Adding a comment explaining that the function's behavior is tested elsewhere
🧪 Option 1: Verify the function behavior
+        const { getArtistGenres } = require('../../../spotify')
+        
         await replenishQueue(queue)
 
-        expect(createArtistTagFetcher).toHaveBeenCalledWith(
-            expect.any(Function),
-        )
+        const callArgs = createArtistTagFetcher.mock.calls[0]
+        expect(callArgs[0]).toBeInstanceOf(Function)
+        
+        // Verify the fallback function invokes Spotify API
+        await callArgs[0]('Test Artist')
+        expect(getArtistGenres).toHaveBeenCalledWith('Test Artist', 'test-token')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bot/src/utils/music/autoplay/replenisher.spec.ts` around lines 338 -
340, The test's assertion that createArtistTagFetcher was called with
expect.any(Function) is weak; instead capture the actual callback argument from
createArtistTagFetcher's mock (e.g., const fallback =
createArtistTagFetcher.mock.calls[0][0]) and invoke it with a representative
artist id or object, then assert the Spotify API mock (the Spotify genre
fallback function) was called with the expected params; update the assertion to
verify the captured function delegates to the Spotify call, or if you choose not
to invoke it here add a concise comment referencing the separate test that
covers the Spotify genre fallback behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/bot/src/utils/music/autoplay/artistTagCache.spec.ts`:
- Around line 147-160: Add a case-insensitive caching test for the Spotify
fallback path: using the existing mocks (getArtistTopTags and spotifyFallback)
set getArtistTopTags.mockResolvedValue([]) and
spotifyFallback.mockResolvedValue([...]), create the fetcher via
createArtistTagFetcher(spotifyFallback), call fetcher('Artist') then
fetcher('artist'), and assert spotifyFallback was called only once and both
results are equal; reference createArtistTagFetcher, spotifyFallback,
getArtistTopTags, and fetcher to locate where to add the test.
- Line 12: The test currently uses require('../../../lastfm') to get
getArtistTopTags after ES module imports; switch to a dynamic import inside the
beforeEach so the module is loaded asynchronously and consistently with other
ES6 imports: replace the require call with await import('../../../lastfm')
inside the beforeEach and extract getArtistTopTags from the imported module (or
reassign the module variable), ensuring any mocks are applied before the module
is used in tests (refer to getArtistTopTags and beforeEach).

In `@packages/bot/src/utils/music/autoplay/replenisher.spec.ts`:
- Line 20: The mock property getByDiscordId on the spotifyLinkService mock in
replenisher.spec.ts is not used by any test; either remove getByDiscordId from
the spotifyLinkService mock object to keep the test surface minimal, or if
future tests will call it, add a focused test that uses
spotifyLinkService.getByDiscordId (mock its return value via jest.fn()) and
assert expected behavior—update the spotifyLinkService mock declaration and any
related beforeEach/arrange code accordingly.
- Around line 338-340: The test's assertion that createArtistTagFetcher was
called with expect.any(Function) is weak; instead capture the actual callback
argument from createArtistTagFetcher's mock (e.g., const fallback =
createArtistTagFetcher.mock.calls[0][0]) and invoke it with a representative
artist id or object, then assert the Spotify API mock (the Spotify genre
fallback function) was called with the expected params; update the assertion to
verify the captured function delegates to the Spotify call, or if you choose not
to invoke it here add a concise comment referencing the separate test that
covers the Spotify genre fallback behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 54fb0ca0-a20b-4f78-a1b5-880553f54b70

📥 Commits

Reviewing files that changed from the base of the PR and between 19f02d9 and 4fedca9.

📒 Files selected for processing (9)
  • packages/bot/src/handlers/player/soundcloudMatcher.ts
  • packages/bot/src/handlers/player/streamBridge.spec.ts
  • packages/bot/src/lastfm/lastFmApi.spec.ts
  • packages/bot/src/lastfm/lastFmApi.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.spec.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/queueManipulation.spec.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/bot/src/handlers/player/streamBridge.spec.ts
  • packages/bot/src/utils/music/queueManipulation.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/bot/src/handlers/player/soundcloudMatcher.ts
  • packages/bot/src/utils/music/autoplay/replenisher.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Quality Gates
  • GitHub Check: SonarCloud Scan
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use the isPrisma*Error() helper functions to check for specific Prisma error types (e.g., isPrismaForeignKeyError, isPrismaUniqueConstraintError) instead of manually checking error codes
Use Prisma's $transaction() method to ensure database operations are atomic and avoid partial updates when multiple related tables are modified
Always use select or include in Prisma queries to explicitly specify which fields to return, avoiding unnecessary data transfer
For Redis operations, use connection pooling and implement exponential backoff retry logic for transient failures
Always use logAndRethrow() or logAndSwallow() utilities when handling errors to ensure errors are logged with context before propagating or suppressing

Files:

  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
  • packages/bot/src/lastfm/lastFmApi.spec.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.spec.ts
  • packages/bot/src/lastfm/lastFmApi.ts
packages/bot/src/utils/music/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Implement error recovery in music queue operations by using rescueQueue() when tracks fail to load or dependencies are unavailable

Files:

  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.spec.ts
packages/{bot,backend,shared}/src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/{bot,backend,shared}/src/**/*.ts: For feature toggles, check both global and guild-specific toggles using FeatureToggleService.isEnabledForGuild() rather than checking them separately
Use branded types (e.g., GuildId, UserId, ChannelId) for Discord IDs throughout the codebase to prevent type-level ID confusion

Files:

  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
  • packages/bot/src/lastfm/lastFmApi.spec.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.spec.ts
  • packages/bot/src/lastfm/lastFmApi.ts
packages/bot/src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

When building Discord embeds, use EmbedBuilderService.createTemplate() or EmbedBuilderService.getTemplate() instead of constructing embeds directly

Files:

  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
  • packages/bot/src/lastfm/lastFmApi.spec.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.spec.ts
  • packages/bot/src/lastfm/lastFmApi.ts
packages/bot/src/utils/music/autoplay/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

For autoplay candidate selection, use genre-family-based penalties via getGenreFamilies() to avoid monotonous genre clustering

Files:

  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.spec.ts
packages/bot/src/{spotify,utils/music}/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Extract Spotify track IDs using extractSpotifyTrackId() before passing to Spotify API calls to prevent malformed requests

Files:

  • packages/bot/src/utils/music/autoplay/replenisher.spec.ts
  • packages/bot/src/utils/music/autoplay/artistTagCache.spec.ts
🔇 Additional comments (4)
packages/bot/src/lastfm/lastFmApi.ts (2)

14-23: Good addition of a typed session-expiration error.

This gives callers a stable way to branch re-auth logic instead of parsing strings.


97-99: Nice propagation of error code 9 into typed detection.

Mapping Last.fm error === 9 to LastFmSessionExpiredError and recognizing it in isLastFmInvalidSessionError closes the brittle message-matching gap.

Also applies to: 348-348

packages/bot/src/lastfm/lastFmApi.spec.ts (1)

25-25: Strong coverage for the new typed-error path.

These tests validate constructor behavior and protect the new isLastFmInvalidSessionError branch from regressions.

Also applies to: 237-258, 545-554

packages/bot/src/utils/music/autoplay/artistTagCache.spec.ts (1)

163-188: LGTM!

The hasGenreTag test suite provides comprehensive coverage of edge cases (empty arrays, case-insensitivity, whitespace trimming) and the core matching logic.

@sonarqubecloud

Copy link
Copy Markdown

@LucasSantana-Dev
LucasSantana-Dev merged commit 7aff629 into release/v2.10.0 May 10, 2026
17 checks passed
LucasSantana-Dev added a commit that referenced this pull request May 13, 2026
…#820)

* fix(autoplay): block Spanish gospel tracks when Last.fm is not linked

When Last.fm is unlinked the artist-tag fetcher returned [] for every
artist, silently bypassing the cross-locale Spanish veto.  Spanish
gospel artists like Marcos Witt, Alex Zurdo, and Christine D'Clario
carry no Spanish diacritics in their names, so the text-only heuristic
also missed them.

Three-layer fix:
• artistTagCache: accept an optional Spotify genre fallback so the veto
  fires on Spotify genre strings ('latin worship', 'musica cristiana',
  'latin gospel') even without Last.fm
• replenisher: fetch a single Spotify token early and thread it through
  createArtistTagFetcher, covering every candidate collector in the pass
• candidateFallback: wire genreContext (including getArtistTags) into
  collectBroadFallbackCandidates, the only path that previously received
  no genre context at all
• languageHeuristics: add 'latin worship', 'ccm en español',
  'spanish ccm' to SPANISH_GENRE_MARKERS and 11 gospel-specific tokens
  to SPANISH_DISTINCT_TOKENS

* test(autoplay): improve coverage for SonarCloud quality gate

Add comprehensive test suite for artistTagCache utility, covering cache behavior,
Spotify fallback logic, and concurrent request coalescing.

Add tests for new lastFmApi functions: parseArtists (splits featured artists from
primary artist) and LastFmSessionExpiredError (error class for expired session keys).

Suppress SonarCloud hotspot for internal Last.fm API key in GET request URLs
(not user-controlled data) with NOSONAR comments.

- artistTagCache.spec.ts: 13 test cases covering cache mechanics, fallback behavior
- lastFmApi tests: 12 new tests for parseArtists, 3 for LastFmSessionExpiredError
- All 246 affected tests pass

* test(replenisher): cover Spotify genre fallback path in createArtistTagFetcher

When getValidAccessToken returns a token, the code passes a Spotify
genre-fetching function to createArtistTagFetcher instead of undefined.
Add a test that verifies this branch is exercised to satisfy
SonarCloud new-code coverage requirement.

* fix(test): add getByDiscordId to spotifyLinkService mock in replenisher spec

The coverage test triggers the Spotify path which calls getUserSpotifySeeds,
which calls spotifyLinkService.getByDiscordId. Without this in the mock the
test throws TypeError at runtime.

* test(replenisher): fix Spotify genre fallback test by setting requestedBy on mock track

The test for createArtistTagFetcher Spotify path requires currentTrack.requestedBy.id
to be truthy so the token fetch branch is entered.

* test(lastfm): add getTrackMetadata coverage for PR #820

Adds 9 tests covering success path, cache hit, !response.ok,
data.error, missing track, empty artist/title, network error,
and metadata without album — pushing new code coverage above 80%.

* fix(lastfm): make isLastFmInvalidSessionError recognize LastFmSessionExpiredError

The new LastFmSessionExpiredError class is thrown by signedPost on
error code 9, but its default message ('Last.fm session key has
expired (error code 9)') doesn't match isLastFmInvalidSessionError's
patterns ('error':9 regex, ' 9 - ' substring, 'invalid session key'
substring), so the typed error was being silently dropped by the
detector — exactly the path callers rely on for re-auth.

Add an instanceof short-circuit at the top of isLastFmInvalidSessionError.
Add a regression test covering both the default message and a
custom-message variant.

Addresses Greptile feedback on PR #820.

* fix: address CodeRabbit minor issues on PR #820

1. soundcloudMatcher.ts: use type-safe error message extraction
   ((err as Error).message produces 'undefined' for non-Error throws;
   use 'err instanceof Error ? err.message : String(err)' instead)

2. streamBridge.spec.ts: protect against fake-timer leak on assertion
   failure by moving jest.useRealTimers() into afterEach inside the
   process-lifecycle describe block

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
LucasSantana-Dev added a commit that referenced this pull request May 13, 2026
Promote [Unreleased] entries into the v2.10.0 section, bump root + workspace
versions from 2.9.0 to 2.10.0, and update the lockfile.

Release highlights:
- Spotify 429 retry hardening (#808)
- Last.fm canonical metadata + multi-artist scrobble fix (#821)
- Autoplay Spanish-gospel-block + sertanejo prioritization series
  (#817-#820, #827, #829, #830)
- Review-tools revamp: Claude review + Danger + chilled CodeRabbit
  via org-level reusable workflows (#838)
- Coverage threshold pinned for phase-2 test cleanup (#835)
- CI extended to release/** branches (#816)
@LucasSantana-Dev
LucasSantana-Dev deleted the fix/autoplay-spanish-gospel-filter branch May 23, 2026 02:21

This branch was successfully deployed

1 active deployment
Preview — 4fedca9b Deployed May 10, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant