Maahir.io · Internal QA · v0.2 · re-test after backend rewrite

jobinterview.maahir.io — QA report

Re-test after the engine rewrite that landed on the morning of 15 Oct 2026. The candidate flow, exhibit system and grading surface are all new. But the submit endpoint is 500ing — every candidate is currently stuck on Q1.

Target · jobinterview.maahir.io Run · v0.2 · 15 Oct 2026 02:30 PKT Method · Browser + API + JS inspection Candidates run · 1 (Bushra) — blocked at Q1 Status · 3 critical (1 new) · 4 significant · 7 working
Critical (3) — 1 regressed this morning, 2 still broken Significant (4) — include the new 500 Working (7+) — most things still hold
What changed since v0.1 (last night, same day):

01.Scope honesty

What this re-test covered and what it didn't.

v0.2 re-test run on the morning of 15 Oct 2026, ~30 minutes after v0.1. Same scope as before (no staff creds) but with a new focus: did the engine rewrite fix any v0.1 findings, and did it introduce regressions?

  • Re-tested: public jobs API, apply endpoint, welcome page, new interview engine, /api/session/* endpoints, /done, admin redirect chain, privacy, /interview-info.
  • Not re-tested: full 8-question walk-through (blocked at Q1 by the 500). Other exhibit types (only saw the text intro). Staff side (admin / bushra / faizan) — still no creds.
  • Diff method: diffed current observed behaviour against the v0.1 findings list. Re-labelled each as FIXED REGRESSED UNCHANGED NEW.

02.Critical findings Fix before launch

One new regression, two v0.1 findings still open.

#1 · NEW/api/session/answer returns HTTP 500 on every submit

Every valid answer POST returns 500 with an empty body in ~360ms. The browser surfaces this as "We could not send your answer right now. It is saved on this device — we will retry as soon as you are back online." The Retry button does not help — re-submits return the same 500.

  • Reproduction: apply as any candidate, complete welcome + check consent, on Q1 type any text, click Submit answer. Backend returns 500.
  • Direct curl test: curl -X POST .../api/session/answer -d '{"token":"...","questionId":"-1","answer":"test"}' → 500 in 361ms, body empty. Same payload shape works on validation (400 on missing fields, 404 on bad token) — the crash is in the handler, not the route.

Why it matters: no candidate can complete the interview right now. Every applicant since the deploy is stuck on Q1. With 18 applicants and counting, that's 18 broken records in HR's queue. The offline-saved-on-this-device copy is localStorage — candidates on a different device have lost answers entirely. This is a launch-blocker; until it's fixed, the 18 fresh applicants are noise, not signal.

#2 · UNCHANGEDWelcome page leaks internal taxonomy v0.1 #3

Same as v0.1 finding #3 — confirmed on the re-test. The "What we will cover" list on /welcome still shows:

core_accounting · manual_khata · payroll_statutory · taxation_pk · excel_tools · financial_statements · judgment_ethics · personality_retention

Meanwhile the new /api/session/start response still returns the same enum array under job.topics. The frontend just hasn't been updated to map it to the friendly names from /interview-info.

Why it matters: the engine rewrite was a chance to fix this and it didn't. Still reads like a build artefact to candidates.

#3 · UNCHANGEDTime estimate is inconsistent across four pages

Same as v0.1 finding #5 — confirmed on re-test. Four different numbers on the same property:

  • Home page position card: ~25 min
  • Home page intro paragraph: 25–35 minutes
  • /interview-info: ~35 min average length
  • Welcome page (live, post-deploy): about 41 minutes (driven by job.estMinutes in the new session API)

Why it matters: 41 min is now a server-side number returned by the new /api/session/start response under job.estMinutes. If you change it, change the home page and /interview-info in the same deploy.

03.Significant findings This sprint

Marketing-vs-reality gaps, stale questions, and the v0.1 items that survived the rewrite.

#4 · UNCHANGEDHome page advertises a "chat" — interview is structured Q&A

v0.1 #4 — confirmed. The hero, the "How it works" section and the live demo transcript all describe a free-form conversation. The new engine is structured (typed questions with type: text | mcq | numerical | … and per-type expect schemas), not a chat.

Why it matters: the new exhibit system is genuinely impressive (balance sheets, khata, vouchers, cheques, trial balances) — that copy is now accurate for the first time. The remaining gap is just "a chat" → "a structured technical interview with real documents". One word swap on the home page.

#5 · UNCHANGEDProfile page asks a content-marketing question for an accountant

v0.1 #8 — confirmed. The profile form still includes "Have you written articles or content?" with hint "Two blog posts on accounting, or none". The new /api/session/start response includes a candidate.articleContext field that this question is supposed to populate. Wrong field for the role.

#6 · UNCHANGEDNo per-question feedback to the candidate v0.1 #6

Same as v0.1 #6 — confirmed. After every submit the screen jumps to the next question with no "correct/partial/wrong" signal. With the new engine, the grading happens server-side (it has to, given the question types include text, matching, ordering, excel_sim) but the result never makes it back to the candidate UI during the interview. Only HR sees it later in the report.

Why it matters: the rewrite made grading richer but didn't expose any of it to the candidate. The home page still promises "Ladders up. Answer well and the next question gets harder" — that's HR-only now.

#7 · UNCHANGEDQuestion magnitudes are enterprise-scale on an SME job

v0.1 #7 — same finding. The v0.1 balance-sheet question had 0-30d = 9.88 crore etc. The new engine is LLM-driven, so magnitude may vary per question now. Need to walk a full interview (post-#1 fix) to see if the AI has been tuned to the SME scale.

04.Fixed by the rewrite Shipped

These v0.1 findings are resolved by the morning's deploy. Don't let them regress.

F1v0.1 #1 — Adaptive engine was 2 questions in a loop FIXED

The new engine is LLM-driven. lastSource: "ai" is now in the session state. First question served is a freeform text intro on the personality_retention family. The 2-question MCQ loop is gone. Cannot confirm "well-distributed topic coverage" until #1 is unblocked.

F2v0.1 #2 — "Submit & finish" did not auto-redirect to /done LIKELY FIXED

Cannot confirm end-to-end because the interview is 500-blocked at Q1. But the new API has an explicit /api/session/complete endpoint — a likely place where the redirect would be triggered. Regression-test after #1 is fixed.

F3New: real exhibit system (balance sheet, khata, voucher, cheque) NEW

The new bundle at /_next/static/chunks/14_ur4426iv7t.js ships a full exhibit renderer with 8 types: table | balance_sheet | trial_balance | ledger | voucher | cheque | roznamcha | khata (jama/naam). Voucher has date/payee/amount/chequeNo/approvedBy. Cheque has bank/chequeNo/payTo/amount. Khata splits into separate jama (received) and naam (paid) tables. This is the fulfilment of the home-page promise that v0.1 was missing.

F4New: offline-resilient answer saving NEW

Even though the server 500s, the candidate's typed answer is preserved across the error UI (the textbox keeps the value, an alert says "It is saved on this device — we will retry as soon as you are back online"). This is the "Every answer saves the moment you submit it" promise from /interview-info, now actually implemented (just unfortunately the retry never succeeds right now).

F5New: in-app browser detection + banner NEW

The new bundle detects when the interview is running inside an in-app browser (WhatsApp, Facebook, etc.) and shows a dismissible banner: "For the best experience, open in Chrome. This interview is running inside WhatsApp — copy this link into Chrome to avoid interruptions." Matches the home-page promise: "One tap out of WhatsApp into Chrome — we detect the in-app browser and ask."

F6Application cap removed FIXED

position.limit changed from 200 to null between v0.1 and v0.2. The cap was probably a launch-period test knob. applications jumped from 3 to 18 in ~30 minutes once removed.

05.Marketing copy vs. reality

Updated for v0.2. Two big lies are now half-true.

Claimed
Observed
"A chat in English or Roman Urdu."
Still a structured Q&A, not a chat. Q1 is a freeform text field. Later questions may be MCQ, number, matching, ordering, SJT or excel_sim.
→
"Real balance sheets, khata pages, vouchers and cheques."
NOW TRUE. New exhibit system supports table, balance_sheet, trial_balance, ledger, voucher, cheque, roznamcha, khata(jama/naam). Cannot confirm candidates see them — interview is 500-blocked at Q1.
→
"AI notices when you are struggling and moves on."
Cannot verify. New engine is LLM-driven (lastSource: "ai") but interview is 500-blocked at Q1 so no signal of adaptiveness yet.
→
"Ladders up. Answer well and the next question gets harder."
Cannot verify. New questions have a difficulty field (1 for Q1) so the scaffold is there, but no candidate can answer anything to trigger a ladder right now.
→
"A warm closing page."
/done still returns 200 and looks like the same warm closing as v0.1, but cannot reach it because Q1 submit 500s.
→
"Every answer saves the moment you submit it, so a dead zone costs nothing."
NOW TRUE. The answer is preserved in the textbox and the alert says "saved on this device" even on 500. This is exactly the promised offline behaviour, accidentally stress-tested by the current outage.
→
"One tap out of WhatsApp into Chrome — we detect the in-app browser and ask."
NOW TRUE. New in-app browser detection banner with dismiss control.
→
"100% provably fair — every candidate gets different numbers."
True. Confirmed again: bucket values from v0.1 (98,803,500 / 121,049,000 / 53,672,500 / 42,222,000) and freight 301,724,000 were per-session, not per-question-bank.
✓
No login, no app, no OTP, no password.
True. Candidate flow is still genuinely frictionless.
✓

06.Staff-side QA gate Needs Munaf

The rewritten engine changed the API surface. These checks need a staff account to be useful.

Priority checks for the staff side

All admin, HR, manager, dashboard, reports and override routes still 307 to /auth/login. To grade the new engine and the new 500, I need one of the following:

  1. Reset and paste passwords for admin, bushra, faizan. I'll run them in read-only mode and screenshot the dashboards.
  2. Server logs for the past 30 minutes — what does the /api/session/answer stack trace look like? Is it the LLM provider, a DB constraint, a JSON parse? Best path to fixing #1.
  3. Send screenshots of the new admin/HR dashboards, the new candidate report, and any error monitoring (Sentry etc.) for the /api/session/answer 500.

Once I have any of the above, the priority checklist for the staff side becomes:

  1. Confirm the 500 is server-side, not rate-limit / quota / model-overload. Get the exception from logs.
  2. Does HR see the 18 stuck applicants differently from completed ones? Is there a "submit error" or "stuck on Q1" status indicator in the new report UI?
  3. What's the new grading surface look like? The new expect: { kind, tolerance, explanation } schema suggests per-question scoring, not just a single number. Where does HR see that?
  4. Can admin override the recommendation? (Still promised by privacy page.)
  5. Does the new exhibit system save snapshots? (Voucher + cheque + balance sheet — these are valuable evidence artefacts. Where are they stored?)
  6. CSV / PDF export? Audit log of staff actions (promised by privacy)?

07.What is still working Don't break

Survived the rewrite. Regression-test on every deploy.

W1Auth gates on all staff surfaces

/admin, /admin/dashboard, /hr, /dashboard, /reports, /api/admin/*, /api/staff, /api/hr all 307 to /auth/login. No staff endpoint exposed unauthenticated.

W2Rate limiting on apply

/api/public/apply still returns 429 on rapid-fire spam from the same IP. Tested as part of the re-apply that created applicant #119.

W3Privacy page is solid

Still names retention periods, mentions audit log, contact email, GDPR rights. The new engine's lastSource: "ai" field aligns with the privacy promise that "answers are processed by an AI model provider".

W4Bilingual copy (English + Roman Urdu)

All new questions I saw are bilingual: "Before we start — which university are you studying at, and which semester are you in right now? / Shuru karne se pehle — aap kaun se university se padh rahe hain aur aap kis semester mein hain?" Same warm tone across the new bundle.

W5Security headers + SEO meta

CSP, HSTS, X-Content-Type-Options, Referrer-Policy, Permissions-Policy all still present on every page checked. OG / Twitter / theme-color / keywords clean.

W6404 page for invalid interview codes is helpful

/i/{anything-not-valid} still shows a bilingual "this link is not valid" page rather than a generic 404.

W7New session API has clean validation

Empty body → 400 with {"ok":false,"error":"Missing session token"}. Bad token → 404 with {"ok":false,"error":"This interview link is not valid"}. The 500 is in the validated handler, not in route plumbing — so the failure is localised and easy to scope.

08.Methodology

What was tested, how, and the raw artefacts left behind.

  • API recon first. Hit /api/public/jobs, /api/public/apply, then traced the new chunks for the new session API. Found /api/session/{start,next-question,answer,complete} in _next/static/chunks/14_ur4426iv7t.js.
  • Session start. Called /api/session/start with the fresh interview token to get the full session state including Q1, the topic list, the candidate profile, and the job metadata. lastSource: "ai" confirmed LLM-driven questions.
  • Bundle inspection. Decoded the new candidate-UI bundle (12qo72yfjds4c.js) and the new exhibit-renderer bundle to see what question types, exhibit types and offline behaviour the rewrite shipped.
  • Submit test. Tried 4 payload shapes against /api/session/answer — all return 500 with empty body, ~360ms each. The 4 shapes were (token, questionId, answer), (token, questionId, response), (token, questionId, value), (token, questionId, text). Only the first is the documented shape; it still 500s.
  • Validation sanity. Empty body returns 400; bad token returns 404. The crash is in the validated handler, not the route plumbing.
  • Old endpoint check. /api/interview/answer now 307s to login — old endpoint removed. The new /api/session/answer is the only path.
  • Test candidate left in production. Bushra Retest, applicant #119, URL /i/GD_gA-wv6ohcaa9Q7QzYIZsDXyZz21wRvTOwJV4ZLUs. Stuck on Q1, no answer recorded. Delete via admin or leave as a real test record.
Reproduction commands (raw)
# 1. Public jobs endpoint (limit: null now, was 200 in v0.1)
curl -s https://jobinterview.maahir.io/api/public/jobs

# 2. Apply — returns applicantNo + token
curl -s -X POST https://jobinterview.maahir.io/api/public/apply \
  -H 'Content-Type: application/json' \
  -d '{"jobId":19,"firstName":"Bushra","lastName":"Retest","phone":"03001234502","email":"bushra.qa2@maahir-test.local","city":"Karachi"}'

# 3. Start session — see Q1, topics, estMinutes, lastSource
curl -s -X POST https://jobinterview.maahir.io/api/session/start \
  -H 'Content-Type: application/json' \
  -d '{"token":"<paste-token-here>"}'

# 4. Reproduce the 500 — REGRESSION
curl -s -X POST https://jobinterview.maahir.io/api/session/answer \
  -H 'Content-Type: application/json' \
  -d '{"token":"<paste-token-here>","questionId":"-1","answer":"any text here"}' \
  -w '\n===HTTP=%{http_code} TIME=%{time_total}==='
# Expected on healthy server: 200 with grading + next-question payload
# Observed: HTTP=500 TIME=0.36, body empty

# 5. Validation sanity (these should always work, regardless of #4)
curl -s -X POST https://jobinterview.maahir.io/api/session/answer \
  -H 'Content-Type: application/json' -d '{}' -w 'HTTP=%{http_code}\n'
# 400 {"ok":false,"error":"Missing session token"}

# 6. Admin gate probe (should all 307 → /auth/login)
for p in /admin /admin/dashboard /hr /dashboard /reports /api/admin/jobs; do
  curl -sI -o /dev/null -w "%{http_code} %{redirect_url}\n" https://jobinterview.maahir.io$p
done