131 lines
4.0 KiB
JavaScript
131 lines
4.0 KiB
JavaScript
/**
|
||
* 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')
|