mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-16 08:06:24 +03:00
Fix: close fetch sockets before context helper exit (#573)
On Windows/Node 24, a live undici keep-alive from the update-check fetch aborted libuv during teardown after valid stdout. Destroy the dispatcher first, matching concept-seed. AI assistance: implemented with Cursor Grok 4.6. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1013,6 +1013,16 @@ async function fetchLatestSkillVersion() {
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy fetch's global undici dispatcher before process.exit(): a live
|
||||
// keep-alive socket trips a libuv assertion on Windows/Node 24 after a
|
||||
// successful boot (nodejs/node#56645, issue #573).
|
||||
async function destroyFetchDispatcher() {
|
||||
const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')];
|
||||
if (dispatcher && typeof dispatcher.destroy === 'function') {
|
||||
try { await dispatcher.destroy(); } catch { /* exit regardless */ }
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -1160,6 +1170,7 @@ async function cli() {
|
||||
appendStalenessDirective(parts, ctx, cliOptions);
|
||||
if (updateDirective) parts.push(updateDirective);
|
||||
process.stdout.write(parts.join('\n\n---\n\n') + '\n');
|
||||
await destroyFetchDispatcher();
|
||||
process.exit(0);
|
||||
}
|
||||
const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`];
|
||||
@@ -1207,6 +1218,8 @@ async function cli() {
|
||||
}
|
||||
if (updateDirective) parts.push(updateDirective);
|
||||
process.stdout.write(parts.join('\n\n---\n\n') + '\n');
|
||||
await destroyFetchDispatcher();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function parseCliOptions(args) {
|
||||
|
||||
@@ -1588,4 +1588,39 @@ describe('context.mjs update check', () => {
|
||||
assert.equal(typeof cache.lastCheck, 'number'); // stamped so we don't re-poll every boot
|
||||
assert.equal(cache.latestVersion, undefined); // nothing learned
|
||||
});
|
||||
|
||||
// Targeted live-fetch boot: the Windows abort in issue #573 fired after
|
||||
// stdout was already complete, so the contract is exit 0 with the full
|
||||
// context still on stdout.
|
||||
it('exits 0 after a targeted live-fetch boot writes full context', async () => {
|
||||
const { srv, host } = await startStub({ skills: '2.0.0' });
|
||||
try {
|
||||
const { skillScript, project, env } = setup({}, { host });
|
||||
fs.writeFileSync(
|
||||
path.join(project, 'package.json'),
|
||||
JSON.stringify({ private: true, workspaces: ['packages/*'] }),
|
||||
);
|
||||
const jervPi = path.join(project, 'packages', 'jerv-pi');
|
||||
fs.mkdirSync(jervPi, { recursive: true });
|
||||
fs.writeFileSync(path.join(jervPi, 'PRODUCT.md'), '# Jerv Pi product\n');
|
||||
|
||||
const result = await new Promise((resolveRun, rejectRun) => {
|
||||
const child = spawn(process.execPath, [skillScript, '--target', 'packages/jerv-pi'], {
|
||||
cwd: project,
|
||||
env,
|
||||
});
|
||||
let stdout = '';
|
||||
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
||||
child.on('error', rejectRun);
|
||||
child.on('close', (status) => resolveRun({ status, stdout }));
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0);
|
||||
assert.match(result.stdout, /RESOLVED_CONTEXT:/);
|
||||
assert.match(result.stdout, /# Jerv Pi product/);
|
||||
assert.match(result.stdout, /UPDATE_AVAILABLE/);
|
||||
} finally {
|
||||
srv.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user