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);
});