evOo-local/packages/adapters/src/voice-http.test.ts
cynarAI 459d7c99e4
Pick the voice from a carousel in the app language, local voice only (#166)
* Pick the voice from a carousel in the app language, local voice only

Voice settings now list only the voices that speak the app language
(German shows German voices, English shows English ones) as a horizontal
carousel: one card per voice, a play button next to the name, tap to
make it the active voice. Changing the app language moves the saved
voice to that language. The hosted voice providers (ElevenLabs, OpenAI,
Cartesia, Fish Audio) are removed; Kokoro on the server is the only
voice. E2E runs against a stub Kokoro server so the carousel is covered.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KxoSHAyzMXvdtwcDJZcn57

* test(e2e): measure the workspace panel width after its transition settles

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KxoSHAyzMXvdtwcDJZcn57

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-05 22:20:06 +02:00

51 lines
1.8 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { MAX_VOICE_JSON_BYTES, readVoiceJson, voiceDeadline } from "./voice-http.js";
describe("voiceDeadline", () => {
it("aborts when the client signal aborts", () => {
const client = new AbortController();
const combined = voiceDeadline(client.signal, 20_000);
expect(combined.aborted).toBe(false);
client.abort();
expect(combined.aborted).toBe(true);
});
it("aborts when the deadline elapses even if the client stays connected", async () => {
const combined = voiceDeadline(new AbortController().signal, 1);
await new Promise<void>((resolve, reject) => {
const guard = setTimeout(() => reject(new Error("deadline did not abort")), 1_000);
combined.addEventListener(
"abort",
() => {
clearTimeout(guard);
resolve();
},
{ once: true },
);
});
expect(combined.aborted).toBe(true);
});
});
describe("readVoiceJson", () => {
it("rejects an oversized provider response without waiting for cancellation", async () => {
const cancel = vi.fn(() => new Promise<void>(() => undefined));
const response = new Response(new ReadableStream({ cancel }), {
headers: { "content-length": String(MAX_VOICE_JSON_BYTES + 1) },
});
await expect(readVoiceJson(response)).rejects.toThrow("Voice response is too large.");
expect(cancel).toHaveBeenCalledOnce();
});
it("rejects malformed successful JSON when the caller requires a payload", async () => {
await expect(readVoiceJson(new Response("not json"), { requireValid: true })).rejects.toThrow(
"Voice provider returned invalid JSON.",
);
});
it("keeps malformed error bodies optional", async () => {
await expect(readVoiceJson(new Response("not json"))).resolves.toBeNull();
});
});