import { afterEach, describe, expect, it, vi } from "vitest"; import { KOKORO_LOCAL_TOKEN, KokoroVoiceProvider } from "./kokoro-voice.js"; import { SCRIPTED_MPEG, SCRIPTED_TRANSCRIPT, SCRIPTED_VOICE_ID, ScriptedVoiceProvider, } from "./scripted-voice.js"; import { createVoiceProvider, isVoiceProviderId, listVoiceCatalog, VOICE_CATALOG, } from "./voice-factory.js"; import { MAX_SYNTHESIZED_AUDIO_BYTES } from "./voice-http.js"; const ctx = { operationId: "voice", traceId: "voice", spaceId: "w", userId: "u", signal: new AbortController().signal, }; const previousRuntime = process.env.AGENT_RUNTIME; afterEach(() => { vi.unstubAllGlobals(); vi.unstubAllEnvs(); if (previousRuntime === undefined) delete process.env.AGENT_RUNTIME; else process.env.AGENT_RUNTIME = previousRuntime; }); describe("createVoiceProvider", () => { it("offers only the local voice server", () => { process.env.AGENT_RUNTIME = "pi"; expect(VOICE_CATALOG.map((entry) => entry.id)).toEqual(["kokoro"]); expect(listVoiceCatalog().map((entry) => entry.id)).toEqual(["kokoro"]); expect(createVoiceProvider("kokoro").describe().id).toBe("kokoro"); expect(createVoiceProvider("kokoro").describe().capabilities.transcribe).toBe(false); expect(isVoiceProviderId("kokoro")).toBe(true); expect(isVoiceProviderId("elevenlabs")).toBe(false); expect(() => createVoiceProvider("elevenlabs")).toThrow(/unknown voice provider/i); expect(isVoiceProviderId("scripted")).toBe(false); expect(isVoiceProviderId("piper")).toBe(false); expect(() => createVoiceProvider("piper")).toThrow(/unknown voice provider/i); expect(() => createVoiceProvider("scripted")).toThrow(/unknown voice provider/i); }); it("adds the scripted fixture only when the agent runtime is scripted", () => { process.env.AGENT_RUNTIME = "scripted"; expect(listVoiceCatalog().some((entry) => entry.id === "scripted")).toBe(true); expect(isVoiceProviderId("scripted")).toBe(true); expect(createVoiceProvider("scripted").describe().id).toBe("scripted"); }); }); describe("KokoroVoiceProvider", () => { it("verifies against the local /v1/audio/voices endpoint", async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ voices: ["af_heart"] }), }); vi.stubGlobal("fetch", fetchMock); const provider = new KokoroVoiceProvider(); await expect(provider.verify(KOKORO_LOCAL_TOKEN, ctx)).resolves.toEqual({ ok: true }); expect(String(fetchMock.mock.calls[0]?.[0])).toBe("http://127.0.0.1:8880/v1/audio/voices"); }); it("returns a helpful message when the local server is down", async () => { vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("fetch failed"))); const provider = new KokoroVoiceProvider(); const result = await provider.verify(KOKORO_LOCAL_TOKEN, ctx); expect(result.ok).toBe(false); expect(result.message).toMatch(/Lokaler Sprachdienst nicht erreichbar/i); }); it("maps plain-string and object voice payloads", async () => { vi.stubGlobal( "fetch", vi.fn().mockResolvedValue( new Response( JSON.stringify({ voices: ["af_heart", { id: "af_bella", name: "Bella" }], }), ), ), ); const voices = await new KokoroVoiceProvider().listVoices(KOKORO_LOCAL_TOKEN, ctx); expect(voices.map((voice) => voice.id)).toEqual(["af_heart", "af_bella"]); expect(voices[0]?.label).toBe("Heart"); }); it("posts non-streaming speech to /v1/audio/speech", async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, arrayBuffer: async () => new Uint8Array([6, 7]).buffer, }); vi.stubGlobal("fetch", fetchMock); const clip = await new KokoroVoiceProvider().synthesize( { text: "Hallo", voiceId: "af_heart", apiKey: KOKORO_LOCAL_TOKEN }, ctx, ); expect(clip.mimeType).toBe("audio/mpeg"); expect([...clip.bytes]).toEqual([6, 7]); expect(String(fetchMock.mock.calls[0]?.[0])).toBe("http://127.0.0.1:8880/v1/audio/speech"); const init = fetchMock.mock.calls[0]?.[1] as RequestInit; expect(JSON.parse(String(init.body))).toMatchObject({ model: "kokoro", input: "Hallo", voice: "af_heart", response_format: "mp3", stream: false, }); }); it("honors KOKORO_BASE_URL", async () => { vi.stubEnv("KOKORO_BASE_URL", "http://kokoro.test:9000/"); const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ voices: [] }) }); vi.stubGlobal("fetch", fetchMock); await new KokoroVoiceProvider().verify(KOKORO_LOCAL_TOKEN, ctx); expect(String(fetchMock.mock.calls[0]?.[0])).toBe("http://kokoro.test:9000/v1/audio/voices"); }); }); describe("voice response limits", () => { it.each([["Kokoro", () => new KokoroVoiceProvider(), "af_heart"] as const])( "rejects an oversized %s speech response before buffering it", async (_name, create, voiceId) => { const cancel = vi.fn().mockResolvedValue(undefined); vi.stubGlobal( "fetch", vi.fn().mockResolvedValue({ ok: true, headers: new Headers({ "content-length": String(MAX_SYNTHESIZED_AUDIO_BYTES + 1), }), body: { cancel }, }), ); await expect( create().synthesize({ text: "Hi", voiceId, apiKey: "sk-test" }, ctx), ).rejects.toThrow("Voice response is too large."); expect(cancel).toHaveBeenCalledOnce(); }, ); }); describe("ScriptedVoiceProvider", () => { it("verifies, lists, speaks, and transcribes without a network", async () => { const provider = new ScriptedVoiceProvider(); await expect(provider.verify("short", ctx)).resolves.toEqual({ ok: false, message: "That key is too short.", }); await expect(provider.verify("fake-scripted-voice-key", ctx)).resolves.toEqual({ ok: true }); expect(await provider.listVoices("fake-scripted-voice-key", ctx)).toEqual([ { id: SCRIPTED_VOICE_ID, label: "Scripted", description: "Test voice" }, ]); const clip = await provider.synthesize( { text: "Hello", voiceId: SCRIPTED_VOICE_ID, apiKey: "fake-scripted-voice-key" }, ctx, ); expect(clip.mimeType).toBe("audio/mpeg"); expect([...clip.bytes]).toEqual([...SCRIPTED_MPEG]); await expect( provider.transcribe( { audio: new Uint8Array([1]), mimeType: "audio/webm", apiKey: "fake-scripted-voice-key", }, ctx, ), ).resolves.toEqual({ text: SCRIPTED_TRANSCRIPT }); }); });