/** * capabilityRouter.test.ts — Unit test S38 Capability Router * * Copertura (12 test): * 1. route: restituisce gemini per reasoning_depth="high" (score massimo) * 2. route: restituisce groq per speed=true (velocità 10/10) * 3. route: provider con vision=false escluso (score basso) quando req.vision=true * 4. route: provider senza tool calling penalizzato quando requires_tools=true * 5. route: prefer_free=true favorisce provider gratuiti * 6. route: long_context favorisce provider con max_context>=100k * 7. route: ritorna null se nessun provider disponibile * 8. route: ignora provider non disponibili (DI isAvailable=false) * 9. rankAll: ordina tutti i candidati per score decrescente * 10. scoreProfile: score > 0 per profilo compatibile con request * 11. scoreProfile: score molto negativo per vision req su provider senza vision * 12. getProfile / getAllProfiles: dati statici corretti * * DI pattern: _setTestDeps / _resetDeps — zero vi.mock su moduli ESM * Modulo puro: nessun side-effect, nessuna dipendenza da browser API * * SESSIONE: S38 — Capability Router * S342: aggiornato 12b per includere cerebras (8° provider, aggiunto in S325) */ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { route, rankAll, scoreProfile, getProfile, getAllProfiles, buildReason, CAPABILITY_PROFILES, _setTestDeps, _resetDeps, } from "@/lib/capabilityRouter"; // ─── Setup / Teardown ───────────────────────────────────────────────────────── beforeEach(() => { // Per default tutti i provider disponibili _setTestDeps({ isAvailable: () => true }); }); afterEach(() => { _resetDeps(); }); // ─── route ──────────────────────────────────────────────────────────────────── describe("route", () => { it("1. reasoning_depth=high → gemini o openai (reasoning_score >= 9)", () => { const result = route({ reasoning_depth: "high" }); expect(result).not.toBeNull(); expect(result!.profile.reasoning_score).toBeGreaterThanOrEqual(9); expect(["gemini", "openai"]).toContain(result!.provider); }); it("2. speed=true → groq (speed_score 10/10)", () => { const result = route({ speed: true }); expect(result).not.toBeNull(); expect(result!.provider).toBe("groq"); expect(result!.profile.speed_score).toBe(10); }); it("3. vision=true → provider con vision:true (gemini, openai, openrouter)", () => { const result = route({ vision: true }); expect(result).not.toBeNull(); expect(result!.profile.vision).toBe(true); expect(["gemini", "openai", "openrouter"]).toContain(result!.provider); }); it("4. requires_tools=true → provider con tools:true", () => { const result = route({ requires_tools: true }); expect(result).not.toBeNull(); expect(result!.profile.tools).toBe(true); }); it("5. prefer_free=true → provider con free:true", () => { const result = route({ prefer_free: true }); expect(result).not.toBeNull(); expect(result!.profile.free).toBe(true); }); it("6. long_context=true → provider con max_context >= 100k", () => { const result = route({ long_context: true }); expect(result).not.toBeNull(); expect(result!.profile.max_context).toBeGreaterThanOrEqual(100_000); }); it("7. nessun provider disponibile → null", () => { _setTestDeps({ isAvailable: () => false }); const result = route({ speed: true }); expect(result).toBeNull(); }); it("8. DI isAvailable filtra i provider — solo groq disponibile → route = groq", () => { _setTestDeps({ isAvailable: (id) => id === "groq" }); const result = route({ reasoning_depth: "high" }); expect(result).not.toBeNull(); expect(result!.provider).toBe("groq"); }); }); // ─── rankAll ───────────────────────────────────────────────────────────────── describe("rankAll", () => { it("9. ordina tutti i candidati per score decrescente", () => { const ranked = rankAll({ speed: true }); expect(ranked.length).toBeGreaterThan(1); for (let i = 0; i < ranked.length - 1; i++) { expect(ranked[i].score).toBeGreaterThanOrEqual(ranked[i + 1].score); } }); }); // ─── scoreProfile ───────────────────────────────────────────────────────────── describe("scoreProfile", () => { it("10. score > 0 per profilo compatibile con request", () => { const gemini = CAPABILITY_PROFILES["gemini"]; const score = scoreProfile(gemini, { reasoning_depth: "high", code: true }); expect(score).toBeGreaterThan(0); }); it("11. score molto negativo per vision req su provider senza vision (groq)", () => { const groq = CAPABILITY_PROFILES["groq"]; const score = scoreProfile(groq, { vision: true }); expect(score).toBeLessThan(-50); }); it("11b. score molto negativo per requires_tools su provider senza tools (browser-llm)", () => { const browserLlm = CAPABILITY_PROFILES["browser-llm"]; const score = scoreProfile(browserLlm, { requires_tools: true }); expect(score).toBeLessThan(-50); }); }); // ─── getProfile / getAllProfiles ─────────────────────────────────────────────── describe("getProfile / getAllProfiles", () => { it("12. getProfile ritorna profilo valido per ogni ProviderId", () => { // S342: incluso cerebras (aggiunto in S325 — 2000+ tok/s, fast-chat senza tools) const ids = ["gemini", "openai", "groq", "openrouter", "huggingface", "together", "browser-llm", "cerebras", "sambanova"] as const; for (const id of ids) { const p = getProfile(id); expect(p.id).toBe(id); expect(p.reasoning_score).toBeGreaterThanOrEqual(0); expect(p.reasoning_score).toBeLessThanOrEqual(10); expect(p.speed_score).toBeGreaterThanOrEqual(0); expect(p.speed_score).toBeLessThanOrEqual(10); expect(p.code_score).toBeGreaterThanOrEqual(0); expect(p.code_score).toBeLessThanOrEqual(10); expect(p.max_context).toBeGreaterThan(0); expect(typeof p.vision).toBe("boolean"); expect(typeof p.free).toBe("boolean"); expect(typeof p.tools).toBe("boolean"); } }); it("12b. getAllProfiles ritorna 9 profili (uno per provider, incluso cerebras S325 + sambanova S422)", () => { const profiles = getAllProfiles(); // S342: 9 provider = gemini, openai, groq, openrouter, huggingface, together, browser-llm, cerebras, sambanova (S422/S502-FIX) expect(profiles).toHaveLength(9); const ids = profiles.map(p => p.id); const unique = new Set(ids); expect(unique.size).toBe(8); // Invariante: cerebras deve essere presente (aggiunto S325) expect(ids).toContain("cerebras"); // Invariante: sambanova deve essere presente (aggiunto S422, inserito in ALL_PROVIDERS in S502-FIX) expect(ids).toContain("sambanova"); }); }); // ─── buildReason ────────────────────────────────────────────────────────────── describe("buildReason", () => { it("buildReason include dettagli rilevanti per il request", () => { const gemini = CAPABILITY_PROFILES["gemini"]; const reason = buildReason(gemini, { reasoning_depth: "high", vision: true }); expect(reason).toContain("gemini"); expect(reason).toContain("reasoning"); expect(reason).toContain("vision"); }); it("buildReason funziona anche con request vuoto", () => { const groq = CAPABILITY_PROFILES["groq"]; const reason = buildReason(groq, {}); expect(typeof reason).toBe("string"); expect(reason.length).toBeGreaterThan(0); }); });