Skip to content

rhwinter/vitest-loggingError

Repository files navigation

silent: 'passed-only' reporter crash — caused by the VSCode Vitest extension shadowing onUserConsoleLog

TL;DR

When tests are run through the VSCode Vitest extension, the extension's worker mutates every registered vitest reporter by assigning onUserConsoleLog = undefined as an instance property:

// vitest.explorer-1.50.4/dist/workerNew.js (and workerLegacy.js)
p.reporters.forEach(e => { e instanceof Z || (e.onUserConsoleLog = void 0) });

That shadows the inherited BaseReporter.prototype.onUserConsoleLog. Vitest's BaseReporter.logFailedTask then calls this.onUserConsoleLog(log, "failed") unguarded when silent: 'passed-only' is set and a failing test has captured console output, producing:

⎯⎯ Unhandled Reporter Error ⎯⎯
TypeError: this.onUserConsoleLog is not a function
 ❯ DefaultReporter.logFailedTask  …/base.ts logFailedTask
 ❯ DefaultReporter.onTestCaseResult  …

The test run survives (the dispatcher catches the throw), but every failing test's captured logs — the exact output passed-only exists to surface — is silently dropped.

Verified in our environment: when the same configuration is run from the CLI (no VSCode extension in the loop), this.onUserConsoleLog is defined and no crash occurs. The mutation done by the extension is the trigger.

