Some checks are pending
CI / quality (push) Waiting to run
- 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
202 lines
6.5 KiB
TypeScript
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");
|
|
});
|
|
});
|