Repository navigation
feat(bot): show recommendation reason tags in queue display #275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0f43e7d
0963bbf
f828cab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| import { beforeEach, describe, expect, it, jest } from '@jest/globals' | ||
| import { | ||
| createQueueSummary, | ||
| createTrackListDisplay, | ||
| findSimilarTracksInQueue, | ||
| formatTrackForDisplay, | ||
| } from './queueDisplay' | ||
| import type { QueueDisplayOptions } from './types' | ||
|
|
||
| const getTrackInfoMock = jest.fn() | ||
| const isSimilarTitleMock = jest.fn() | ||
|
|
||
| jest.mock('../../../../utils/music/trackUtils', () => ({ | ||
| getTrackInfo: (...args: unknown[]) => getTrackInfoMock(...args), | ||
| })) | ||
|
|
||
| jest.mock('../../../../utils/music/titleComparison', () => ({ | ||
| isSimilarTitle: (...args: unknown[]) => isSimilarTitleMock(...args), | ||
| })) | ||
|
|
||
| const defaultOptions: QueueDisplayOptions = { | ||
| showCurrentTrack: true, | ||
| showUpcomingTracks: true, | ||
| maxTracksToShow: 10, | ||
| showTotalDuration: true, | ||
| showQueueStats: true, | ||
| } | ||
|
|
||
| function createTrack(overrides: Record<string, unknown> = {}): unknown { | ||
| return { | ||
| title: 'Test Track', | ||
| author: 'Test Artist', | ||
| url: 'https://example.com/track', | ||
| thumbnail: undefined, | ||
| requestedBy: undefined, | ||
| metadata: {}, | ||
| ...overrides, | ||
| } | ||
| } | ||
|
|
||
| describe('queueDisplay', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks() | ||
| getTrackInfoMock.mockResolvedValue({ duration: '3:30' }) | ||
| isSimilarTitleMock.mockResolvedValue(false) | ||
| }) | ||
|
|
||
| describe('formatTrackForDisplay', () => { | ||
| it('returns basic display info without recommendation reason for normal tracks', async () => { | ||
| const track = createTrack() | ||
|
|
||
| const result = await formatTrackForDisplay(track as any, 1, defaultOptions) | ||
|
|
||
| expect(result.title).toBe('Test Track') | ||
| expect(result.author).toBe('Test Artist') | ||
| expect(result.isAutoplay).toBeUndefined() | ||
| expect(result.recommendationReason).toBeUndefined() | ||
| }) | ||
|
|
||
| it('surfaces isAutoplay and recommendationReason from track metadata', async () => { | ||
| const track = createTrack({ | ||
| metadata: { isAutoplay: true, recommendationReason: 'fresh artist rotation' }, | ||
| }) | ||
|
|
||
| const result = await formatTrackForDisplay(track as any, 1, defaultOptions) | ||
|
|
||
| expect(result.isAutoplay).toBe(true) | ||
| expect(result.recommendationReason).toBe('fresh artist rotation') | ||
| }) | ||
| }) | ||
|
|
||
| describe('createTrackListDisplay', () => { | ||
| it('renders plain track entry without reason tag for non-autoplay tracks', async () => { | ||
| const track = createTrack() | ||
| const result = await createTrackListDisplay([track as any], defaultOptions) | ||
|
|
||
| expect(result).toContain('[Test Track]') | ||
| expect(result).not.toContain('_') | ||
| }) | ||
|
|
||
| it('appends recommendation reason tag for autoplay tracks', async () => { | ||
| const track = createTrack({ | ||
| metadata: { isAutoplay: true, recommendationReason: 'fresh artist rotation' }, | ||
| }) | ||
|
|
||
| const result = await createTrackListDisplay([track as any], defaultOptions) | ||
|
|
||
| expect(result).toContain('fresh artist rotation') | ||
| expect(result).toContain('_fresh artist rotation_') | ||
| }) | ||
|
|
||
| it('does not append reason tag when isAutoplay is true but reason is empty', async () => { | ||
| const track = createTrack({ | ||
| metadata: { isAutoplay: true, recommendationReason: '' }, | ||
| }) | ||
|
|
||
| const result = await createTrackListDisplay([track as any], defaultOptions) | ||
|
|
||
| expect(result).not.toContain('_') | ||
| }) | ||
|
|
||
| it('shows overflow message when tracks exceed maxTracksToShow', async () => { | ||
| const tracks = Array.from({ length: 15 }, (_, i) => | ||
| createTrack({ title: `Track ${i}` }), | ||
| ) | ||
| const options = { ...defaultOptions, maxTracksToShow: 10 } | ||
| const result = await createTrackListDisplay(tracks as any[], options) | ||
|
|
||
| expect(result).toContain('5 more tracks') | ||
| }) | ||
| }) | ||
|
|
||
| describe('findSimilarTracksInQueue', () => { | ||
| it('returns tracks whose titles are similar to the current track', async () => { | ||
| const current = createTrack({ title: 'Amazing Song' }) | ||
| const similar = createTrack({ title: 'Amazing Song (Remix)' }) | ||
| const different = createTrack({ title: 'Something Else' }) | ||
|
|
||
| isSimilarTitleMock | ||
| .mockResolvedValueOnce(true) | ||
| .mockResolvedValueOnce(false) | ||
|
|
||
| const result = await findSimilarTracksInQueue( | ||
| current as any, | ||
| [similar as any, different as any], | ||
| ) | ||
|
|
||
| expect(result).toHaveLength(1) | ||
| expect(result[0]).toBe(similar) | ||
| }) | ||
|
|
||
| it('returns empty array when no tracks are similar', async () => { | ||
| const current = createTrack({ title: 'Amazing Song' }) | ||
| const unrelated = createTrack({ title: 'Completely Different' }) | ||
|
|
||
| isSimilarTitleMock.mockResolvedValue(false) | ||
|
|
||
| const result = await findSimilarTracksInQueue(current as any, [unrelated as any]) | ||
|
|
||
| expect(result).toHaveLength(0) | ||
| }) | ||
| }) | ||
|
|
||
| describe('createQueueSummary', () => { | ||
| it('returns total tracks and duration without position when currentPosition is 0', () => { | ||
| const result = createQueueSummary(10, '45:00', 0) | ||
|
|
||
| expect(result).toContain('**Total Tracks:** 10') | ||
| expect(result).toContain('**Total Duration:** 45:00') | ||
| expect(result).not.toContain('Current Position') | ||
| }) | ||
|
|
||
| it('includes current position formatted as M:SS when currentPosition is greater than 0', () => { | ||
| const result = createQueueSummary(5, '20:00', 90) | ||
|
|
||
| expect(result).toContain('**Total Tracks:** 5') | ||
| expect(result).toContain('**Total Duration:** 20:00') | ||
| expect(result).toContain('**Current Position:** 1:30') | ||
| }) | ||
| }) | ||
|
Comment on lines
+41
to
+160
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search the bot test tree for coverage of the new now-playing recommendation branch.
rg -n --glob '*.{spec,test}.ts' "createQueueEmbed|Recommended because:" packages/botRepository: LucasSantana-Dev/Lucky Length of output: 708 🏁 Script executed: #!/bin/bash
# Search for queueEmbed spec files and check the implementation
fd -e 'spec.ts' -e 'test.ts' 'queueEmbed' packages/bot
# Also check if queueEmbed.ts exists and see its structure
fd -e 'ts' -e 'tsx' 'queueEmbed' packages/bot | head -20Repository: LucasSantana-Dev/Lucky Length of output: 127 🏁 Script executed: #!/bin/bash
# Read the queueEmbed.ts file
cat -n packages/bot/src/functions/music/commands/queue/queueEmbed.tsRepository: LucasSantana-Dev/Lucky Length of output: 5090 Add unit test for the new autoplay recommendation branch in The 🤖 Prompt for AI Agents |
||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ export async function formatTrackForDisplay( | |
| _options: QueueDisplayOptions, | ||
| ): Promise<TrackDisplayInfo> { | ||
| const trackInfo = await getTrackInfo(track) | ||
| const metadata = (track.metadata ?? {}) as { isAutoplay?: boolean; recommendationReason?: string } | ||
|
|
||
| return { | ||
| title: track.title, | ||
|
|
@@ -21,6 +22,8 @@ export async function formatTrackForDisplay( | |
| thumbnail: track.thumbnail, | ||
| requestedBy: track.requestedBy?.username, | ||
| position, | ||
| isAutoplay: metadata.isAutoplay, | ||
| recommendationReason: metadata.recommendationReason, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -38,7 +41,11 @@ export async function createTrackListDisplay( | |
| const track = displayTracks[i] | ||
| const trackInfo = await formatTrackForDisplay(track, i + 1, options) | ||
|
|
||
| const trackDisplay = `${i + 1}. [${trackInfo.title}](${trackInfo.url}) - ${trackInfo.author} (${trackInfo.duration})` | ||
| const reasonTag = | ||
| trackInfo.isAutoplay && trackInfo.recommendationReason | ||
| ? ` — _${trackInfo.recommendationReason}_` | ||
| : '' | ||
| const trackDisplay = `${i + 1}. [${trackInfo.title}](${trackInfo.url}) - ${trackInfo.author} (${trackInfo.duration})${reasonTag}` | ||
|
Comment on lines
+44
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Truncate the inline reason tag before rendering it. Issue ✂️ Suggested fix- const reasonTag =
- trackInfo.isAutoplay && trackInfo.recommendationReason
- ? ` — _${trackInfo.recommendationReason}_`
- : ''
+ const normalizedReason = trackInfo.recommendationReason?.trim()
+ const truncatedReason =
+ normalizedReason && normalizedReason.length > 24
+ ? `${normalizedReason.slice(0, 23)}…`
+ : normalizedReason
+ const reasonTag =
+ trackInfo.isAutoplay && truncatedReason
+ ? ` — _${truncatedReason}_`
+ : ''🤖 Prompt for AI Agents |
||
| trackDisplays.push(trackDisplay) | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add the linked issue/PR reference to these changelog bullets.
These are the only new
2.6.19entries without a traceability suffix, which makes it harder to tie the behavior back to issue#266/ PR#275.📝 Suggested changelog tweak
Based on learnings, "Update CHANGELOG.md with all changes, include breaking changes documentation, and reference issues and PRs."
🤖 Prompt for AI Agents