import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, } from "@suaveplan/testing/runner"; import { writeWav } from "../../tests/helpers.js"; import { AudioCLI } from "../cli/cli.js"; import { AUDIO_CLI_CODES } from "../error-codes/error-codes.js"; import { noopSpeaker, runPlay } from "./play.js"; import type { SpeakerAdapter } from "./play.types.js"; describe("runPlay", () => { let dir: string; let cli: AudioCLI; beforeEach(async () => { dir = await mkdtemp(join(tmpdir(), "audio-cli-play-")); cli = new AudioCLI(); }); afterEach(async () => { await rm(dir, { recursive: true, force: true }); }); it("runs against the default noopSpeaker", { timeout: 30_000, }, async () => { const input = join(dir, "in.wav"); await writeWav(input, 64); await runPlay(cli, input); }); it("dispatches to the injected speaker", { timeout: 30_000 }, async () => { const input = join(dir, "in.wav"); await writeWav(input, 64); let called = false; const speaker: SpeakerAdapter = { async play() { called = true; }, }; await runPlay(cli, input, { speaker }); expect(called).toBe(true); }); it("threads a non-aborted signal through to the speaker", { timeout: 30_000, }, async () => { const input = join(dir, "in.wav"); await writeWav(input, 32); const ac = new AbortController(); let receivedSignal: AbortSignal | undefined; const speaker: SpeakerAdapter = { async play(_doc, { signal }) { receivedSignal = signal; }, }; await runPlay(cli, input, { speaker, signal: ac.signal }); expect(receivedSignal).toBe(ac.signal); }); it("missing input throws INPUT_NOT_FOUND", { timeout: 5000 }, async () => { await expect( runPlay(cli, join(dir, "missing.wav")) ).rejects.toMatchObject({ code: AUDIO_CLI_CODES.INPUT_NOT_FOUND }); }); it("invalid options throws INVALID_ARGS", { timeout: 5000 }, async () => { await expect( runPlay(cli, "in.wav", { speaker: { play: "not-a-fn" } as never }) ).rejects.toMatchObject({ code: AUDIO_CLI_CODES.INVALID_ARGS }); }); it("speaker error wraps as PIPELINE_FAILED", { timeout: 30_000, }, async () => { const input = join(dir, "in.wav"); await writeWav(input, 32); const speaker: SpeakerAdapter = { async play() { throw new Error("speaker boom"); }, }; await expect(runPlay(cli, input, { speaker })).rejects.toMatchObject({ code: AUDIO_CLI_CODES.PIPELINE_FAILED, }); }); it("pre-aborted signal short-circuits", { timeout: 5000 }, async () => { const ac = new AbortController(); ac.abort(); await expect( runPlay(cli, "in.wav", { signal: ac.signal }) ).rejects.toMatchObject({ code: AUDIO_CLI_CODES.ABORTED }); }); it("noopSpeaker.play is a no-op", { timeout: 5000 }, async () => { await noopSpeaker.play({} as never, {}); }); });