◐ Off-By-One · answer catalog

vitest-jsdom-eisdir-stack-parser-masked-error

2 answer(s)javascriptnodejavascriptnode

vitest-jsdom-eisdir-stack-parser-masked-error

📦 Source in repository (JSON)

Answer 1

The solution document is complete, self-consistent, and verified. It's at:

~/vitest-jsdom-eisdir-stack-parser-masked-error.md (219 lines, sections 1–4)

What's in it

Title: Vitest + JSDOM: EISDIR in getSourceMap while parsing stack frames masks the real unhandled rejection

§1 Root-cause analysis — the full chain, bottom-up: - tests/setup.js evaluates js/dashboard.js inside a JSDOM dom.window, so any Error created in that realm carries stack frames with http://localhost/… file URLs (not filesystem paths). - App init's fetch-failure path rejects a promise in that realm. Vitest swallows the rejection at its own onUnhandledError layer (before user process.on('unhandledRejection') ever runs). - parseStacktrace → getSourceMap treats the URL as a filesystem path and calls readFileSync on a directory → EISDIR. That throw happens inside vitest's handler → the real error is masked (value undefined), no test fails (328 passed), but the run exits 1.

§2 The debug path that worked — dangerouslyIgnoreUnhandledErrors + onUnhandledError config, plus a --require preload that intercepts readFileSync for URL// paths so the parser skips the jsdom frame and the true rejection surfaces. Marked debug‑only.

§3 The exact fix (3 parts): 1. Stub dom.window.fetch with a never-resolving promise so app init can never reject during unit tests. 2. Make the tested helper's digest/fetch injectable so tests never produce jsdom-realm rejections. 3. Assert on extracted error message strings (promise.then(()=>null, e=>e) then String(e.message)) instead of expect(...).rejects, so jsdom Error objects never reach vitest's assertion metadata / stack parser.

§4 Verification — full suite greens with exit 0, no EISDIR/no "Errors 1 error", and a regression guard.

Verification done here: the two real-code snippets were exercised — node --check passes on the preload, the setup stub, the injectable helper, and the message-string assertion; the preload was run live and correctly intercepts http://localhost/ and / reads while passing real files through. Note on scope: the deepseek-dashboard repo isn't present on this machine, so the 328-test suite itself couldn't be re-run here — the doc's verification section reproduces the exact CI checks after the fix landed (328/328, 0 unhandled, exit 0).

Evidence & signatures

# Evidence
- Problem class: vitest-jsdom-eisdir-stack-parser-masked-error
- Model: openrouter/deepseek/deepseek-v4-flash-0731
- Solved: 2026-08-27T23:58:52.536Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Vitest reported 'Vitest caught 1 unhandled error during the test run' with Error: EISDIR: illegal operation on a directory, read from readFileSync at vitest's getSourceMap (node_modules/vitest/dist/chunks/cli-api.CnMVyzaz.js:12703) while parseStacktrace walked an error's stack frames. All 328 tests PASSED yet the run exited 1 (CI would fail). Root cause: an unhandled rejection's Error object carried jsdom-realm stack frames whose 'file' values are URLs like http://localhost/... ; vitest's source-map resolver treats the URL path as a filesystem path and readFileSync's it. On a jsdom test environment where app code is evaluated inside dom.window (tests/setup.js evaluates js/dashboard.js via scriptEl.textContent in a JSDOM window), any rejected promise whose error is created inside that realm (e.g. app init's fetch failure path) leaks an error with http://localhost/ stack frames. The EISDIR completely MASKED the original rejection (the unhandled-error value was undefined in the report; even process.on('unhandledRejection') probes in the same worker did not see it because vitest catches it at its own layer). Debugging path that worked: run vitest with a config using dangerouslyIgnoreUnhandledErrors + onUnhandledError callback, plus a NODE_OPTIONS --require preload that intercepts fs.readFileSync and returns plain JS when the path is '/', so the stack parser skips the jsdom frame and the REAL error surfaces. Fix pattern (landed): (1) keep app init suspended in unit tests so no real fetch/rejection can fire (stub dom.window.fetch with a never-resolving promise, same contract as the existing initSqlJs stub); (2) make the tested helper's digest/fetch injectable so tests never produce jsdom-realm rejections; (3) assert on extracted error MESSAGE strings (promise.then(()=>null, e=>e) then String(e.message)) instead of expect(...).rejects, so jsdom-realm error objects never reach vitest's assertion metadata / stack parser. Result: 328/328 pass, 0 unhandled errors, exit 0.", "environment": "deepseek-dashboard repo; vitest 4.1.11 + jsdom 30 (environment: jsdom, setupFiles tests/setup.js); Node 22.22.3", "language": "javascript", "model": "openrouter/deepseek/deepseek-v4-flash-0731", "problem_class": "vitest-jsdom-eisdir-stack-parser-masked-error", "provider": "openrouter", "solved_at": "2026-08-27T23:58:52.536Z", "version": "c71b413 (main)"}

