import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { activateModel, generate, getActiveModel, listModels } from "@/lib/api"; import { generateDialog } from "@/lib/api"; const fetchMock = vi.fn(); beforeEach(() => { vi.stubGlobal("fetch", fetchMock); }); afterEach(() => { vi.unstubAllGlobals(); fetchMock.mockReset(); }); describe("api", () => { it("listModels GETs /api/models", async () => { fetchMock.mockResolvedValue(new Response(JSON.stringify([{ id: "x" }]))); const out = await listModels(); expect(fetchMock).toHaveBeenCalledWith("/api/models"); expect(out[0].id).toBe("x"); }); it("getActiveModel returns status object", async () => { fetchMock.mockResolvedValue( new Response(JSON.stringify({ id: "x", status: "loaded" })), ); const out = await getActiveModel(); expect(out.status).toBe("loaded"); }); it("activateModel posts to /api/models/{id}/activate", async () => { fetchMock.mockResolvedValue(new Response("{}", { status: 202 })); await activateModel("foo"); expect(fetchMock).toHaveBeenCalledWith( "/api/models/foo/activate", expect.objectContaining({ method: "POST" }), ); }); it("generate posts multipart and returns response blob with seed", async () => { fetchMock.mockResolvedValue( new Response("RIFFFAKE", { status: 200, headers: { "X-Seed-Used": "777" }, }), ); const out = await generate({ modelId: "x", text: "hi", params: {} }); expect(typeof out.blob.size).toBe("number"); expect(out.seedUsed).toBe(777); expect(fetchMock).toHaveBeenCalledWith( "/api/generate", expect.objectContaining({ method: "POST" }), ); }); it("generate surfaces error JSON on 4xx", async () => { fetchMock.mockResolvedValue( new Response( JSON.stringify({ error: { code: "model_not_found", message: "x" } }), { status: 404, headers: { "content-type": "application/json" } }, ), ); await expect( generate({ modelId: "x", text: "hi", params: {} }), ).rejects.toThrow(/model_not_found/); }); }); describe("generateDialog", () => { it("posts multipart with engine_id and per-speaker clips", async () => { fetchMock.mockResolvedValue( new Response("RIFFOK", { status: 200, headers: { "X-Seed-Used": "33" }, }), ); const out = await generateDialog({ engineId: "x", text: "SPEAKER A: hi\nSPEAKER B: hi", params: { temperature: 0.8 }, speakers: [ { letter: "A", reference: new Blob(["a"], { type: "audio/wav" }) }, { letter: "B", reference: new Blob(["b"], { type: "audio/wav" }) }, ], }); expect(out.seedUsed).toBe(33); expect(typeof out.blob.size).toBe("number"); const call = fetchMock.mock.calls[0]; expect(call[0]).toBe("/api/generate/dialog"); const body = call[1].body as FormData; expect(body.get("engine_id")).toBe("x"); expect(body.get("text")).toContain("SPEAKER A:"); expect(body.get("reference_wav_a")).toBeInstanceOf(Blob); expect(body.get("reference_wav_b")).toBeInstanceOf(Blob); }); it("forwards language only when provided", async () => { fetchMock.mockResolvedValue(new Response("RIFF", { status: 200 })); await generateDialog({ engineId: "x", text: "SPEAKER A: hi", language: "fr", params: {}, speakers: [{ letter: "A", reference: new Blob(["a"]) }], }); const body = fetchMock.mock.calls[0][1].body as FormData; expect(body.get("language")).toBe("fr"); }); });