fix: doctor's FAIL verdict was zeroed by the owned exit — sweep stragglers + class pin

The merged-state suite caught it: doctor --fast --json reported FAIL but
exited 0. Master's v0.42.41.0 brought raw `process.exitCode =` writes
(doctor.ts hasFail ternary, extract.ts) that the #2084 verdict-owning exit
silently zeroes — getCliExitCode() deliberately never reads ambient
process.exitCode (the PGLite-Emscripten pollution defense), so any setter
that bypasses setCliExitCode reports success on failure.

Swept both sites and added the structural class pin: a test greps src/ for
raw `process.exitCode =` outside cli-force-exit.ts, so the next merge that
introduces one fails loudly instead of lying about exit codes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-06-12 06:41:54 -07:00
co-authored by Claude Fable 5
parent 62d54056c0
commit de4c29d037
3 changed files with 25 additions and 5 deletions
+7 -4
View File
@@ -1,5 +1,6 @@
import type { BrainEngine } from '../core/engine.ts';
import * as db from '../core/db.ts';
import { setCliExitCode } from '../core/cli-force-exit.ts';
import { LATEST_VERSION, getIdleBlockers } from '../core/migrate.ts';
import { checkResolvable } from '../core/check-resolvable.ts';
import { autoFixDryViolations, type AutoFixReport, type FixOutcome } from '../core/dry-fix.ts';
@@ -7225,10 +7226,12 @@ export async function runDoctor(
} catch { /* best-effort */ }
}
// Use process.exitCode instead of process.exit() so cleanup handlers
// (e.g. Bun unload events, open database connections) still run before
// the process terminates. process.exit() is a hard kill that bypasses them.
process.exitCode = hasFail ? 1 : 0;
// Use the owned exit verdict instead of process.exit() so cleanup handlers
// (e.g. Bun unload events, open database connections) still run before the
// process terminates — and so the deliberate flush-exit (#2084) reports
// doctor's verdict: a RAW process.exitCode write is silently zeroed by
// getCliExitCode() at the entrypoint (the PGLite-pollution defense).
setCliExitCode(hasFail ? 1 : 0);
}
// ---------------------------------------------------------------------------
+2 -1
View File
@@ -28,6 +28,7 @@
* pagination). FS-source preserves the original v0.10.1 walker behavior.
*/
import { setCliExitCode } from '../core/cli-force-exit.ts';
import { readFileSync, readdirSync, lstatSync, existsSync } from 'fs';
import { join, relative, dirname } from 'path';
import type { BrainEngine, LinkBatchInput, TimelineBatchInput } from '../core/engine.ts';
@@ -864,7 +865,7 @@ Status (v0.42):
(r.first_batch_error ? ` (first error: ${r.first_batch_error})` : '') +
` — timeline is incomplete.`,
);
process.exitCode = 1;
setCliExitCode(1);
}
} else if (byMention || ner) {
// v0.41.18.0 (T7): combined --by-mention + --ner walk shares one
+16
View File
@@ -137,3 +137,19 @@ describe('flushStdoutThenExit — deliberate exit after output drains', () => {
expect(stream.drainListeners()).toBe(0);
});
});
describe('exit-verdict ownership — no raw process.exitCode assignments (#2084 class pin)', () => {
test('every exit-code write in src/ routes through setCliExitCode', async () => {
// A RAW `process.exitCode = N` is silently ZEROED by the deliberate
// flush-exit: getCliExitCode() reads only gbrain's owned verdict (the
// PGLite-Emscripten-pollution defense), so a command that bypasses the
// setter reports success on failure. Caught live: doctor's FAIL path
// exited 0 after the v0.42.41.0 merge introduced a raw write.
const { execSync } = await import('child_process');
const hits = execSync(
`grep -rn "process.exitCode = " src --include='*.ts' | grep -v "core/cli-force-exit.ts" || true`,
{ encoding: 'utf-8', cwd: new URL('..', import.meta.url).pathname },
).trim();
expect(hits).toBe('');
});
});