-
-
Notifications
You must be signed in to change notification settings - Fork 10.8k
Expand file tree
/
Copy pathtracker-columns-tests.mjs
More file actions
147 lines (131 loc) · 6.38 KB
/
Copy pathtracker-columns-tests.mjs
File metadata and controls
147 lines (131 loc) · 6.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/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 { readFileSync, writeFileSync, mkdtempSync, mkdirSync, rmSync } 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;
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 || ''}` };
}
}
// 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 });
}
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);