/** * Offline verification: end-of-course flow (Session 11 → training done → post → finished). * Run: npx tsx --tsconfig tsconfig.json scripts/verify-end-to-end-course-flow.ts * * Does NOT call AI. Uses programService + journey permission rules + storage helpers. */ import { PROGRAM_SESSION_COUNT } from '../src/data/program/sessions'; import { completeSession, getProgramProgress, isProgramCompleted, resetProgramProgress } from '../src/services/programService'; import { getLatestByPhase, isPhaseCompleted, saveAssessment, type AssessmentSubmission } from '../src/services/assessmentService'; import { PCQ24_QUESTIONS } from '../src/config/pcq24'; // Minimal localStorage for Node const store = new Map(); (globalThis as unknown as { localStorage: Storage }).localStorage = { getItem: (k: string) => store.get(k) ?? null, setItem: (k: string, v: string) => { store.set(k, v); }, removeItem: (k: string) => { store.delete(k); }, clear: () => store.clear(), key: () => null, get length() { return store.size; } }; type LearningProgress = { user_id: string; current_step: string; pre_test_completed_at: string | null; training_started_at: string | null; training_completed_at: string | null; post_test_completed_at: string | null; }; const journeyKey = (userId: string) => `learning-progress:${userId}`; const readJourney = (userId: string): LearningProgress => { const raw = localStorage.getItem(journeyKey(userId)); if (!raw) { return { user_id: userId, current_step: 'registered', pre_test_completed_at: null, training_started_at: null, training_completed_at: null, post_test_completed_at: null }; } return JSON.parse(raw) as LearningProgress; }; const writeJourney = (progress: LearningProgress) => { localStorage.setItem(journeyKey(progress.user_id), JSON.stringify(progress)); }; /** Mirrors useLearningJourney permissions */ const permissionsOf = (progress: LearningProgress) => ({ canTakePreTest: !progress.pre_test_completed_at, canOpenTraining: Boolean(progress.pre_test_completed_at), canTakePostTest: Boolean( progress.pre_test_completed_at && progress.training_completed_at && !progress.post_test_completed_at ), isCompleted: Boolean(progress.post_test_completed_at) }); /** Mirrors HomeRedirect */ const resolveRoot = (progress: LearningProgress): string => { if (!progress.pre_test_completed_at) return '/assessment/pre'; if (!progress.training_completed_at) return '/chat'; if (!progress.post_test_completed_at) return '/assessment/post'; return '/finished'; }; /** Mirrors ChatPage Session 11 completion side-effects (without React) */ const applyFinalSessionCompletion = (userId: string, sessionOrder: number) => { const updated = completeSession(userId, sessionOrder); const isFinal = sessionOrder >= PROGRAM_SESSION_COUNT || Boolean(updated.completedAt); if (isFinal) { const now = new Date().toISOString(); const base = readJourney(userId); if (!base.training_completed_at) { writeJourney({ ...base, training_started_at: base.training_started_at || now, training_completed_at: now }); } } return { updated, isFinal }; }; let passed = 0; let failed = 0; const assert = (name: string, condition: boolean) => { if (condition) { passed += 1; console.log(`PASS: ${name}`); return; } failed += 1; console.error(`FAIL: ${name}`); }; const USER = 'e2e-flow-user'; store.clear(); resetProgramProgress(USER); writeJourney({ user_id: USER, current_step: 'pre_test_done', pre_test_completed_at: new Date().toISOString(), training_started_at: new Date().toISOString(), training_completed_at: null, post_test_completed_at: null }); // --- Root routing before training complete --- assert('root → /chat when training incomplete', resolveRoot(readJourney(USER)) === '/chat'); assert( 'post locked before training_completed_at', permissionsOf(readJourney(USER)).canTakePostTest === false ); // --- Complete sessions 1..10 (sequential), then 11 --- for (let order = 1; order <= 10; order++) { completeSession(USER, order); } assert('after 10 sessions program not complete', isProgramCompleted(USER) === false); assert( 'training still incomplete after 10', readJourney(USER).training_completed_at === null ); const final = applyFinalSessionCompletion(USER, 11); assert('session 11 marks program completedAt', Boolean(final.updated.completedAt)); assert('session 11 isFinal', final.isFinal === true); assert('training_completed_at set after session 11', Boolean(readJourney(USER).training_completed_at)); assert( 'post unlocked after training_completed_at', permissionsOf(readJourney(USER)).canTakePostTest === true ); assert('root → /assessment/post after training', resolveRoot(readJourney(USER)) === '/assessment/post'); assert( 'all 11 sessions listed completed', getProgramProgress(USER).completedSessions.length === PROGRAM_SESSION_COUNT ); // --- Refresh persistence (re-read storage) --- const journeyAfterReload = readJourney(USER); const programAfterReload = getProgramProgress(USER); assert('persist training_completed_at', Boolean(journeyAfterReload.training_completed_at)); assert('persist program completedAt', Boolean(programAfterReload.completedAt)); assert( 'persist post unlock', permissionsOf(journeyAfterReload).canTakePostTest === true ); // --- Post-test submission (scoring storage) + journey mark --- const answers = PCQ24_QUESTIONS.map((q) => ({ questionId: q.id, selectedValue: '4' })); await saveAssessment({ userId: USER, phase: 'post', answers }); assert('post assessment stored', isPhaseCompleted(USER, 'post') === true); const now = new Date().toISOString(); writeJourney({ ...readJourney(USER), current_step: 'completed', post_test_completed_at: now }); assert('root → /finished after post', resolveRoot(readJourney(USER)) === '/finished'); assert('permissions.isCompleted', permissionsOf(readJourney(USER)).isCompleted === true); assert('canTakePostTest false after post', permissionsOf(readJourney(USER)).canTakePostTest === false); // --- Duplicate post guard (existing storage + journey) --- const beforeDup = getLatestByPhase(USER, 'post') as AssessmentSubmission; assert('existing post submission present', beforeDup !== null); assert( 'duplicate blocked by journey permission', permissionsOf(readJourney(USER)).canTakePostTest === false ); assert( 'duplicate blocked by isPhaseCompleted', isPhaseCompleted(USER, 'post') === true ); // --- Finished refresh --- assert('finished still available after reload', resolveRoot(readJourney(USER)) === '/finished'); console.log(`\n${passed} passed, ${failed} failed`); if (failed > 0) process.exit(1);