/** * Offline tests: AI Coach session completion schema + validation. * Run: npx tsx --tsconfig tsconfig.json scripts/verify-coach-completion.ts */ import { getSessionByOrder } from '../src/data/program/sessions'; import { buildAiCoachSessionContext } from '../src/lib/aiCoach/sessionContext'; import { COACH_SESSION_COMPLETE_CLOSE, COACH_SESSION_COMPLETE_OPEN, exampleCompletionSignal, extractSessionExerciseLabels, extractSessionHeadings, parseCoachCompletionFromAssistantText, validateCoachSessionCompletionSignal, type CoachSessionCompletionSignal } from '../src/lib/aiCoach/sessionCompletion'; import { buildCoachSystemPrompt } from '../src/lib/aiCoach/buildCoachSystemPrompt'; const USER_ID = 'verify-coach-user'; const session1 = getSessionByOrder(1); if (!session1) throw new Error('Session 1 missing'); const ctx = buildAiCoachSessionContext(session1); const prompt = buildCoachSystemPrompt(ctx); const validSignal = exampleCompletionSignal(ctx); const wrap = (signal: CoachSessionCompletionSignal, visible = 'جلسه به پایان رسید.') => `${visible}\n${COACH_SESSION_COMPLETE_OPEN}\n${JSON.stringify(signal)}\n${COACH_SESSION_COMPLETE_CLOSE}`; let passed = 0; let failed = 0; const assert = (name: string, condition: boolean) => { if (condition) { passed += 1; return; } failed += 1; console.error(`FAIL: ${name}`); }; // --- parse --- const parsed = parseCoachCompletionFromAssistantText(wrap(validSignal)); assert('parse extracts signal', parsed.signal?.sessionComplete === true); assert('parse strips marker from display', !parsed.displayContent.includes(COACH_SESSION_COMPLETE_OPEN)); assert('parse keeps visible Persian text', parsed.displayContent.includes('جلسه به پایان رسید')); // --- validate valid --- const ok = validateCoachSessionCompletionSignal(validSignal, ctx, { userId: USER_ID, userTurnCount: 3 }); assert('valid signal passes', ok.valid === true); // --- reject: single user turn --- const lowTurns = validateCoachSessionCompletionSignal(validSignal, ctx, { userId: USER_ID, userTurnCount: 1 }); assert('rejects single user turn', lowTurns.valid === false); // --- reject: session id mismatch --- const badId = validateCoachSessionCompletionSignal( { ...validSignal, sessionId: 'session-99' }, ctx, { userId: USER_ID, userTurnCount: 3 } ); assert('rejects sessionId mismatch', badId.valid === false); // --- reject: user not confirmed --- const noConfirm = validateCoachSessionCompletionSignal( { ...validSignal, userConfirmed: false }, ctx, { userId: USER_ID, userTurnCount: 3 } ); assert('rejects missing userConfirmed', noConfirm.valid === false); // --- reject: missing exercise --- const missingExercise = validateCoachSessionCompletionSignal( { ...validSignal, completedExercises: [] }, ctx, { userId: USER_ID, userTurnCount: 3 } ); assert('rejects missing completed exercises', missingExercise.valid === false); // --- reject: insufficient covered topics --- const thinTopics = validateCoachSessionCompletionSignal( { ...validSignal, coveredTopics: ['نامرتبط'] }, ctx, { userId: USER_ID, userTurnCount: 3 } ); assert('rejects insufficient coveredTopics', thinTopics.valid === false); // --- prompt includes completion instructions --- assert('prompt includes COACH_SESSION_COMPLETE marker', prompt.includes('COACH_SESSION_COMPLETE')); assert('prompt includes sessionComplete field', prompt.includes('"sessionComplete":true')); assert('prompt includes userConfirmed rule', prompt.includes('userConfirmed')); // --- session 1 has pretest exercise in labels --- const exerciseLabels = extractSessionExerciseLabels(ctx.blocks); assert('session 1 has pretest exercise label', exerciseLabels.some((l) => l.includes('پیش‌آزمون'))); const headings = extractSessionHeadings(ctx.blocks); assert('session 1 has headings', headings.length > 0); console.log(JSON.stringify({ passed, failed, exerciseLabels, headingCount: headings.length }, null, 2)); if (failed > 0) { console.error(`Verification failed: ${failed} check(s)`); process.exit(1); } console.log('Coach completion schema + validation OK');