Files
santifer--career-ops/set-status-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

560 lines
24 KiB
JavaScript

#!/usr/bin/env node
/**
* set-status-tests.mjs — regression tests for the set-status.mjs CLI (#1428).
*
* set-status.mjs is the canonical write path for tracker status updates, so
* these tests pin down the full CLI contract: row resolution (by number, by
* company, --role disambiguation), strict state validation against
* templates/states.yml, idempotent note appends, dry-run, JSON output, exit
* codes, and layout tolerance (9-col and 10-col Location trackers).
*
* Tests provision a throwaway tracker via the CAREER_OPS_TRACKER /
* CAREER_OPS_TRACKER_LOCK env overrides (same sandbox pattern as
* tracker-columns-tests.mjs).
*
* Exit-code contract under test:
* 0 — success (including no-op re-runs)
* 1 — usage error or non-canonical state
* 2 — row not found (bad number, unknown company)
* 3 — ambiguous company match (candidates listed on stderr / in JSON)
*/
import { execFileSync } from 'child_process';
import { readFileSync, writeFileSync, mkdtempSync, mkdirSync, rmSync, chmodSync } from 'fs';
import { join, dirname } from 'path';
import { tmpdir } from 'os';
import { fileURLToPath } from 'url';
import { acquireTrackerLock } from './tracker-utils.mjs';
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 set-status.mjs with the tracker redirected to a sandbox. Returns
// { code, stdout, stderr }.
function runSetStatus(args, sandbox, extraEnv = {}) {
const env = {
...process.env,
CAREER_OPS_TRACKER: sandbox.tracker,
CAREER_OPS_TRACKER_LOCK: sandbox.lock,
...extraEnv,
};
try {
const stdout = execFileSync(NODE, [join(ROOT, 'set-status.mjs'), ...args], {
cwd: ROOT, env, encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'],
});
return { code: 0, stdout, stderr: '' };
} catch (e) {
return { code: e.status ?? 1, stdout: e.stdout || '', stderr: e.stderr || '' };
}
}
// Create a sandbox dir holding a tracker file.
function makeSandbox(trackerContent) {
const dir = mkdtempSync(join(tmpdir(), 'co-setstatus-'));
const tracker = join(dir, 'applications.md');
writeFileSync(tracker, trackerContent);
// The lock env value must live under tmpdir and use the career-ops prefix
// (see trackerLockDirFor) or it is ignored — which would still be safe,
// just contending on the real default lock.
const lock = join(dir, 'career-ops-merge-tracker-test.lock');
return { dir, tracker, lock };
}
function readTracker(sandbox) {
return readFileSync(sandbox.tracker, 'utf-8');
}
const TRACKER_9 = `# Applications Tracker
| # | Date | Company | Role | Score | Status | PDF | Report | Notes |
|---|------|---------|------|-------|--------|-----|--------|-------|
| 1 | 2026-06-01 | Acme | Backend Engineer | 4.2/5 | Evaluated | ✅ | [1](../reports/001-acme-2026-06-01.md) | strong infra fit |
| 2 | 2026-06-02 | Globex | Platform Engineer | 4.0/5 | Evaluated | ✅ | [2](../reports/002-globex-2026-06-02.md) | — |
| 3 | 2026-06-03 | Acme | Data Engineer | 3.9/5 | Evaluated | ❌ | [3](../reports/003-acme-2026-06-03.md) | pipeline heavy |
`;
const TRACKER_10 = `# Applications Tracker
| # | Date | Company | Role | Location | Score | Status | PDF | Report | Notes |
|---|------|---------|------|----------|-------|--------|-----|--------|-------|
| 1 | 2026-06-01 | Initech | AI Engineer | Remote | 4.5/5 | Evaluated | ✅ | [1](../reports/001-initech-2026-06-01.md) | — |
`;
// Two unrelated rows share tracker number 5 (the #1704 bug: merge-tracker.mjs
// once trusted a stale TSV number as-is when it was numerically ahead of that
// run's max, even though the number was already used by an unrelated row
// merged in a separate, earlier invocation).
const TRACKER_DUP_NUM = `# Applications Tracker
| # | Date | Company | Role | Score | Status | PDF | Report | Notes |
|---|------|---------|------|-------|--------|-----|--------|-------|
| 5 | 2026-05-29 | University of Alberta | Curriculum Coordinator | 3.8/5 | Evaluated | ❌ | — | — |
| 5 | 2026-06-03 | Esri Canada | Manager Talent and Organizational Development | 4.1/5 | Evaluated | ❌ | — | — |
`;
// ── 1. Update by report number ──────────────────────────────────
{
const sb = makeSandbox(TRACKER_9);
const r = runSetStatus(['2', 'Applied'], sb);
const content = readTracker(sb);
if (r.code === 0 && /\| 2 \| 2026-06-02 \| Globex \| Platform Engineer \| 4.0\/5 \| Applied \|/.test(content)) {
pass('by-num: status updated to Applied');
} else {
fail(`by-num: code=${r.code}; row not updated correctly\n${r.stdout}${r.stderr}`);
}
if (content.includes('| 1 | 2026-06-01 | Acme | Backend Engineer | 4.2/5 | Evaluated |')) {
pass('by-num: other rows untouched');
} else {
fail('by-num: other rows were modified');
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 2. Update by company name (single match) ────────────────────
{
const sb = makeSandbox(TRACKER_9);
const r = runSetStatus(['globex', 'Responded'], sb);
if (r.code === 0 && /\| Globex \| Platform Engineer \| 4.0\/5 \| Responded \|/.test(readTracker(sb))) {
pass('by-company: fuzzy company resolves single match');
} else {
fail(`by-company: code=${r.code}\n${r.stdout}${r.stderr}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 3. State aliases resolve to canonical labels ────────────────
{
const sb = makeSandbox(TRACKER_9);
const r = runSetStatus(['2', 'aplicado'], sb);
if (r.code === 0 && /\| Globex \| Platform Engineer \| 4.0\/5 \| Applied \|/.test(readTracker(sb))) {
pass('alias: "aplicado" resolves to canonical "Applied"');
} else {
fail(`alias: code=${r.code}\n${r.stdout}${r.stderr}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 4. Non-canonical state rejected ─────────────────────────────
{
const sb = makeSandbox(TRACKER_9);
const before = readTracker(sb);
const r = runSetStatus(['2', 'Ghosted'], sb);
if (r.code === 1 && readTracker(sb) === before && /Evaluated/.test(r.stderr) && /SKIP/.test(r.stderr)) {
pass('bad-state: exit 1, valid states listed, tracker untouched');
} else {
fail(`bad-state: code=${r.code} (want 1)\n${r.stdout}${r.stderr}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 5. Not found: number and company ────────────────────────────
{
const sb = makeSandbox(TRACKER_9);
const r1 = runSetStatus(['99', 'Applied'], sb);
if (r1.code === 2) {
pass('not-found: unknown report number exits 2');
} else {
fail(`not-found num: code=${r1.code} (want 2)\n${r1.stdout}${r1.stderr}`);
}
const r2 = runSetStatus(['hooli', 'Applied'], sb);
if (r2.code === 2) {
pass('not-found: unknown company exits 2');
} else {
fail(`not-found company: code=${r2.code} (want 2)\n${r2.stdout}${r2.stderr}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 6. Ambiguous company: candidates listed, --role disambiguates ─
{
const sb = makeSandbox(TRACKER_9);
const r = runSetStatus(['acme', 'Applied'], sb);
if (r.code === 3 && r.stderr.includes('#1') && r.stderr.includes('#3') && r.stderr.includes('Backend Engineer') && r.stderr.includes('Data Engineer')) {
pass('ambiguous: exit 3 with numbered candidate list');
} else {
fail(`ambiguous: code=${r.code} (want 3)\n${r.stdout}${r.stderr}`);
}
const r2 = runSetStatus(['acme', 'Applied', '--role', 'Data Engineer'], sb);
if (r2.code === 0 && /\| 3 \| 2026-06-03 \| Acme \| Data Engineer \| 3.9\/5 \| Applied \|/.test(readTracker(sb))) {
pass('ambiguous: --role disambiguates to the right row');
} else {
fail(`ambiguous --role: code=${r2.code}\n${r2.stdout}${r2.stderr}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 6b. Duplicate tracker #: bare number refuses to guess (#1704) ─
{
const sb = makeSandbox(TRACKER_DUP_NUM);
const before = readTracker(sb);
const r = runSetStatus(['5', 'Rejected'], sb);
if (r.code === 3 && readTracker(sb) === before
&& r.stderr.includes('University of Alberta') && r.stderr.includes('Esri Canada')) {
pass('dup-num: bare #5 matching 2 rows exits 3, tracker untouched, both companies listed');
} else {
fail(`dup-num: code=${r.code} (want 3)\n${r.stdout}${r.stderr}`);
}
// --role disambiguates exactly like the company-selector ambiguous path.
const r2 = runSetStatus(['5', 'Rejected', '--role', 'Manager Talent and Organizational Development'], sb);
if (r2.code === 0 && /\| 5 \| 2026-06-03 \| Esri Canada \|.*\| Rejected \|/.test(readTracker(sb))) {
pass('dup-num: --role disambiguates to the right row');
} else {
fail(`dup-num --role: code=${r2.code}\n${r2.stdout}${r2.stderr}`);
}
// The OTHER row (University of Alberta) must stay untouched by the --role
// disambiguated write above.
if (readTracker(sb).includes('| 5 | 2026-05-29 | University of Alberta | Curriculum Coordinator | 3.8/5 | Evaluated |')) {
pass('dup-num: unrelated row with the same # untouched after disambiguation');
} else {
fail(`dup-num: unrelated row was modified\n${readTracker(sb)}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 6c. Duplicate tracker # + --json: structured candidates ─────
{
const sb = makeSandbox(TRACKER_DUP_NUM);
const r = runSetStatus(['5', 'Rejected', '--json'], sb);
let parsed = null;
try { parsed = JSON.parse(r.stdout || r.stderr); } catch {}
if (r.code === 3 && parsed && parsed.code === 'ambiguous' && Array.isArray(parsed.candidates)
&& parsed.candidates.length === 2) {
pass('dup-num json: structured ambiguous error with 2 candidates');
} else {
fail(`dup-num json: code=${r.code}\n${r.stdout}${r.stderr}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 7. Note append: idempotent, separator, pipe-sanitized ───────
{
const sb = makeSandbox(TRACKER_9);
runSetStatus(['1', 'Applied', '--note', 'sent via referral'], sb);
const after1 = readTracker(sb);
if (/\| strong infra fit; sent via referral \|/.test(after1)) {
pass('note: appended to existing notes with "; "');
} else {
fail(`note: append missing\n${after1}`);
}
// Retry with the identical note must not duplicate it.
runSetStatus(['1', 'Applied', '--note', 'sent via referral'], sb);
const after2 = readTracker(sb);
if ((after2.match(/sent via referral/g) || []).length === 1) {
pass('note: identical retry does not duplicate the note');
} else {
fail('note: retry duplicated the note');
}
// A note that itself contains "; " must also stay idempotent.
runSetStatus(['3', 'Responded', '--note', 'Called; left voicemail'], sb);
runSetStatus(['3', 'Responded', '--note', 'Called; left voicemail'], sb);
if ((readTracker(sb).match(/left voicemail/g) || []).length === 1) {
pass('note: retry with semicolon-bearing note does not duplicate');
} else {
fail('note: semicolon-bearing note duplicated on retry');
}
// Pipes/newlines in a note would corrupt the table — must be sanitized.
runSetStatus(['2', 'Applied', '--note', 'weird | note'], sb);
const after3 = readTracker(sb);
if (!/weird \| note/.test(after3) && /weird \/ note/.test(after3)) {
pass('note: literal pipe sanitized');
} else {
fail('note: pipe not sanitized');
}
// A literal newline would split the row into two lines and break the table:
// the stored row must stay a single line with the newline collapsed.
runSetStatus(['2', 'Applied', '--note', 'first line\nsecond line'], sb);
const after4 = readTracker(sb);
const row2 = after4.split('\n').filter(l => /^\| 2 \|/.test(l));
if (row2.length === 1 && row2[0].includes('first line second line')) {
pass('note: literal newline sanitized to a single table row');
} else {
fail(`note: newline broke the row\n${after4}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 7b. Note dedup is delimiter-aware, not substring ────────────
{
const sb = makeSandbox(TRACKER_9);
// Row 1 notes: "strong infra fit". A note that is a mere substring of an
// existing entry is NOT a duplicate — only whole "; "-delimited entries
// (or the entire field) count.
const r = runSetStatus(['1', 'Applied', '--note', 'infra'], sb);
if (r.code === 0 && /\| strong infra fit; infra \|/.test(readTracker(sb))) {
pass('note-dedup: substring of an existing entry still appends');
} else {
fail(`note-dedup: substring wrongly suppressed\n${readTracker(sb)}`);
}
// The exact same note re-added must still be suppressed. "infra" appears
// twice after the append (inside "infra fit" + the new entry); a duplicate
// append would make it three.
runSetStatus(['1', 'Applied', '--note', 'infra'], sb);
if ((readTracker(sb).match(/infra/g) || []).length === 2) {
pass('note-dedup: exact retry still idempotent');
} else {
fail(`note-dedup: exact retry duplicated\n${readTracker(sb)}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 8. No-op re-run: exit 0, file byte-identical ────────────────
{
const sb = makeSandbox(TRACKER_9);
runSetStatus(['2', 'Applied'], sb);
const before = readTracker(sb);
const r = runSetStatus(['2', 'Applied', '--json'], sb);
let parsed = null;
try { parsed = JSON.parse(r.stdout); } catch {}
if (r.code === 0 && readTracker(sb) === before && parsed && parsed.changed === false) {
pass('no-op: same status again exits 0, changed:false, file untouched');
} else {
fail(`no-op: code=${r.code} changed=${parsed?.changed}\n${r.stdout}${r.stderr}`);
}
// #1430 hook must fire only on a real transition into Applied — a re-run
// must not invite the consumer to seed a duplicate follow-up.
if (parsed && parsed.followupSeedCandidate === undefined) {
pass('no-op: followupSeedCandidate absent on idempotent Applied re-run');
} else {
fail(`no-op: followupSeedCandidate leaked on re-run\n${r.stdout}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 9. Dry-run: reports change, writes nothing ──────────────────
{
const sb = makeSandbox(TRACKER_9);
const before = readTracker(sb);
const r = runSetStatus(['2', 'Applied', '--dry-run', '--json'], sb);
let parsed = null;
try { parsed = JSON.parse(r.stdout); } catch {}
if (r.code === 0 && readTracker(sb) === before && parsed && parsed.changed === true && parsed.dryRun === true) {
pass('dry-run: reports change without writing');
} else {
fail(`dry-run: code=${r.code}\n${r.stdout}${r.stderr}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 10. JSON output shape + #1430 follow-up hook ────────────────
{
const sb = makeSandbox(TRACKER_9);
const r = runSetStatus(['2', 'Applied', '--json'], sb);
let parsed = null;
try { parsed = JSON.parse(r.stdout); } catch {}
if (parsed && parsed.num === 2 && parsed.company === 'Globex' && parsed.oldStatus === 'Evaluated'
&& parsed.newStatus === 'Applied' && parsed.changed === true && parsed.followupSeedCandidate === true) {
pass('json: full output shape + followupSeedCandidate on Applied');
} else {
fail(`json: bad shape\n${r.stdout}${r.stderr}`);
}
const r2 = runSetStatus(['1', 'Rejected', '--json'], sb);
let parsed2 = null;
try { parsed2 = JSON.parse(r2.stdout); } catch {}
if (parsed2 && parsed2.followupSeedCandidate === undefined) {
pass('json: no followupSeedCandidate on non-Applied transitions');
} else {
fail(`json: followupSeedCandidate leaked on Rejected\n${r2.stdout}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 11. Ambiguous + --json: machine-readable candidates ─────────
{
const sb = makeSandbox(TRACKER_9);
const r = runSetStatus(['acme', 'Applied', '--json'], sb);
let parsed = null;
try { parsed = JSON.parse(r.stdout || r.stderr); } catch {}
if (r.code === 3 && parsed && parsed.code === 'ambiguous' && Array.isArray(parsed.candidates)
&& parsed.candidates.length === 2 && parsed.candidates[0].num === 1) {
pass('json ambiguous: error object with candidates array');
} else {
fail(`json ambiguous: code=${r.code}\n${r.stdout}${r.stderr}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 12. 10-column Location layout ───────────────────────────────
{
const sb = makeSandbox(TRACKER_10);
const r = runSetStatus(['1', 'Interview', '--note', 'onsite loop scheduled'], sb);
const content = readTracker(sb);
if (r.code === 0 && /\| Initech \| AI Engineer \| Remote \| 4.5\/5 \| Interview \|/.test(content)
&& /onsite loop scheduled/.test(content)) {
pass('location-layout: 10-col tracker updates the right columns');
} else {
fail(`location-layout: code=${r.code}\n${content}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 13. Usage errors ────────────────────────────────────────────
{
const sb = makeSandbox(TRACKER_9);
const r = runSetStatus([], sb);
if (r.code === 1 && /Usage/i.test(r.stderr + r.stdout)) {
pass('usage: no args exits 1 with usage text');
} else {
fail(`usage: code=${r.code}\n${r.stdout}${r.stderr}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 13b. --note/--role must not eat a following flag as their value ─
{
const sb = makeSandbox(TRACKER_9);
const before = readTracker(sb);
// "--note --dry-run" once consumed "--dry-run" as the note text — silently
// disabling dry-run and turning a preview into a real write. It must be a
// usage error, and nothing may be written.
const r = runSetStatus(['2', 'Applied', '--note', '--dry-run'], sb);
if (r.code === 1 && readTracker(sb) === before) {
pass('flag-eating: --note followed by a flag exits 1 without writing');
} else {
fail(`flag-eating: code=${r.code} (want 1) written=${readTracker(sb) !== before}\n${r.stdout}${r.stderr}`);
}
// Missing value at the end of argv is the same usage error.
const r2 = runSetStatus(['2', 'Applied', '--role'], sb);
if (r2.code === 1 && /--role/.test(r2.stderr) && readTracker(sb) === before) {
pass('flag-eating: trailing --role without value exits 1');
} else {
fail(`flag-eating trailing: code=${r2.code}\n${r2.stdout}${r2.stderr}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 13c. Usage errors honor --json with a structured payload ────
{
const sb = makeSandbox(TRACKER_9);
const r = runSetStatus(['--json'], sb);
let parsed = null;
try { parsed = JSON.parse(r.stdout); } catch {}
if (r.code === 1 && parsed && parsed.code === 'usage' && typeof parsed.error === 'string') {
pass('json usage: structured usage-error payload on stdout');
} else {
fail(`json usage: code=${r.code} stdout=${r.stdout}\n${r.stderr}`);
}
// failUsage can fire mid-parse ("--note --json" fails before --json is
// reached), so JSON mode must be detected from the raw argv.
const r2 = runSetStatus(['2', 'Applied', '--note', '--json'], sb);
let parsed2 = null;
try { parsed2 = JSON.parse(r2.stdout); } catch {}
if (r2.code === 1 && parsed2 && parsed2.code === 'usage') {
pass('json usage: --json detected even when parsing fails mid-argv');
} else {
fail(`json usage mid-parse: code=${r2.code} stdout=${r2.stdout}\n${r2.stderr}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 14. Lock timeout: structured exit 4, JSON error, no write ──
{
const sb = makeSandbox(TRACKER_9);
const before = readTracker(sb);
// A live-owner lock (our own PID) can never be recovered, so the CLI must
// time out and fail through the structured error path instead of throwing.
mkdirSync(sb.lock, { recursive: true });
writeFileSync(join(sb.lock, 'owner.json'), JSON.stringify({ pid: process.pid, token: 'test', tracker: sb.tracker }));
const r = runSetStatus(['2', 'Applied', '--json'], sb, { CAREER_OPS_TRACKER_LOCK_TIMEOUT_MS: '300' });
let parsed = null;
try { parsed = JSON.parse(r.stdout); } catch {}
if (r.code === 4 && parsed && parsed.code === 'lock-timeout' && readTracker(sb) === before) {
pass('lock-timeout: exit 4 with structured JSON error, tracker untouched');
} else {
fail(`lock-timeout: code=${r.code} (want 4)\n${r.stdout}${r.stderr}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 14b. Non-timeout lock failure is NOT reported as lock-timeout ─
{
const sb = makeSandbox(TRACKER_9);
// Point the lock at a path whose parent is a regular FILE (inside tmpdir,
// with the required prefix, so trackerLockDirFor accepts it). mkdirSync
// then fails with ENOTDIR/ENOENT — a config error, not a busy lock — and
// must map to exit 1 / lock-error, keeping exit 4 reserved for retryable
// timeouts.
const blocker = join(sb.dir, 'career-ops-merge-tracker-blocker');
writeFileSync(blocker, 'not a directory');
const badLock = join(blocker, 'career-ops-merge-tracker-bad.lock');
const r = runSetStatus(['2', 'Applied', '--json'], { ...sb, lock: badLock });
let parsed = null;
try { parsed = JSON.parse(r.stdout); } catch {}
if (r.code === 1 && parsed && parsed.code === 'lock-error') {
pass('lock-error: filesystem lock failure exits 1, not lock-timeout');
} else {
fail(`lock-error: code=${r.code} (want 1) json=${parsed?.code}\n${r.stdout}${r.stderr}`);
}
rmSync(sb.dir, { recursive: true, force: true });
}
// ── 15. Orphaned recovery guard does not block stale-lock recovery ─
{
const dir = mkdtempSync(join(tmpdir(), 'co-setstatus-guard-'));
const lockDir = join(dir, 'career-ops-merge-tracker-guardtest.lock');
// Stale lock: dead owner PID → recoverable.
mkdirSync(lockDir, { recursive: true });
writeFileSync(join(lockDir, 'owner.json'), JSON.stringify({ pid: 999999999, token: 'dead', tracker: 'x' }));
// Orphaned guard left behind by a killed process (no owner.json → judged by age).
mkdirSync(`${lockDir}.recover`, { recursive: true });
await new Promise(r => setTimeout(r, 150));
try {
const lock = await acquireTrackerLock(lockDir, { timeoutMs: 3000, retryMs: 25, staleMs: 50, tracker: 'x' });
if (lock.staleRecovered) {
pass('recover-guard: orphaned guard is aged out and stale lock still recovers');
} else {
fail('recover-guard: lock acquired but stale recovery did not run');
}
lock.release();
} catch (e) {
fail(`recover-guard: acquisition failed — orphaned guard blocked recovery (${e.message})`);
}
rmSync(dir, { recursive: true, force: true });
}
// ── 16. Write failure surfaces as a structured error, not a stack ─
{
if (process.platform !== 'win32' && process.getuid?.() === 0) {
pass('write-failure: skipped (running as root — directory permissions are not enforced)');
} else {
const dir = mkdtempSync(join(tmpdir(), 'co-setstatus-wf-'));
const roDir = join(dir, 'ro');
mkdirSync(roDir);
const tracker = join(roDir, 'applications.md');
writeFileSync(tracker, TRACKER_9);
const lock = join(dir, 'career-ops-merge-tracker-wf.lock');
// Make the tracker's directory readable but unwritable, so the atomic
// temp-file write fails after a successful read. On Windows, directory
// read-only bits don't block file creation — deny write-data/append-data
// for Everyone (*S-1-1-0) via icacls instead.
const denyWrite = () => process.platform === 'win32'
? execFileSync('icacls', [roDir, '/deny', '*S-1-1-0:(WD,AD)'])
: chmodSync(roDir, 0o555);
const restore = () => process.platform === 'win32'
? execFileSync('icacls', [roDir, '/remove:d', '*S-1-1-0'])
: chmodSync(roDir, 0o755);
denyWrite();
try {
const r = runSetStatus(['2', 'Applied', '--json'], { tracker, lock });
let parsed = null;
try { parsed = JSON.parse(r.stdout); } catch {}
if (r.code === 1 && parsed && parsed.code === 'write-failure') {
pass('write-failure: structured JSON error instead of a raw stack');
} else {
fail(`write-failure: code=${r.code} json=${parsed?.code}\n${r.stdout}${r.stderr}`);
}
} finally {
restore();
rmSync(dir, { recursive: true, force: true });
}
}
}
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);