Skip to content

Commit ea67b09

Browse files
committed
feat: Make Vitest runs more token efficient when run through a coding assistant.
1 parent d0ee546 commit ea67b09

7 files changed

Lines changed: 158 additions & 7 deletions

File tree

packages/vitest/src/node/cli/cac.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,10 @@ function normalizeCliOptions(cliFilters: string[], argv: CliOptions): CliOptions
300300

301301
async function start(mode: VitestRunMode, cliFilters: string[], options: CliOptions): Promise<void> {
302302
try {
303+
const { detectAgent } = await import('../../utils/env')
304+
if (detectAgent()) {
305+
options.env = { AI_AGENT: '1', ...options.env }
306+
}
303307
const { startVitest } = await import('./cli-api')
304308
const ctx = await startVitest(mode, cliFilters.map(normalize), normalizeCliOptions(cliFilters, options))
305309
if (!ctx.shouldKeepServer()) {

packages/vitest/src/node/reporters/base.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const BADGE_PADDING = ' '
3737

3838
export interface BaseOptions {
3939
isTTY?: boolean
40+
isAgent?: boolean
4041
}
4142

4243
export abstract class BaseReporter implements Reporter {
@@ -49,12 +50,14 @@ export abstract class BaseReporter implements Reporter {
4950
renderSucceed = false
5051

5152
protected verbose = false
53+
protected isAgent = false
5254

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

5658
constructor(options: BaseOptions = {}) {
5759
this.isTTY = options.isTTY ?? isTTY
60+
this.isAgent = options.isAgent ?? false
5861
}
5962

6063
onInit(ctx: Vitest): void {
@@ -118,7 +121,7 @@ export abstract class BaseReporter implements Reporter {
118121
}
119122

120123
private logFailedTask(task: Task) {
121-
if (this.ctx.config.silent === 'passed-only') {
124+
if (this.ctx.config.silent === 'passed-only' || this.isAgent) {
122125
for (const log of task.logs || []) {
123126
this.onUserConsoleLog(log, 'failed')
124127
}
@@ -131,6 +134,10 @@ export abstract class BaseReporter implements Reporter {
131134
return
132135
}
133136

137+
if (this.isAgent && moduleState !== 'failed') {
138+
return
139+
}
140+
134141
let testsCount = 0
135142
let failedCount = 0
136143
let skippedCount = 0
@@ -200,6 +207,10 @@ export abstract class BaseReporter implements Reporter {
200207
this.log(c.red(` ${padding}${taskFail} ${this.getTestName(test.task, separator)}`) + suffix)
201208
}
202209

210+
else if (this.isAgent) {
211+
// In agent mode, only print failed tests
212+
}
213+
203214
// also print slow tests
204215
else if (duration > this.ctx.config.slowTestThreshold) {
205216
this.log(` ${padding}${c.yellow(c.dim(F_CHECK))} ${this.getTestName(test.task, separator)} ${suffix}`)
@@ -500,6 +511,10 @@ export abstract class BaseReporter implements Reporter {
500511
return false
501512
}
502513

514+
if (this.isAgent && taskState !== 'failed') {
515+
return false
516+
}
517+
503518
if (this.ctx.config.onConsoleLog) {
504519
const task = log.taskId ? this.ctx.state.idMap.get(log.taskId) : undefined
505520
const entity = task && this.ctx.state.getReportedEntity(task)

packages/vitest/src/node/reporters/default.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export class DefaultReporter extends BaseReporter {
2222
...options,
2323
}
2424

25-
if (!this.isTTY) {
25+
if (!this.isTTY || this.isAgent) {
2626
this.options.summary = false
2727
}
2828

@@ -32,7 +32,10 @@ export class DefaultReporter extends BaseReporter {
3232
}
3333

3434
onTestRunStart(specifications: ReadonlyArray<TestSpecification>): void {
35-
if (this.isTTY) {
35+
if (this.isAgent) {
36+
this.renderSucceed = false
37+
}
38+
else if (this.isTTY) {
3639
if (this.renderSucceed === undefined) {
3740
this.renderSucceed = !!this.renderSucceed
3841
}

packages/vitest/src/node/reporters/utils.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,30 +37,34 @@ function createReporters(
3737
ctx: Vitest,
3838
): Promise<Array<Reporter | DefaultReporter | BlobReporter | DotReporter | JsonReporter | TapReporter | JUnitReporter | HangingProcessReporter | GithubActionsReporter>> {
3939
const runner = ctx.runner
40+
const isAgent = !!ctx.config.env?.AI_AGENT
4041
const promisedReporters = reporterReferences.map(
4142
async (referenceOrInstance) => {
4243
if (Array.isArray(referenceOrInstance)) {
4344
const [reporterName, reporterOptions] = referenceOrInstance
45+
const options = isAgent && reporterName in ReportersMap && (reporterOptions as any)?.isAgent == null
46+
? { ...reporterOptions, isAgent: true }
47+
: reporterOptions
4448

4549
if (reporterName === 'html') {
4650
await ctx.packageInstaller.ensureInstalled('@vitest/ui', ctx.config.root, ctx.version)
4751
const CustomReporter = await loadCustomReporterModule(
4852
'@vitest/ui/reporter',
4953
runner,
5054
)
51-
return new CustomReporter(reporterOptions)
55+
return new CustomReporter(options)
5256
}
5357
else if (reporterName in ReportersMap) {
5458
const BuiltinReporter
5559
= ReportersMap[reporterName as BuiltinReporters]
56-
return new BuiltinReporter(reporterOptions)
60+
return new BuiltinReporter(options)
5761
}
5862
else {
5963
const CustomReporter = await loadCustomReporterModule(
6064
reporterName,
6165
runner,
6266
)
63-
return new CustomReporter(reporterOptions)
67+
return new CustomReporter(options)
6468
}
6569
}
6670

packages/vitest/src/utils/env.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,25 @@ export const isDeno: boolean
1212
export const isWindows: boolean = (isNode || isDeno) && process.platform === 'win32'
1313
export const isBrowser: boolean = typeof window !== 'undefined'
1414
export const isTTY: boolean = ((isNode || isDeno) && process.stdout?.isTTY && !isCI)
15+
export function detectAgent(): boolean {
16+
if (!(isNode || isDeno)) {
17+
return false
18+
}
19+
const env = process.env
20+
if (env.AI_AGENT === '0' || env.AI_AGENT === 'false') {
21+
return false
22+
}
23+
return !!(
24+
env.AI_AGENT
25+
|| env.CURSOR_TRACE_ID
26+
|| env.CURSOR_AGENT
27+
|| env.GEMINI_CLI
28+
|| env.CODEX_SANDBOX
29+
|| env.AUGMENT_AGENT
30+
|| env.OPENCODE_CLIENT
31+
|| env.CLAUDECODE
32+
|| env.CLAUDE_CODE
33+
|| env.REPL_ID
34+
)
35+
}
1536
export { isCI, provider as stdProvider } from 'std-env'

test/cli/test/reporters/default.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,101 @@ describe('default reporter', async () => {
299299
})
300300
expect(stderr).toMatchSnapshot()
301301
})
302+
303+
describe('agent mode', () => {
304+
test('hides passed module headers and shows only failed tests', async () => {
305+
const { stdout } = await runVitest({
306+
include: ['b1.test.ts', 'b2.test.ts'],
307+
root: 'fixtures/reporters/default',
308+
reporters: [['default', { isAgent: true }]],
309+
fileParallelism: false,
310+
sequence: {
311+
sequencer: StableTestFileOrderSorter,
312+
},
313+
})
314+
315+
const output = trimReporterOutput(stdout)
316+
expect(output).toMatchInlineSnapshot(`
317+
"❯ b1.test.ts (13 tests | 1 failed) [...]ms
318+
× b failed test [...]ms
319+
❯ b2.test.ts (13 tests | 1 failed) [...]ms
320+
× b failed test [...]ms"
321+
`)
322+
})
323+
324+
test('hides all output for passed-only modules', async () => {
325+
const { stdout } = await runVitest({
326+
include: ['b1.test.ts', 'b2.test.ts'],
327+
root: 'fixtures/reporters/default',
328+
reporters: [['default', { isAgent: true }]],
329+
fileParallelism: false,
330+
testNamePattern: 'passed',
331+
sequence: {
332+
sequencer: StableTestFileOrderSorter,
333+
},
334+
})
335+
336+
const output = trimReporterOutput(stdout)
337+
expect(output).toMatchInlineSnapshot(`""`)
338+
})
339+
340+
test('still prints end summary', async () => {
341+
const { stdout } = await runVitest({
342+
include: ['b1.test.ts', 'b2.test.ts'],
343+
root: 'fixtures/reporters/default',
344+
reporters: [['default', { isAgent: true }]],
345+
fileParallelism: false,
346+
sequence: {
347+
sequencer: StableTestFileOrderSorter,
348+
},
349+
})
350+
351+
expect(stdout).toContain('Test Files')
352+
expect(stdout).toContain('Tests')
353+
})
354+
355+
test('suppresses console logs from passed tests', async () => {
356+
const { stdout } = await runVitest({
357+
config: false,
358+
include: ['./fixtures/reporters/console-some-failing.test.ts'],
359+
reporters: [['default', { isAgent: true, isTTY: true }]],
360+
})
361+
362+
expect(stdout).not.toContain('Log from passed test')
363+
expect(stdout).not.toContain('Log from passed suite')
364+
})
365+
366+
test('shows console logs from failed tests', async () => {
367+
const { stdout } = await runVitest({
368+
config: false,
369+
include: ['./fixtures/reporters/console-some-failing.test.ts'],
370+
reporters: [['default', { isAgent: true, isTTY: true }]],
371+
})
372+
373+
expect(stdout).toContain('Log from failed test')
374+
expect(stdout).toContain('Log from failed suite')
375+
expect(stdout).toContain('Log from failed file')
376+
})
377+
378+
test('activates via AI_AGENT env config', async () => {
379+
const { stdout } = await runVitest({
380+
include: ['b1.test.ts', 'b2.test.ts'],
381+
root: 'fixtures/reporters/default',
382+
reporters: ['default'],
383+
fileParallelism: false,
384+
env: { AI_AGENT: '1' },
385+
sequence: {
386+
sequencer: StableTestFileOrderSorter,
387+
},
388+
})
389+
390+
const output = trimReporterOutput(stdout)
391+
expect(output).toMatchInlineSnapshot(`
392+
"❯ b1.test.ts (13 tests | 1 failed) [...]ms
393+
× b failed test [...]ms
394+
❯ b2.test.ts (13 tests | 1 failed) [...]ms
395+
× b failed test [...]ms"
396+
`)
397+
})
398+
})
302399
}, 120000)

test/test-utils/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export async function runVitest(
172172
...cliOptions,
173173
env: {
174174
NO_COLOR: 'true',
175+
AI_AGENT: '',
175176
...rest.env,
176177
...cliOptions?.env,
177178
},
@@ -281,7 +282,13 @@ async function runCli(command: 'vitest', _options?: CliOptions | string, ...args
281282
args.push('--maxWorkers=1')
282283
}
283284

284-
const subprocess = x(command, args, options as Options).process!
285+
const subprocess = x(command, args, {
286+
...options as Options,
287+
nodeOptions: {
288+
...(options as Options)?.nodeOptions,
289+
env: { ...process.env, AI_AGENT: '0', ...(options as Options)?.nodeOptions?.env },
290+
},
291+
}).process!
285292
const cli = new Cli({
286293
stdin: subprocess.stdin!,
287294
stdout: subprocess.stdout!,

0 commit comments

Comments
 (0)