Reports to file

  1. Primary — VSCode Vitest extension (vitest-dev/vscode). Don't shadow onUserConsoleLog with undefined. The intent is to suppress duplicate console output, which can be done without breaking the prototype contract:

    • Assign a no-op function: e.onUserConsoleLog = () => {}.
    • Or override e.shouldLog = () => false (already used by BaseReporter to gate output).
    • Or wrap the reporter in a proxy that filters the call rather than deleting the method.
  2. Secondary / defensive — vitest core. The call site in BaseReporter.logFailedTask should not assume this.onUserConsoleLog remains defined on the receiver. Either guard it (this.onUserConsoleLog?.(log, "failed"), only safe if combined with a real delivery path so logs aren't silently dropped) or, preferably, route the replay through the existing event broadcast (vitest.report("onUserConsoleLog", log)), which doesn't depend on the shape of any one reporter.

Versions

  • vitest@4.0.18 and vitest@4.1.6 — code path identical and also present on main (5.0.0-beta.2).
  • vitest.explorer@1.50.4 — VSCode extension where the mutation lives.

This repo pins vitest@4.1.6.

Quick start

npm install
npm run repro:forced   # deterministic crash — mimics the extension's mutation
npm test               # natural conditions, but only crashes when the
                       # mutation is also present (i.e. real VSCode runs).

The buggy code

In vitest

packages/vitest/src/node/reporters/base.tslogFailedTask (installed dist: node_modules/vitest/dist/chunks/index.UpGiHP7g.js:2238-2240):

logFailedTask(task) {
    if (this.silent === "passed-only")
        for (const log of task.logs || [])
            this.onUserConsoleLog(log, "failed");
}

The method is reachable on failing test cases, suites, and modules:

  • BaseReporter.onTestCaseResultlogFailedTask on failed test cases
  • BaseReporter.onTestSuiteResultlogFailedTask on failed suites
  • BaseReporter.onTestModuleEndlogFailedTask on failed modules

onUserConsoleLog is defined on BaseReporter.prototype. The unguarded call assumes it remains resolvable on the receiver. Nothing in vitest itself breaks that assumption — but the extension does.

In the VSCode extension

vitest.explorer-1.50.4/dist/workerNew.js:

p.reporters.forEach(e => {
    e instanceof Z || (e.onUserConsoleLog = void 0)
});

(Same pattern in workerLegacy.js with class kn instead of Z.) Z is the extension's own RPC reporter that forwards logs over birpc to the extension host UI. Reporters that are not Z get their onUserConsoleLog cleared so vitest's default terminal output doesn't double-print logs that the UI is already displaying.

Setting the instance property to undefined (rather than a no-op function, or overriding the gating predicate) shadows the inherited method on BaseReporter, which is what makes logFailedTask's call site crash.

Reproductions

Forced — npm run repro:forced

broken-reporter.js constructs a DefaultReporter instance and assigns onUserConsoleLog = undefined on it, identical to the extension's mutation. vitest.forced.config.js registers it as the sole reporter with silent: 'passed-only'. forced-repro.test.js has one failing test with a console.log before its assertion.

Output:

⎯⎯ Unhandled Reporter Error ⎯⎯
TypeError: this.onUserConsoleLog is not a function
 ❯ ExtensionShadowedReporter.logFailedTask node_modules/vitest/dist/chunks/index.UpGiHP7g.js:2239:78
 ❯ ExtensionShadowedReporter.onTestCaseResult node_modules/vitest/dist/chunks/index.UpGiHP7g.js:2229:50
 ❯ ExtensionShadowedReporter.onTestCaseResult node_modules/vitest/dist/chunks/index.UpGiHP7g.js:3123:9
 ❯ Vitest.report          node_modules/vitest/dist/chunks/cli-api.B5majYba.js:13968:36
 ❯ TestRun.reportEvent    node_modules/vitest/dist/chunks/cli-api.B5majYba.js:12653:85
 ❯ TestRun.updated        node_modules/vitest/dist/chunks/cli-api.B5majYba.js:12577:54
 ❯ Proxy.onTaskUpdate     node_modules/vitest/dist/chunks/cli-api.B5majYba.js:2725:9

This is a frame-for-frame match (modulo the reporter class name) of the stack observed when running daphne's test suite through the VSCode Vitest extension.

Natural — npm test

repro.test.js + vitest.config.js exercise the three conditions vitest's code path requires:

  1. test.silent === 'passed-only' is set.
  2. A test transitions to failed.
  3. The failing test has at least one entry in task.logs.

Running from the CLI directly: no crash, because nothing has shadowed onUserConsoleLog on the receiver. Running through the VSCode extension: crash, because the extension's mutation has run.

How we confirmed the cause

We instrumented daphne's installed vitest dist (additively, keeping the existing guard so tests still pass) to dump the reporters list and the prototype-chain state of any reporter that hit the missing-method path:

[VITEST-DIAGNOSTIC] reporters list (n=1):
  {"i":0,"hasOnUserConsoleLog":true,"hasOnTestCaseResult":true,
   "hasLogFailedTask":true,
   "chain":["MinimalReporter","MinimalReporter","DefaultReporter","BaseReporter","Object"]}

CLI run: hasOnUserConsoleLog: true — the method is resolvable, the bug cannot fire. No reporter in the list lacks the method.

Grepping the VSCode Vitest extension dist for onUserConsoleLog then turned up the explicit e.onUserConsoleLog = void 0 assignment in workerNew.js, which is the only place we could find that shadows the method.

Workaround we are using

A yarn patch on vitest@4.1.6's dist/chunks/index.UpGiHP7g.js:

logFailedTask(task) {
    if (this.silent !== "passed-only") return;
    if (process.env.VITEST_VSCODE === "true") return;
    for (const log of task.logs || []) {
        if (typeof this.onUserConsoleLog === "function") {
            this.onUserConsoleLog(log, "failed");
        } else if (this.ctx && this.ctx.logger) {
            const stream = log.type === "stdout"
                ? this.ctx.logger.outputStream
                : this.ctx.logger.errorStream;
            const taskName = log.taskId && this.ctx.state
                ? (this.ctx.state.idMap.get(log.taskId)?.name || log.taskId)
                : "unknown test";
            stream.write(`${log.type} | ${taskName}\n${log.content}\n`);
        }
    }
}

The typeof === "function" guard prevents the crash. The fallback preserves the "show captured logs on failure" semantic. The VITEST_VSCODE skip is project-specific: when the extension is the host, its own RPC reporter is already replaying logs through the UI, so replaying through the default reporter here would duplicate. Once the extension stops nuking the method, that skip can come out and the fallback can come out, leaving only the defensive guard or the suggested broadcast-based replay.

About

When silent: 'passed-only' is configured AND a failed test has captured console output (via console.log, console.error, etc., not just raw process.stdout.write writes — anything routed through vitest's stdout/stderr capture), vitest emits an unhandled reporter error for every failed test+module result.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors