Files
paseo/packages/app/src/audio/playback.test.ts
T
Mohamed Boudra 42806cdbf9 Let plugins play audio on every client (#5976)
* feat(plugins): play audio files on every client

* fix(audio): preserve voice output and isolate waiting cues

* fix(plugins): normalize audio MIME parameters

* refactor(audio): share PCM conversion and keep plugin playback file-only
2026-10-03 16:56:59 +01:00

137 lines
4.3 KiB
TypeScript

import { expect, test } from "vitest";
import { createPlaybackQueue } from "./playback";
function harness() {
const started: string[] = [];
const finishes: (() => void)[] = [];
const queue = createPlaybackQueue<string>((source, signal) => {
started.push(source);
return new Promise<number>((resolve, reject) => {
finishes.push(() => resolve(1));
signal.addEventListener("abort", () => reject(new Error("Playback stopped")), { once: true });
});
});
return { queue, started, finishes };
}
test("plays in order and resolves only after playback completes", async () => {
const h = harness();
const first = h.queue.play("first");
const second = h.queue.play("second");
expect(h.started).toEqual(["first"]);
h.finishes[0]();
await expect(first).resolves.toBe(1);
expect(h.started).toEqual(["first", "second"]);
h.finishes[1]();
await expect(second).resolves.toBe(1);
});
test("cancels one owner's queued and active work without stopping another owner", async () => {
const h = harness();
const owner = new AbortController();
const first = h.queue.play("first", owner.signal);
const queued = h.queue.play("cancelled", owner.signal);
const other = h.queue.play("other");
const rejected = Promise.all([
expect(first).rejects.toThrow("Playback stopped"),
expect(queued).rejects.toThrow("Playback stopped"),
]);
owner.abort(new Error("unloaded"));
await rejected;
expect(h.started).toEqual(["first", "other"]);
h.finishes[1]();
await expect(other).resolves.toBe(1);
});
test("stop during loading cannot start cancelled audio or strand the next call", async () => {
const h = harness();
const first = h.queue.play("loading");
const rejected = expect(first).rejects.toThrow("Playback stopped");
h.queue.stop();
await rejected;
const second = h.queue.play("next");
h.finishes[1]();
await expect(second).resolves.toBe(1);
});
test("a failed file does not block the queue", async () => {
const queue = createPlaybackQueue<string>(async (source) => {
if (source === "invalid") throw new Error("Invalid audio");
return 2;
});
const failed = queue.play("invalid");
const next = queue.play("valid");
await expect(failed).rejects.toThrow("Invalid audio");
await expect(next).resolves.toBe(2);
});
test("ending the voice waiting cue leaves a plugin's active audio playing", async () => {
const { createVoiceRuntime } = await import("../voice/voice-runtime");
let finishPlugin!: () => void;
let cueQueued!: () => void;
const queued = new Promise<void>((resolve) => {
cueQueued = resolve;
});
const output = createPlaybackQueue<{ type: string }>(
(_source, signal) =>
new Promise((resolve, reject) => {
finishPlugin = () => resolve(1);
signal.addEventListener("abort", () => reject(new Error("Playback stopped")), {
once: true,
});
}),
);
const engine = {
initialize: async () => {},
destroy: async () => {
output.destroy();
},
startCapture: async () => {},
stopCapture: async () => {},
toggleMute: () => false,
isMuted: () => false,
play(source: { type: string }, signal?: AbortSignal) {
if (source.type.startsWith("audio/pcm")) cueQueued();
return output.play(source, signal);
},
stop: output.stop,
clearQueue: output.clearQueue,
isPlaying: output.isPlaying,
};
const runtime = createVoiceRuntime({
engine,
getServerInfo: () => ({
serverId: "host",
hostname: "host",
version: "1.0.0",
capabilities: {
voice: { dictation: { enabled: true, reason: "" }, voice: { enabled: true, reason: "" } },
},
}),
activateKeepAwake: async () => {},
deactivateKeepAwake: async () => {},
});
runtime.registerSession({
serverId: "host",
setVoiceMode: async () => {},
sendVoiceAudioChunk: async () => {},
audioPlayed: async () => {},
abortRequest: async () => {},
setAssistantAudioPlaying: () => {},
});
try {
await runtime.startVoice("host", "agent");
const result = engine.play({ type: "audio/wav" }).then(
() => "finished",
() => "stopped",
);
runtime.onTurnEvent("host", "agent", "turn_started");
await queued;
runtime.onAssistantAudioStarted("host");
finishPlugin();
expect(await result).toBe("finished");
} finally {
await runtime.destroy();
}
});