File size: 8,232 Bytes
cc11e77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/**
 * 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);
  });
});