Files
veille-reglementaire/server/aiClassifier.test.ts

167 lines
6.0 KiB
TypeScript

/**
* 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 quand les deux étapes réussissent
*/
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 = () => ({
categorie: "Autre" as const,
typeVeille: "generale" as const,
});
// ─── Tests ────────────────────────────────────────────────────────────────────
describe("classifyArticle", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("retourne le fallback (rules) si le LLM échoue à l'étape 1 (pertinence)", async () => {
mockInvokeLLM.mockRejectedValueOnce(new Error("LLM timeout"));
const result = await classifyArticle("Titre test", "Résumé test", fallbackFn);
expect(result.classifiedBy).toBe("rules");
expect(result.categorie).toBe("Autre");
expect(result.relevant).toBe(true); // on suppose pertinent par défaut
});
it("retourne le fallback (rules) si le LLM retourne un JSON malformé à l'étape 1", 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.categorie).toBe("Autre");
});
it("retourne relevant=false et categorie=Autre si l'article est hors périmètre", async () => {
mockInvokeLLM.mockResolvedValueOnce(
makeLLMResponse(JSON.stringify({ pertinent: false, 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.categorie).toBe("Autre");
expect(result.classifiedBy).toBe("ia");
expect(result.reason).toContain("politique");
});
it("classifie correctement un article pertinent en deux étapes", async () => {
// Étape 1 : pertinent = true
mockInvokeLLM.mockResolvedValueOnce(
makeLLMResponse(JSON.stringify({ pertinent: true, raison: "Concerne les EHPAD et la perte d'autonomie" }))
);
// Étape 2 : classification
mockInvokeLLM.mockResolvedValueOnce(
makeLLMResponse(
JSON.stringify({
categorie: "PA",
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.categorie).toBe("PA");
expect(result.typeVeille).toBe("reglementaire");
expect(result.classifiedBy).toBe("ia");
});
it("retourne le fallback (rules) si l'étape 2 (classification) échoue", async () => {
// Étape 1 : pertinent = true
mockInvokeLLM.mockResolvedValueOnce(
makeLLMResponse(JSON.stringify({ pertinent: true, raison: "Article pertinent" }))
);
// Étape 2 : erreur LLM
mockInvokeLLM.mockRejectedValueOnce(new Error("LLM quota exceeded"));
const result = await classifyArticle("Titre pertinent", "Résumé pertinent", fallbackFn);
expect(result.classifiedBy).toBe("rules");
expect(result.relevant).toBe(true);
expect(result.reason).toContain("Pertinent");
});
it("accepte les réponses LLM avec balises markdown ```json```", async () => {
// Étape 1 avec balises markdown
mockInvokeLLM.mockResolvedValueOnce(
makeLLMResponse("```json\n" + JSON.stringify({ pertinent: false, 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.classifiedBy).toBe("ia");
});
it("classe un article handicap correctement", async () => {
mockInvokeLLM.mockResolvedValueOnce(
makeLLMResponse(JSON.stringify({ pertinent: true, raison: "Concerne les IME et l'inclusion scolaire" }))
);
mockInvokeLLM.mockResolvedValueOnce(
makeLLMResponse(
JSON.stringify({
categorie: "Handicap",
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.categorie).toBe("Handicap");
expect(result.typeVeille).toBe("reglementaire");
expect(result.classifiedBy).toBe("ia");
});
});