Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/config/reporters.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Note that the [coverage](/guide/coverage) feature uses a different [`coverage.re
- [`tap-flat`](/guide/reporters#tap-flat-reporter)
- [`hanging-process`](/guide/reporters#hanging-process-reporter)
- [`github-actions`](/guide/reporters#github-actions-reporter)
- [`agent`](/guide/reporters#agent-reporter)
- [`blob`](/guide/reporters#blob-reporter)
## Example
Expand Down
2 changes: 0 additions & 2 deletions docs/guide/browser/visual-regression-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,6 @@ screenshots should use the service.

The cleanest approach is using [Test Projects](/guide/projects):


```ts [vitest.config.ts]
import { env } from 'node:process'
import { defineConfig } from 'vitest/config'
Expand Down Expand Up @@ -663,7 +662,6 @@ export default defineConfig({
})
```


Follow the [official guide to create a Playwright Workspace](https://learn.microsoft.com/en-us/azure/app-testing/playwright-workspaces/quickstart-run-end-to-end-tests?tabs=playwrightcli&pivots=playwright-test-runner#create-a-workspace).

Once your workspace is created, configure Vitest to use it:
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/cli-generated.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Hide logs for skipped tests
- **CLI:** `--reporter <name>`
- **Config:** [reporters](/config/reporters)

Specify reporters (default, blob, verbose, dot, json, tap, tap-flat, junit, tree, hanging-process, github-actions)
Specify reporters (default, agent, blob, verbose, dot, json, tap, tap-flat, junit, tree, hanging-process, github-actions)

### outputFile

Expand Down
1 change: 0 additions & 1 deletion docs/guide/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,3 @@ Merges every blob report located in the specified folder (`.vitest-reports` by d
```sh
vitest --merge-reports --reporter=junit
```

24 changes: 24 additions & 0 deletions docs/guide/reporters.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ This example will write separate JSON and XML reports as well as printing a verb

By default (i.e. if no reporter is specified), Vitest will display summary of running tests and their status at the bottom. Once a suite passes, its status will be reported on top of the summary.

::: tip
When Vitest detects it is running inside an AI coding agent, the [`agent`](#agent-reporter) reporter is used instead to reduce output and minimize token usage. You can override this by explicitly configuring the [`reporters`](/config/reporters) option.
:::

You can disable the summary by configuring the reporter:

:::code-group
Expand Down Expand Up @@ -637,6 +641,26 @@ export default defineConfig({
})
```

### Agent Reporter

Outputs a minimal report optimized for AI coding assistants and LLM-based workflows. Only failed tests and their error messages are displayed. Console logs from passing tests and the summary section are suppressed to reduce token usage.

This reporter is automatically enabled when no `reporters` option is configured and Vitest detects it is running inside an AI coding agent. If you configure custom reporters, you can explicitly add `agent`:

:::code-group
```bash [CLI]
npx vitest --reporter=agent
```

```ts [vitest.config.ts]
export default defineConfig({
test: {
reporters: ['agent']
},
})
```
:::

### Blob Reporter

Stores test results on the machine so they can be later merged using [`--merge-reports`](/guide/cli#merge-reports) command.
Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/node/config/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
defaultPort,
} from '../../constants'
import { benchmarkConfigDefaults, configDefaults } from '../../defaults'
import { isCI, stdProvider } from '../../utils/env'
import { isAgent, isCI, stdProvider } from '../../utils/env'
import { getWorkersCountByPercentage } from '../../utils/workers'
import { BaseSequencer } from '../sequencers/BaseSequencer'
import { RandomSequencer } from '../sequencers/RandomSequencer'
Expand Down Expand Up @@ -729,7 +729,7 @@ export function resolveConfig(
}

if (!resolved.reporters.length) {
resolved.reporters.push(['default', {}])
resolved.reporters.push([isAgent ? 'agent' : 'default', {}])

// also enable github-actions reporter as a default
if (process.env.GITHUB_ACTIONS === 'true') {
Expand Down
31 changes: 31 additions & 0 deletions packages/vitest/src/node/reporters/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { TestSpecification } from '../test-specification'
import type { DefaultReporterOptions } from './default'
import type { TestCase, TestModule, TestModuleState } from './reported-tasks'
import { DefaultReporter } from './default'

export class AgentReporter extends DefaultReporter {
renderSucceed = false

constructor(options: DefaultReporterOptions = {}) {
super({ silent: 'passed-only', ...options, summary: false })
}

onTestRunStart(specifications: ReadonlyArray<TestSpecification>): void {
super.onTestRunStart(specifications)
this.renderSucceed = false
}

protected printTestModule(testModule: TestModule): void {
if (testModule.state() !== 'failed') {
return
}
super.printTestModule(testModule)
}

protected printTestCase(moduleState: TestModuleState, test: TestCase): void {
const testResult = test.result()
if (testResult.state === 'failed') {
super.printTestCase(moduleState, test)
}
}
}
12 changes: 8 additions & 4 deletions packages/vitest/src/node/reporters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const BADGE_PADDING = ' '

export interface BaseOptions {
isTTY?: boolean
silent?: boolean | 'passed-only'
}

export abstract class BaseReporter implements Reporter {
Expand All @@ -49,16 +50,19 @@ export abstract class BaseReporter implements Reporter {
renderSucceed = false

protected verbose = false
protected silent?: boolean | 'passed-only'

private _filesInWatchMode = new Map<string, number>()
private _timeStart = formatTimeString(new Date())

constructor(options: BaseOptions = {}) {
this.isTTY = options.isTTY ?? isTTY
this.silent = options.silent
}

onInit(ctx: Vitest): void {
this.ctx = ctx
this.silent ??= this.ctx.config.silent

this.ctx.logger.printBanner()
}
Expand Down Expand Up @@ -117,8 +121,8 @@ export abstract class BaseReporter implements Reporter {
this.printTestModule(testModule)
}

private logFailedTask(task: Task) {
if (this.ctx.config.silent === 'passed-only') {
protected logFailedTask(task: Task): void {
if (this.silent === 'passed-only') {
for (const log of task.logs || []) {
this.onUserConsoleLog(log, 'failed')
}
Expand Down Expand Up @@ -504,11 +508,11 @@ export abstract class BaseReporter implements Reporter {
}

shouldLog(log: UserConsoleLog, taskState?: TestResult['state']): boolean {
if (this.ctx.config.silent === true) {
if (this.silent === true) {
return false
}

if (this.ctx.config.silent === 'passed-only' && taskState !== 'failed') {
if (this.silent === 'passed-only' && taskState !== 'failed') {
return false
}

Expand Down
4 changes: 4 additions & 0 deletions packages/vitest/src/node/reporters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { GithubActionsReporterOptions } from './github-actions'
import type { HTMLOptions } from './html'
import type { JsonOptions } from './json'
import type { JUnitOptions } from './junit'
import { AgentReporter } from './agent'
import { BlobReporter } from './blob'
import { DefaultReporter } from './default'
import { DotReporter } from './dot'
Expand All @@ -19,6 +20,7 @@ import { TreeReporter } from './tree'
import { VerboseReporter } from './verbose'

export {
AgentReporter,
DefaultReporter,
DotReporter,
GithubActionsReporter,
Expand Down Expand Up @@ -46,6 +48,7 @@ export type {

export const ReportersMap = {
'default': DefaultReporter as typeof DefaultReporter,
'agent': AgentReporter as typeof AgentReporter,
'blob': BlobReporter as typeof BlobReporter,
'verbose': VerboseReporter as typeof VerboseReporter,
'dot': DotReporter as typeof DotReporter,
Expand All @@ -62,6 +65,7 @@ export type BuiltinReporters = keyof typeof ReportersMap

export interface BuiltinReporterOptions {
'default': DefaultReporterOptions
'agent': DefaultReporterOptions
'verbose': DefaultReporterOptions
'dot': BaseOptions
'tree': BaseOptions
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/public/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export { VmThreadsPoolWorker } from '../node/pools/workers/vmThreadsWorker'
export type { SerializedTestProject, TestProject } from '../node/project'

export {
AgentReporter,
BenchmarkReporter,
BenchmarkReportsMap,
DefaultReporter,
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/public/reporters.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export {
AgentReporter,
BenchmarkReporter,
BenchmarkReportsMap,
DefaultReporter,
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ export const isDeno: boolean
export const isWindows: boolean = (isNode || isDeno) && process.platform === 'win32'
export const isBrowser: boolean = typeof window !== 'undefined'
export const isTTY: boolean = ((isNode || isDeno) && process.stdout?.isTTY && !isCI)
export { isCI, provider as stdProvider } from 'std-env'
export { isAgent, isCI, provider as stdProvider } from 'std-env'
13 changes: 9 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ catalog:
pathe: ^2.0.3
playwright: ^1.58.2
sirv: ^3.0.2
std-env: ^3.10.0
std-env: ^4.0.0-rc.1
strip-literal: ^3.1.0
tinyexec: ^1.0.2
tinyglobby: ^0.2.15
Expand Down
2 changes: 1 addition & 1 deletion test/cli/test/config/browser-configs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ function getCliConfig(options: TestUserConfig, cli: string[], fs: TestFsStructur
{
nodeOptions: {
env: {
CI: 'false',
CI: '',
GITHUB_ACTIONS: undefined,
},
},
Expand Down
92 changes: 92 additions & 0 deletions test/cli/test/reporters/agent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { runVitest, StableTestFileOrderSorter } from '#test-utils'
import { describe, expect, test } from 'vitest'
import { DefaultReporter } from 'vitest/node'
import { trimReporterOutput } from './utils'

describe('agent reporter', async () => {
test('hides passed module headers, shows only failed tests, and prints end summary', async () => {
const { stdout } = await runVitest({
include: ['b1.test.ts', 'b2.test.ts'],
root: 'fixtures/reporters/default',
reporters: [['agent', {}]],
fileParallelism: false,
sequence: {
sequencer: StableTestFileOrderSorter,
},
})

const output = trimReporterOutput(stdout)
expect(output).toMatchInlineSnapshot(`
"❯ b1.test.ts (13 tests | 1 failed) [...]ms
× b failed test [...]ms
❯ b2.test.ts (13 tests | 1 failed) [...]ms
× b failed test [...]ms"
`)

const summary = stdout.replace(/\d+ms/g, '[...]ms').split('\n').filter(line => /Test Files|^\s*Tests\b/.test(line)).map(line => line.trim()).join('\n')
expect(summary).toMatchInlineSnapshot(`
"Test Files 2 failed (2)
Tests 2 failed | 24 passed (26)"
`)
})

test('hides all output for passed-only modules', async () => {
const { stdout } = await runVitest({
include: ['b1.test.ts', 'b2.test.ts'],
root: 'fixtures/reporters/default',
reporters: [['agent', {}]],
fileParallelism: false,
testNamePattern: 'passed',
sequence: {
sequencer: StableTestFileOrderSorter,
},
})

const output = trimReporterOutput(stdout)
expect(output).toMatchInlineSnapshot(`""`)
})

test('shows console logs only from failed file, suite, and tests', async () => {
const { stdout } = await runVitest({
config: false,
include: ['./fixtures/reporters/console-some-failing.test.ts'],
reporters: [['agent', { isTTY: true }]],
})

const logs = stdout.split('\n').filter(line => line.includes('Log from')).join('\n')
expect(logs).toMatchInlineSnapshot(`
"Log from failed test
Log from failed test
Log from failed suite
Log from failed file"
`)
})

test('does not change silent behavior for other reporters', async () => {
const { stdout } = await runVitest({
config: false,
include: ['./fixtures/reporters/console-some-failing.test.ts'],
reporters: [new LogReporter(), ['agent', { isTTY: true }]],
})

const logs = stdout.split('\n').filter(line => line.includes('Log from')).join('\n')
expect(logs).toMatchInlineSnapshot(`
"Log from failed file
Log from passed test
Log from failed test
Log from failed suite
Log from passed test
Log from failed test
Log from passed suite
Log from passed test
Log from failed test
Log from failed test
Log from failed suite
Log from failed file"
`)
})
}, 120000)

class LogReporter extends DefaultReporter {
isTTY = true
}
Loading
Loading