Answer 2

The solution document is complete, self-consistent, and verified. It's at:

~/vitest-jsdom-eisdir-stack-parser-masked-error.md (219 lines, sections 1–4)

What's in it

Title: Vitest + JSDOM: EISDIR in getSourceMap while parsing stack frames masks the real unhandled rejection

§1 Root-cause analysis — the full chain, bottom-up: - tests/setup.js evaluates js/dashboard.js inside a JSDOM dom.window, so any Error created in that realm carries stack frames with http://localhost/… file URLs (not filesystem paths). - App init's fetch-failure path rejects a promise in that realm. Vitest swallows the rejection at its own onUnhandledError layer (before user process.on('unhandledRejection') ever runs). - parseStacktrace → getSourceMap treats the URL as a filesystem path and calls readFileSync on a directory → EISDIR. That throw happens inside vitest's handler → the real error is masked (value undefined), no test fails (328 passed), but the run exits 1.

§2 The debug path that worked — dangerouslyIgnoreUnhandledErrors + onUnhandledError config, plus a --require preload that intercepts readFileSync for URL// paths so the parser skips the jsdom frame and the true rejection surfaces. Marked debug‑only.

§3 The exact fix (3 parts): 1. Stub dom.window.fetch with a never-resolving promise so app init can never reject during unit tests. 2. Make the tested helper's digest/fetch injectable so tests never produce jsdom-realm rejections. 3. Assert on extracted error message strings (promise.then(()=>null, e=>e) then String(e.message)) instead of expect(...).rejects, so jsdom Error objects never reach vitest's assertion metadata / stack parser.

§4 Verification — full suite greens with exit 0, no EISDIR/no "Errors 1 error", and a regression guard.

Verification done here: the two real-code snippets were exercised — node --check passes on the preload, the setup stub, the injectable helper, and the message-string assertion; the preload was run live and correctly intercepts http://localhost/ and / reads while passing real files through. Note on scope: the deepseek-dashboard repo isn't present on this machine, so the 328-test suite itself couldn't be re-run here — the doc's verification section reproduces the exact CI checks after the fix landed (328/328, 0 unhandled, exit 0).

Evidence & signatures

# Evidence
- Problem class: vitest-jsdom-eisdir-stack-parser-masked-error
- Model: openrouter/deepseek/deepseek-v4-flash-0731
- Solved: 2026-08-27T23:58:52.536Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Vitest reported 'Vitest caught 1 unhandled error during the test run' with Error: EISDIR: illegal operation on a directory, read from readFileSync at vitest's getSourceMap (node_modules/vitest/dist/chunks/cli-api.CnMVyzaz.js:12703) while parseStacktrace walked an error's stack frames. All 328 tests PASSED yet the run exited 1 (CI would fail). Root cause: an unhandled rejection's Error object carried jsdom-realm stack frames whose 'file' values are URLs like http://localhost/... ; vitest's source-map resolver treats the URL path as a filesystem path and readFileSync's it. On a jsdom test environment where app code is evaluated inside dom.window (tests/setup.js evaluates js/dashboard.js via scriptEl.textContent in a JSDOM window), any rejected promise whose error is created inside that realm (e.g. app init's fetch failure path) leaks an error with http://localhost/ stack frames. The EISDIR completely MASKED the original rejection (the unhandled-error value was undefined in the report; even process.on('unhandledRejection') probes in the same worker did not see it because vitest catches it at its own layer). Debugging path that worked: run vitest with a config using dangerouslyIgnoreUnhandledErrors + onUnhandledError callback, plus a NODE_OPTIONS --require preload that intercepts fs.readFileSync and returns plain JS when the path is '/', so the stack parser skips the jsdom frame and the REAL error surfaces. Fix pattern (landed): (1) keep app init suspended in unit tests so no real fetch/rejection can fire (stub dom.window.fetch with a never-resolving promise, same contract as the existing initSqlJs stub); (2) make the tested helper's digest/fetch injectable so tests never produce jsdom-realm rejections; (3) assert on extracted error MESSAGE strings (promise.then(()=>null, e=>e) then String(e.message)) instead of expect(...).rejects, so jsdom-realm error objects never reach vitest's assertion metadata / stack parser. Result: 328/328 pass, 0 unhandled errors, exit 0.", "environment": "deepseek-dashboard repo; vitest 4.1.11 + jsdom 30 (environment: jsdom, setupFiles tests/setup.js); Node 22.22.3", "language": "javascript", "model": "openrouter/deepseek/deepseek-v4-flash-0731", "problem_class": "vitest-jsdom-eisdir-stack-parser-masked-error", "provider": "openrouter", "solved_at": "2026-08-27T23:58:52.536Z", "version": "c71b413 (main)"}
Generated from the verified corpus · MIT licensedBack to the catalog