import { describe, it, expect, vi } from "vitest"; import { handleFusionChat } from "../../open-sse/services/combo.js"; const log = { info: () => {}, warn: () => {}, debug: () => {} }; // Minimal OpenAI-chat Response stub with the .ok + .clone().json() surface the engine uses. function okResponse(content, { delayMs = 0 } = {}) { const json = { choices: [{ message: { role: "assistant", content } }] }; const make = () => ({ ok: true, status: 200, clone: make, json: async () => json }); const res = make(); return delayMs > 0 ? new Promise((r) => setTimeout(() => r(res), delayMs)) : res; } function errResponse(status = 500) { const make = () => ({ ok: false, status, clone: make, json: async () => ({ error: { message: "boom" } }) }); return make(); } describe("fusion combo", () => { it("answers directly with a single-model panel (nothing to fuse)", async () => { const handleSingleModel = vi.fn(async () => okResponse("solo")); await handleFusionChat({ body: { messages: [{ role: "user", content: "hi" }] }, models: ["p/only"], handleSingleModel, log, }); expect(handleSingleModel).toHaveBeenCalledTimes(1); expect(handleSingleModel.mock.calls[0][1]).toBe("p/only"); }); it("fans out to the panel then routes a synthesis turn to the judge", async () => { const seen = []; const handleSingleModel = vi.fn(async (body, model) => { seen.push(model); if (model === "p/judge") return okResponse("FINAL"); return okResponse(`ans-${model}`); }); const res = await handleFusionChat({ body: { messages: [{ role: "user", content: "Q" }], stream: true, tools: [{ name: "x" }] }, models: ["p/a", "p/b", "p/c"], handleSingleModel, log, judgeModel: "p/judge", }); // 3 panel calls + 1 judge call. expect(handleSingleModel).toHaveBeenCalledTimes(4); expect(seen.slice(0, 3).sort()).toEqual(["p/a", "p/b", "p/c"]); expect(seen[3]).toBe("p/judge"); // Panel calls are non-streaming with tools stripped. for (const [body, model] of handleSingleModel.mock.calls.filter(([, m]) => m !== "p/judge")) { expect(body.stream).toBe(false); expect(body.tools).toBeUndefined(); } // Judge call carries every panel answer + keeps the client's stream flag. const [judgeBody] = handleSingleModel.mock.calls.find(([, m]) => m === "p/judge"); const judgeText = judgeBody.messages.at(-1).content; expect(judgeText).toContain("ans-p/a"); expect(judgeText).toContain("ans-p/b"); expect(judgeText).toContain("ans-p/c"); expect(judgeText).toContain("Source 1"); expect(judgeBody.stream).toBe(true); expect(res.ok).toBe(true); }); it("defaults the judge to the first panel model when none is set", async () => { const seen = []; const handleSingleModel = vi.fn(async (_body, model) => { seen.push(model); return okResponse(`ans-${model}`); }); await handleFusionChat({ body: { messages: [{ role: "user", content: "Q" }] }, models: ["p/first", "p/second"], handleSingleModel, log, }); // Last call is the judge; defaults to panel[0]. expect(seen.at(-1)).toBe("p/first"); }); it("proceeds on quorum without waiting for a straggler (grace window)", async () => { const handleSingleModel = vi.fn(async (_body, model) => { if (model === "p/slow") return okResponse("slow", { delayMs: 5000 }); if (model === "p/judge") return okResponse("FINAL"); return okResponse(`fast-${model}`); }); const t0 = Date.now(); await handleFusionChat({ body: { messages: [{ role: "user", content: "Q" }] }, models: ["p/x", "p/y", "p/slow"], handleSingleModel, log, judgeModel: "p/judge", tuning: { minPanel: 2, stragglerGraceMs: 50, panelHardTimeoutMs: 10000 }, }); const elapsed = Date.now() - t0; // Two fast answers reach quorum; grace is 50ms, so we never wait ~5s for p/slow. expect(elapsed).toBeLessThan(2000); const judgeCall = handleSingleModel.mock.calls.find(([, m]) => m === "p/judge"); const judgeText = judgeCall[0].messages.at(-1).content; expect(judgeText).toContain("fast-p/x"); expect(judgeText).toContain("fast-p/y"); expect(judgeText).not.toContain("slow"); }); it("returns the lone survivor directly when only one panel model succeeds", async () => { const handleSingleModel = vi.fn(async (_body, model) => { if (model === "p/ok") return okResponse("lone"); return errResponse(500); }); await handleFusionChat({ body: { messages: [{ role: "user", content: "Q" }] }, models: ["p/ok", "p/bad"], handleSingleModel, log, judgeModel: "p/judge", tuning: { minPanel: 2, stragglerGraceMs: 50, panelHardTimeoutMs: 5000 }, }); // No judge call — single answer means there is nothing to fuse. const judged = handleSingleModel.mock.calls.some(([, m]) => m === "p/judge"); expect(judged).toBe(false); }); it("returns 503 when the whole panel fails", async () => { const handleSingleModel = vi.fn(async () => errResponse(500)); const res = await handleFusionChat({ body: { messages: [{ role: "user", content: "Q" }] }, models: ["p/a", "p/b"], handleSingleModel, log, tuning: { minPanel: 2, stragglerGraceMs: 50, panelHardTimeoutMs: 5000 }, }); expect(res.status).toBe(503); }); });