Fix production TypeScript builds and add DEPLOY.md so the app can be hosted from this repo. Co-authored-by: Cursor <cursoragent@cursor.com>
116 lines
4.1 KiB
TypeScript
116 lines
4.1 KiB
TypeScript
/**
|
|
* 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');
|