expria-backend/src/routes/__tests__/t1live.test.ts
Hermann_Kitio 74770b6402
Some checks are pending
CI / quality (push) Waiting to run
fix(t1-live): remove questionnaire dependency from T1 Live session
- buildT1SystemPrompt() now static (no reponses param); examiner
  formulates questions from what it hears in real-time audio stream
- Remove context guard + close 4004 CONTEXT_MISSING; Gemini session
  opens immediately after auth (aligns with T2 flow)
- Remove parseT1Context, validateReponses import from route
- Unknown WS message types silently ignored (debug log + return)
- Update Prompt_t1live.md and CHANGELOG-backend
- Tests: 309/309 green
2026-06-30 02:57:17 +03:00

202 lines
6.5 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { EventEmitter } from "node:events";
// ─── Mocks ───────────────────────────────────────────────────────────────────
vi.mock("../../lib/supabase", () => ({
supabase: {
auth: {
getUser: vi.fn(),
},
from: vi.fn(),
},
}));
vi.mock("../../lib/deepseek", async () => {
const actual =
await vi.importActual<typeof import("../../lib/deepseek")>(
"../../lib/deepseek",
);
return {
...actual,
correctEO: vi.fn(),
};
});
vi.mock("../../lib/geminiPhonology", () => ({
PHONOLOGY_STUB: {
score: 2,
commentaire: "Stub",
note_phonologie: "Stub",
},
}));
import { supabase } from "../../lib/supabase";
import { correctEO as deepseekCorrectEO } from "../../lib/deepseek";
import { runT1LiveCorrection } from "../t1live";
import type { WebSocketLike } from "../../lib/geminiLive";
// ─── Helpers ─────────────────────────────────────────────────────────────────
class FakeWs extends EventEmitter implements WebSocketLike {
public sent: unknown[] = [];
public closed = false;
public closeCode?: number;
public closeReason?: string;
send(data: unknown): void {
this.sent.push(data);
}
close(code?: number, reason?: string): void {
if (this.closed) return;
this.closed = true;
this.closeCode = code;
this.closeReason = reason;
}
}
function mockProductionInsert(
resultId: string | null,
errorMsg: string | null = null,
) {
vi.mocked(supabase.from).mockReturnValueOnce({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
insert: vi.fn(() => ({
select: vi.fn(() => ({
single: vi.fn(async () =>
errorMsg
? { data: null, error: { message: errorMsg } }
: { data: { id: resultId }, error: null },
),
})),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
})) as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
}
function mockProductionUpdate(errorMsg: string | null = null) {
vi.mocked(supabase.from).mockReturnValueOnce({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
update: vi.fn(() => ({
eq: vi.fn(async () =>
errorMsg ? { error: { message: errorMsg } } : { error: null },
),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
})) as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
}
const FAKE_RAPPORT = {
score: 14,
nclc: 8,
nclc_cible: 9 as const,
revelation: { croyance: "a", realite: "b", consequence: "c" },
diagnostic: "d",
criteres: [],
conseil_nclc: { nclc_cible: "NCLC 9", ecart: "e", action_prioritaire: "p" },
erreurs_codes: [],
};
// ─── Tests ───────────────────────────────────────────────────────────────────
describe("runT1LiveCorrection", () => {
beforeEach(() => {
vi.clearAllMocks();
});
const profile = { id: "u1", plan: "premium" as const };
it("transcript vide → EMPTY_TRANSCRIPT + close 1000 sans appeler DeepSeek", async () => {
const ws = new FakeWs();
await runT1LiveCorrection({ clientWs: ws, profile, transcript: " " });
expect(deepseekCorrectEO).not.toHaveBeenCalled();
expect(ws.closed).toBe(true);
expect(ws.closeCode).toBe(1000);
const sent = JSON.parse(ws.sent[0] as string);
expect(sent).toMatchObject({ type: "error", code: "EMPTY_TRANSCRIPT" });
});
it("flux nominal : insert EO_T1 (sujet_id null) → DeepSeek → update → report → close 1000", async () => {
const ws = new FakeWs();
const insertSpy = vi.fn(() => ({
select: vi.fn(() => ({
single: vi.fn(async () => ({ data: { id: "prod-t1" }, error: null })),
})),
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(supabase.from).mockReturnValueOnce({ insert: insertSpy } as any);
vi.mocked(deepseekCorrectEO).mockResolvedValueOnce(FAKE_RAPPORT);
mockProductionUpdate();
await runT1LiveCorrection({
clientWs: ws,
profile,
transcript:
"Candidat : Je m'appelle Hermann\nExaminateur : Où vivez-vous ?",
});
// Persistance : tache EO_T1, sujet_id NULL.
expect(insertSpy).toHaveBeenCalledWith(
expect.objectContaining({
user_id: "u1",
tache: "EO_T1",
sujet_id: null,
mode: "entrainement",
}),
);
// Correction : tache EO_T1, nclcCible 9, pas de consigne.
expect(deepseekCorrectEO).toHaveBeenCalledWith(
"Candidat : Je m'appelle Hermann\nExaminateur : Où vivez-vous ?",
"EO_T1",
9,
null,
);
expect(ws.closed).toBe(true);
expect(ws.closeCode).toBe(1000);
const reportFrame = ws.sent.find(
(f) => typeof f === "string" && f.includes('"report"'),
);
expect(reportFrame).toBeDefined();
const parsed = JSON.parse(reportFrame as string);
expect(parsed.type).toBe("report");
// Score textuel 14 + phonologie stub 2 = 16.
expect(parsed.data.score).toBe(16);
expect(parsed.data.nclc).toBe(8);
expect(parsed.data.simulation_id).toBe("prod-t1");
});
it("insert production échoue → PERSISTENCE_FAILED + close 1011", async () => {
const ws = new FakeWs();
mockProductionInsert(null, "db down");
await runT1LiveCorrection({
clientWs: ws,
profile,
transcript: "Candidat : Bonjour",
});
expect(deepseekCorrectEO).not.toHaveBeenCalled();
expect(ws.closed).toBe(true);
expect(ws.closeCode).toBe(1011);
const sent = JSON.parse(ws.sent[0] as string);
expect(sent.code).toBe("PERSISTENCE_FAILED");
});
it("DeepSeek throw → CORRECTION_FAILED + close 1011", async () => {
const ws = new FakeWs();
mockProductionInsert("prod-t1");
vi.mocked(deepseekCorrectEO).mockRejectedValueOnce(new Error("timeout"));
await runT1LiveCorrection({
clientWs: ws,
profile,
transcript: "Candidat : Bonjour",
});
expect(ws.closed).toBe(true);
expect(ws.closeCode).toBe(1011);
const sent = JSON.parse(ws.sent[0] as string);
expect(sent.code).toBe("CORRECTION_FAILED");
});
});