From 834408c52817476eb6a5abee750c6958f53d8cad Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Thu, 21 May 2026 20:33:13 -0300 Subject: [PATCH] feat(music): cover and re-enable collaborative playlist mode (#825) - Added 30 tests to collaborativePlaylist.spec.ts covering setMode, getState, resetContributions, contribution tracking, per-user limit enforcement, and edge cases - Added 21 tests to playlist.spec.ts covering all actions (enable/disable/status/reset), per_user_limit option handling, and guild validation - Feature toggle COLLABORATIVE_PLAYLIST re-enabled in packages/shared/src/config/featureToggles.ts - Play command integration already covers queue-add enforcement with per-user limits - Collaborative mode allows guild members to share a per-user track quota in the queue with predictable state isolation and contribution tracking This feature has stable test coverage (51 new tests) and correctly enforces per-user queue limits when enabled. --- .../functions/music/commands/playlist.spec.ts | 442 ++++++++++++++++++ .../utils/music/collaborativePlaylist.spec.ts | 369 +++++++++++++++ packages/shared/src/config/featureToggles.ts | 5 + packages/shared/src/types/featureToggle.ts | 1 + 4 files changed, 817 insertions(+) create mode 100644 packages/bot/src/functions/music/commands/playlist.spec.ts create mode 100644 packages/bot/src/utils/music/collaborativePlaylist.spec.ts diff --git a/packages/bot/src/functions/music/commands/playlist.spec.ts b/packages/bot/src/functions/music/commands/playlist.spec.ts new file mode 100644 index 000000000..dce366ad6 --- /dev/null +++ b/packages/bot/src/functions/music/commands/playlist.spec.ts @@ -0,0 +1,442 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals' +import type { ChatInputCommandInteraction } from 'discord.js' + +const interactionReplyMock = jest.fn() +const requireGuildMock = jest.fn<(interaction: unknown) => Promise>() +const collaborativePlaylistServiceMock = { + setMode: jest.fn(), + getState: jest.fn(), + resetContributions: jest.fn(), +} +const createInfoEmbedMock = jest.fn() +const createSuccessEmbedMock = jest.fn() +const createWarningEmbedMock = jest.fn() + +jest.mock('../../../utils/general/interactionReply.js', () => ({ + interactionReply: (...args: unknown[]) => interactionReplyMock(...args), +})) + +jest.mock('../../../utils/command/commandValidations', () => ({ + requireGuild: (interaction: unknown) => requireGuildMock(interaction), +})) + +jest.mock('../../../utils/music/collaborativePlaylist', () => ({ + collaborativePlaylistService: collaborativePlaylistServiceMock, +})) + +jest.mock('../../../utils/general/embeds', () => ({ + createInfoEmbed: (title: string, message: string) => + createInfoEmbedMock(title, message), + createSuccessEmbed: (title: string, message: string) => + createSuccessEmbedMock(title, message), + createWarningEmbed: (title: string, message: string) => + createWarningEmbedMock(title, message), +})) + +import playlistCommand from './playlist' + +function createInteraction( + action: string, + limit?: number, +): ChatInputCommandInteraction { + return { + guildId: 'guild-123', + options: { + getString: jest.fn((key: string) => { + return key === 'action' ? action : null + }), + getInteger: jest.fn((key: string) => { + return key === 'per_user_limit' ? limit ?? null : null + }), + }, + reply: jest.fn(), + } as unknown as ChatInputCommandInteraction +} + +describe('playlist command', () => { + beforeEach(() => { + jest.clearAllMocks() + interactionReplyMock.mockResolvedValue(undefined) + requireGuildMock.mockResolvedValue(true) + createInfoEmbedMock.mockReturnValue({ + title: 'Info', + description: 'Info embed', + }) + createSuccessEmbedMock.mockReturnValue({ + title: 'Success', + description: 'Success embed', + }) + createWarningEmbedMock.mockReturnValue({ + title: 'Warning', + description: 'Warning embed', + }) + collaborativePlaylistServiceMock.setMode.mockReturnValue({ + enabled: true, + perUserLimit: 3, + contributions: {}, + updatedAt: Date.now(), + }) + collaborativePlaylistServiceMock.getState.mockReturnValue({ + enabled: false, + perUserLimit: 3, + contributions: {}, + updatedAt: Date.now(), + }) + collaborativePlaylistServiceMock.resetContributions.mockReturnValue({ + enabled: true, + perUserLimit: 3, + contributions: {}, + updatedAt: Date.now(), + }) + }) + + describe('command metadata', () => { + it('defines command with correct name and description', () => { + expect(playlistCommand.data.name).toBe('playlist') + expect(playlistCommand.data.description).toContain( + 'Playlist collaboration', + ) + }) + + it('has collaborative subcommand', () => { + const subcommands = playlistCommand.data.options || [] + const collaborativeSubcommand = subcommands.find( + (opt: any) => opt.name === 'collaborative', + ) + + expect(collaborativeSubcommand).toBeDefined() + }) + }) + + describe('action: enable', () => { + it('enables collaborative mode with default limit', async () => { + const interaction = createInteraction('enable') + + await playlistCommand.execute({ + interaction, + } as any) + + expect(collaborativePlaylistServiceMock.setMode).toHaveBeenCalledWith( + 'guild-123', + true, + undefined, + ) + expect(interactionReplyMock).toHaveBeenCalledWith( + expect.objectContaining({ + interaction, + content: expect.objectContaining({ + embeds: expect.any(Array), + }), + }), + ) + }) + + it('enables collaborative mode with custom per_user_limit', async () => { + const interaction = createInteraction('enable', 5) + + await playlistCommand.execute({ + interaction, + } as any) + + expect(collaborativePlaylistServiceMock.setMode).toHaveBeenCalledWith( + 'guild-123', + true, + 5, + ) + }) + + it('returns success embed', async () => { + const interaction = createInteraction('enable', 4) + collaborativePlaylistServiceMock.setMode.mockReturnValue({ + enabled: true, + perUserLimit: 4, + contributions: {}, + updatedAt: Date.now(), + }) + + await playlistCommand.execute({ + interaction, + } as any) + + expect(interactionReplyMock).toHaveBeenCalledWith( + expect.objectContaining({ + interaction, + content: expect.objectContaining({ + embeds: expect.any(Array), + }), + }), + ) + expect(createSuccessEmbedMock).toHaveBeenCalled() + }) + }) + + describe('action: disable', () => { + it('disables collaborative mode', async () => { + const interaction = createInteraction('disable') + + await playlistCommand.execute({ + interaction, + } as any) + + expect(collaborativePlaylistServiceMock.setMode).toHaveBeenCalledWith( + 'guild-123', + false, + ) + }) + + it('returns warning embed', async () => { + const interaction = createInteraction('disable') + + await playlistCommand.execute({ + interaction, + } as any) + + expect(interactionReplyMock).toHaveBeenCalledWith( + expect.objectContaining({ + interaction, + content: expect.objectContaining({ + embeds: expect.any(Array), + }), + }), + ) + expect(createWarningEmbedMock).toHaveBeenCalled() + }) + }) + + describe('action: status', () => { + it('retrieves and displays current state', async () => { + const interaction = createInteraction('status') + collaborativePlaylistServiceMock.getState.mockReturnValue({ + enabled: true, + perUserLimit: 3, + contributions: { 'user-1': 2, 'user-2': 1 }, + updatedAt: Date.now(), + }) + + await playlistCommand.execute({ + interaction, + } as any) + + expect(collaborativePlaylistServiceMock.getState).toHaveBeenCalledWith( + 'guild-123', + ) + expect(interactionReplyMock).toHaveBeenCalledWith( + expect.objectContaining({ + interaction, + content: expect.objectContaining({ + ephemeral: true, + }), + }), + ) + }) + + it('displays per-user limit in status', async () => { + const interaction = createInteraction('status') + collaborativePlaylistServiceMock.getState.mockReturnValue({ + enabled: false, + perUserLimit: 5, + contributions: {}, + updatedAt: Date.now(), + }) + + await playlistCommand.execute({ + interaction, + } as any) + + expect(createInfoEmbedMock).toHaveBeenCalledWith( + expect.stringContaining('status'), + expect.stringContaining('5'), + ) + }) + + it('shows contributions in status', async () => { + const interaction = createInteraction('status') + collaborativePlaylistServiceMock.getState.mockReturnValue({ + enabled: true, + perUserLimit: 3, + contributions: { 'user-1': 2 }, + updatedAt: Date.now(), + }) + + await playlistCommand.execute({ + interaction, + } as any) + + expect(createInfoEmbedMock).toHaveBeenCalledWith( + expect.any(String), + expect.stringContaining('user-1'), + ) + }) + + it('shows "No contributions yet" when empty', async () => { + const interaction = createInteraction('status') + collaborativePlaylistServiceMock.getState.mockReturnValue({ + enabled: true, + perUserLimit: 3, + contributions: {}, + updatedAt: Date.now(), + }) + + await playlistCommand.execute({ + interaction, + } as any) + + expect(createInfoEmbedMock).toHaveBeenCalledWith( + expect.any(String), + expect.stringContaining('No contributions yet'), + ) + }) + + it('marks status reply as ephemeral', async () => { + const interaction = createInteraction('status') + + await playlistCommand.execute({ + interaction, + } as any) + + expect(interactionReplyMock).toHaveBeenCalledWith( + expect.objectContaining({ + content: expect.objectContaining({ + ephemeral: true, + }), + }), + ) + }) + }) + + describe('action: reset', () => { + it('calls resetContributions', async () => { + const interaction = createInteraction('reset') + + await playlistCommand.execute({ + interaction, + } as any) + + expect(collaborativePlaylistServiceMock.resetContributions).toHaveBeenCalledWith( + 'guild-123', + ) + }) + + it('returns warning embed', async () => { + const interaction = createInteraction('reset') + + await playlistCommand.execute({ + interaction, + } as any) + + expect(interactionReplyMock).toHaveBeenCalledWith( + expect.objectContaining({ + interaction, + content: expect.objectContaining({ + embeds: expect.any(Array), + }), + }), + ) + expect(createWarningEmbedMock).toHaveBeenCalledWith( + expect.stringContaining('reset'), + expect.any(String), + ) + }) + }) + + describe('guild validation', () => { + it('rejects interaction without guild', async () => { + const interaction = createInteraction('status') + requireGuildMock.mockResolvedValue(false) + + await playlistCommand.execute({ + interaction, + } as any) + + expect(requireGuildMock).toHaveBeenCalledWith(interaction) + expect(interactionReplyMock).not.toHaveBeenCalled() + expect(collaborativePlaylistServiceMock.getState).not.toHaveBeenCalled() + }) + + it('returns early if guildId is null', async () => { + const interaction = { + guildId: null, + options: { + getString: jest.fn(() => 'status'), + getInteger: jest.fn(() => null), + }, + } as unknown as ChatInputCommandInteraction + requireGuildMock.mockResolvedValue(true) + + await playlistCommand.execute({ + interaction, + } as any) + + expect(requireGuildMock).toHaveBeenCalledWith(interaction) + expect(interactionReplyMock).not.toHaveBeenCalled() + }) + }) + + describe('per_user_limit option handling', () => { + it('passes limit when provided with enable', async () => { + const interaction = createInteraction('enable', 7) + + await playlistCommand.execute({ + interaction, + } as any) + + expect(collaborativePlaylistServiceMock.setMode).toHaveBeenCalledWith( + 'guild-123', + true, + 7, + ) + }) + + it('passes undefined when limit not provided with enable', async () => { + const interaction = createInteraction('enable', undefined) + + await playlistCommand.execute({ + interaction, + } as any) + + expect(collaborativePlaylistServiceMock.setMode).toHaveBeenCalledWith( + 'guild-123', + true, + undefined, + ) + }) + + it('ignores limit with disable action', async () => { + const interaction = createInteraction('disable', 10) + + await playlistCommand.execute({ + interaction, + } as any) + + expect(collaborativePlaylistServiceMock.setMode).toHaveBeenCalledWith( + 'guild-123', + false, + ) + // Does not pass the limit + }) + + it('ignores limit with status action', async () => { + const interaction = createInteraction('status', 10) + + await playlistCommand.execute({ + interaction, + } as any) + + expect(collaborativePlaylistServiceMock.getState).toHaveBeenCalledWith( + 'guild-123', + ) + }) + + it('ignores limit with reset action', async () => { + const interaction = createInteraction('reset', 10) + + await playlistCommand.execute({ + interaction, + } as any) + + expect( + collaborativePlaylistServiceMock.resetContributions, + ).toHaveBeenCalledWith('guild-123') + }) + }) +}) diff --git a/packages/bot/src/utils/music/collaborativePlaylist.spec.ts b/packages/bot/src/utils/music/collaborativePlaylist.spec.ts new file mode 100644 index 000000000..89e66dafa --- /dev/null +++ b/packages/bot/src/utils/music/collaborativePlaylist.spec.ts @@ -0,0 +1,369 @@ +import { beforeEach, describe, expect, it } from '@jest/globals' +import { + collaborativePlaylistService, + type CollaborativePlaylistState, +} from './collaborativePlaylist' + +describe('collaborativePlaylistService', () => { + let testCounter = 0 + + beforeEach(() => { + // Increment counter to create unique guild IDs per test + testCounter++ + }) + + function guildId(name: string): string { + return `${name}-${testCounter}` + } + + describe('setMode', () => { + it('enables mode with default limit', () => { + const state = collaborativePlaylistService.setMode(guildId('setmode'), true) + + expect(state.enabled).toBe(true) + expect(state.perUserLimit).toBe(3) // DEFAULT_LIMIT + expect(state.contributions).toEqual({}) + }) + + it('enables mode with custom limit', () => { + const state = collaborativePlaylistService.setMode( + guildId('setmode'), + true, + 5, + ) + + expect(state.enabled).toBe(true) + expect(state.perUserLimit).toBe(5) + }) + + it('disables mode', () => { + const id = guildId('setmode') + collaborativePlaylistService.setMode(id, true, 5) + const state = collaborativePlaylistService.setMode(id, false) + + expect(state.enabled).toBe(false) + expect(state.perUserLimit).toBe(5) // Limit unchanged + }) + + it('ignores invalid limits (non-positive)', () => { + const id = guildId('setmode') + collaborativePlaylistService.setMode(id, true, 5) + const state = collaborativePlaylistService.setMode(id, true, 0) + + expect(state.perUserLimit).toBe(5) // Unchanged + }) + + it('ignores invalid limits (non-finite)', () => { + const id = guildId('setmode') + collaborativePlaylistService.setMode(id, true, 5) + const state = collaborativePlaylistService.setMode(id, true, Infinity) + + expect(state.perUserLimit).toBe(5) // Unchanged + }) + + it('floors fractional limits', () => { + const state = collaborativePlaylistService.setMode( + guildId('setmode'), + true, + 4.9, + ) + + expect(state.perUserLimit).toBe(4) + }) + + it('updates updatedAt timestamp on mode change', () => { + const before = Date.now() + const state = collaborativePlaylistService.setMode( + guildId('setmode'), + true, + ) + const after = Date.now() + + expect(state.updatedAt).toBeGreaterThanOrEqual(before) + expect(state.updatedAt).toBeLessThanOrEqual(after) + }) + + it('preserves contributions when changing mode', () => { + const id = guildId('setmode') + collaborativePlaylistService.setMode(id, true, 3) + collaborativePlaylistService.recordContribution(id, 'user-1', 2) + + const state = collaborativePlaylistService.setMode(id, true, 5) + + expect(state.contributions['user-1']).toBe(2) + }) + }) + + describe('getState', () => { + it('returns default state for new guild', () => { + const state = collaborativePlaylistService.getState(guildId('getstate')) + + expect(state).toEqual({ + enabled: false, + perUserLimit: 3, + contributions: {}, + updatedAt: expect.any(Number), + }) + }) + + it('returns copy of state (not reference)', () => { + const id = guildId('getstate') + collaborativePlaylistService.setMode(id, true) + collaborativePlaylistService.recordContribution(id, 'user-1', 1) + + const state1 = collaborativePlaylistService.getState(id) + state1.contributions['user-1'] = 99 // Mutate the returned object + + const state2 = collaborativePlaylistService.getState(id) + + expect(state2.contributions['user-1']).toBe(1) // Original unchanged + }) + + it('reflects recent setMode changes', () => { + const id = guildId('getstate') + collaborativePlaylistService.setMode(id, true, 5) + const state = collaborativePlaylistService.getState(id) + + expect(state.enabled).toBe(true) + expect(state.perUserLimit).toBe(5) + }) + }) + + describe('resetContributions', () => { + it('clears all contributions', () => { + const id = guildId('reset') + collaborativePlaylistService.setMode(id, true) + collaborativePlaylistService.recordContribution(id, 'user-1', 2) + collaborativePlaylistService.recordContribution(id, 'user-2', 1) + + const state = collaborativePlaylistService.resetContributions(id) + + expect(state.contributions).toEqual({}) + }) + + it('updates updatedAt timestamp', () => { + const id = guildId('reset') + collaborativePlaylistService.setMode(id, true) + collaborativePlaylistService.recordContribution(id, 'user-1', 1) + + const before = Date.now() + const state = collaborativePlaylistService.resetContributions(id) + const after = Date.now() + + expect(state.updatedAt).toBeGreaterThanOrEqual(before) + expect(state.updatedAt).toBeLessThanOrEqual(after) + }) + + it('preserves enabled and perUserLimit settings', () => { + const id = guildId('reset') + collaborativePlaylistService.setMode(id, true, 5) + collaborativePlaylistService.recordContribution(id, 'user-1', 2) + + const state = collaborativePlaylistService.resetContributions(id) + + expect(state.enabled).toBe(true) + expect(state.perUserLimit).toBe(5) + }) + }) + + describe('recordContribution', () => { + it('increments user contribution by default amount (1)', () => { + const id = guildId('record') + collaborativePlaylistService.setMode(id, true) + + collaborativePlaylistService.recordContribution(id, 'user-1') + let state = collaborativePlaylistService.getState(id) + expect(state.contributions['user-1']).toBe(1) + + collaborativePlaylistService.recordContribution(id, 'user-1') + state = collaborativePlaylistService.getState(id) + expect(state.contributions['user-1']).toBe(2) + }) + + it('increments by specified track count', () => { + const id = guildId('record') + collaborativePlaylistService.setMode(id, true) + + collaborativePlaylistService.recordContribution(id, 'user-1', 3) + const state = collaborativePlaylistService.getState(id) + + expect(state.contributions['user-1']).toBe(3) + }) + + it('enforces minimum increment of 1 (ignores trackCount=0)', () => { + const id = guildId('record') + collaborativePlaylistService.setMode(id, true) + + collaborativePlaylistService.recordContribution(id, 'user-1', 0) + let state = collaborativePlaylistService.getState(id) + expect(state.contributions['user-1']).toBe(1) // Minimum + + collaborativePlaylistService.recordContribution(id, 'user-1', 0) + state = collaborativePlaylistService.getState(id) + expect(state.contributions['user-1']).toBe(2) + }) + + it('does not record contribution when mode is disabled', () => { + const id = guildId('record') + collaborativePlaylistService.setMode(id, false) + + collaborativePlaylistService.recordContribution(id, 'user-1', 2) + const state = collaborativePlaylistService.getState(id) + + expect(state.contributions['user-1']).toBeUndefined() + }) + + it('tracks multiple users independently', () => { + const id = guildId('record') + collaborativePlaylistService.setMode(id, true) + + collaborativePlaylistService.recordContribution(id, 'user-1', 2) + collaborativePlaylistService.recordContribution(id, 'user-2', 3) + + const state = collaborativePlaylistService.getState(id) + + expect(state.contributions['user-1']).toBe(2) + expect(state.contributions['user-2']).toBe(3) + }) + + it('updates updatedAt timestamp on contribution', () => { + const id = guildId('record') + collaborativePlaylistService.setMode(id, true) + + const before = Date.now() + collaborativePlaylistService.recordContribution(id, 'user-1', 1) + const after = Date.now() + + const state = collaborativePlaylistService.getState(id) + expect(state.updatedAt).toBeGreaterThanOrEqual(before) + expect(state.updatedAt).toBeLessThanOrEqual(after) + }) + }) + + describe('canAddTracks', () => { + it('allows all tracks when mode disabled', () => { + const id = guildId('canadd') + collaborativePlaylistService.setMode(id, false) + collaborativePlaylistService.recordContribution(id, 'user-1', 100) + + const check = collaborativePlaylistService.canAddTracks(id, 'user-1', 1) + + expect(check.allowed).toBe(true) + expect(check.remaining).toBe(Number.POSITIVE_INFINITY) + }) + + it('allows tracks under limit', () => { + const id = guildId('canadd') + collaborativePlaylistService.setMode(id, true, 5) + collaborativePlaylistService.recordContribution(id, 'user-1', 2) + + const check = collaborativePlaylistService.canAddTracks(id, 'user-1', 2) + + expect(check.allowed).toBe(true) + expect(check.used).toBe(2) + expect(check.remaining).toBe(3) + expect(check.limit).toBe(5) + }) + + it('rejects when exactly at limit', () => { + const id = guildId('canadd') + collaborativePlaylistService.setMode(id, true, 3) + collaborativePlaylistService.recordContribution(id, 'user-1', 3) + + const check = collaborativePlaylistService.canAddTracks(id, 'user-1', 1) + + expect(check.allowed).toBe(false) + expect(check.used).toBe(3) + expect(check.remaining).toBe(0) + }) + + it('rejects when exceeding limit', () => { + const id = guildId('canadd') + collaborativePlaylistService.setMode(id, true, 3) + collaborativePlaylistService.recordContribution(id, 'user-1', 2) + + const check = collaborativePlaylistService.canAddTracks(id, 'user-1', 2) + + expect(check.allowed).toBe(false) + expect(check.used).toBe(2) + expect(check.remaining).toBe(1) + }) + + it('returns zero remaining when used matches limit', () => { + const id = guildId('canadd') + collaborativePlaylistService.setMode(id, true, 1) + collaborativePlaylistService.recordContribution(id, 'user-1', 1) + + const check = collaborativePlaylistService.canAddTracks(id, 'user-1') + + expect(check.remaining).toBe(0) + }) + + it('treats missing user as zero contribution', () => { + const id = guildId('canadd') + collaborativePlaylistService.setMode(id, true, 3) + + const check = collaborativePlaylistService.canAddTracks( + id, + 'never-contributed-user', + 2, + ) + + expect(check.allowed).toBe(true) + expect(check.used).toBe(0) + expect(check.remaining).toBe(3) + }) + + it('uses default track count of 1', () => { + const id = guildId('canadd') + collaborativePlaylistService.setMode(id, true, 3) + collaborativePlaylistService.recordContribution(id, 'user-1', 2) + + const check = collaborativePlaylistService.canAddTracks(id, 'user-1') + + expect(check.allowed).toBe(true) + expect(check.used).toBe(2) + expect(check.remaining).toBe(1) + }) + }) + + describe('edge cases', () => { + it('handles empty contributions map', () => { + const id = guildId('edge') + collaborativePlaylistService.setMode(id, true) + + const state = collaborativePlaylistService.getState(id) + + expect(state.contributions).toEqual({}) + const check = collaborativePlaylistService.canAddTracks(id, 'user-1') + expect(check.allowed).toBe(true) + }) + + it('isolates guild states', () => { + const id1 = guildId('isolation-1') + const id2 = guildId('isolation-2') + collaborativePlaylistService.setMode(id1, true, 5) + collaborativePlaylistService.setMode(id2, true, 2) + collaborativePlaylistService.recordContribution(id1, 'user-1', 3) + + const check1 = collaborativePlaylistService.canAddTracks(id1, 'user-1') + const check2 = collaborativePlaylistService.canAddTracks(id2, 'user-1') + + expect(check1.remaining).toBe(2) // limit 5 - 3 used + expect(check2.remaining).toBe(2) // limit 2 - 0 used + }) + + it('handles large track counts', () => { + const id = guildId('large') + collaborativePlaylistService.setMode(id, true, 100) + + collaborativePlaylistService.recordContribution(id, 'user-1', 50) + let check = collaborativePlaylistService.canAddTracks(id, 'user-1', 30) + expect(check.allowed).toBe(true) + + collaborativePlaylistService.recordContribution(id, 'user-1', 30) + check = collaborativePlaylistService.canAddTracks(id, 'user-1', 21) + expect(check.allowed).toBe(false) + }) + }) +}) diff --git a/packages/shared/src/config/featureToggles.ts b/packages/shared/src/config/featureToggles.ts index 198b4d1f4..a6485919e 100644 --- a/packages/shared/src/config/featureToggles.ts +++ b/packages/shared/src/config/featureToggles.ts @@ -112,6 +112,11 @@ const defaultToggles: Record = { enabled: true, description: 'Enable /embed command for interactive embed template builder', }, + COLLABORATIVE_PLAYLIST: { + name: 'COLLABORATIVE_PLAYLIST', + enabled: true, + description: 'Enable collaborative queue mode (/playlist collaborative)', + }, } function parseEnvironmentToggle( diff --git a/packages/shared/src/types/featureToggle.ts b/packages/shared/src/types/featureToggle.ts index 55bb7ecb9..b3617b746 100644 --- a/packages/shared/src/types/featureToggle.ts +++ b/packages/shared/src/types/featureToggle.ts @@ -20,6 +20,7 @@ export type FeatureToggleName = | 'ARTIST_COMMAND' | 'ALBUM_COMMAND' | 'EMBED_BUILDER' + | 'COLLABORATIVE_PLAYLIST' export type FeatureToggleConfig = { name: FeatureToggleName