mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-21 10:36:27 +03:00
Fix: drain context stdout before process.exit (#573)
process.exit after a queued write truncated boot output on a backpressured pipe. Await the write callback, then close the fetch dispatcher. AI assistance: implemented with Cursor Grok 4.6. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1023,6 +1023,17 @@ async function destroyFetchDispatcher() {
|
||||
}
|
||||
}
|
||||
|
||||
// Drain the boot payload before process.exit(): a live pipe that has not
|
||||
// flushed yet is truncated when Node tears down (issue #573 review). Then
|
||||
// close fetch so Windows teardown does not abort on the keep-alive socket.
|
||||
async function finishCli(output) {
|
||||
await new Promise((resolve) => {
|
||||
process.stdout.write(output, () => resolve());
|
||||
});
|
||||
await destroyFetchDispatcher();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Two instructions used to sit in one directive: ask, and "if they agree, run
|
||||
// it". Nothing gated the second on an answer, and the same sentence said to
|
||||
// continue without waiting, so a run that could never establish agreement was
|
||||
@@ -1169,9 +1180,7 @@ async function cli() {
|
||||
appendImageToolsDirective(parts);
|
||||
appendStalenessDirective(parts, ctx, cliOptions);
|
||||
if (updateDirective) parts.push(updateDirective);
|
||||
process.stdout.write(parts.join('\n\n---\n\n') + '\n');
|
||||
await destroyFetchDispatcher();
|
||||
process.exit(0);
|
||||
await finishCli(parts.join('\n\n---\n\n') + '\n');
|
||||
}
|
||||
const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`];
|
||||
if (ctx.hasDesign) {
|
||||
@@ -1217,9 +1226,7 @@ async function cli() {
|
||||
}
|
||||
}
|
||||
if (updateDirective) parts.push(updateDirective);
|
||||
process.stdout.write(parts.join('\n\n---\n\n') + '\n');
|
||||
await destroyFetchDispatcher();
|
||||
process.exit(0);
|
||||
await finishCli(parts.join('\n\n---\n\n') + '\n');
|
||||
}
|
||||
|
||||
function parseCliOptions(args) {
|
||||
|
||||
@@ -1060,6 +1060,27 @@ describe('context.mjs CLI', () => {
|
||||
assert.match(res.stdout, /detect\.mjs --json <changed targets>/);
|
||||
});
|
||||
|
||||
it('drains stdout before exit when the parent pipe is paused', async () => {
|
||||
const MARKER = 'END_MARKER_573';
|
||||
write('PRODUCT.md', `# Acme\n\n${'x'.repeat(256 * 1024)}\n\n${MARKER}\n`);
|
||||
const child = spawn(process.execPath, [SCRIPT_PATH], {
|
||||
cwd: scratch,
|
||||
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
||||
});
|
||||
let stdout = '';
|
||||
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
||||
child.stdout.pause();
|
||||
const resume = setTimeout(() => child.stdout.resume(), 100);
|
||||
const status = await new Promise((resolve, reject) => {
|
||||
child.on('error', reject);
|
||||
child.on('close', resolve);
|
||||
});
|
||||
clearTimeout(resume);
|
||||
assert.equal(status, 0);
|
||||
assert.match(stdout, /END_MARKER_573/);
|
||||
assert.match(stdout, /RESOLVED_CONTEXT:/);
|
||||
});
|
||||
|
||||
// The build-path preference rides the unified config beside hook and
|
||||
// detector settings. The local file wins because whether a machine can
|
||||
// generate images is a property of that machine, not of the committed
|
||||
|
||||
Reference in New Issue
Block a user