/** * Tests Vitest pour server/aiClassifier.ts * * Ces tests vérifient le comportement du module de classification IA : * - Fallback sur les règles en cas d'erreur LLM * - Résultat "hors périmètre" quand pertinent = false * - Classification correcte en une seule étape (pertinence + type de veille) */ import { describe, it, expect, vi, beforeEach } from "vitest"; // ─── Mock du module LLM ─────────────────────────────────────────────────────── vi.mock("./_core/llm", () => ({ invokeLLM: vi.fn(), })); import { invokeLLM } from "./_core/llm"; import { classifyArticle } from "./aiClassifier"; const mockInvokeLLM = vi.mocked(invokeLLM); // ─── Helpers ────────────────────────────────────────────────────────────────── function makeLLMResponse(content: string) { return { id: "test", created: Date.now(), model: "test", choices: [ { index: 0, message: { role: "assistant" as const, content }, finish_reason: "stop", }, ], }; } const fallbackFn = () => ({ typeVeille: "informationnelle" as const, }); // ─── Tests ──────────────────────────────────────────────────────────────────── describe("classifyArticle", () => { beforeEach(() => { vi.clearAllMocks(); }); it("retourne le fallback (rules) si le LLM échoue", async () => { mockInvokeLLM.mockRejectedValueOnce(new Error("LLM timeout")); const result = await classifyArticle("Titre test", "Résumé test", fallbackFn); expect(result.classifiedBy).toBe("rules"); expect(result.typeVeille).toBe("informationnelle"); expect(result.relevant).toBe(true); // on suppose pertinent par défaut expect(result.reason).toContain("règles"); }); it("retourne le fallback (rules) si le LLM retourne un JSON malformé", async () => { mockInvokeLLM.mockResolvedValueOnce(makeLLMResponse("pas du JSON valide ici")); const result = await classifyArticle("Titre test", "Résumé test", fallbackFn); expect(result.classifiedBy).toBe("rules"); expect(result.typeVeille).toBe("informationnelle"); }); it("retourne relevant=false et typeVeille=null si l'article est hors périmètre", async () => { mockInvokeLLM.mockResolvedValueOnce( makeLLMResponse(JSON.stringify({ pertinent: false, typeVeille: null, raison: "Actualité politique générale sans lien médico-social", })) ); const result = await classifyArticle( "Résultats des élections législatives", "Les partis politiques ont obtenu les résultats suivants...", fallbackFn ); expect(result.relevant).toBe(false); expect(result.typeVeille).toBeNull(); expect(result.classifiedBy).toBe("ia"); expect(result.reason).toContain("politique"); }); it("classifie correctement un article pertinent réglementaire en une seule étape", async () => { mockInvokeLLM.mockResolvedValueOnce( makeLLMResponse(JSON.stringify({ pertinent: true, typeVeille: "reglementaire", raison: "Décret relatif au financement des EHPAD", })) ); const result = await classifyArticle( "Nouveau décret sur le financement des EHPAD", "Le gouvernement publie un décret encadrant les tarifs des établissements pour personnes âgées dépendantes.", fallbackFn ); expect(result.relevant).toBe(true); expect(result.typeVeille).toBe("reglementaire"); expect(result.classifiedBy).toBe("ia"); }); it("accepte les réponses LLM avec balises markdown ```json```", async () => { mockInvokeLLM.mockResolvedValueOnce( makeLLMResponse("```json\n" + JSON.stringify({ pertinent: false, typeVeille: null, raison: "Hors périmètre", }) + "\n```") ); const result = await classifyArticle("Titre sport", "Résultats du championnat de football", fallbackFn); expect(result.relevant).toBe(false); expect(result.typeVeille).toBeNull(); expect(result.classifiedBy).toBe("ia"); }); it("classe un article handicap comme réglementaire", async () => { mockInvokeLLM.mockResolvedValueOnce( makeLLMResponse(JSON.stringify({ pertinent: true, typeVeille: "reglementaire", raison: "Circulaire sur l'inclusion scolaire des enfants en situation de handicap", })) ); const result = await classifyArticle( "Circulaire sur l'inclusion scolaire des enfants handicapés", "Le ministère publie une circulaire relative à l'accompagnement des élèves en situation de handicap dans les IME.", fallbackFn ); expect(result.relevant).toBe(true); expect(result.typeVeille).toBe("reglementaire"); expect(result.classifiedBy).toBe("ia"); }); it("classe un article technologique correctement", async () => { mockInvokeLLM.mockResolvedValueOnce( makeLLMResponse(JSON.stringify({ pertinent: true, typeVeille: "technologique", raison: "Déploiement d'un nouveau logiciel de gestion pour les ESMS", })) ); const result = await classifyArticle( "Nouveau logiciel de gestion pour les établissements médico-sociaux", "Un éditeur lance une solution numérique dédiée à la gestion des ESMS.", fallbackFn ); expect(result.relevant).toBe(true); expect(result.typeVeille).toBe("technologique"); expect(result.classifiedBy).toBe("ia"); }); it("utilise informationnelle comme typeVeille par défaut si pertinent mais typeVeille null", async () => { mockInvokeLLM.mockResolvedValueOnce( makeLLMResponse(JSON.stringify({ pertinent: true, typeVeille: null, raison: "Article pertinent mais type non déterminé", })) ); const result = await classifyArticle( "Rapport annuel sur le secteur médico-social", "Publication du rapport annuel de la CNSA sur l'état du secteur.", fallbackFn ); expect(result.relevant).toBe(true); expect(result.typeVeille).toBe("informationnelle"); // fallback sur informationnelle expect(result.classifiedBy).toBe("ia"); }); });