SmartfolioBackend / scripts /test-e2e-http.mjs
ibrahimgulbutt's picture
deploy: scraper pipeline (Postgres storage, ranking, expiry, tracker, hardened LLM plumbing)
6fb7573
Raw History Blame Contribute Delete
7.25 kB
// 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);
});