* feat(#3907): gates report no-input instead of asserting a verdict they never reached The three stdin-reading gates bound 2 to a stdin read error only, with no arm for stdin closed at zero bytes - so empty input flowed into the detector, found nothing, and exited 1, which each module's own comment defines as a negative verdict. An unset PHASE_SECTION made the UI gate assert the phase has no UI. Empty and whitespace-only input now exit NO_INPUT, and a read error exits UNAVAILABLE rather than a locally-invented 2, both resolved through the registry and delivered by terminateNow. The exit code was only half of it: under --json the same input emitted {detected:false}, byte-identical to the fabricated payload #3909 exists to fix, and the blocking coverage gate reads that payload. Empty input now emits the in-tree {skipped:true,reason} form with no detected key at all. teams-status is excluded: it never reads stdin and has no invented 2, so the four-module framing in the issue and ADR is wrong. The dead root bin/lib/ui-safety-gate.cjs is deleted - no installer reference, no workflow invocation, and the live fallback chains are for other modules. Its removal restores the unit tests to the module that actually ships; they had been asserting the stale copy's two-field shape, which is why it drifted unnoticed. * fix(#3907): drive gate tests through the process seam, and make removed-but-needed basename-precise CONTRIBUTING requires every subprocess go through tests/helpers/process-seam.cjs; two of the three gate suites hand-rolled spawnSync while the third, added in the same change, used runNode correctly for the identical injection case. Converted the blocks this change added, leaving pre-existing ones alone. Deleting one of two files sharing a basename made lint-removed-but-needed report 14 references that were all to the surviving canonical module - the false-positive class its own docstring names. It now matches on the deleted file's full path when a surviving file shares its basename, which is more precise rather than weaker: a genuine full-path reference still fails, and behaviour is unchanged when no basename collides. It immediately caught a docstring on this branch that spelled the deleted path. * test(#3907): update the one existing assertion that pinned the old empty-stdin verdict A pre-existing test asserted exit 1 on empty stdin - the defect this phase removes - and was missed because the change added new blocks without auditing existing ones pinning the old contract. Audited the rest: the other three status-1 assertions in that file all feed real input and are the genuine-negative controls that must keep returning 1, so exactly one was stale. The retired 2 is gone from the describe's contract comment too. * chore(#3907): backfill changeset pr number --------- Co-authored-by: sim <sim@local>
42 lines
1.7 KiB
JavaScript
42 lines
1.7 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Deterministic stdin-read-failure injector for exit-code tests (ADR-3889
|
|
* Phase 3, #3907).
|
|
*
|
|
* Cross-platform IO-failure injection must be done via method monkeypatching,
|
|
* never mode-bit tricks (`fs.chmodSync(path, 0o000)` is bypassed by root in
|
|
* Docker/CI and would assert zero coverage there). This module replaces
|
|
* `process.stdin` with a fake EventEmitter BEFORE the target CLI module loads
|
|
* — load it with `node -r tests/helpers/inject-stdin-error.cjs <target.cjs>`
|
|
* — so the target's own `process.stdin.on('data'|'end'|'error', ...)`
|
|
* subscriptions attach to the fake, and the fake emits ONLY `'error'`
|
|
* (never `'data'`/`'end'`), deterministically exercising the module's
|
|
* stdin-read-failure arm without any real pipe/fd involved.
|
|
*
|
|
* `-r` (require preload) runs before Node loads the main module, so the
|
|
* spawned CLI still sees `require.main === module` for ITSELF — the
|
|
* preload script's own module identity never satisfies that check — which is
|
|
* what lets the target's `if (require.main === module)` CLI entry point run
|
|
* normally, reading from (the now-fake) `process.stdin`.
|
|
*/
|
|
|
|
const { EventEmitter } = require('node:events');
|
|
|
|
const fakeStdin = new EventEmitter();
|
|
fakeStdin.setEncoding = () => {};
|
|
fakeStdin.resume = () => {};
|
|
fakeStdin.pause = () => {};
|
|
|
|
Object.defineProperty(process, 'stdin', {
|
|
value: fakeStdin,
|
|
configurable: true,
|
|
});
|
|
|
|
// Fire on the next tick — after the target module's require.main===module
|
|
// block has synchronously registered its 'data'/'end'/'error' listeners —
|
|
// so the 'error' event is never emitted to zero listeners.
|
|
setImmediate(() => {
|
|
fakeStdin.emit('error', new Error('simulated stdin read failure (injected by inject-stdin-error.cjs)'));
|
|
});
|