/** * CV Bank mapper — the two populations, and the two numbers that must not be * confused (free rank_score vs paid ai_score). * * node cvbank.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-bank-')) 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 { toBankRowView, expiryLabel } = await import(pathToFileURL(outFile).href) let failed = 0 function ok(name, cond, extra) { if (cond) { console.log(`ok ${name}`) } else { failed += 1 console.log(`FAIL ${name}`) if (extra) console.log(` ${extra}`) } } /* --- a speculative upload: extracted, never scored ------------------------ */ const speculative = toBankRowView({ id: 'bank:11111111-1111-1111-1111-111111111111', record_id: '11111111-1111-1111-1111-111111111111', bank_source: 'speculative', name: 'Ada Lovelace', email: 'ada@example.com', phone: '0321-5551234', file_name: 'ada.pdf', file_path: 'https://s3/Temp/x/ada.pdf', current_company: 'Acme', current_position: 'Backend Engineer', education: 'BS CS', skills: ['Python', 'FastAPI', 'Docker'], years_experience: 6, ai_score: null, recommendation: null, rank_score: null, bank_reason: 'speculative', bank_expires_at: '2028-09-03T00:00:00Z', created_at: '2026-09-03T10:00:00Z', }) ok('speculative row keeps the prefixed list id', speculative.id === 'bank:11111111-1111-1111-1111-111111111111') ok('recordId is the raw uuid the delete/file routes take', speculative.recordId === '11111111-1111-1111-1111-111111111111') ok('source label is human', speculative.sourceLabel === 'Speculative') ok('speculative rows are removable stored CVs', speculative.isStoredCv === true) ok('extracted skills come through', speculative.skills.join(',') === 'Python,FastAPI,Docker') ok('years is numeric', speculative.years === 6) ok('an unscored CV has no ATS score', speculative.aiScore === null) ok('and no invented band', speculative.recommendation === null) ok('and no rank until a job is picked', speculative.rankScore === null) ok('expiry parses to a Date', speculative.expiresAt instanceof Date) ok('added parses to a Date', speculative.added instanceof Date) /* --- a silver medalist: scored, read live, not the bank's to delete ------- */ const silver = toBankRowView({ id: 'app:42', record_id: '42', bank_source: 'silver_medalist', name: 'Grace Hopper', email: 'grace@example.com', current_company: 'Navy', current_position: 'Rear Admiral', skills: ['COBOL', 'Compilers'], years_experience: 20, ai_score: 88, recommendation: 'Strong Match', last_job_title: 'Principal Engineer', user_id: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', created_at: '2026-08-01T10:00:00Z', }) ok('silver medalist is keyed off the application', silver.id === 'app:42') ok('silver medalist label', silver.sourceLabel === 'Silver medalist') ok('silver medalist is NOT a stored CV, so the bank cannot delete it', silver.isStoredCv === false) ok('paid ATS score survives', silver.aiScore === 88) ok('band survives', silver.recommendation === 'Strong Match') ok('the job they were rejected from is kept for context', silver.lastJobTitle === 'Principal Engineer') ok('userId is kept so the row can open a real profile', silver.userId === 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') /* --- rank_score is the free number, and separate from ai_score ----------- */ const ranked = toBankRowView({ id: 'bank:2', record_id: '2', bank_source: 'speculative', name: 'Ranked', skills: [], rank_score: 72, ai_score: null, }) ok('rank_score maps without becoming an ATS score', ranked.rankScore === 72 && ranked.aiScore === null) const bothNumbers = toBankRowView({ id: 'app:3', record_id: '3', bank_source: 'silver_medalist', name: 'Both', rank_score: 61, ai_score: 84, }) ok('a row can carry both numbers independently', bothNumbers.rankScore === 61 && bothNumbers.aiScore === 84) /* --- absent values stay absent ------------------------------------------- */ const sparse = toBankRowView({ id: 'bank:4', record_id: '4', bank_source: 'speculative', file_name: 'unknown.pdf', }) ok('a nameless CV falls back rather than rendering blank', sparse.name === 'Unknown') ok('no skills is an empty array, not null', Array.isArray(sparse.skills) && sparse.skills.length === 0) ok('unknown years stays null, never 0', sparse.years === null) ok('no expiry stays null', sparse.expiresAt === null) ok('unknown source defaults to speculative', sparse.source === 'speculative') const zeroYears = toBankRowView({ id: 'bank:5', record_id: '5', bank_source: 'speculative', name: 'Fresh Grad', years_experience: 0, }) ok('0 years is a real value and must not collapse to null', zeroYears.years === 0) const derivedBand = toBankRowView({ id: 'app:6', record_id: '6', bank_source: 'silver_medalist', name: 'Derived', ai_score: 70, }) ok('a score without a band derives one', derivedBand.recommendation === 'Potential Match') /* --- expiry wording ------------------------------------------------------ */ const now = new Date('2026-09-03T00:00:00Z') ok('no expiry has no label', expiryLabel(null, now) === null) ok('months are counted forward', expiryLabel(new Date('2027-04-01T00:00:00Z'), now) === 'expires in 7 months') ok('one month is singular', expiryLabel(new Date('2026-10-03T00:00:00Z'), now) === 'expires in 1 month') ok( 'a past window reads as expired, not a negative count', expiryLabel(new Date('2026-01-01T00:00:00Z'), now) === 'expired', ) rmSync(outDir, { recursive: true, force: true }) if (failed) { console.log(`\n${failed} check(s) failed`) process.exit(1) } console.log('\nAll CV Bank mapper checks passed')