188 lines
6.4 KiB
TypeScript
188 lines
6.4 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 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");
|
|
expect(result.technicalError).toBe("LLM timeout");
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|