/** * Candidates table mapper — score, stage, recruiter, rejected. * * node candidates-table.test.mjs */ import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { pathToFileURL } from 'node:url' import esbuild from 'esbuild' const outDir = mkdtempSync(join(tmpdir(), 'tf-cand-')) const outFile = join(outDir, 'candidates.mjs') await esbuild.build({ entryPoints: ['src/api/candidates.js'], outfile: outFile, bundle: true, format: 'esm', platform: 'node', target: 'node20', logLevel: 'error', define: { 'import.meta.env': JSON.stringify({ VITE_API_BASE: '' }) }, }) const api = await import(pathToFileURL(outFile).href) const { toApplicationListView } = api let failed = 0 function ok(name, cond, extra) { if (cond) { console.log(`ok ${name}`) if (extra) console.log(` ${extra}`) } else { failed += 1 console.log(`FAIL ${name}`) if (extra) console.log(` ${extra}`) } } const scored = toApplicationListView({ inbox_id: 41, user_id: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', name: 'Ada Lovelace', email: 'ada@example.com', job_title: 'Backend Engineer', recruiter: 'Sam Recruiter', application_status: 'PENDING', ai_score: 88, recommendation: 'Strong Match', created_at: '2026-09-03T10:00:00Z', source: 'Email', is_active: true, }) ok('row is application-keyed', scored.id === 'inbox:41', `id=${scored.id}`) ok('keeps userId for profile navigation', scored.userId === 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') ok('job title comes through', scored.jobTitle === 'Backend Engineer') ok('ATS score is numeric', scored.aiScore === 88) ok('band is Strong Match', scored.recommendation === 'Strong Match') ok('PENDING maps to Shortlist', scored.stage === 'Shortlist', `stage=${scored.stage}`) ok('recruiter name is kept', scored.recruiter === 'Sam Recruiter') const rejected = toApplicationListView({ inbox_id: 42, user_id: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', name: 'Rejected Candidate', email: 'r@example.com', application_status: 'REJECTED', ai_score: 40, created_at: '2026-09-01T10:00:00Z', }) ok('REJECTED maps to Rejected stage', rejected.stage === 'Rejected', `stage=${rejected.stage}`) ok('CLOSED also reads as Rejected', toApplicationListView({ inbox_id: 43, application_status: 'CLOSED', name: 'Closed', }).stage === 'Rejected') const hired = toApplicationListView({ inbox_id: 44, application_status: 'HIRED', name: 'Hired Person', ai_score: 91, }) ok('HIRED maps to Hired', hired.stage === 'Hired') const unscored = toApplicationListView({ manual_upload_candidate_id: 'cccccccc-cccc-cccc-cccc-cccccccccccc', user_id: 'dddddddd-dddd-dddd-dddd-dddddddddddd', name: 'No Score Yet', email: 'ns@example.com', assigned_job_post: { title: 'Brand Manager' }, recruiter: null, application_status: 'SCREENING', }) ok('manual row key', unscored.id === 'manual:cccccccc-cccc-cccc-cccc-cccccccccccc') ok('job falls back to assigned post title', unscored.jobTitle === 'Brand Manager') ok('missing score stays null, not 0', unscored.aiScore === null) ok('unscored has no invented band', unscored.recommendation === null) ok('SCREENING maps to Screening', unscored.stage === 'Screening') ok('missing recruiter is null so the table can say Unassigned', unscored.recruiter === null) const weak = toApplicationListView({ inbox_id: 45, name: 'Weak', ai_score: 50, }) ok('score without band derives Weak Match', weak.recommendation === 'Weak Match') const potential = toApplicationListView({ inbox_id: 46, name: 'Mid', ai_score: 70, }) ok('65–81 derives Potential Match', potential.recommendation === 'Potential Match') const enumStatus = toApplicationListView({ inbox_id: 47, name: 'Enum', application_status: { value: 'OFFER' }, }) ok('enum-shaped status still maps', enumStatus.stage === 'Offer') rmSync(outDir, { recursive: true, force: true }) if (failed) { console.log(`\n${failed} check(s) failed`) process.exit(1) } console.log('\nAll candidates-table mapper checks passed')