Files
pwa/scripts/verify-end-to-end-course-flow.ts
T
ar.badieiandCursor 370ac15471 Ship Novya PWA with backend for deployable production use.
Fix production TypeScript builds and add DEPLOY.md so the app can be hosted from this repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 21:01:52 +03:30

208 lines
6.7 KiB
TypeScript

/**
* 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<string, string>();
(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);