Spaces:
Sleeping
Sleeping
File size: 7,253 Bytes
6fb7573 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | // 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=<seeded-user-uuid> node scripts/test-e2e-http.mjs
// or: node scripts/test-e2e-http.mjs <seeded-user-uuid>
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);
});
|