Files
santifer--career-ops/tracker-columns-tests.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

551 lines
28 KiB
JavaScript

#!/usr/bin/env node
/**
* tracker-columns-tests.mjs — regression tests for header-name column mapping.
*
* merge-tracker.mjs and verify-pipeline.mjs used to parse applications.md by
* fixed column position. Inserting a column (e.g. a Location column after Role)
* shifted every later index by one — Location was read as Score, Score as
* Status — so verify-pipeline flagged false errors and merge-tracker wrote
* malformed rows. Both now map columns by header NAME (see #946).
*
* These tests provision a throwaway tracker + additions dir via the
* CAREER_OPS_TRACKER / CAREER_OPS_ADDITIONS env overrides and assert:
* 1. A 10-column tracker (with Location) merges a new row into the correct
* columns — Score/Status are NOT shifted, Location is populated.
* 2. verify-pipeline reports a clean bill of health on that 10-column tracker.
* 3. The original 9-column layout still works unchanged (back-compat).
*/
import { execFileSync } from 'child_process';
import { existsSync, readFileSync, writeFileSync, mkdtempSync, mkdirSync, rmSync, utimesSync } from 'fs';
import { join, dirname } from 'path';
import { tmpdir } from 'os';
import { fileURLToPath } from 'url';
const ROOT = dirname(fileURLToPath(import.meta.url));
const NODE = process.execPath;
// web/ lives deliberately OUTSIDE the auto-updater's world (its own
// release-please component; see validate-system-paths-coverage.mjs
// EXCLUDE_PREFIXES), so installs updated via `update-system.mjs apply` have
// the core WITHOUT the web/ tree. The web-reader tests below exercise the
// real alias chain on fresh clones and CI, and skip cleanly on core-only
// installs instead of crashing the whole suite with ERR_MODULE_NOT_FOUND.
const HAS_WEB = existsSync(join(ROOT, 'web', 'src', 'lib', 'tracker-table.mjs'));
function skipWeb(m) { console.log(`SKIP ${m} — web/ not present (core-only install; web/ is excluded from the auto-updater by design)`); }
let passed = 0;
let failed = 0;
function pass(m) { console.log(`PASS ${m}`); passed++; }
function fail(m) { console.error(`FAIL ${m}`); failed++; }
// Run a script with tracker/additions redirected to a sandbox. Returns
// { code, stdout } — code is 0 on success, the process exit code otherwise.
function runScript(script, args, sandbox) {
const env = {
...process.env,
CAREER_OPS_TRACKER: sandbox.tracker,
CAREER_OPS_ADDITIONS: sandbox.additions,
CAREER_OPS_TRACKER_LOCK: sandbox.lock,
};
try {
const stdout = execFileSync(NODE, [join(ROOT, script), ...args], {
cwd: ROOT, env, encoding: 'utf-8', timeout: 30000,
});
return { code: 0, stdout };
} catch (e) {
return { code: e.status ?? 1, stdout: `${e.stdout || ''}${e.stderr || ''}` };
}
}
// Sync the sandbox tracker into the tracker.mjs index and return one parsed
// row by company name (row is null when sync/query fails or the row is absent).
function syncAndQueryRow(sb, company) {
const sync = runScript('tracker.mjs', ['sync'], sb);
const query = runScript('tracker.mjs', ['query', '--json'], sb);
let row = null;
try { row = JSON.parse(query.stdout).find(r => r.company === company) ?? null; } catch { /* malformed output → null */ }
return { sync, query, row };
}
// Create a sandbox dir holding a tracker file and an additions dir.
function makeSandbox(trackerContent, additions = {}) {
const dir = mkdtempSync(join(tmpdir(), 'co-cols-'));
const tracker = join(dir, 'applications.md');
const additionsDir = join(dir, 'tracker-additions');
const lock = join(dir, 'lock');
mkdirSync(additionsDir, { recursive: true });
writeFileSync(tracker, trackerContent);
for (const [name, content] of Object.entries(additions)) {
writeFileSync(join(additionsDir, name), content);
}
return { dir, tracker, additions: additionsDir, lock };
}
// Return the data rows of a tracker (pipe lines that aren't header/separator).
function dataRows(trackerPath) {
return readFileSync(trackerPath, 'utf-8')
.split('\n')
.filter(l => l.startsWith('|') && !l.includes('---') && !/\bScore\b/.test(l));
}
const HEADER_10 = `# Applications Tracker
| # | Date | Company | Role | Location | Score | Status | PDF | Report | Notes |
|---|------|---------|------|----------|-------|--------|-----|--------|-------|
| 1 | 2026-01-01 | Acme | Engineer | Remote | 4.0/5 | Applied | ✅ | — | seed row |
`;
const HEADER_9 = `# Applications Tracker
| # | Date | Company | Role | Score | Status | PDF | Report | Notes |
|---|------|---------|------|-------|--------|-----|--------|-------|
| 1 | 2026-01-01 | Acme | Engineer | 4.0/5 | Applied | ✅ | — | seed row |
`;
// TSV column order (status BEFORE score): num,date,company,role,status,score,pdf,report,notes[,location]
const TSV_WITH_LOCATION = '2\t2026-02-02\tGlobex\tManager\tApplied\tN/A\t✅\t—\tnew row\tSingapore\n';
const TSV_NO_LOCATION = '2\t2026-02-02\tGlobex\tManager\tApplied\tN/A\t✅\t—\tnew row\n';
// ── Test 1: 10-column tracker merges into the correct columns ──────────────
{
const sb = makeSandbox(HEADER_10, { '2-globex.tsv': TSV_WITH_LOCATION });
const res = runScript('merge-tracker.mjs', [], sb);
if (res.code !== 0) {
fail(`merge into 10-col tracker exits 0 (got ${res.code})\n${res.stdout}`);
} else {
pass('merge into 10-col tracker exits 0');
const row = dataRows(sb.tracker).find(l => l.includes('Globex'));
const cells = row ? row.split('|').map(s => s.trim()) : [];
// cells: ['', num, date, company, role, location, score, status, pdf, report, notes, '']
if (cells[5] === 'Singapore') pass('Location column populated (not shifted into Score)');
else fail(`Location column populated — got "${cells[5]}" in row: ${row}`);
if (cells[6] === 'N/A') pass('Score sits in the Score column');
else fail(`Score in Score column — got "${cells[6]}" in row: ${row}`);
if (cells[7] === 'Applied') pass('Status sits in the Status column');
else fail(`Status in Status column — got "${cells[7]}" in row: ${row}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── Test 2: verify-pipeline is clean on a 10-column tracker ────────────────
{
const sb = makeSandbox(HEADER_10);
const res = runScript('verify-pipeline.mjs', [], sb);
if (res.code === 0 && /0 errors/.test(res.stdout)) {
pass('verify-pipeline clean on 10-col tracker (no false column errors)');
} else {
fail(`verify-pipeline clean on 10-col tracker (code ${res.code})\n${res.stdout}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── Test 3: legacy 9-column layout still works (back-compat) ───────────────
{
const sb = makeSandbox(HEADER_9, { '2-globex.tsv': TSV_NO_LOCATION });
const merge = runScript('merge-tracker.mjs', [], sb);
const verify = runScript('verify-pipeline.mjs', [], sb);
const row = dataRows(sb.tracker).find(l => l.includes('Globex'));
const cells = row ? row.split('|').map(s => s.trim()) : [];
// cells: ['', num, date, company, role, score, status, pdf, report, notes, '']
if (merge.code === 0 && cells[5] === 'N/A' && cells[6] === 'Applied') {
pass('9-col tracker still merges into correct columns');
} else {
fail(`9-col tracker merge (code ${merge.code}) row: ${row}`);
}
if (verify.code === 0 && /0 errors/.test(verify.stdout)) {
pass('verify-pipeline clean on legacy 9-col tracker');
} else {
fail(`verify-pipeline clean on 9-col tracker (code ${verify.code})\n${verify.stdout}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── Test 4: tracker.mjs CLI maps a 10-column tracker by header (#1596) ──────
// tracker.mjs used a fixed 9-cell destructure, so a Location column shifted
// Score into Status and folded the real Notes cell away.
{
const sb = makeSandbox(HEADER_10);
const { sync, query, row } = syncAndQueryRow(sb, 'Acme');
if (sync.code === 0 && query.code === 0 && row) {
if (row.role === 'Engineer') pass('tracker.mjs: Role read from Role column on 10-col tracker');
else fail(`tracker.mjs: Role on 10-col tracker — got "${row.role}"`);
if (row.score === '4.0/5') pass('tracker.mjs: Score not shifted on 10-col tracker');
else fail(`tracker.mjs: Score on 10-col tracker — got "${row.score}"`);
if (row.status === 'Applied') pass('tracker.mjs: Status not shifted on 10-col tracker');
else fail(`tracker.mjs: Status on 10-col tracker — got "${row.status}"`);
if (row.notes === 'seed row') pass('tracker.mjs: Notes intact on 10-col tracker');
else fail(`tracker.mjs: Notes on 10-col tracker — got "${row.notes}"`);
} else {
fail(`tracker.mjs sync/query on 10-col tracker (sync ${sync.code}, query ${query.code})\n${sync.stdout}${query.stdout}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── Test 5: removeRowByNum resolves the Report column by header ─────────────
{
const { removeRowByNum } = await import('./tracker.mjs');
const tenCol = HEADER_10.replace('| — | seed row |', '| [1](reports/001-acme-2026-01-01.md) | seed row |');
const res = removeRowByNum(tenCol, 1);
if (res.removed && res.report === '[1](reports/001-acme-2026-01-01.md)') {
pass('removeRowByNum: report column resolved by header on 10-col tracker');
} else {
fail(`removeRowByNum: report on 10-col tracker — got "${res.report}"`);
}
}
// ── Test 6: scan.mjs seen-set maps company/role by header ───────────────────
// loadSeenCompanyRoles used a positional regex, so a 10-col tracker produced
// keys like "engineer::remote" and scan dedup missed real matches.
{
const { loadSeenCompanyRoles } = await import('./scan.mjs');
const sb = makeSandbox(HEADER_10);
const seen = loadSeenCompanyRoles(sb.tracker);
if (seen.has('acme::engineer')) pass('scan.mjs: seen-set keys company::role on 10-col tracker');
else fail(`scan.mjs: seen-set on 10-col tracker — got [${[...seen].join(', ')}]`);
if (![...seen].some(k => k.includes('remote') || k.includes('4.0/5'))) {
pass('scan.mjs: seen-set has no shifted-column garbage keys');
} else {
fail(`scan.mjs: shifted keys present — [${[...seen].join(', ')}]`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── Test 7: schema contract — every consumer maps an UNKNOWN extra column ───
// The header-name contract (#1596): a column no consumer recognizes must be
// skipped by ALL of them, never silently shifted into a known field. This is
// the guard that makes the next column insertion a one-place change instead of
// a repo-wide incident. normalize-statuses.mjs is excluded until PR #1114
// (which retrofits it) lands — add it here when that merges.
{
const HEADER_UNKNOWN = `# Applications Tracker
| # | Date | Company | Priority | Role | Score | Status | PDF | Report | Notes |
|---|------|---------|----------|------|-------|--------|-----|--------|-------|
| 1 | 2026-01-01 | Acme | high | Engineer | 4.0/5 | Applied | ✅ | — | seed row |
`;
const sb = makeSandbox(HEADER_UNKNOWN);
const verify = runScript('verify-pipeline.mjs', [], sb);
if (verify.code === 0 && /0 errors/.test(verify.stdout)) {
pass('contract: verify-pipeline skips an unknown extra column');
} else {
fail(`contract: verify-pipeline on unknown-column tracker (code ${verify.code})\n${verify.stdout}`);
}
const { sync, row } = syncAndQueryRow(sb, 'Acme');
if (sync.code === 0 && row && row.role === 'Engineer' && row.score === '4.0/5' && row.status === 'Applied') {
pass('contract: tracker.mjs skips an unknown extra column');
} else {
fail(`contract: tracker.mjs on unknown-column tracker — got ${JSON.stringify(row)}`);
}
const { loadSeenCompanyRoles } = await import('./scan.mjs');
const seen = loadSeenCompanyRoles(sb.tracker);
if (seen.has('acme::engineer') && seen.size === 1) {
pass('contract: scan.mjs seen-set skips an unknown extra column');
} else {
fail(`contract: scan.mjs seen-set on unknown-column tracker — [${[...seen].join(', ')}]`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── Test 8: web read path resolves headers via the SHARED alias table ───────
// web/src/lib/tracker-table.mjs (behind readApplications() in career-ops.ts)
// loads tracker-aliases.json — the same file tracker-parse.mjs exports as
// HEADER_ALIASES — instead of mirroring it. Passing ROOT here exercises the
// REAL alias file, so an alias added/renamed there is either honored by the
// web reader too or fails this test; a second drifting table can't come back.
if (!HAS_WEB) {
skipWeb('web reader: shared alias table tests');
} else {
const { parseApplications, loadHeaderAliases } = await import('./web/src/lib/tracker-table.mjs');
const { HEADER_ALIASES } = await import('./tracker-parse.mjs');
const WEB_10COL = `# Applications Tracker
| # | Date | Company | Role | Location | Score | Status | PDF | Report | Priority | Notes |
|---|------|---------|------|----------|-------|--------|-----|--------|----------|-------|
| 1 | 2026-01-01 | Acme | Engineer | Remote | 4.0/5 | Applied | ✅ | — | high | seed row |
`;
const rows = parseApplications(WEB_10COL, ROOT);
const r = rows[0];
if (rows.length === 1 && r.company === 'Acme' && r.role === 'Engineer') {
pass('web reader: Company/Role read by header on 10-col tracker');
} else {
fail(`web reader: Company/Role on 10-col tracker — got ${JSON.stringify(r)}`);
}
if (r && r.score === '4.0/5' && r.status === 'Applied') {
pass('web reader: Score/Status not shifted by Location column');
} else {
fail(`web reader: Score/Status on 10-col tracker — got ${JSON.stringify(r)}`);
}
if (r && r.notes === 'seed row') {
pass('web reader: unknown Priority column skipped, Notes intact');
} else {
fail(`web reader: Notes past unknown column — got "${r && r.notes}"`);
}
// The web reader and the Node tooling must consume the IDENTICAL table.
const webAliases = loadHeaderAliases(ROOT);
if (JSON.stringify(webAliases) === JSON.stringify(HEADER_ALIASES) && Object.keys(webAliases).length > 0) {
pass('web reader: alias table is byte-identical to tracker-parse HEADER_ALIASES');
} else {
fail(`web reader: alias table drifted from HEADER_ALIASES — web ${JSON.stringify(webAliases)} vs core ${JSON.stringify(HEADER_ALIASES)}`);
}
}
// ═══ Stage 2 (#1596): Via column ════════════════════════════════════════════
const HEADER_VIA = `# Applications Tracker
| # | Date | Company | Via | Role | Score | Status | PDF | Report | Notes |
|---|------|---------|-----|------|-------|--------|-----|--------|-------|
| 1 | 2026-01-01 | Acme | — | Engineer | 4.0/5 | Applied | ✅ | — | direct seed row |
| 2 | 2026-01-05 | ? | Hays | Data Engineer | 4.2/5 | Applied | ✅ | — | fintech, Leeds |
`;
// ── Test 9: parseTrackerRow surfaces the Via column ─────────────────────────
{
const { resolveColumns, parseTrackerRow } = await import('./tracker-parse.mjs');
const lines = HEADER_VIA.split('\n');
const colmap = resolveColumns(lines);
const rows = lines.map(l => parseTrackerRow(l, colmap)).filter(Boolean);
const direct = rows.find(r => r.num === 1);
const blind = rows.find(r => r.num === 2);
if (direct && direct.via === '—' && direct.role === 'Engineer' && direct.score === '4.0/5') {
pass('parseTrackerRow: Via column mapped, later columns not shifted');
} else {
fail(`parseTrackerRow: Via layout — got ${JSON.stringify(direct)}`);
}
if (blind && blind.company === '?' && blind.via === 'Hays' && blind.status === 'Applied') {
pass('parseTrackerRow: unknown-employer (?) row carries via');
} else {
fail(`parseTrackerRow: ? row — got ${JSON.stringify(blind)}`);
}
}
// ── Test 10: TSV `via=` tagged field merges into the Via column ──────────────
// The batch TSV is header-less and positional; Via travels as a tagged extra
// field (`via=Hays`) instead of another positional slot, so a stale writer
// omitting the empty-location pad can't silently shift columns.
{
const TSV_VIA = '3\t2026-02-02\t?\tPlatform Engineer\tApplied\t4.1/5\t✅\t—\tblind agency listing\tvia=Hays\n';
const sb = makeSandbox(HEADER_VIA, { '3-blind.tsv': TSV_VIA });
const res = runScript('merge-tracker.mjs', [], sb);
const row = dataRows(sb.tracker).find(l => l.includes('Platform Engineer'));
const cells = row ? row.split('|').map(s => s.trim()) : [];
// cells: ['', num, date, company, via, role, score, status, pdf, report, notes, '']
if (res.code === 0 && cells[3] === '?' && cells[4] === 'Hays' && cells[6] === '4.1/5' && cells[7] === 'Applied') {
pass('merge: via= tag lands in the Via column, ? company preserved');
} else {
fail(`merge: via= tag (code ${res.code}) row: ${row}\n${res.stdout}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── Test 11: ambiguous TSV extras are rejected loudly, never merged ─────────
{
const TWO_UNTAGGED = '4\t2026-02-02\tGlobex\tManager\tApplied\tN/A\t✅\t—\tnote\tSingapore\tHays\n';
const TWO_TAGS = '5\t2026-02-02\tGlobex\tManager\tApplied\tN/A\t✅\t—\tnote\tvia=Hays\tvia=Randstad\n';
const sb = makeSandbox(HEADER_VIA, { '4-a.tsv': TWO_UNTAGGED, '5-b.tsv': TWO_TAGS });
const res = runScript('merge-tracker.mjs', [], sb);
const rows = dataRows(sb.tracker);
// Rejection is loud on stderr; runScript only captures stdout on success, so
// assert via the merge summary (2 skipped) plus the tracker staying clean.
if (!rows.some(l => l.includes('Globex')) && /2 skipped/.test(res.stdout)) {
pass('merge: ambiguous extras (two untagged / duplicate via=) rejected, not merged');
} else {
fail(`merge: ambiguous extras — rows: ${rows.length}\n${res.stdout}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── Test 12: cross-channel guard — ? rows never fuzzy-merge across agencies ─
// Two blind listings for the same role via DIFFERENT agencies are distinct
// submissions (#1596): merging them silently is exactly the double-submission
// hazard the Via column exists to surface. Same agency + same role IS the
// re-blast duplicate and must still merge/update.
{
const OTHER_AGENCY = '6\t2026-02-02\t?\tData Engineer\tApplied\t4.5/5\t✅\t—\tsame role, other agency\tvia=Randstad\n';
const SAME_AGENCY = '7\t2026-02-03\t?\tData Engineer\tApplied\t4.6/5\t✅\t—\tre-blast, higher score\tvia=Hays\n';
const sb = makeSandbox(HEADER_VIA, { '6-other.tsv': OTHER_AGENCY });
const res1 = runScript('merge-tracker.mjs', [], sb);
const rowsAfter1 = dataRows(sb.tracker).filter(l => l.includes('Data Engineer'));
if (res1.code === 0 && rowsAfter1.length === 2 && rowsAfter1.some(l => l.includes('Randstad'))) {
pass('merge: ? row via a different agency added as a NEW row (no cross-channel merge)');
} else {
fail(`merge: cross-channel guard — ${rowsAfter1.length} Data Engineer rows\n${res1.stdout}`);
}
writeFileSync(join(sb.additions, '7-same.tsv'), SAME_AGENCY);
const res2 = runScript('merge-tracker.mjs', [], sb);
const hays = dataRows(sb.tracker).filter(l => l.includes('Hays') && l.includes('Data Engineer'));
if (res2.code === 0 && hays.length === 1 && hays[0].includes('4.6/5')) {
pass('merge: same-agency re-blast updates the existing ? row (Via preserved)');
} else {
fail(`merge: same-agency update — ${hays.length} Hays rows: ${hays.join(' / ')}\n${res2.stdout}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── Test 12b: legacy 9-col tracker — via= tag dropped WITHOUT breaking dedup ─
// The tracker has no Via column, so existing rows parse with via=''. The
// addition's via must be cleared before duplicate matching, or the
// cross-channel guard would see 'Hays' ≠ '' and add a second ? row instead of
// updating the same-agency re-blast.
{
const FIRST = '2\t2026-02-02\t?\tData Engineer\tApplied\t4.1/5\t✅\t—\tblind listing\tvia=Hays\n';
const REBLAST = '3\t2026-02-10\t?\tData Engineer\tApplied\t4.3/5\t✅\t—\tre-blast, higher score\tvia=Hays\n';
const sb = makeSandbox(HEADER_9, { '2-first.tsv': FIRST });
const res1 = runScript('merge-tracker.mjs', [], sb);
writeFileSync(join(sb.additions, '3-reblast.tsv'), REBLAST);
const res2 = runScript('merge-tracker.mjs', [], sb);
const blind = dataRows(sb.tracker).filter(l => l.includes('Data Engineer'));
if (res1.code === 0 && res2.code === 0 && blind.length === 1 && blind[0].includes('4.3/5') && /1 updated/.test(res2.stdout)) {
pass('merge: legacy 9-col tracker — via= re-blast UPDATES the ? row (no duplicate)');
} else {
fail(`merge: legacy via= dedup — ${blind.length} rows: ${blind.join(' / ')}\n${res2.stdout}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── Test 13: --migrate-via inserts the column, idempotently ─────────────────
{
const sb = makeSandbox(HEADER_9);
const first = runScript('merge-tracker.mjs', ['--migrate-via'], sb);
const content = readFileSync(sb.tracker, 'utf-8');
const header = content.split('\n').find(l => l.includes('Company'));
const seed = content.split('\n').find(l => l.includes('Acme'));
const headCells = header ? header.split('|').map(s => s.trim()) : [];
const seedCells = seed ? seed.split('|').map(s => s.trim()) : [];
if (first.code === 0 && headCells[4] === 'Via' && headCells[3] === 'Company' && seedCells[4] === '—' && seedCells[6] === '4.0/5') {
pass('--migrate-via: Via column inserted after Company, rows padded with —');
} else {
fail(`--migrate-via: header "${header}" seed "${seed}"\n${first.stdout}`);
}
const second = runScript('merge-tracker.mjs', ['--migrate-via'], sb);
if (second.code === 0 && readFileSync(sb.tracker, 'utf-8') === content) {
pass('--migrate-via: idempotent (second run changes nothing)');
} else {
fail(`--migrate-via: not idempotent\n${second.stdout}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── Test 14: dedup — unknown-employer rows key on Via + role + 90-day window ─
{
const BLIND_TRACKER = `# Applications Tracker
| # | Date | Company | Via | Role | Score | Status | PDF | Report | Notes |
|---|------|---------|-----|------|-------|--------|-----|--------|-------|
| 1 | 2026-01-05 | ? | Hays | Data Engineer | 4.2/5 | Evaluated | ✅ | — | fintech, Leeds |
| 2 | 2026-01-20 | ? | Hays | Data Engineer | 4.3/5 | Evaluated | ✅ | — | re-blast of same listing |
| 3 | 2026-01-06 | ? | Randstad | Data Engineer | 4.0/5 | Evaluated | ✅ | — | different channel |
| 4 | 2026-01-10 | ? | Hays | Platform Engineer | 3.9/5 | Evaluated | ✅ | — | old listing |
| 5 | 2026-06-01 | ? | Hays | Platform Engineer | 4.4/5 | Evaluated | ✅ | — | far outside window |
`;
const sb = makeSandbox(BLIND_TRACKER);
const res = runScript('dedup-tracker.mjs', [], sb);
const rows = dataRows(sb.tracker);
const dataEng = rows.filter(l => l.includes('Data Engineer'));
const platform = rows.filter(l => l.includes('Platform Engineer'));
if (res.code === 0 && dataEng.length === 2 && dataEng.some(l => l.includes('Randstad')) && dataEng.some(l => l.includes('4.3/5'))) {
pass('dedup: same-agency re-blast within 90d deduped; other agency kept');
} else {
fail(`dedup: blind keying — ${dataEng.length} Data Engineer rows:\n${dataEng.join('\n')}\n${res.stdout}`);
}
if (platform.length === 2) {
pass('dedup: same agency+role >90 days apart NOT deduped');
} else {
fail(`dedup: window — ${platform.length} Platform Engineer rows\n${res.stdout}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── Test 15: verify-pipeline Via checks ─────────────────────────────────────
{
const VIA_ISSUES = `# Applications Tracker
| # | Date | Company | Via | Role | Score | Status | PDF | Report | Notes |
|---|------|---------|-----|------|-------|--------|-----|--------|-------|
| 1 | 2026-01-05 | ? | — | Data Engineer | 4.2/5 | Evaluated | ✅ | — | blind row, no agency |
| 2 | 2026-01-06 | Confidential | Hays | ML Engineer | 4.0/5 | Evaluated | ✅ | — | word placeholder |
| 3 | 2026-01-07 | Acme | Hays | Backend Engineer | 4.1/5 | Applied | ✅ | — | via agency |
| 4 | 2026-01-08 | Acme | — | Backend Engineer | 4.1/5 | Applied | ✅ | — | direct too |
`;
const sb = makeSandbox(VIA_ISSUES);
const res = runScript('verify-pipeline.mjs', [], sb);
if (/unknown employer \(\?\) with no Via/.test(res.stdout)) {
pass('verify: ? row with no Via channel is an error');
} else {
fail(`verify: ?-without-via\n${res.stdout}`);
}
if (/looks like a confidentiality placeholder/.test(res.stdout)) {
pass('verify: localized confidentiality word linted toward ?');
} else {
fail(`verify: confidentiality lint\n${res.stdout}`);
}
if (/Cross-channel duplicate/.test(res.stdout)) {
pass('verify: same company+role via different channels warned');
} else {
fail(`verify: cross-channel warning\n${res.stdout}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── Test 16: web alias cache refreshes on change, never caches failure ──────
// loadHeaderAliases caches per file to avoid a disk read+parse per request
// (readApplications runs on every API route / page render), but the cache is
// mtime-keyed: a missing/corrupt file is NEVER cached — recovery is picked up
// without a server restart — and a rewritten file (system update changing the
// alias table) is re-read on the next call.
if (!HAS_WEB) {
skipWeb('web reader: alias cache refresh tests');
} else {
const { loadHeaderAliases } = await import('./web/src/lib/tracker-table.mjs');
const dir = mkdtempSync(join(tmpdir(), 'co-alias-'));
const aliasFile = join(dir, 'tracker-aliases.json');
// Force distinct mtimes between rewrites — same-ms writes are otherwise
// indistinguishable on coarse-timestamp filesystems.
let tick = Date.now();
const bump = () => { tick += 2000; const t = new Date(tick); utimesSync(aliasFile, t, t); };
// (a) missing file → {} and NOT cached: creating the file afterwards is seen.
const missing = loadHeaderAliases(dir);
writeFileSync(aliasFile, JSON.stringify({ '#': 'num', 'company': 'company' }));
const recovered = loadHeaderAliases(dir);
if (Object.keys(missing).length === 0 && recovered['#'] === 'num' && recovered.company === 'company') {
pass('web reader: alias file created after a failed load is picked up (no restart)');
} else {
fail(`web reader: recovery after missing file — first ${JSON.stringify(missing)}, then ${JSON.stringify(recovered)}`);
}
// (b) file rewritten → new aliases visible without a process restart.
writeFileSync(aliasFile, JSON.stringify({ '#': 'num', 'req id': 'num' }));
bump();
const updated = loadHeaderAliases(dir);
if (updated['req id'] === 'num' && updated.company === undefined) {
pass('web reader: rewritten alias file is re-read (mtime-keyed cache)');
} else {
fail(`web reader: update not visible without restart — got ${JSON.stringify(updated)}`);
}
// (c) corrupt file → {} safely, and NOT cached: fixing it is seen.
writeFileSync(aliasFile, '{ not json');
bump();
const corrupt = loadHeaderAliases(dir);
writeFileSync(aliasFile, JSON.stringify({ '#': 'num' }));
bump();
const fixed = loadHeaderAliases(dir);
if (Object.keys(corrupt).length === 0 && fixed['#'] === 'num') {
pass('web reader: corrupt alias file yields {} and later fix is picked up');
} else {
fail(`web reader: corrupt handling — during ${JSON.stringify(corrupt)}, after fix ${JSON.stringify(fixed)}`);
}
rmSync(dir, { recursive: true, force: true });
}
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);