techfreakworm's picture
feat(web): generateDialog client with per-speaker multipart
192ae87 unverified
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");
});
});