Files
santifer--career-ops/invite-match.test.mjs
T
wehub-resource-sync d083df1fdb
CodeQL Analysis / Analyze (javascript-typescript) (push) Failing after 2s
Web CI / web typecheck + build (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
CodeQL Analysis / Analyze (go) (push) Failing after 16s
chore: import upstream snapshot with attribution
2026-07-13 12:02:43 +08:00

95 lines
4.6 KiB
JavaScript

/**
* invite-match.test.mjs — regression tests for invite-match.mjs's ambiguous-
* match ranking, which is the part most likely to silently regress: a wrong
* top candidate is worse than no candidate at all.
*
* Run: node invite-match.test.mjs
*/
import { matchInvite, normalizeCompanyName } from './invite-match.mjs';
let passed = 0;
let failed = 0;
const failures = [];
function eq(label, actual, expected) {
const a = JSON.stringify(actual);
const e = JSON.stringify(expected);
if (a === e) {
passed++;
} else {
failed++;
failures.push(label);
console.log(` FAIL: ${label}`);
console.log(` expected: ${e}`);
console.log(` actual: ${a}`);
}
}
const rows = [
{ num: 201, company: 'Northwind Traders', role: 'Ops Coordinator', status: 'Applied', date: '2026-05-01', notes: '' },
{ num: 202, company: 'Northwind Traders', role: 'HR Assistant', status: 'Interview', date: '2026-05-15', notes: '' },
{ num: 203, company: 'Northwind Traders', role: 'Analyst', status: 'Rejected', date: '2026-04-10', notes: 'Rejected 2026-04-20' },
];
// Three tracker rows for the same company at three different statuses — the
// Interview row must outrank both Applied and Rejected, since an in-progress
// interview is the most likely thing a new invite email is about.
const result = matchInvite({ company: 'Northwind Traders', date: null, reqId: null }, rows);
eq('all three same-company candidates are returned, not just the top one', result.length, 3);
eq('Interview-status row ranks first among same-name candidates', result[0].appNumber, 202);
eq('Rejected-status row ranks last among same-name candidates', result[result.length - 1].appNumber, 203);
// A company name that only partially overlaps (e.g. recruiter drops a
// division name) must still resolve, but must not outrank an exact match
// when both are present in the tracker.
const mixedRows = [
...rows,
{ num: 204, company: 'Northwind', role: 'Coordinator', status: 'Applied', date: '2026-06-01', notes: '' },
];
const partial = matchInvite({ company: 'Northwind', date: null, reqId: null }, mixedRows);
eq('exact "Northwind" match outranks the longer "Northwind Traders" partial matches', partial[0].appNumber, 204);
// normalizeCompanyName must be idempotent — normalizing an already-normalized
// string must return it unchanged, otherwise repeated normalization could
// drift the matching key across call sites.
const once = normalizeCompanyName('Acme Technologies Inc.');
eq('normalizeCompanyName is idempotent', normalizeCompanyName(once), once);
// A req ID that appears verbatim in a row's notes must outrank a same-name
// row without it, even though both have identical name similarity — this is
// the strongest disambiguation signal the matcher has, so it must actually
// move the ranking, not just add a negligible tiebreaker.
const reqIdRows = [
{ num: 301, company: 'Fabrikam', role: 'Engineer', status: 'Applied', date: '2026-05-01', notes: '' },
{ num: 302, company: 'Fabrikam', role: 'Engineer II', status: 'Applied', date: '2026-05-02', notes: 'req R-4821 mentioned' },
];
const reqIdResult = matchInvite({ company: 'Fabrikam', date: null, reqId: 'R-4821' }, reqIdRows);
eq('row with matching reqId in notes outranks identical-name row without it', reqIdResult[0].appNumber, 302);
// The req-ID boost must be case-insensitive: the invite and the tracker
// notes may case the same ID differently ("r-4821" vs "R-4821"), and a
// casing mismatch silently dropping the strongest signal is exactly the
// kind of regression this suite exists to catch.
const reqIdCaseResult = matchInvite({ company: 'Fabrikam', date: null, reqId: 'r-4821' }, reqIdRows);
eq('reqId boost still applies when invite cases the ID differently than the notes', reqIdCaseResult[0].appNumber, 302);
// Two distinct companies that each end in a *different pair* of chained
// generic descriptor words must not erode down to the same root — this is
// the actual over-stripping bug raised on PR #1497: chaining generic-word
// removal (not just legal-suffix removal) let "X Solutions Group" and
// "X Technologies Holdings" both collapse all the way to "x". Limiting
// generic-descriptor stripping to a single, non-chained pass stops at the
// first strip instead of eating through both words.
eq(
'chained generic descriptors ("Solutions Group" vs "Technologies Holdings") do not erode to the same key',
normalizeCompanyName('Northwind Solutions Group') === normalizeCompanyName('Northwind Technologies Holdings'),
false
);
console.log(`\n${passed} passed, ${failed} failed`);
if (failed > 0) {
console.log('Failures:', failures.join(', '));
process.exit(1);
}