Skip to content

Run vitest 4.1.11 in the browser shell - #349

Closed
vanilla-wave wants to merge 4 commits into
mainfrom
vitest-run-in-browser
Closed

vanilla-wave wants to merge 4 commits into
mainfrom
vitest-run-in-browser

Conversation

@vanilla-wave

Copy link
Copy Markdown
Owner

Summary

  • npm install accepts a bare override version ("vite": "8.0.16") and installs that exact vite for vitest 4.1.11.
  • vitest run loads vitest.config.ts and TypeScript tests, prints the default reporter (1 passed / 1 failed), and returns Node's exit code. npm test, --reporter=verbose, and --pool=threads match.
  • Outside that pair, jsdom, watch, coverage, vm pools, and other vite/vitest versions stay loud ceilings. Compat page: docs/public/compat/vitest.md.

Test plan

  • pnpm pr:check
  • tests/e2e/vitest-run.spec.ts (chromium-heavy): clean install, failing run exits 1, fixed run exits 0, threads pool matches

Made with Cursor

Bare npm overrides, the Node surfaces that package links, and both test pools now report pass/fail counts and Node's exit code.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

@greptile-apps

greptile-apps Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 0/5

The PR is not safe to merge because it weakens a Function-constructor guard and introduces multiple incorrect cross-runtime behaviors, including source corruption, shared IPC objects, leaked VM offsets, and a silently incompatible builtin.

Findings

  1. P1 Security Computed keys bypass guards ▶
  2. P1 Text replacement corrupts modules ▶
  3. P1 Stack offsets leak globally ▶
  4. P1 FD check skips stream endings ▶
  5. P1 Advanced IPC shares objects ▶
  6. P1 Win32 builtin returns POSIX ▶
  7. P1 Aliases become version ranges ▶

Summary

This PR adds browser-shell support for Vitest 4.1.11 with Vite 8.0.16, including npm override parsing, worker and child-process lifecycle support, advanced IPC, VM stack offsets, CLI promise keepalive, and end-to-end coverage.

  • Adds exact-version Vitest installation and browser-shell execution coverage for forks and threads.
  • Expands worker, IPC, process lifecycle, VM, builtin-module, and stream compatibility surfaces.
  • Introduces several cross-cutting regressions in module guarding, source rewriting, IPC isolation, VM stack attribution, override parsing, and builtin fidelity that should be addressed before merge.

Reviews (1) · Last reviewed commit: "Run vitest 4.1.11 in the browser shell."

Comment on lines +126 to +129
// A bare identifier is a runtime name, not the string 'Function'. Expressions stay loud.
if (asNode(node)?.type === 'Identifier') return false;
return literal === undefined;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Computed keys bypass guards

A computed identifier is now considered unable to name Function, even though const key = 'Function' or a loop variable can hold that value at runtime. Both module guards consequently allow paths such as Reflect.get(globalThis, key) to obtain the host Function constructor without the existing routing ceiling, permitting dynamically constructed imports to escape the module loader.

How this was verified: The CJS and ESM guards both delegate computed global keys to this function, which returns false for identifiers and loop bindings regardless of their runtime string value.

Comment on lines +9 to +15
export function trackUnawaitedCliAction(source: string): string {
if (source.includes(TRACKER) || !source.includes(BARE_CALL)) return source;
return source.replaceAll(
BARE_CALL,
`var __riftyAction = this.runMatchedCommand();
if (__riftyAction && typeof __riftyAction.then === "function" && globalThis.${TRACKER}) globalThis.${TRACKER}(__riftyAction);`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Text replacement corrupts modules

This unrestricted textual replacement runs on every transformed ESM module. An unrelated string, template, or comment containing this.runMatchedCommand(); is therefore rewritten as executable multi-line code, which can make the module fail to compile or change its behavior. The replacement also inserts lines after the source-map line map has been recorded, so later stack locations become incorrect.

Comment on lines +19 to +23
if (lineOffset === 0 && columnOffset === 0) {
OFFSETS.delete(filename);
return;
}
OFFSETS.set(filename, { line: lineOffset, column: columnOffset });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stack offsets leak globally

VM offsets are stored globally by filename rather than with the compiled Script. If two scripts named foo.js use different offsets, the most recently registered offset rewrites both scripts' stack frames. Creating an unoffset script with that filename also deletes the first script's offset, producing incorrect diagnostics whenever filenames are reused.

Comment on lines +265 to +268
function isProcessStdioSink(dest: object): boolean {
const fd = (dest as { fd?: unknown }).fd;
return fd === 1 || fd === 2;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 FD check skips stream endings

Skipping end() based only on fd === 1 || fd === 2 misclassifies any custom or file-backed writable carrying one of those descriptor values as process stdio. Piping into such a distinct destination leaves it unterminated, so finish never fires and buffered output may remain unflushed. The exception needs to identify the actual process stdout and stderr sinks rather than every matching fd value.

Comment on lines +12 to +21
const type = typeof message;
if (type === 'function' || type === 'symbol' || type === 'bigint') {
throw Object.assign(
new TypeError(
'The "message" argument must be one of type string, object, number, or boolean',
),
{ code: 'ERR_INVALID_ARG_TYPE' },
);
}
return message;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Advanced IPC shares objects

Advanced IPC returns the original object instead of serializing and cloning it. On the same-realm child-process route, the parent and child therefore receive the same reference, so mutations cross the process boundary. Nested unsupported values such as functions are also delivered instead of raising a serialization error; the existing round-trip test only checks types and passes because identity is preserved.


registerBuiltin('path', () => pathModule);
registerBuiltin('path/posix', () => pathModule.posix);
registerBuiltin('path/win32', () => pathModule.win32);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Win32 builtin returns POSIX

Registering node:path/win32 exposes the existing POSIX alias as though it were a supported Node builtin: sep remains /, delimiter remains :, and join('a', 'b') returns a/b. Before this change, the unsupported builtin failed loudly. Returning materially wrong Windows path behavior violates the repository directive that missing features must remain explicit NotImplementedError ceilings.

Context Used: CLAUDE.md (source)

Comment on lines 56 to +59
if (str.startsWith('npm:')) str = str.slice(4);
const at = str.lastIndexOf('@');
if (at <= 0) return { name: str, range: null };
return { name: str.slice(0, at), range: str.slice(at + 1) };
if (at > 0) return { name: str.slice(0, at), range: str.slice(at + 1) };
if (isNpmBareVersionRange(str)) return { name: overriddenName, range: str };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Aliases become version ranges

The range heuristic runs after stripping npm:, so a valid alias such as npm:v8 becomes v8 and is classified as a version range of the overridden package rather than as the replacement package name. The resulting name and range directly control installation, causing the installer to resolve the wrong package or fail on a bogus range.

Forked program children still stay alive for json and advanced message listeners, so the workbench owner can exit and vitest's forks pool still runs.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vanilla-wave
vanilla-wave marked this pull request as draft September 22, 2026 17:37
vanilla-wave and others added 2 commits September 22, 2026 19:37
Final+GREEN is not recorded yet, so the pull request stays a draft until that review.

Co-authored-by: Cursor <cursoragent@cursor.com>
The review findings were real: a text rewrite that could corrupt modules, stack offsets that clobbered each other, advanced IPC that shared objects, a pipe end skipped for any fd 1/2 writable, npm:v8 read as a version, path.win32 returning POSIX, and a const 'Function' key slipping the ceiling.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vanilla-wave

Copy link
Copy Markdown
Owner Author

Closed after a comparative review of the four vitest-run-in-browser attempts (#349, #351, #352, #353): #351 continues as the base. Review artifacts kept locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant