import { describe, it, expect, vi } from "vitest"; import { getComboModelsFromData, discardResponse, handleFusionChat } from "../../open-sse/services/combo.js"; import { tokensForDetail, shouldPersistRequestDetail } from "../../open-sse/handlers/chatCore/requestDetail.js"; vi.mock("@/lib/usageDb.js", () => ({ saveRequestUsage: vi.fn(), saveRequestDetail: vi.fn(), })); vi.mock("../../open-sse/utils/stream.js", () => ({ COLORS: {}, formatSSE: vi.fn(), })); const log = { info: () => {}, warn: () => {}, debug: () => {} }; describe("nested combo members stay units", () => { it("keeps comboB as a member of comboA (does not flatten leaves into the parent)", () => { const combos = [ { name: "comboA", models: ["comboB", "comboC", "x/y"] }, { name: "comboB", models: ["a/m1", "b/m2"] }, { name: "comboC", models: ["c/m3"] }, ]; expect(getComboModelsFromData("comboA", combos)).toEqual(["comboB", "comboC", "x/y"]); }); it("returns null for a plain provider/model string", () => { expect(getComboModelsFromData("openai/gpt-4", [{ name: "comboA", models: ["a/m1"] }])).toBeNull(); }); it("still returns direct members of a top-level combo", () => { const combos = [{ name: "solo", models: ["a/m1", "b/m2"] }]; expect(getComboModelsFromData("solo", combos)).toEqual(["a/m1", "b/m2"]); }); it("ignores a disabled combo", () => { const combos = [{ name: "solo", enabled: false, models: ["a/m1"] }]; expect(getComboModelsFromData("solo", combos)).toBeNull(); }); }); describe("shouldPersistRequestDetail", () => { it("never persists the streaming-start placeholder (0-token fake success)", () => { expect(shouldPersistRequestDetail("all", "streaming-start")).toBe(false); expect(shouldPersistRequestDetail("success-only", "streaming-start")).toBe(false); }); it("combo hops skip failed attempts so nested fallback does not inflate Details", () => { expect(shouldPersistRequestDetail("success-only", "error")).toBe(false); expect(shouldPersistRequestDetail("success-only", "success")).toBe(true); }); it("direct (non-combo) requests still persist errors", () => { expect(shouldPersistRequestDetail("all", "error")).toBe(true); expect(shouldPersistRequestDetail("all", "success")).toBe(true); }); }); describe("tokensForDetail", () => { it("canonicalizes Claude input_tokens/output_tokens so the Details tab is not 0/0", () => { expect(tokensForDetail({ input_tokens: 120, output_tokens: 40 })).toEqual( expect.objectContaining({ prompt_tokens: 120, completion_tokens: 40 }), ); }); it("keeps OpenAI prompt/completion names", () => { expect(tokensForDetail({ prompt_tokens: 10, completion_tokens: 3 })).toEqual( expect.objectContaining({ prompt_tokens: 10, completion_tokens: 3 }), ); }); it("returns zeros for missing usage (streaming placeholder)", () => { expect(tokensForDetail(null)).toEqual({ prompt_tokens: 0, completion_tokens: 0 }); }); }); describe("discardResponse", () => { it("cancels an unused body so a dropped stream can finish", () => { const cancel = vi.fn(); discardResponse({ body: { cancel } }); expect(cancel).toHaveBeenCalledTimes(1); }); it("is a no-op for timeout/error sentinels and missing bodies", () => { expect(() => discardResponse({ __timeout: true, body: { cancel: () => { throw new Error("no"); } } })).not.toThrow(); expect(() => discardResponse(null)).not.toThrow(); }); }); describe("fusion cancels unused panel bodies", () => { it("cancels a late panel Response so it is not left as a 0-token streaming row", async () => { const cancel = vi.fn(); const ok = (content) => { const json = { choices: [{ message: { role: "assistant", content } }] }; const make = () => ({ ok: true, status: 200, clone: make, json: async () => json }); return make(); }; const slow = () => new Promise((resolve) => { setTimeout(() => { resolve({ ok: true, status: 200, body: { cancel }, clone() { return this; }, json: async () => ({ choices: [{ message: { content: "late" } }] }), }); }, 40); }); const handleSingleModel = vi.fn(async (_body, model) => { if (model === "p/slow") return slow(); if (model === "p/judge") return ok("FINAL"); return ok(`ans-${model}`); }); await handleFusionChat({ body: { messages: [{ role: "user", content: "Q" }] }, models: ["p/a", "p/b", "p/slow"], handleSingleModel, log, judgeModel: "p/judge", tuning: { minPanel: 2, stragglerGraceMs: 1, panelHardTimeoutMs: 200 }, }); await new Promise((r) => setTimeout(r, 80)); expect(cancel).toHaveBeenCalled(); }); });