import { test } from "bun:test"; import assert from "node:assert/strict"; import { createTestSettings } from "../testSettings.ts"; import type { MusicSelectionResult } from "./voiceSessionTypes.ts"; import { completePendingMusicDisambiguationSelection, getMusicPromptContext, isMusicDisambiguationResolutionTurn, maybeHandlePendingMusicDisambiguationTurn, resolvePendingMusicDisambiguationSelection } from "./voiceMusicDisambiguation.ts";
const OPTIONS: MusicSelectionResult[] = [ { id: "track-1", title: "Midnight City", artist: "M83", platform: "youtube", externalUrl: null, durationSeconds: 250 }, { id: "track-2", title: "Genesis", artist: "Grimes", platform: "youtube", externalUrl: null, durationSeconds: 271 }, { id: "track-3", title: "Windowlicker", artist: "Aphex Twin", platform: "soundcloud", externalUrl: null, durationSeconds: 360 } ];
function createSession(overrides: Record<string, unknown> = {}) { return { id: "session-1", guildId: "guild-1", textChannelId: "text-1", settingsSnapshot: null, voiceCommandState: null, ...overrides }; }
function createDisambiguationHost({ promptContext = null, snapshot = null, queueState = { tracks: [ { id: "now-playing", title: "Current Song", artist: "Current Artist", source: "yt" } ], nowPlayingIndex: 0, isPaused: false }, isVoiceCommandSessionActive = true, llmGenerate = null }: { promptContext?: Record<string, unknown> | null; snapshot?: Record<string, unknown> | null; queueState?: { tracks: Array<Record<string, unknown>>; nowPlayingIndex: number | null; isPaused: boolean; }; isVoiceCommandSessionActive?: boolean; llmGenerate?: ((args: Record<string, unknown>) => Promise<Record<string, unknown>> | Record<string, unknown>) | null; } = {}) { const requestPlayCalls: Array<Record<string, unknown>> = []; const composeCalls: Array<Record<string, unknown>> = []; const sentMessages: string[] = []; const clearMusicDisambiguationSessions: unknown[] = []; const clearVoiceCommandSessions: unknown[] = []; const llmCalls: Array<Record<string, unknown>> = [];
const host = {
appConfig: {},
client: {
user: {
id: "bot-1"
}
},
store: {
logAction() {},
getSettings() {
return createTestSettings({});
}
},
getMusicDisambiguationPromptContext() {
return promptContext;
},
snapshotMusicRuntimeState() {
return snapshot;
},
isVoiceCommandSessionActiveForUser() {
return isVoiceCommandSessionActive;
},
clearMusicDisambiguationState(session: unknown) {
clearMusicDisambiguationSessions.push(session);
if (promptContext && typeof promptContext === "object") {
promptContext.active = false;
}
},
clearVoiceCommandSession(session: unknown) {
clearVoiceCommandSessions.push(session);
},
async composeOperationalMessage(payload: Record<string, unknown>) {
composeCalls.push(payload);
return queued ${String(payload.details?.trackId || "")}.trim();
},
async requestPlayMusic(args: Record<string, unknown>) {
requestPlayCalls.push(args);
return { ok: true };
},
llm: llmGenerate
? {
async generate(args: Record<string, unknown>) {
llmCalls.push(args);
return await llmGenerate(args);
}
}
: null,
ensureToolMusicQueueState() {
return queueState;
},
isMusicPlaybackActive() {
return true;
},
async playVoiceQueueTrackByIndex() {
throw new Error("should_not_autoplay_in_test");
},
buildVoiceQueueStatePayload() {
return {
tracks: queueState.tracks,
nowPlayingIndex: queueState.nowPlayingIndex,
isPaused: queueState.isPaused
};
}
};
const channel = { id: "text-1", async send(content: string) { sentMessages.push(content); return true; } };
return { host, channel, queueState, requestPlayCalls, composeCalls, sentMessages, clearMusicDisambiguationSessions, clearVoiceCommandSessions, llmCalls }; }
test("getMusicPromptContext derives current playback, last action, and upcoming queue entries", () => { const { host } = createDisambiguationHost({ snapshot: { active: true, lastTrackId: "track-1", lastTrackTitle: "Midnight City", lastTrackArtists: ["M83"], lastCommandReason: "voice_tool_music_play", lastQuery: "midnight city", queueState: { tracks: [ { id: "track-1", title: "Midnight City", artist: "M83" }, { id: "track-2", title: "Genesis", artist: "Grimes" }, { id: "track-3", title: "Windowlicker", artist: "Aphex Twin" }, { id: "track-4", title: "Hyperballad", artist: "Bjork" } ], nowPlayingIndex: 0, isPaused: false } } });
const context = getMusicPromptContext(host, createSession());
assert.deepEqual(context, { playbackState: "playing", replyHandoffMode: null, currentTrack: { id: "track-1", title: "Midnight City", artists: ["M83"] }, lastTrack: { id: "track-1", title: "Midnight City", artists: ["M83"] }, queueLength: 4, upcomingTracks: [ { id: "track-2", title: "Genesis", artist: "Grimes" }, { id: "track-3", title: "Windowlicker", artist: "Aphex Twin" }, { id: "track-4", title: "Hyperballad", artist: "Bjork" } ], lastAction: "play_now", lastQuery: "midnight city" }); });
test("getMusicPromptContext keeps the last known track visible while playback is idle", () => { const { host } = createDisambiguationHost({ snapshot: { active: false, lastTrackId: "track-1", lastTrackTitle: "Midnight City", lastTrackArtists: ["M83"], lastCommandReason: null, lastQuery: "midnight city", queueState: { tracks: [], nowPlayingIndex: null, isPaused: false } } });
const context = getMusicPromptContext(host, createSession());
assert.deepEqual(context, { playbackState: "idle", replyHandoffMode: null, currentTrack: { id: "track-1", title: "Midnight City", artists: ["M83"] }, lastTrack: { id: "track-1", title: "Midnight City", artists: ["M83"] }, queueLength: 0, upcomingTracks: [], lastAction: null, lastQuery: "midnight city" }); });
test("getMusicPromptContext carries a pending reply handoff mode into prompt context", () => { const { host } = createDisambiguationHost({ snapshot: { active: true, replyHandoffMode: "pause", lastTrackId: "track-1", lastTrackTitle: "Midnight City", lastTrackArtists: ["M83"], lastCommandReason: "voice_tool_music_play", lastQuery: "midnight city", queueState: { tracks: [ { id: "track-1", title: "Midnight City", artist: "M83" } ], nowPlayingIndex: 0, isPaused: true } } });
const context = getMusicPromptContext(host, createSession());
assert.equal(context?.replyHandoffMode, "pause"); });
test("resolvePendingMusicDisambiguationSelection supports ordinal and title matching", () => { const promptContext = { active: true, query: "electronic", platform: "youtube", action: "play_now", requestedByUserId: "user-1", options: OPTIONS }; const { host } = createDisambiguationHost({ promptContext });
assert.equal( resolvePendingMusicDisambiguationSelection(host, createSession(), "second one please")?.id, "track-2" ); assert.equal( resolvePendingMusicDisambiguationSelection(host, createSession(), "windowlicker by aphex twin")?.id, "track-3" ); });
test("isMusicDisambiguationResolutionTurn requires the requesting user and an active music command session", () => { const promptContext = { active: true, query: "electronic", platform: "youtube", action: "play_now", requestedByUserId: "user-1", options: OPTIONS }; const activeHost = createDisambiguationHost({ promptContext, isVoiceCommandSessionActive: true }).host; const inactiveHost = createDisambiguationHost({ promptContext, isVoiceCommandSessionActive: false }).host; const session = createSession();
assert.equal( isMusicDisambiguationResolutionTurn(activeHost, session, "user-1", "never mind"), true ); assert.equal( isMusicDisambiguationResolutionTurn(activeHost, session, "user-2", "2"), false ); assert.equal( isMusicDisambiguationResolutionTurn(inactiveHost, session, "user-1", "2"), false ); });
test("completePendingMusicDisambiguationSelection delegates play_now requests to requestPlayMusic", async () => { const promptContext = { active: true, query: "midnight city", platform: "youtube", action: "play_now", requestedByUserId: "user-1", options: OPTIONS }; const { host, requestPlayCalls, clearMusicDisambiguationSessions, clearVoiceCommandSessions } = createDisambiguationHost({ promptContext }); const session = createSession({ settingsSnapshot: createTestSettings({}) });
const handled = await completePendingMusicDisambiguationSelection(host, { session, settings: createTestSettings({}), userId: "user-1", selected: OPTIONS[0], source: "voice_disambiguation" });
assert.equal(handled, true); assert.equal(requestPlayCalls.length, 1); assert.deepEqual(requestPlayCalls[0], { guildId: "guild-1", channel: null, channelId: "text-1", requestedByUserId: "user-1", settings: createTestSettings({}), query: "midnight city", platform: "youtube", trackId: "track-1", searchResults: OPTIONS, reason: "voice_music_disambiguation_selection", source: "voice_disambiguation", mustNotify: false }); assert.deepEqual(clearMusicDisambiguationSessions, []); assert.deepEqual(clearVoiceCommandSessions, []); });
test("completePendingMusicDisambiguationSelection queues the chosen track next and sends an operational message", async () => { const promptContext = { active: true, query: "genesis", platform: "youtube", action: "queue_next", requestedByUserId: "user-1", options: OPTIONS }; const { host, channel, queueState, composeCalls, sentMessages, clearMusicDisambiguationSessions, clearVoiceCommandSessions } = createDisambiguationHost({ promptContext }); const settings = createTestSettings({}); const session = createSession({ settingsSnapshot: settings });
const handled = await completePendingMusicDisambiguationSelection(host, { session, settings, userId: "user-1", selected: OPTIONS[1], channel, messageId: "msg-1", source: "voice_disambiguation" });
assert.equal(handled, true); assert.equal(queueState.tracks[1]?.id, "track-2"); assert.equal(session.toolMusicTrackCatalog.get("track-2")?.title, "Genesis"); assert.equal(clearMusicDisambiguationSessions.length, 1); assert.equal(clearVoiceCommandSessions.length, 1); assert.equal(composeCalls.length, 1); assert.equal(composeCalls[0]?.reason, "queued_next"); assert.deepEqual(composeCalls[0]?.details, { source: "voice_disambiguation", query: "genesis", trackId: "track-2", trackTitle: "Genesis", trackArtists: ["Grimes"] }); assert.deepEqual(sentMessages, ["queued track-2"]); });
test("maybeHandlePendingMusicDisambiguationTurn can use an LLM resolver for fuzzy followups", async () => { const promptContext = { active: true, query: "minecraft music", platform: "youtube", action: "play_now", requestedByUserId: "user-1", options: [ { id: "track-1", title: "Minecraft Calm Music by C418", artist: "CozyCraft", platform: "youtube", externalUrl: null, durationSeconds: 600 }, { id: "track-2", title: "Minecraft Cliffside Waterfall Ambience", artist: "CozyCraft", platform: "youtube", externalUrl: null, durationSeconds: 600 } ] }; const settings = createTestSettings({ agentStack: { runtimeConfig: { voice: { musicBrain: { mode: "dedicated_model", model: { provider: "anthropic", model: "claude-3-5-haiku-latest" } } } } } }); const { host, requestPlayCalls, llmCalls } = createDisambiguationHost({ promptContext, llmGenerate: async () => ({ text: JSON.stringify({ selection_id: "track-2" }), provider: "anthropic", model: "claude-3-5-haiku-latest" }) }); const session = createSession({ settingsSnapshot: settings });
const handled = await maybeHandlePendingMusicDisambiguationTurn(host, { session, settings, userId: "user-1", transcript: "the cliff side water fall one" });
assert.equal(handled, true); assert.equal(llmCalls.length, 1); assert.equal(requestPlayCalls.length, 1); assert.equal(requestPlayCalls[0]?.trackId, "track-2"); });
test("maybeHandlePendingMusicDisambiguationTurn rejects hallucinated LLM selection ids", async () => { const promptContext = { active: true, query: "minecraft music", platform: "youtube", action: "play_now", requestedByUserId: "user-1", options: [ { id: "track-1", title: "Minecraft Calm Music by C418", artist: "CozyCraft", platform: "youtube", externalUrl: null, durationSeconds: 600 }, { id: "track-2", title: "Minecraft Cliffside Waterfall Ambience", artist: "CozyCraft", platform: "youtube", externalUrl: null, durationSeconds: 600 } ] }; const { host, requestPlayCalls } = createDisambiguationHost({ promptContext, llmGenerate: async () => ({ text: JSON.stringify({ selection_id: "track-999" }), provider: "anthropic", model: "claude-3-5-haiku-latest" }) });
const handled = await maybeHandlePendingMusicDisambiguationTurn(host, { session: createSession(), settings: createTestSettings({}), userId: "user-1", transcript: "the cliff side water fall one" });
assert.equal(handled, false); assert.equal(requestPlayCalls.length, 0); });
test("maybeHandlePendingMusicDisambiguationTurn requires an active music command session", async () => { const promptContext = { active: true, query: "minecraft music", platform: "youtube", action: "play_now", requestedByUserId: "user-1", options: OPTIONS }; const { host, requestPlayCalls, clearMusicDisambiguationSessions, clearVoiceCommandSessions } = createDisambiguationHost({ promptContext, isVoiceCommandSessionActive: false });
const handled = await maybeHandlePendingMusicDisambiguationTurn(host, { session: createSession(), settings: createTestSettings({}), userId: "user-1", transcript: "2" });
assert.equal(handled, false); assert.equal(requestPlayCalls.length, 0); assert.equal(clearMusicDisambiguationSessions.length, 0); assert.equal(clearVoiceCommandSessions.length, 0); });
