feat(eo): align correction EO on 3.6a format + Deepgram token + T1 presentation generation
Sprint 4a:
- correctEO aligned on CorrectionRapport format (revelation, diagnostic, criteres, conseil_nclc, erreurs_codes)
- nclc_cible parameter (default 9, accepts 9|10)
- Fire-and-forget modele + exercices jobs (same pattern as EE)
- EO-specific DeepSeek prompt (oral transcript tolerance, 4 TCF criteria)
- Gemini transcribeAudio: 30s timeout + 1 retry
- POST /presentations/generate: 5-field questionnaire → DeepSeek generates oral presentation (~220-260 words, NCLC 7-8)
- Migration 006_sprint_4a_eo.sql (documentation only — no audio storage)
Sprint 4b:
- POST /transcriptions/token: Deepgram temporary API key (600s TTL)
- Removed audio storage pipeline (audioStorage.ts, XOR validation, 14MB limit)
- Backend receives transcript text only, no audio files
- TD-10/TD-11 resolved (Sprint 3.6c), TD-16/17/18 resolved (4b cleanup)
Typecheck: OK · Tests: 241/241 ✅
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f5954e6d72
commit
7cac057062
18 changed files with 2907 additions and 911 deletions
148
src/controllers/__tests__/presentationGenerate.test.ts
Normal file
148
src/controllers/__tests__/presentationGenerate.test.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
// Validation pure (pas de fetch).
|
||||
|
||||
describe("presentationController.validateReponses", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("accepte les 5 champs requis non vides", async () => {
|
||||
const { validateReponses } = await import("../presentationController");
|
||||
const result = validateReponses({
|
||||
prenom_age_ville: "Pierre, 30 ans, Alger",
|
||||
formation_metier: "Ingénieur",
|
||||
situation_familiale: "Marié, deux enfants",
|
||||
loisirs: "Lecture, randonnée",
|
||||
motivation_canada: "Opportunités professionnelles",
|
||||
});
|
||||
expect("ok" in result && result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("rejette si reponses non objet", async () => {
|
||||
const { validateReponses } = await import("../presentationController");
|
||||
const result = validateReponses("string");
|
||||
expect("error" in result).toBe(true);
|
||||
if ("error" in result) expect(result.code).toBe("VALIDATION_ERROR");
|
||||
});
|
||||
|
||||
it.each([
|
||||
"prenom_age_ville",
|
||||
"formation_metier",
|
||||
"situation_familiale",
|
||||
"loisirs",
|
||||
"motivation_canada",
|
||||
])("rejette si %s manquant", async (field) => {
|
||||
const { validateReponses } = await import("../presentationController");
|
||||
const all: Record<string, string> = {
|
||||
prenom_age_ville: "a",
|
||||
formation_metier: "b",
|
||||
situation_familiale: "c",
|
||||
loisirs: "d",
|
||||
motivation_canada: "e",
|
||||
};
|
||||
delete all[field];
|
||||
const result = validateReponses(all);
|
||||
expect("error" in result).toBe(true);
|
||||
});
|
||||
|
||||
it("rejette les champs vides ou whitespace", async () => {
|
||||
const { validateReponses } = await import("../presentationController");
|
||||
const result = validateReponses({
|
||||
prenom_age_ville: " ",
|
||||
formation_metier: "b",
|
||||
situation_familiale: "c",
|
||||
loisirs: "d",
|
||||
motivation_canada: "e",
|
||||
});
|
||||
expect("error" in result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// Pipeline complet — fetch mocké.
|
||||
|
||||
describe("presentationController.generate", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const VALID_REPONSES = {
|
||||
prenom_age_ville: "Pierre, 30 ans, Alger",
|
||||
formation_metier: "Ingénieur",
|
||||
situation_familiale: "Marié",
|
||||
loisirs: "Lecture",
|
||||
motivation_canada: "Travail",
|
||||
};
|
||||
|
||||
it("succès → renvoie { presentation: string }", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
choices: [
|
||||
{ message: { content: "Bonjour, je m'appelle Pierre. Voilà." } },
|
||||
],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const { generate } = await import("../presentationController");
|
||||
const result = await generate(VALID_REPONSES);
|
||||
|
||||
expect("data" in result).toBe(true);
|
||||
if ("data" in result) {
|
||||
expect(result.data.presentation).toContain("Pierre");
|
||||
}
|
||||
});
|
||||
|
||||
it("DeepSeek non-OK → INTERNAL_ERROR 500", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({ ok: false, status: 500, statusText: "I" }),
|
||||
);
|
||||
|
||||
const { generate } = await import("../presentationController");
|
||||
const result = await generate(VALID_REPONSES);
|
||||
|
||||
expect("error" in result).toBe(true);
|
||||
if ("error" in result) {
|
||||
expect(result.code).toBe("INTERNAL_ERROR");
|
||||
expect(result.status).toBe(500);
|
||||
}
|
||||
});
|
||||
|
||||
it("réponse vide → INTERNAL_ERROR 500", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ choices: [{ message: { content: "" } }] }),
|
||||
}),
|
||||
);
|
||||
const { generate } = await import("../presentationController");
|
||||
const result = await generate(VALID_REPONSES);
|
||||
expect("error" in result).toBe(true);
|
||||
});
|
||||
|
||||
it("fetch throw (timeout) → INTERNAL_ERROR 500", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockRejectedValue(new Error("network down")),
|
||||
);
|
||||
const { generate } = await import("../presentationController");
|
||||
const result = await generate(VALID_REPONSES);
|
||||
expect("error" in result).toBe(true);
|
||||
if ("error" in result) expect(result.code).toBe("INTERNAL_ERROR");
|
||||
});
|
||||
|
||||
it("rejette les body invalides en court-circuitant fetch", async () => {
|
||||
const fetchSpy = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
const { generate } = await import("../presentationController");
|
||||
const result = await generate({ prenom_age_ville: "" });
|
||||
expect("error" in result).toBe(true);
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue