// Full HTTP E2E against the running backend (port from .env) + docker Postgres. // Covers Phase 1 (tiers) + Phase 2 (resume tailoring, adaptive follow-ups, model // answers) + Phase 4.4 (seniority/focus knobs). Forges a JWT for the seeded user. // Usage: E2E_USER_ID= node scripts/test-e2e-http.mjs // or: node scripts/test-e2e-http.mjs import 'dotenv/config'; import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const jwt = require('jsonwebtoken'); const PORT = (process.env.PORT || '3000').trim(); const BASE = `http://localhost:${PORT}`; const userId = (process.env.E2E_USER_ID || process.argv[2] || '').trim(); if (!userId) { console.error('Missing seeded user id: set E2E_USER_ID or pass it as the first argument.'); process.exit(1); } const token = jwt.sign( { sub: userId, email: 'e2e-test@smartfolio.local' }, process.env.JWT_ACCESS_SECRET, { expiresIn: '1h' }, ); const H = { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }; const JOB = `We are hiring a Backend Engineer with strong Node.js, TypeScript and PostgreSQL experience. You will design scalable REST services and own database schema decisions.`; (async () => { console.log(`Server: ${BASE} | user: ${userId}\n`); // 1) GENERATE (standard tier + seniority + focus + resume tailoring) process.stdout.write('[POST /generate standard +knobs +resume] … '); let res = await fetch(`${BASE}/mock-interview/generate`, { method: 'POST', headers: H, body: JSON.stringify({ jobDescription: JOB, lengthTier: 'standard', seniority: 'senior', focusAreas: ['Node.js', 'PostgreSQL'], useResume: true, }), }); assert.equal(res.status, 200, `generate status ${res.status}`); const gen = await res.json(); assert.equal(gen.lengthTier, 'standard'); assert.equal(gen.questions.length, 10, `expected 10, got ${gen.questions.length}`); const mcq = gen.questions.find((q) => q.type === 'mcq'); assert.ok(mcq?.options?.length === 4, 'MCQ must expose 4 options'); for (const q of gen.questions) { assert.equal(q.correctIndex, undefined, `correctIndex leaked on q${q.id}`); assert.equal(q.evaluationCriteria, undefined, `evaluationCriteria leaked on q${q.id}`); } console.log(`OK — 10 Q, resume+knobs accepted, no keys leaked`); // 2) ADAPTIVE FOLLOW-UP on a behavioral question with a vague answer process.stdout.write('[POST /follow-up (vague behavioral answer)] … '); const behavioral = gen.questions.find((q) => q.type === 'behavioral'); res = await fetch(`${BASE}/mock-interview/follow-up`, { method: 'POST', headers: H, body: JSON.stringify({ sessionId: gen.sessionId, questionId: behavioral.id, answer: 'I worked on some stuff and it went fine.' }), }); assert.equal(res.status, 200, `follow-up status ${res.status}`); const fu = await res.json(); assert.ok(fu.followUp === null || (typeof fu.followUp === 'string' && fu.followUp.length > 0), 'followUp must be string|null'); const followUpAnswers = fu.followUp ? [{ parentQuestionId: behavioral.id, answer: 'Specifically, I migrated a monolith to microservices, cutting p95 latency by 40%.' }] : []; console.log(fu.followUp ? `OK — got follow-up: "${fu.followUp.slice(0, 60)}…"` : 'OK — model chose no follow-up (valid)'); // follow-up must not be asked on MCQ types process.stdout.write('[POST /follow-up on MCQ -> expect null] … '); res = await fetch(`${BASE}/mock-interview/follow-up`, { method: 'POST', headers: H, body: JSON.stringify({ sessionId: gen.sessionId, questionId: mcq.id, answer: 'A' }), }); const fuMcq = await res.json(); assert.equal(fuMcq.followUp, null, 'MCQ must never get a follow-up'); console.log('OK — null'); // 3) SUBMIT with follow-up answers -> evaluation + model answers process.stdout.write('[POST /submit +followUpAnswers] … '); const answers = gen.questions.map((q) => ({ questionId: q.id, answer: q.type === 'mcq' ? 0 : 'I approached it methodically, weighed trade-offs, acted, and measured the outcome.', })); res = await fetch(`${BASE}/mock-interview/submit`, { method: 'POST', headers: H, body: JSON.stringify({ sessionId: gen.sessionId, answers, followUpAnswers }), }); assert.equal(res.status, 200, `submit status ${res.status}`); const sub = await res.json(); assert.equal(sub.evaluation.perQuestion.length, 10); const withIdeal = sub.evaluation.perQuestion.filter((p) => typeof p.idealAnswer === 'string' && p.idealAnswer.length > 0).length; assert.ok(withIdeal >= 8, `expected most perQuestion to carry idealAnswer, got ${withIdeal}/10`); // Phase 4.2 — delivery/communication scoring const d = sub.evaluation.delivery; assert.ok(d, 'delivery block present'); assert.equal(typeof d.clarity, 'number'); assert.equal(typeof d.fillerWords, 'number'); console.log(`OK — overall ${sub.evaluation.overallScore}/100, model answers on ${withIdeal}/10, delivery {clarity:${d.clarity}, filler:${d.fillerWords}}`); // 4) GET detail returns tier + followUps process.stdout.write('[GET /:id] … '); res = await fetch(`${BASE}/mock-interview/${gen.sessionId}`, { headers: H }); const detail = await res.json(); assert.equal(detail.lengthTier, 'standard'); assert.ok(Array.isArray(detail.followUps)); console.log(`OK — tier=${detail.lengthTier}, followUps=${detail.followUps.length}, submitted=${!!detail.submittedAt}`); // 4b) GET progress aggregates the submitted session process.stdout.write('[GET /progress] … '); res = await fetch(`${BASE}/mock-interview/progress`, { headers: H }); assert.equal(res.status, 200, `progress status ${res.status}`); const prog = await res.json(); assert.ok(prog.summary.attempts >= 1, 'expected >=1 attempt'); assert.ok(prog.points.length >= 1, 'expected >=1 point'); assert.equal(typeof prog.summary.latest, 'number'); assert.ok(prog.points[prog.points.length - 1].roundScores, 'points carry roundScores'); console.log(`OK — attempts=${prog.summary.attempts}, latest=${prog.summary.latest}, best=${prog.summary.best}`); // 4c) TTS proxy — 200 if Kokoro is installed on the Python service, else a graceful 503 process.stdout.write('[POST /tts] … '); res = await fetch(`${BASE}/mock-interview/tts`, { method: 'POST', headers: H, body: JSON.stringify({ text: 'Tell me about yourself.' }), }); assert.ok(res.status === 200 || res.status === 503, `tts status ${res.status} (want 200 or 503)`); console.log(res.status === 200 ? `OK — neural audio (${res.headers.get('content-type')})` : 'OK — 503 (Kokoro not installed; client falls back to browser TTS)'); // 5) Double submit rejected process.stdout.write('[POST /submit again -> 400] … '); res = await fetch(`${BASE}/mock-interview/submit`, { method: 'POST', headers: H, body: JSON.stringify({ sessionId: gen.sessionId, answers }), }); assert.equal(res.status, 400); console.log('OK — rejected'); console.log('\nFull HTTP E2E (Phase 1 + 2 + 4.4) passed ✅'); })().catch((e) => { console.error('\n❌ E2E failed:', e?.message || e); process.exit(1); });