pediatric-ai-scribe-v3/e2e/tests/ai-endpoints-contract.spec.js
Daniel 8e1ab2fea3 test(e2e): +120 tests across 8 new specs; baseline fixes
8 new spec files covering sections previously only smoke-tested:
- ai-endpoints-contract.spec.js  — hits 8 real AI endpoints via request
  context and fails if the response leaks TypeError / ReferenceError /
  'Cannot read properties of undefined' / 'is not defined' / 'is not a
  function'. This is the class of bug that shipped the PE-narrative
  regression to prod because every page-level mock prevented the real
  handler from running.
- encounter-workflow.spec.js — generate HPI, refine, clear transcript.
- encounter-save-load.spec.js — save draft, load popover, repopulate.
- wellvisit-workflow.spec.js — byvisit, milestones, SSHADESS (12+
  reveal), visit note.
- vaxschedule-content.spec.js — schedule + catch-up panels populate
  beyond "Loading".
- chart-review-workflow.spec.js — generate + load popover.
- learning-tab.spec.js — search filter, category pills, feed.
- settings-faq-dictation.spec.js — voice/password/2FA/Nextcloud
  sections, FAQ expand/collapse, dictation generate flow.

Baseline fixes:
- Added CORS_ORIGINS + API_RATE_LIMIT_MAX env overrides so the e2e
  container accepts the browser's Origin header and can absorb the
  full suite's API traffic without tripping the 200/min guard.
- Server's /api/ rate limit is now configurable via
  API_RATE_LIMIT_MAX (default stays 200).
- extensions-crud: replaced native page.on('dialog') listeners with
  #confirm-modal-ok clicks (we moved off native confirm()).
- pe-guide-smoke + extensions-crud: mobile viewport opens the hamburger
  before clicking sidebar tabs.
- fixtures.js: /api/refine mock uses 'refined' (real API shape), not
  'content'. /api/chart-review replaced with /api/generate-chart-review.

Suite: 230 passed / 0 failed in 4m36s.
2026-04-23 18:58:38 +02:00

114 lines
4.4 KiB
JavaScript

// ============================================================
// AI ENDPOINT CONTRACT TESTS — hit the REAL server handlers
// ============================================================
// The UI smoke tests mock AI responses via page.route() so they never
// exercise the server-side handler. That meant a route whose require()
// statement was wrong (undefined PROMPTS → 500) shipped to prod without
// any test failing. This spec calls each AI generation endpoint
// through the Playwright request fixture (bypasses page.route) with a
// minimal valid payload and only checks the server didn't crash with a
// ReferenceError / TypeError. A mock model is installed on the server
// side via MOCK_AI=1 env var (if configured) so we don't spend API
// credits; otherwise the server still processes the request and returns
// a structured error (which is fine — we're guarding against 500s from
// bad imports, not end-to-end AI generation).
//
// A non-500 response (200 OK or 4xx with structured JSON) means the
// handler at least ran — that's the contract we're verifying.
// ============================================================
const { test, expect, E2E_BASE, getAuthToken } = require('../fixtures');
test.describe('AI endpoint contracts — handler loads + accepts POST', () => {
let token;
test.beforeAll(async ({ request }) => {
token = await getAuthToken(request);
});
// Each entry: path, minimal body that should make the handler run past
// its import statements. We don't need a valid AI key — a 500 from
// a require() bug will still fail, but a 4xx from "missing API key"
// is acceptable because it proves the route loaded.
const endpoints = [
{
path: '/api/generate-pe-narrative',
body: {
steps: [{ component: 'Inspection', label: 'General', method: 'Observed', status: 'normal' }],
ageGroup: 'School-Age (6-11 yr)',
system: 'neuro',
patientAge: '8 years',
patientGender: 'male',
format: 'narrative',
},
},
{
path: '/api/generate-milestone-narrative',
body: {
milestones: [{ domain: 'Motor', label: 'Walks', status: 'achieved' }],
ageGroup: '12 months',
patientAge: '12 months',
patientGender: 'male',
},
},
{
path: '/api/generate-hpi-encounter',
body: { transcript: 'Patient with cough for 3 days.', setting: 'outpatient' },
},
{
path: '/api/sick-visit/note',
body: { chiefComplaint: 'Cough', transcript: 'Cough x 3 days.' },
},
{
path: '/api/generate-soap',
body: { transcript: 'Patient with cough x 3 days.' },
},
{
path: '/api/generate-chart-review',
body: { pmh: 'None', outpatientVisits: [], edVisits: [], subspecialtyVisits: [], labs: [] },
},
{
path: '/api/refine',
body: { currentDocument: 'MOCK doc', instructions: 'Add severity.' },
},
{
path: '/api/well-visit/shadess',
body: { answers: { home: 'lives with parents' } },
},
];
// Signatures of runtime bugs that mean the handler crashed before it
// could reach its try/catch (i.e. the exact class of bug being guarded).
const CRASH_SIGNATURES = [
/Cannot read properties of undefined/i,
/is not a function/i,
/is not defined/i,
/ReferenceError/i,
/TypeError/i,
];
for (const { path, body } of endpoints) {
test(`${path} — handler loads, returns structured JSON, no import-bug crash`, async ({ request }) => {
const r = await request.post(E2E_BASE + path, {
headers: { Authorization: 'Bearer ' + token },
data: body,
});
// Must always be JSON — a 500 HTML page means the express error handler
// caught an unhandled exception (our bug class).
let json = null;
try { json = await r.json(); } catch (_) { /* remains null */ }
expect(json, `${path} did not return JSON (HTTP ${r.status()})`).toBeTruthy();
// If the response leaked a JS error message through to the client,
// that's the import/destructure-bug signature we want to catch.
const errText = (json && (json.error || json.message)) || '';
for (const sig of CRASH_SIGNATURES) {
expect(errText, `${path} leaked a runtime error: ${errText}`).not.toMatch(sig);
}
// Errors should be plain strings — never raw error objects.
if (json && json.success === false) {
expect(typeof json.error).toBe('string');
}
});
}
});