Files
santifer--career-ops/test-salary-filter.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

594 lines
22 KiB
JavaScript

#!/usr/bin/env node
// @ts-check
/**
* Comprehensive test suite for salary filter and Ashby compensation parsing.
* Run: node test-salary-filter.mjs
*
* Tests cover:
* - buildSalaryFilter: range overlap, currency matching, edge cases, validation
* - parseCompensation (Ashby): interval normalization, malformed data, ordering
*/
import { parseCompensation } from './providers/ashby.mjs';
import { buildSalaryFilter } from './scan.mjs';
// ── Test runner ──────────────────────────────────────────────────────
let passed = 0;
let failed = 0;
function assert(condition, testName) {
if (condition) {
passed++;
console.log(` ✓ ${testName}`);
} else {
failed++;
console.error(` ✗ FAIL: ${testName}`);
}
}
function section(name) {
console.log(`\n━━━ ${name} ━━━`);
}
// ══════════════════════════════════════════════════════════════════════
// PART 1: parseCompensation (Ashby provider)
// ══════════════════════════════════════════════════════════════════════
section('parseCompensation — basic cases');
assert(
parseCompensation(null) === null,
'null job → null'
);
assert(
parseCompensation({}) === null,
'empty job (no compensation) → null'
);
assert(
parseCompensation({ compensation: null }) === null,
'compensation: null → null'
);
assert(
parseCompensation({ compensation: {} }) === null,
'compensation: {} (no minValue/maxValue) → null'
);
section('parseCompensation — yearly interval');
{
const result = parseCompensation({
compensation: { minValue: 100000, maxValue: 150000, currency: 'usd', interval: '1 YEAR' }
});
assert(result != null, 'yearly salary returns non-null');
assert(result.min === 100000, 'yearly min = 100000');
assert(result.max === 150000, 'yearly max = 150000');
assert(result.currency === 'USD', 'currency uppercased to USD');
}
section('parseCompensation — hourly interval (annualization)');
{
const result = parseCompensation({
compensation: { minValue: 50, maxValue: 75, currency: 'USD', interval: '1 HOUR' }
});
assert(result != null, 'hourly salary returns non-null');
assert(result.min === 50 * 2080, `hourly min annualized: ${result?.min} = ${50 * 2080}`);
assert(result.max === 75 * 2080, `hourly max annualized: ${result?.max} = ${75 * 2080}`);
}
section('parseCompensation — monthly interval');
{
const result = parseCompensation({
compensation: { minValue: 8000, maxValue: 12000, currency: 'EUR', interval: '1 MONTH' }
});
assert(result != null, 'monthly salary returns non-null');
assert(result.min === 8000 * 12, `monthly min annualized: ${result?.min} = ${8000 * 12}`);
assert(result.max === 12000 * 12, `monthly max annualized: ${result?.max} = ${12000 * 12}`);
}
section('parseCompensation — weekly interval');
{
const result = parseCompensation({
compensation: { minValue: 2000, maxValue: 3000, currency: 'GBP', interval: '1 WEEK' }
});
assert(result != null, 'weekly salary returns non-null');
assert(result.min === 2000 * 52, `weekly min annualized: ${result?.min}`);
assert(result.max === 3000 * 52, `weekly max annualized: ${result?.max}`);
}
section('parseCompensation — bi-weekly interval');
{
const result = parseCompensation({
compensation: { minValue: 4000, maxValue: 5000, currency: 'USD', interval: '2 WEEK' }
});
assert(result != null, 'bi-weekly salary returns non-null');
assert(result.min === 4000 * 26, `bi-weekly min annualized: ${result?.min}`);
assert(result.max === 5000 * 26, `bi-weekly max annualized: ${result?.max}`);
}
section('parseCompensation — partial range (min only, max only)');
{
const minOnly = parseCompensation({
compensation: { minValue: 80000, maxValue: null, currency: 'USD', interval: '1 YEAR' }
});
assert(minOnly != null, 'min-only returns non-null');
assert(minOnly.min === 80000, 'min-only: min = 80000');
assert(minOnly.max === 80000, 'min-only: max falls back to min = 80000');
}
{
const maxOnly = parseCompensation({
compensation: { minValue: null, maxValue: 200000, currency: 'USD', interval: '1 YEAR' }
});
assert(maxOnly != null, 'max-only returns non-null');
assert(maxOnly.min === 200000, 'max-only: min falls back to max = 200000');
assert(maxOnly.max === 200000, 'max-only: max = 200000');
}
section('parseCompensation — unknown interval');
{
const result = parseCompensation({
compensation: { minValue: 5000, maxValue: 10000, currency: 'USD', interval: 'QUARTERLY' }
});
assert(result === null, 'unknown interval → null');
}
section('parseCompensation — default interval (missing)');
{
const result = parseCompensation({
compensation: { minValue: 120000, maxValue: 180000, currency: 'USD' }
});
assert(result != null, 'missing interval defaults to 1 YEAR');
assert(result.min === 120000, 'default interval: min = 120000');
assert(result.max === 180000, 'default interval: max = 180000');
}
section('parseCompensation — malformed data (CodeRabbit hardening)');
{
const nanResult = parseCompensation({
compensation: { minValue: 'not-a-number', maxValue: 100000, currency: 'USD', interval: '1 YEAR' }
});
assert(nanResult != null, 'NaN minValue treated as null, maxValue used');
assert(nanResult.min === 100000, 'NaN min: falls back to max = 100000');
assert(nanResult.max === 100000, 'NaN min: max = 100000');
}
{
const infResult = parseCompensation({
compensation: { minValue: Infinity, maxValue: 100000, currency: 'USD', interval: '1 YEAR' }
});
assert(infResult != null, 'Infinity minValue treated as null, maxValue used');
assert(infResult.min === 100000, 'Infinity min: falls back to max = 100000');
}
{
const negInfResult = parseCompensation({
compensation: { minValue: -Infinity, maxValue: 100000, currency: 'USD', interval: '1 YEAR' }
});
assert(negInfResult != null, '-Infinity minValue treated as null, maxValue used');
assert(negInfResult.min === 100000, '-Infinity min: falls back to max = 100000');
}
{
const negativeResult = parseCompensation({
compensation: { minValue: -50000, maxValue: 100000, currency: 'USD', interval: '1 YEAR' }
});
assert(negativeResult != null, 'Negative minValue treated as null, maxValue used');
assert(negativeResult.min === 100000, 'Negative min: falls back to max = 100000');
}
{
const numericCurrency = parseCompensation({
compensation: { minValue: 100000, maxValue: 150000, currency: 12345, interval: '1 YEAR' }
});
assert(numericCurrency != null, 'numeric currency does not crash');
assert(numericCurrency.currency === '', 'numeric currency treated as empty string');
}
{
const boolCurrency = parseCompensation({
compensation: { minValue: 100000, maxValue: 150000, currency: true, interval: '1 YEAR' }
});
assert(boolCurrency != null, 'boolean currency does not crash');
assert(boolCurrency.currency === '', 'boolean currency treated as empty string');
}
{
const stringMin = parseCompensation({
compensation: { minValue: '100000', maxValue: 150000, currency: 'USD', interval: '1 YEAR' }
});
assert(stringMin != null, 'string minValue coerced to number');
assert(stringMin.min === 100000, 'string "100000" coerced to 100000');
}
section('parseCompensation — blank string values (normalizeNum)');
{
const blankMin = parseCompensation({
compensation: { minValue: '', maxValue: 150000, currency: 'USD', interval: '1 YEAR' }
});
assert(blankMin != null, 'blank minValue treated as null, maxValue used');
assert(blankMin.min === 150000, 'blank min: falls back to max = 150000');
}
{
const blankBoth = parseCompensation({
compensation: { minValue: '', maxValue: '', currency: 'USD', interval: '1 YEAR' }
});
assert(blankBoth === null, 'both blank → null');
}
{
const spacesOnly = parseCompensation({
compensation: { minValue: ' ', maxValue: 100000, currency: 'USD', interval: '1 YEAR' }
});
assert(spacesOnly != null, 'whitespace-only minValue treated as null');
assert(spacesOnly.min === 100000, 'whitespace min: falls back to max');
}
section('parseCompensation — trimmed currency');
{
const spacedCurrency = parseCompensation({
compensation: { minValue: 100000, maxValue: 150000, currency: ' usd ', interval: '1 YEAR' }
});
assert(spacedCurrency != null, 'whitespace-padded currency does not crash');
assert(spacedCurrency.currency === 'USD', 'whitespace-padded currency trimmed to USD');
}
section('parseCompensation — min/max ordering (reversed values)');
{
const reversed = parseCompensation({
compensation: { minValue: 200000, maxValue: 100000, currency: 'USD', interval: '1 YEAR' }
});
assert(reversed != null, 'reversed min/max returns non-null');
assert(reversed.min === 100000, 'reversed: min corrected to 100000');
assert(reversed.max === 200000, 'reversed: max corrected to 200000');
}
section('parseCompensation — zero values');
{
const zeroMin = parseCompensation({
compensation: { minValue: 0, maxValue: 150000, currency: 'USD', interval: '1 YEAR' }
});
assert(zeroMin != null, 'zero minValue preserved (not treated as null)');
assert(zeroMin.min === 0, 'zero min = 0');
assert(zeroMin.max === 150000, 'zero min: max = 150000');
}
{
const bothZero = parseCompensation({
compensation: { minValue: 0, maxValue: 0, currency: 'USD', interval: '1 YEAR' }
});
assert(bothZero != null, 'both zero returns non-null');
assert(bothZero.min === 0, 'both zero: min = 0');
assert(bothZero.max === 0, 'both zero: max = 0');
}
// ══════════════════════════════════════════════════════════════════════
// PART 2: buildSalaryFilter (scan.mjs)
// ══════════════════════════════════════════════════════════════════════
section('buildSalaryFilter — disabled / no-op cases');
{
const filter = buildSalaryFilter(null);
assert(filter({ min: 50000, max: 80000, currency: 'USD' }) === true, 'null config → pass all');
}
{
const filter = buildSalaryFilter(undefined);
assert(filter({ min: 50000, max: 80000, currency: 'USD' }) === true, 'undefined config → pass all');
}
{
const filter = buildSalaryFilter({ min: 0, max: 0 });
assert(filter({ min: 50000, max: 80000, currency: 'USD' }) === true, 'min=0, max=0 → pass all');
}
{
const filter = buildSalaryFilter({});
assert(filter({ min: 50000, max: 80000, currency: 'USD' }) === true, 'empty object → pass all');
}
section('buildSalaryFilter — basic range filtering');
{
const filter = buildSalaryFilter({ min: 100000, max: 200000, currency: 'USD' });
assert(filter({ min: 120000, max: 180000, currency: 'USD' }) === true,
'job fully inside range → pass');
assert(filter({ min: 50000, max: 80000, currency: 'USD' }) === false,
'job entirely below range → reject');
assert(filter({ min: 250000, max: 300000, currency: 'USD' }) === false,
'job entirely above range → reject');
assert(filter({ min: 90000, max: 150000, currency: 'USD' }) === true,
'job overlaps lower bound → pass');
assert(filter({ min: 180000, max: 250000, currency: 'USD' }) === true,
'job overlaps upper bound → pass');
assert(filter({ min: 50000, max: 300000, currency: 'USD' }) === true,
'job completely encompasses filter range → pass');
}
section('buildSalaryFilter — exact boundary matches');
{
const filter = buildSalaryFilter({ min: 100000, max: 200000, currency: 'USD' });
assert(filter({ min: 100000, max: 200000, currency: 'USD' }) === true,
'job exactly matches filter range → pass');
assert(filter({ min: 100000, max: 100000, currency: 'USD' }) === true,
'job at exact minimum → pass');
assert(filter({ min: 200000, max: 200000, currency: 'USD' }) === true,
'job at exact maximum → pass');
assert(filter({ min: 99999, max: 99999, currency: 'USD' }) === false,
'job 1 below minimum → reject');
assert(filter({ min: 200001, max: 200001, currency: 'USD' }) === false,
'job 1 above maximum → reject');
}
section('buildSalaryFilter — min-only filter (no upper limit)');
{
const filter = buildSalaryFilter({ min: 100000, max: 0, currency: 'USD' });
assert(filter({ min: 150000, max: 200000, currency: 'USD' }) === true,
'min-only: job above minimum → pass');
assert(filter({ min: 50000, max: 80000, currency: 'USD' }) === false,
'min-only: job entirely below minimum → reject');
assert(filter({ min: 500000, max: 1000000, currency: 'USD' }) === true,
'min-only: very high salary → pass (no upper limit)');
}
section('buildSalaryFilter — max-only filter');
{
const filter = buildSalaryFilter({ min: 0, max: 200000, currency: 'USD' });
assert(filter({ min: 50000, max: 100000, currency: 'USD' }) === true,
'max-only: job below maximum → pass');
assert(filter({ min: 250000, max: 300000, currency: 'USD' }) === false,
'max-only: job entirely above maximum → reject');
}
section('buildSalaryFilter — currency handling');
{
const filter = buildSalaryFilter({ min: 100000, max: 200000, currency: 'USD' });
assert(filter({ min: 120000, max: 180000, currency: 'EUR' }) === false,
'currency mismatch (USD vs EUR) → reject');
assert(filter({ min: 120000, max: 180000, currency: 'usd' }) === true,
'currency case-insensitive (usd vs USD) → pass');
assert(filter({ min: 120000, max: 180000, currency: '' }) === true,
'job has empty currency → pass (conservative)');
assert(filter({ min: 120000, max: 180000 }) === true,
'job has no currency field → pass (conservative)');
}
{
const noCurrencyFilter = buildSalaryFilter({ min: 100000, max: 200000 });
assert(noCurrencyFilter({ min: 120000, max: 180000, currency: 'EUR' }) === true,
'filter has no currency → pass regardless of job currency');
}
{
// Whitespace-padded currency in filter config
const spacedFilter = buildSalaryFilter({ min: 100000, max: 200000, currency: ' USD ' });
assert(spacedFilter({ min: 120000, max: 180000, currency: 'USD' }) === true,
'filter currency with whitespace trimmed → matches USD');
assert(spacedFilter({ min: 120000, max: 180000, currency: 'EUR' }) === false,
'filter currency with whitespace: still rejects EUR mismatch');
}
{
// Whitespace-padded currency in job data
const filter = buildSalaryFilter({ min: 100000, max: 200000, currency: 'USD' });
assert(filter({ min: 120000, max: 180000, currency: ' USD ' }) === true,
'job currency with whitespace trimmed → matches USD');
assert(filter({ min: 120000, max: 180000, currency: ' EUR ' }) === false,
'job currency with whitespace: still rejects EUR mismatch');
}
section('buildSalaryFilter — missing salary data (conservative)');
{
const filter = buildSalaryFilter({ min: 100000, max: 200000, currency: 'USD' });
assert(filter(null) === true,
'null salary → pass (conservative)');
assert(filter(undefined) === true,
'undefined salary → pass (conservative)');
assert(filter({}) === true,
'empty salary object (no min/max) → pass (conservative)');
assert(filter({ min: null, max: null, currency: 'USD' }) === true,
'salary with null min and max → pass (conservative)');
}
section('buildSalaryFilter — partial job salary data');
{
const filter = buildSalaryFilter({ min: 100000, max: 200000, currency: 'USD' });
assert(filter({ min: 150000, max: null, currency: 'USD' }) === true,
'job has only min (in range) → pass');
assert(filter({ min: null, max: 150000, currency: 'USD' }) === true,
'job has only max (in range) → pass');
assert(filter({ min: 50000, max: null, currency: 'USD' }) === false,
'job has only min (below range, treated as single point) → reject');
assert(filter({ min: null, max: 50000, currency: 'USD' }) === false,
'job has only max (below min) → reject');
assert(filter({ min: 300000, max: null, currency: 'USD' }) === false,
'job has only min (above max) → reject');
}
section('buildSalaryFilter — validation (CodeRabbit hardening)');
{
// Capture console.error output
const originalError = console.error;
let warnings = [];
console.error = (msg) => warnings.push(msg);
const nanFilter = buildSalaryFilter({ min: 'abc', max: 100000 });
assert(nanFilter({ min: 50000, max: 80000, currency: 'USD' }) === true,
'NaN min → filter disabled (no-op)');
assert(warnings.length > 0, 'NaN min → warning logged');
warnings = [];
const negFilter = buildSalaryFilter({ min: -50000, max: 100000 });
assert(negFilter({ min: 50000, max: 80000, currency: 'USD' }) === true,
'negative min → filter disabled (no-op)');
assert(warnings.length > 0, 'negative min → warning logged');
warnings = [];
const invertedFilter = buildSalaryFilter({ min: 200000, max: 100000 });
assert(invertedFilter({ min: 150000, max: 180000, currency: 'USD' }) === true,
'min > max → filter disabled (no-op)');
assert(warnings.length > 0, 'min > max → warning logged');
warnings = [];
const negMaxFilter = buildSalaryFilter({ min: 0, max: -100000 });
assert(negMaxFilter({ min: 50000, max: 80000, currency: 'USD' }) === true,
'negative max → filter disabled (no-op)');
assert(warnings.length > 0, 'negative max → warning logged');
// "100k" style string
warnings = [];
const stringFilter = buildSalaryFilter({ min: '100k', max: 200000 });
assert(stringFilter({ min: 50000, max: 80000, currency: 'USD' }) === true,
'"100k" string min → filter disabled (no-op)');
// Restore console.error
console.error = originalError;
}
section('buildSalaryFilter — valid string numbers (coercion)');
{
const filter = buildSalaryFilter({ min: '100000', max: '200000', currency: 'USD' });
assert(filter({ min: 120000, max: 180000, currency: 'USD' }) === true,
'string "100000"/"200000" coerced to valid numbers → works correctly');
assert(filter({ min: 50000, max: 80000, currency: 'USD' }) === false,
'string coercion: job below range → still rejects');
}
section('buildSalaryFilter — zero salary jobs');
{
const filter = buildSalaryFilter({ min: 100000, max: 200000, currency: 'USD' });
assert(filter({ min: 0, max: 0, currency: 'USD' }) === false,
'zero salary job (volunteer/intern) → reject (below min)');
assert(filter({ min: 0, max: 150000, currency: 'USD' }) === true,
'zero min with valid max in range → pass');
}
// ══════════════════════════════════════════════════════════════════════
// PART 3: End-to-end (parseCompensation → buildSalaryFilter)
// ══════════════════════════════════════════════════════════════════════
section('End-to-end — Ashby job through salary filter');
{
const filter = buildSalaryFilter({ min: 100000, max: 250000, currency: 'USD' });
// Hourly $60/hr → $124,800/yr → should pass
const hourlyJob = parseCompensation({
compensation: { minValue: 50, maxValue: 60, currency: 'USD', interval: '1 HOUR' }
});
assert(hourlyJob != null, 'e2e: hourly job parsed');
assert(filter(hourlyJob) === true, `e2e: $50-60/hr (${hourlyJob?.min}-${hourlyJob?.max}/yr) → pass`);
// Monthly €3000/mo → €36,000/yr → should fail (below min, wrong currency)
const monthlyJob = parseCompensation({
compensation: { minValue: 3000, maxValue: 4000, currency: 'EUR', interval: '1 MONTH' }
});
assert(monthlyJob != null, 'e2e: monthly EUR job parsed');
assert(filter(monthlyJob) === false, `e2e: €3000-4000/mo → currency mismatch → reject`);
// Yearly $180k → should pass
const yearlyJob = parseCompensation({
compensation: { minValue: 180000, maxValue: 220000, currency: 'USD', interval: '1 YEAR' }
});
assert(yearlyJob != null, 'e2e: yearly job parsed');
assert(filter(yearlyJob) === true, `e2e: $180k-220k/yr → pass`);
// Yearly $30k → should fail (entirely below range)
const lowJob = parseCompensation({
compensation: { minValue: 20000, maxValue: 30000, currency: 'USD', interval: '1 YEAR' }
});
assert(lowJob != null, 'e2e: low yearly job parsed');
assert(filter(lowJob) === false, `e2e: $20k-30k/yr → reject`);
// No compensation → null → filter passes conservatively
const noComp = parseCompensation({});
assert(noComp === null, 'e2e: no comp → null');
assert(filter(noComp) === true, 'e2e: null salary → pass (conservative)');
// Malformed → null → filter passes conservatively
const malformed = parseCompensation({
compensation: { minValue: 'garbage', maxValue: null, currency: 'USD' }
});
assert(malformed === null, 'e2e: malformed comp → null');
assert(filter(malformed) === true, 'e2e: malformed → null → pass (conservative)');
}
// ══════════════════════════════════════════════════════════════════════
// Summary
// ══════════════════════════════════════════════════════════════════════
console.log(`\n${'═'.repeat(50)}`);
console.log(` Results: ${passed} passed, ${failed} failed, ${passed + failed} total`);
console.log(`${'═'.repeat(50)}`);
if (failed > 0) {
console.error(`\n❌ ${failed} test(s) FAILED`);
process.exit(1);
} else {
console.log(`\n✅ All ${passed} tests passed!`);
}