From 9252f61e474aa5ada302ddaa60863c63361367ab Mon Sep 17 00:00:00 2001 From: JF Date: Thu, 20 Aug 2026 15:32:22 -0400 Subject: [PATCH 1/3] fix(vendor): surface mid-extraction death as exit 1 in vendor-codelldb.js Fixes #389. A stalled extract-zip promise (yauzl floating-promise design: resolves only on 'close', rejects only on 'error') left main() forever pending; the event loop drained and Node exited 0 with no failure output, bypassing every deliberate failure path. Three independent layers: - extractVsixWithWatchdog: Promise.race of extractZip against a timeout (default 120s, CODELLDB_EXTRACT_TIMEOUT_MS). The pending timer keeps the event loop alive during extraction, and a stall becomes a rejection that flows through the existing retry loop and failure summary. - process.on('exit') guard (registered only when invoked directly): exiting 0 before main() settles forces exit code 1 with a requested/completed/ unresolved-platforms diagnostic. - invokedDirectly now realpaths both sides so a symlinked invocation path cannot silently skip main(). Test hooks CODELLDB_TEST_STALL_EXTRACTION / CODELLDB_TEST_SIMULATE_DRAIN enable network-free regression coverage; new spawn-based tests pin the whole-process exit-code contract (skip=0, drain=1, summary failure=1). Co-Authored-By: Claude Fable 5 --- docs/rust-debugging.md | 1 + .../scripts/vendor-codelldb.js | 121 ++++++++++++++-- .../tests/vendor-codelldb-script.test.ts | 136 ++++++++++++++++++ 3 files changed, 243 insertions(+), 15 deletions(-) create mode 100644 packages/codelldb-common/tests/vendor-codelldb-script.test.ts diff --git a/docs/rust-debugging.md b/docs/rust-debugging.md index 9809afca..de36420f 100644 --- a/docs/rust-debugging.md +++ b/docs/rust-debugging.md @@ -46,6 +46,7 @@ SKIP_ADAPTER_VENDOR=true pnpm install - `CODELLDB_VENDOR_ALL=false`: opt out of the "vendor every platform" default and fall back to host-only downloads - `CODELLDB_VENDOR_LOCAL_ONLY=true`: disable network downloads entirely and fail if the requested platform isn't already vendored (used by Docker builds that copy pre-fetched artifacts) - `CODELLDB_KEEP_TEMP=true`: retain the downloaded VSIX and extracted temp folders for inspection +- `CODELLDB_EXTRACT_TIMEOUT_MS`: watchdog for VSIX extraction (default `120000`); a stalled unzip is aborted, retried, and surfaced as a failure instead of dying silently - `SKIP_ADAPTER_VENDOR=true`: opt out entirely (used by CI jobs that pre-bake artifacts) ### Troubleshooting vendoring diff --git a/packages/codelldb-common/scripts/vendor-codelldb.js b/packages/codelldb-common/scripts/vendor-codelldb.js index 0da0f16c..150592d5 100644 --- a/packages/codelldb-common/scripts/vendor-codelldb.js +++ b/packages/codelldb-common/scripts/vendor-codelldb.js @@ -12,13 +12,14 @@ * - CODELLDB_VENDOR_ALL: Set to 'true' to vendor all platforms in CI, or 'false' for current-only locally * - CODELLDB_FORCE_REBUILD: Set to 'true' to force re-vendor * - CODELLDB_VENDOR_LOCAL_ONLY: Set to 'true' to forbid downloads (use existing artifacts only) + * - CODELLDB_EXTRACT_TIMEOUT_MS: Watchdog for VSIX extraction (default: 120000) */ import fs from 'fs/promises'; import path from 'path'; import { Readable, Transform } from 'stream'; import { pipeline } from 'stream/promises'; -import { createWriteStream, createReadStream, readFileSync } from 'fs'; +import { createWriteStream, createReadStream, readFileSync, realpathSync } from 'fs'; import extractZip from 'extract-zip'; import ProgressBar from 'progress'; import { fileURLToPath } from 'url'; @@ -49,6 +50,13 @@ const IS_CI = process.env.CI === 'true'; const SKIP_VENDOR = process.env.SKIP_ADAPTER_VENDOR === 'true'; const KEEP_TEMP = process.env.CODELLDB_KEEP_TEMP === 'true'; const LOCAL_ONLY = process.env.CODELLDB_VENDOR_LOCAL_ONLY === 'true'; +const parsedExtractTimeout = Number(process.env.CODELLDB_EXTRACT_TIMEOUT_MS); +const EXTRACT_TIMEOUT_MS = + Number.isFinite(parsedExtractTimeout) && parsedExtractTimeout > 0 ? parsedExtractTimeout : 120000; +// Test-only hooks (issue #389 regression coverage): simulate a stalled extraction / +// a fully drained event loop without touching the network. +const TEST_STALL_EXTRACTION = process.env.CODELLDB_TEST_STALL_EXTRACTION === 'true'; +const TEST_SIMULATE_DRAIN = process.env.CODELLDB_TEST_SIMULATE_DRAIN === 'true'; const RELEASE_BASE_URLS = [ process.env.CODELLDB_RELEASE_BASE?.replace(/\/$/, '') || 'https://github.com/vadimcn/vscode-lldb/releases/download', @@ -414,6 +422,40 @@ async function downloadFile(url, destPath, maxRetries = 3) { } } +/** + * Extract a VSIX with a watchdog timer (issue #389). + * + * extract-zip's promise settles only on yauzl 'close'/'error'; a stalled entry + * pump leaves it forever pending with nothing else on the event loop, so Node + * drains and exits 0 before any failure path runs. The pending watchdog timer + * keeps the event loop alive for the whole extraction window, and converts a + * stall into a rejection that flows through the normal retry/failure paths. + */ +async function extractVsixWithWatchdog(vsixPath, destDir, vsixName) { + const work = TEST_STALL_EXTRACTION + ? new Promise(() => {}) + : extractZip(vsixPath, { dir: destDir }); + // The abandoned extraction may still reject after the watchdog fires; + // swallow it so it cannot surface as a fatal unhandledRejection later. + work.catch(() => {}); + let timer; + const watchdog = new Promise((_, reject) => { + timer = setTimeout(() => { + reject(new Error( + `Extraction of ${vsixName} did not complete within ${EXTRACT_TIMEOUT_MS}ms ` + + `(likely a stalled unzip stream - issue #389). ` + + `Re-run with CODELLDB_KEEP_TEMP=true to inspect ${destDir}, ` + + `or raise CODELLDB_EXTRACT_TIMEOUT_MS if this machine is just slow.` + )); + }, EXTRACT_TIMEOUT_MS); + }); + try { + await Promise.race([work, watchdog]); + } finally { + clearTimeout(timer); + } +} + /** * Extract VSIX and copy required files */ @@ -442,8 +484,10 @@ async function extractAndCopyFiles(vsixPath, platform, platformInfo, vsixName) { // Extract VSIX (which is a zip file) log(`Extracting ${vsixName}...`); - await extractZip(vsixPath, { dir: tempExtractDir }); - + const extractStartedAt = Date.now(); + await extractVsixWithWatchdog(vsixPath, tempExtractDir, vsixName); + log(`Extracted ${vsixName} in ${Date.now() - extractStartedAt}ms`); + // Target directories for adapter and lldb const targetAdapterDir = path.join(VENDOR_DIR, platformInfo.targetDir, 'adapter'); const targetLldbDir = path.join(VENDOR_DIR, platformInfo.targetDir, 'lldb'); @@ -557,8 +601,13 @@ async function isAlreadyVendored(platform, platformInfo) { * Download and extract CodeLLDB for a specific platform */ async function downloadAndExtract(platform) { + if (TEST_SIMULATE_DRAIN) { + log(`TEST HOOK: simulating stalled vendoring for ${platform} (event-loop drain, issue #389)`); + await new Promise(() => {}); + } + const platformInfo = PLATFORMS[platform]; - + if (!platformInfo) { logWarn(`Unsupported platform: ${platform}`); return false; @@ -621,6 +670,9 @@ async function downloadAndExtract(platform) { } catch (error) { lastError = error; logWarn(`Attempt with ${vsixName} via ${baseUrl} failed: ${error.message}`); + if (error?.stack) { + logWarn(error.stack); + } await invalidateCacheEntry(vsixName).catch(() => {}); } finally { if (KEEP_TEMP) { @@ -710,6 +762,9 @@ async function main() { // Check if vendoring should be skipped if (SKIP_VENDOR) { log('Skipping vendoring (SKIP_ADAPTER_VENDOR=true)'); + // Must be marked complete BEFORE process.exit: the premature-exit guard + // honors process.exitCode mutations made inside 'exit' listeners. + runState.completedNormally = true; process.exit(0); } @@ -751,10 +806,11 @@ async function main() { // Determine which platforms to vendor const selectedPlatforms = determinePlatforms(); - + runState.requested = selectedPlatforms; + log(`Platforms to vendor: ${selectedPlatforms.join(', ')}\n`); - - const results = []; + + const results = runState.results; for (const platform of selectedPlatforms) { const success = await downloadAndExtract(platform); results.push({ platform, success }); @@ -802,18 +858,53 @@ async function main() { } } -const invokedDirectly = Boolean(process.argv[1] && path.resolve(process.argv[1]) === __filename); +// Tracks run progress so the premature-exit guard can tell a finished run from +// one whose event loop drained mid-vendoring (issue #389). +const runState = { requested: [], results: [], completedNormally: false }; + +function resolveReal(p) { + try { + return realpathSync(p); + } catch { + return path.resolve(p); + } +} + +const invokedDirectly = Boolean( + process.argv[1] && resolveReal(process.argv[1]) === resolveReal(__filename) +); // Run if called directly if (invokedDirectly) { - main().catch(error => { - logError(`Fatal error: ${error.message}`); - if (error?.stack) { - logError(error.stack); + // Safety net: if an async operation stalls and the event loop drains, Node + // exits 0 without main() ever settling. Force a diagnostic + exit code 1. + process.on('exit', (code) => { + if (code === 0 && !runState.completedNormally) { + const done = runState.results.filter(r => r.success).map(r => r.platform); + const unresolved = runState.requested.filter(p => !done.includes(p)); + logError('Premature exit: process is exiting with code 0 before vendoring finished (issue #389).'); + logError( + `Requested: ${runState.requested.join(', ') || ''}; ` + + `completed: ${done.join(', ') || ''}; unresolved: ${unresolved.join(', ') || ''}` + ); + logError('An async operation likely stalled and the event loop drained. Forcing exit code 1.'); + process.exitCode = 1; } - logWarn('Rust debugging will not be available'); - process.exitCode = 1; }); + + main() + .then(() => { + runState.completedNormally = true; + }) + .catch(error => { + logError(`Fatal error: ${error.message}`); + if (error?.stack) { + logError(error.stack); + } + logWarn('Rust debugging will not be available'); + runState.completedNormally = true; + process.exitCode = 1; + }); } -export { downloadAndExtract, PLATFORMS, CODELLDB_VERSION }; +export { downloadAndExtract, extractVsixWithWatchdog, PLATFORMS, CODELLDB_VERSION }; diff --git a/packages/codelldb-common/tests/vendor-codelldb-script.test.ts b/packages/codelldb-common/tests/vendor-codelldb-script.test.ts new file mode 100644 index 00000000..c87b0aff --- /dev/null +++ b/packages/codelldb-common/tests/vendor-codelldb-script.test.ts @@ -0,0 +1,136 @@ +/** + * Tests for scripts/vendor-codelldb.js exit-code contract (issue #389) + * + * The observed defect: a mid-extraction stall left extract-zip's promise + * forever pending, the event loop drained, and Node exited 0 without any + * failure output. These tests pin the whole-process exit codes via spawned + * children (network-free, using the script's test hooks) and unit-test the + * extraction watchdog via direct import. + */ +import { describe, it, expect, afterAll, afterEach, vi } from 'vitest'; +import { spawnSync } from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const scriptPath = path.resolve(__dirname, '../scripts/vendor-codelldb.js'); + +const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-vendor-script-test-')); + +interface RunResult { + status: number | null; + output: string; +} + +function runScript(extraEnv: Record): RunResult { + const result = spawnSync(process.execPath, [scriptPath], { + env: { + ...process.env, + // Neutralize anything the parent environment (dev box or CI runner) + // could leak into platform selection or exit semantics. + CI: '', + SKIP_ADAPTER_VENDOR: '', + CODELLDB_PLATFORMS: '', + CODELLDB_VENDOR_ALL: '', + CODELLDB_FORCE_REBUILD: '', + CODELLDB_VENDOR_LOCAL_ONLY: '', + CODELLDB_TEST_SIMULATE_DRAIN: '', + CODELLDB_TEST_STALL_EXTRACTION: '', + CODELLDB_CACHE_DIR: path.join(tempDir, 'cache'), + ...extraEnv + }, + encoding: 'utf8', + timeout: 60_000 + }); + return { status: result.status, output: `${result.stdout ?? ''}\n${result.stderr ?? ''}` }; +} + +async function importScript(env: Record): Promise> { + vi.resetModules(); + for (const [key, value] of Object.entries(env)) { + vi.stubEnv(key, value); + } + return (await import('../scripts/vendor-codelldb.js')) as Record; +} + +afterEach(() => { + vi.unstubAllEnvs(); +}); + +afterAll(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); +}); + +describe('vendor-codelldb.js exit codes (spawned)', () => { + it('exits 0 when vendoring is skipped via SKIP_ADAPTER_VENDOR', () => { + const { status, output } = runScript({ SKIP_ADAPTER_VENDOR: 'true' }); + expect(output).toContain('Skipping vendoring'); + expect(output).not.toContain('Premature exit'); + expect(status).toBe(0); + }); + + it('exits 1 with a diagnostic when the event loop drains mid-vendoring (issue #389 repro)', () => { + const { status, output } = runScript({ + CODELLDB_TEST_SIMULATE_DRAIN: 'true', + CODELLDB_PLATFORMS: 'linux-x64' + }); + expect(output).toContain('Premature exit'); + expect(output).toContain('unresolved: linux-x64'); + expect(status).toBe(1); + }); + + it('exits 1 through the failure summary (not the guard) on a normal failure', () => { + const { status, output } = runScript({ + CODELLDB_VENDOR_LOCAL_ONLY: 'true', + CODELLDB_FORCE_REBUILD: 'true', + CODELLDB_PLATFORMS: 'linux-x64' + }); + expect(output).toContain('Failed to vendor: linux-x64'); + // The guard must not double-report a failure the summary already surfaced. + expect(output).not.toContain('Premature exit'); + expect(status).toBe(1); + }); +}); + +describe('extractVsixWithWatchdog (imported)', () => { + it('converts a stalled extraction into a rejection after the timeout', async () => { + const mod = await importScript({ + CODELLDB_TEST_STALL_EXTRACTION: 'true', + CODELLDB_EXTRACT_TIMEOUT_MS: '100' + }); + const extractVsixWithWatchdog = mod.extractVsixWithWatchdog as ( + vsixPath: string, + destDir: string, + vsixName: string + ) => Promise; + await expect( + extractVsixWithWatchdog(path.join(tempDir, 'missing.vsix'), path.join(tempDir, 'out'), 'test.vsix') + ).rejects.toThrow(/did not complete within 100ms/); + }); + + it('resolves on a successful extraction and clears the watchdog timer', async () => { + // Minimal valid zip: the 22-byte end-of-central-directory record. + const emptyZip = Buffer.concat([Buffer.from('504b0506', 'hex'), Buffer.alloc(18)]); + const zipPath = path.join(tempDir, 'empty.zip'); + fs.writeFileSync(zipPath, emptyZip); + const mod = await importScript({}); + const extractVsixWithWatchdog = mod.extractVsixWithWatchdog as ( + vsixPath: string, + destDir: string, + vsixName: string + ) => Promise; + await expect( + extractVsixWithWatchdog(zipPath, path.join(tempDir, 'empty-out'), 'empty.zip') + ).resolves.toBeUndefined(); + // A leaked watchdog timer would keep the fork alive past the suite; the + // clean resolve above plus normal worker shutdown covers it. + }); + + it('registers no exit listener when merely imported', async () => { + const before = process.listenerCount('exit'); + await importScript({}); + expect(process.listenerCount('exit')).toBe(before); + }); +}); From 982f904cfbf6bb3669966a78b92a3b9c92634880 Mon Sep 17 00:00:00 2001 From: JF Date: Thu, 20 Aug 2026 15:51:58 -0400 Subject: [PATCH 2/3] docs: changelog entry for the vendor-script exit-0 fix Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49c5726c..6ef4b1ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- **vendor-codelldb.js can no longer die silently with exit 0** — the script-level root cause behind #389 (the Docker workaround shipped in v0.24.2 stands): a stalled extract-zip promise drained the event loop and Node exited 0 with no failure output. Extraction now runs under a watchdog (default 120 s, `CODELLDB_EXTRACT_TIMEOUT_MS`) whose pending timer keeps the event loop alive and converts a stall into a normal retry/failure, and a premature-exit guard forces exit code 1 with a requested/completed/unresolved-platforms diagnostic if the process would otherwise exit 0 before vendoring finished (#389) + ## [0.24.2] - 2026-08-19 ### Fixed From 4250689e43be9d77edefc35cde01247be7c44c86 Mon Sep 17 00:00:00 2001 From: JF Date: Thu, 20 Aug 2026 16:40:03 -0400 Subject: [PATCH 3/3] refactor(vendor): parameter seam for the extraction watchdog; cap spawn-test timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups: extractVsixWithWatchdog takes an opts seam (extractFn/timeoutMs) so the unit tests inject a stalling extractor directly instead of routing through a production env hook — the CODELLDB_TEST_STALL_EXTRACTION variable is gone, and the import tests drop their stubEnv/re-import gymnastics. CODELLDB_TEST_SIMULATE_DRAIN stays: the premature-exit guard it exercises is registered only on direct whole-process invocation, so no import-level seam can reach it. The spawn-test child timeout drops 60s -> 10s to sit inside the unit project's 15s testTimeout (a hung child now fails on real evidence within the test's own budget). Co-Authored-By: Claude Fable 5 --- .../scripts/vendor-codelldb.js | 22 ++++--- .../tests/vendor-codelldb-script.test.ts | 65 ++++++++----------- 2 files changed, 40 insertions(+), 47 deletions(-) diff --git a/packages/codelldb-common/scripts/vendor-codelldb.js b/packages/codelldb-common/scripts/vendor-codelldb.js index 150592d5..d08c33e1 100644 --- a/packages/codelldb-common/scripts/vendor-codelldb.js +++ b/packages/codelldb-common/scripts/vendor-codelldb.js @@ -53,9 +53,10 @@ const LOCAL_ONLY = process.env.CODELLDB_VENDOR_LOCAL_ONLY === 'true'; const parsedExtractTimeout = Number(process.env.CODELLDB_EXTRACT_TIMEOUT_MS); const EXTRACT_TIMEOUT_MS = Number.isFinite(parsedExtractTimeout) && parsedExtractTimeout > 0 ? parsedExtractTimeout : 120000; -// Test-only hooks (issue #389 regression coverage): simulate a stalled extraction / -// a fully drained event loop without touching the network. -const TEST_STALL_EXTRACTION = process.env.CODELLDB_TEST_STALL_EXTRACTION === 'true'; +// Test-only hook (issue #389 regression coverage): simulate a fully drained +// event loop without touching the network. This must live in the script (not +// a test seam) because the premature-exit guard it exercises is registered +// only when the script is invoked directly as a whole process. const TEST_SIMULATE_DRAIN = process.env.CODELLDB_TEST_SIMULATE_DRAIN === 'true'; const RELEASE_BASE_URLS = [ process.env.CODELLDB_RELEASE_BASE?.replace(/\/$/, '') || @@ -430,11 +431,14 @@ async function downloadFile(url, destPath, maxRetries = 3) { * drains and exits 0 before any failure path runs. The pending watchdog timer * keeps the event loop alive for the whole extraction window, and converts a * stall into a rejection that flows through the normal retry/failure paths. + * + * `opts.extractFn` / `opts.timeoutMs` are test seams (unit tests inject a + * stalling extractor and a short timeout); production callers pass neither. */ -async function extractVsixWithWatchdog(vsixPath, destDir, vsixName) { - const work = TEST_STALL_EXTRACTION - ? new Promise(() => {}) - : extractZip(vsixPath, { dir: destDir }); +async function extractVsixWithWatchdog(vsixPath, destDir, vsixName, opts = {}) { + const extractFn = opts.extractFn ?? extractZip; + const timeoutMs = opts.timeoutMs ?? EXTRACT_TIMEOUT_MS; + const work = extractFn(vsixPath, { dir: destDir }); // The abandoned extraction may still reject after the watchdog fires; // swallow it so it cannot surface as a fatal unhandledRejection later. work.catch(() => {}); @@ -442,12 +446,12 @@ async function extractVsixWithWatchdog(vsixPath, destDir, vsixName) { const watchdog = new Promise((_, reject) => { timer = setTimeout(() => { reject(new Error( - `Extraction of ${vsixName} did not complete within ${EXTRACT_TIMEOUT_MS}ms ` + + `Extraction of ${vsixName} did not complete within ${timeoutMs}ms ` + `(likely a stalled unzip stream - issue #389). ` + `Re-run with CODELLDB_KEEP_TEMP=true to inspect ${destDir}, ` + `or raise CODELLDB_EXTRACT_TIMEOUT_MS if this machine is just slow.` )); - }, EXTRACT_TIMEOUT_MS); + }, timeoutMs); }); try { await Promise.race([work, watchdog]); diff --git a/packages/codelldb-common/tests/vendor-codelldb-script.test.ts b/packages/codelldb-common/tests/vendor-codelldb-script.test.ts index c87b0aff..cdda4ae9 100644 --- a/packages/codelldb-common/tests/vendor-codelldb-script.test.ts +++ b/packages/codelldb-common/tests/vendor-codelldb-script.test.ts @@ -4,19 +4,24 @@ * The observed defect: a mid-extraction stall left extract-zip's promise * forever pending, the event loop drained, and Node exited 0 without any * failure output. These tests pin the whole-process exit codes via spawned - * children (network-free, using the script's test hooks) and unit-test the - * extraction watchdog via direct import. + * children (network-free, using the script's drain hook) and unit-test the + * extraction watchdog via direct import with injected seams. */ -import { describe, it, expect, afterAll, afterEach, vi } from 'vitest'; +import { describe, it, expect, afterAll } from 'vitest'; import { spawnSync } from 'child_process'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { fileURLToPath } from 'url'; +import { fileURLToPath, pathToFileURL } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const scriptPath = path.resolve(__dirname, '../scripts/vendor-codelldb.js'); +type ExtractOpts = { extractFn?: (p: string, o: { dir: string }) => Promise; timeoutMs?: number }; +const { extractVsixWithWatchdog } = (await import(pathToFileURL(scriptPath).href)) as { + extractVsixWithWatchdog: (vsixPath: string, destDir: string, vsixName: string, opts?: ExtractOpts) => Promise; +}; + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-vendor-script-test-')); interface RunResult { @@ -37,28 +42,17 @@ function runScript(extraEnv: Record): RunResult { CODELLDB_FORCE_REBUILD: '', CODELLDB_VENDOR_LOCAL_ONLY: '', CODELLDB_TEST_SIMULATE_DRAIN: '', - CODELLDB_TEST_STALL_EXTRACTION: '', CODELLDB_CACHE_DIR: path.join(tempDir, 'cache'), ...extraEnv }, encoding: 'utf8', - timeout: 60_000 + // Kept below the unit project's 15s testTimeout: a hung child must be + // killed (and fail on real evidence) inside the test's own budget. + timeout: 10_000 }); return { status: result.status, output: `${result.stdout ?? ''}\n${result.stderr ?? ''}` }; } -async function importScript(env: Record): Promise> { - vi.resetModules(); - for (const [key, value] of Object.entries(env)) { - vi.stubEnv(key, value); - } - return (await import('../scripts/vendor-codelldb.js')) as Record; -} - -afterEach(() => { - vi.unstubAllEnvs(); -}); - afterAll(() => { fs.rmSync(tempDir, { recursive: true, force: true }); }); @@ -96,17 +90,15 @@ describe('vendor-codelldb.js exit codes (spawned)', () => { describe('extractVsixWithWatchdog (imported)', () => { it('converts a stalled extraction into a rejection after the timeout', async () => { - const mod = await importScript({ - CODELLDB_TEST_STALL_EXTRACTION: 'true', - CODELLDB_EXTRACT_TIMEOUT_MS: '100' - }); - const extractVsixWithWatchdog = mod.extractVsixWithWatchdog as ( - vsixPath: string, - destDir: string, - vsixName: string - ) => Promise; + // extract-zip's floating-promise failure mode, injected via the seam. + const stallingExtract = () => new Promise(() => {}); await expect( - extractVsixWithWatchdog(path.join(tempDir, 'missing.vsix'), path.join(tempDir, 'out'), 'test.vsix') + extractVsixWithWatchdog( + path.join(tempDir, 'missing.vsix'), + path.join(tempDir, 'out'), + 'test.vsix', + { extractFn: stallingExtract, timeoutMs: 100 } + ) ).rejects.toThrow(/did not complete within 100ms/); }); @@ -115,12 +107,6 @@ describe('extractVsixWithWatchdog (imported)', () => { const emptyZip = Buffer.concat([Buffer.from('504b0506', 'hex'), Buffer.alloc(18)]); const zipPath = path.join(tempDir, 'empty.zip'); fs.writeFileSync(zipPath, emptyZip); - const mod = await importScript({}); - const extractVsixWithWatchdog = mod.extractVsixWithWatchdog as ( - vsixPath: string, - destDir: string, - vsixName: string - ) => Promise; await expect( extractVsixWithWatchdog(zipPath, path.join(tempDir, 'empty-out'), 'empty.zip') ).resolves.toBeUndefined(); @@ -128,9 +114,12 @@ describe('extractVsixWithWatchdog (imported)', () => { // clean resolve above plus normal worker shutdown covers it. }); - it('registers no exit listener when merely imported', async () => { - const before = process.listenerCount('exit'); - await importScript({}); - expect(process.listenerCount('exit')).toBe(before); + it('registers no exit listener when merely imported', () => { + // The premature-exit guard is scoped to direct invocation; the static + // import at the top of this file must not have installed it. + const listeners = process.listeners('exit').map(String); + for (const src of listeners) { + expect(src).not.toContain('Premature exit'); + } }); });