From dfb63f8d2c5f3ee52f145a8fbfd70a13b5a9169c Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Thu, 17 Jul 2025 15:14:31 +0000 Subject: [PATCH 1/9] Add CLAUDECODE=1 environment variable to quiet test output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds support for the CLAUDECODE environment variable to enable quiet test output, which reduces verbosity by only showing failures while still maintaining accurate test summary counts. When CLAUDECODE=1 is set: - Only test failures are displayed with their full output - Passing, skipped, and todo test indicators are hidden - Summary statistics are still shown at the end - JUnit reporter output remains unaffected When CLAUDECODE=0 or unset: - All test output is shown normally (existing behavior) This feature is useful for automated testing environments where reduced output verbosity improves readability while still providing essential failure information. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/cli/test_command.zig | 138 +++++++++++++++++ src/feature_flags.zig | 1 + .../claudecode-flag.test.ts.snap | 5 + test/cli/test/claudecode-flag.test.ts | 142 ++++++++++++++++++ 4 files changed, 286 insertions(+) create mode 100644 test/cli/test/__snapshots__/claudecode-flag.test.ts.snap create mode 100644 test/cli/test/claudecode-flag.test.ts diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index a5b3693993b..64be0c8de83 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -97,6 +97,11 @@ fn fmtStatusTextLine(comptime status: @Type(.enum_literal), comptime emoji_or_co } fn writeTestStatusLine(comptime status: @Type(.enum_literal), writer: anytype) void { + // In quiet mode (CLAUDECODE=1), only print failures + if (bun.getRuntimeFeatureFlag(.CLAUDECODE) and status != .fail) { + return; + } + if (Output.enable_ansi_colors_stderr) writer.print(fmtStatusTextLine(status, true), .{}) catch unreachable else @@ -644,6 +649,139 @@ pub const CommandLineReporter = struct { file_reporter: ?FileReporter, line_number: u32, ) void { + // In quiet mode (CLAUDECODE=1), only print failures + if (bun.getRuntimeFeatureFlag(.CLAUDECODE) and status != .fail) { + // Still need to handle JUnit reporting if enabled + if (file_reporter) |reporter| { + switch (reporter) { + .junit => |junit| { + const filename = brk: { + if (strings.hasPrefix(file, bun.fs.FileSystem.instance.top_level_dir)) { + break :brk strings.withoutLeadingPathSeparator(file[bun.fs.FileSystem.instance.top_level_dir.len..]); + } else { + break :brk file; + } + }; + + if (!strings.eql(junit.current_file, filename)) { + while (junit.suite_stack.items.len > 0 and !junit.suite_stack.items[junit.suite_stack.items.len - 1].is_file_suite) { + junit.endTestSuite() catch bun.outOfMemory(); + } + + if (junit.current_file.len > 0) { + junit.endTestSuite() catch bun.outOfMemory(); + } + + junit.beginTestSuite(filename) catch bun.outOfMemory(); + } + + var scopes_stack = std.BoundedArray(*jest.DescribeScope, 64).init(0) catch unreachable; + var parent_ = parent; + + while (parent_) |scope| { + scopes_stack.append(scope) catch break; + parent_ = scope.parent; + } + + const scopes: []*jest.DescribeScope = scopes_stack.slice(); + + // Replicate the JUnit reporting logic from the normal flow + var needed_suites = std.ArrayList(*jest.DescribeScope).init(bun.default_allocator); + defer needed_suites.deinit(); + + for (scopes, 0..) |_, i| { + const index = (scopes.len - 1) - i; + const scope = scopes[index]; + if (scope.label.len > 0) { + needed_suites.append(scope) catch bun.outOfMemory(); + } + } + + var current_suite_depth: u32 = 0; + if (junit.suite_stack.items.len > 0) { + for (junit.suite_stack.items) |suite_info| { + if (!suite_info.is_file_suite) { + current_suite_depth += 1; + } + } + } + + while (current_suite_depth > needed_suites.items.len) { + if (junit.suite_stack.items.len > 0 and !junit.suite_stack.items[junit.suite_stack.items.len - 1].is_file_suite) { + junit.endTestSuite() catch bun.outOfMemory(); + current_suite_depth -= 1; + } else { + break; + } + } + + var suites_to_close: u32 = 0; + var suite_index: usize = 0; + for (junit.suite_stack.items) |suite_info| { + if (suite_info.is_file_suite) continue; + + if (suite_index < needed_suites.items.len) { + const needed_scope = needed_suites.items[suite_index]; + if (!strings.eql(suite_info.name, needed_scope.label)) { + suites_to_close = @as(u32, @intCast(current_suite_depth)) - @as(u32, @intCast(suite_index)); + break; + } + } else { + suites_to_close = @as(u32, @intCast(current_suite_depth)) - @as(u32, @intCast(suite_index)); + break; + } + suite_index += 1; + } + + while (suites_to_close > 0) { + if (junit.suite_stack.items.len > 0 and !junit.suite_stack.items[junit.suite_stack.items.len - 1].is_file_suite) { + junit.endTestSuite() catch bun.outOfMemory(); + current_suite_depth -= 1; + suites_to_close -= 1; + } else { + break; + } + } + + var describe_suite_index: usize = 0; + for (junit.suite_stack.items) |suite_info| { + if (!suite_info.is_file_suite) { + describe_suite_index += 1; + } + } + + while (describe_suite_index < needed_suites.items.len) { + const scope = needed_suites.items[describe_suite_index]; + junit.beginTestSuiteWithLine(scope.label, scope.line_number, false) catch bun.outOfMemory(); + describe_suite_index += 1; + } + + var arena = std.heap.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + var stack_fallback = std.heap.stackFallback(4096, arena.allocator()); + const allocator = stack_fallback.get(); + var concatenated_describe_scopes = std.ArrayList(u8).init(allocator); + + { + const initial_length = concatenated_describe_scopes.items.len; + for (scopes) |scope| { + if (scope.label.len > 0) { + if (initial_length != concatenated_describe_scopes.items.len) { + concatenated_describe_scopes.appendSlice(" > ") catch bun.outOfMemory(); + } + + escapeXml(scope.label, concatenated_describe_scopes.writer()) catch bun.outOfMemory(); + } + } + } + + const display_label = if (label.len > 0) label else "test"; + junit.writeTestCase(status, filename, display_label, concatenated_describe_scopes.items, assertions, elapsed_ns, line_number) catch bun.outOfMemory(); + }, + } + } + return; + } var scopes_stack = std.BoundedArray(*jest.DescribeScope, 64).init(0) catch unreachable; var parent_ = parent; diff --git a/src/feature_flags.zig b/src/feature_flags.zig index 8521372fcd6..52b078061d1 100644 --- a/src/feature_flags.zig +++ b/src/feature_flags.zig @@ -36,6 +36,7 @@ pub const RuntimeFeatureFlag = enum { BUN_INTERNAL_BUNX_INSTALL, BUN_NO_CODESIGN_MACHO_BINARY, BUN_TRACE, + CLAUDECODE, NODE_NO_WARNINGS, }; diff --git a/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap b/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap new file mode 100644 index 00000000000..06cc7ff5ff9 --- /dev/null +++ b/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap @@ -0,0 +1,5 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`CLAUDECODE=0 shows normal test output 1`] = `"bun test ()"`; + +exports[`CLAUDECODE=1 shows quiet test output (only failures) 1`] = `"bun test ()"`; diff --git a/test/cli/test/claudecode-flag.test.ts b/test/cli/test/claudecode-flag.test.ts new file mode 100644 index 00000000000..7bf855b3f0d --- /dev/null +++ b/test/cli/test/claudecode-flag.test.ts @@ -0,0 +1,142 @@ +import { test, expect } from "bun:test"; +import { bunEnv, bunExe, tempDirWithFiles, normalizeBunSnapshot } from "harness"; +import { spawnSync } from "bun"; + +test("CLAUDECODE=0 shows normal test output", async () => { + const dir = tempDirWithFiles("claudecode-test-normal", { + "test1.test.js": ` + import { test, expect } from "bun:test"; + + test("passing test", () => { + expect(1).toBe(1); + }); + + test("failing test", () => { + expect(1).toBe(2); + }); + + test.skip("skipped test", () => { + expect(1).toBe(1); + }); + + test.todo("todo test"); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "test1.test.js"], + env: { ...bunEnv, CLAUDECODE: "0" }, + cwd: dir, + }); + + const [stdout, stderr] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); + + const output = stderr + stdout; + const normalized = normalizeBunSnapshot(output, dir); + + expect(normalized).toMatchSnapshot(); +}); + +test("CLAUDECODE=1 shows quiet test output (only failures)", async () => { + const dir = tempDirWithFiles("claudecode-test-quiet", { + "test2.test.js": ` + import { test, expect } from "bun:test"; + + test("passing test", () => { + expect(1).toBe(1); + }); + + test("failing test", () => { + expect(1).toBe(2); + }); + + test.skip("skipped test", () => { + expect(1).toBe(1); + }); + + test.todo("todo test"); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "test2.test.js"], + env: { ...bunEnv, CLAUDECODE: "1" }, + cwd: dir, + }); + + const [stdout, stderr] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); + + const output = stderr + stdout; + const normalized = normalizeBunSnapshot(output, dir); + + expect(normalized).toMatchSnapshot(); +}); + +test("CLAUDECODE=1 vs CLAUDECODE=0 comparison", async () => { + const dir = tempDirWithFiles("claudecode-test-compare", { + "test3.test.js": ` + import { test, expect } from "bun:test"; + + test("passing test", () => { + expect(1).toBe(1); + }); + + test("another passing test", () => { + expect(2).toBe(2); + }); + + test.skip("skipped test", () => { + expect(1).toBe(1); + }); + + test.todo("todo test"); + `, + }); + + // Run with CLAUDECODE=0 (normal output) + const result1 = spawnSync({ + cmd: [bunExe(), "test", "test3.test.js"], + env: { ...bunEnv, CLAUDECODE: "0" }, + cwd: dir, + stderr: "pipe", + stdout: "pipe", + }); + + // Run with CLAUDECODE=1 (quiet output) + const result2 = spawnSync({ + cmd: [bunExe(), "test", "test3.test.js"], + env: { ...bunEnv, CLAUDECODE: "1" }, + cwd: dir, + stderr: "pipe", + stdout: "pipe", + }); + + const normalOutput = result1.stderr.toString() + result1.stdout.toString(); + const quietOutput = result2.stderr.toString() + result2.stdout.toString(); + + + // Normal output should contain pass/skip/todo indicators + expect(normalOutput).toContain("(pass)"); // pass indicator + expect(normalOutput).toContain("(skip)"); // skip indicator + expect(normalOutput).toContain("(todo)"); // todo indicator + + // Quiet output should NOT contain pass/skip/todo indicators (only failures) + expect(quietOutput).not.toContain("(pass)"); // pass indicator + expect(quietOutput).not.toContain("(skip)"); // skip indicator + expect(quietOutput).not.toContain("(todo)"); // todo indicator + + // Both should contain the summary at the end + expect(normalOutput).toContain("2 pass"); + expect(normalOutput).toContain("1 skip"); + expect(normalOutput).toContain("1 todo"); + + expect(quietOutput).toContain("2 pass"); + expect(quietOutput).toContain("1 skip"); + expect(quietOutput).toContain("1 todo"); +}); \ No newline at end of file From 45af2b586c890554c73ed6b05fd3961b65fc01a1 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Thu, 17 Jul 2025 15:42:07 +0000 Subject: [PATCH 2/9] Enhance CLAUDECODE=1 to suppress filenames until first failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add filename suppression logic that only shows file paths when there's a failure - Remove duplicate JUnit reporting code for cleaner implementation - Track current file info in CommandLineReporter for deferred filename printing - Add printFilenameIfNeeded function to print filename only when needed - Maintain full JUnit reporting functionality in quiet mode This makes the quiet mode even more focused - showing only essential failure information while completely hiding successful test output. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/cli/test_command.zig | 197 ++++++++++++--------------------------- 1 file changed, 59 insertions(+), 138 deletions(-) diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 64be0c8de83..2bea748d1fb 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -619,6 +619,15 @@ pub const CommandLineReporter = struct { todos_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{}, file_reporter: ?FileReporter = null, + + // Track current file info for CLAUDECODE mode + current_file_title: ?string = null, + current_file_prefix: ?string = null, + current_repeat_info: ?struct { + count: u32, + index: u32, + } = null, + filename_printed_for_current_file: bool = false, pub const FileReporter = union(enum) { junit: *JunitReporter, @@ -649,139 +658,6 @@ pub const CommandLineReporter = struct { file_reporter: ?FileReporter, line_number: u32, ) void { - // In quiet mode (CLAUDECODE=1), only print failures - if (bun.getRuntimeFeatureFlag(.CLAUDECODE) and status != .fail) { - // Still need to handle JUnit reporting if enabled - if (file_reporter) |reporter| { - switch (reporter) { - .junit => |junit| { - const filename = brk: { - if (strings.hasPrefix(file, bun.fs.FileSystem.instance.top_level_dir)) { - break :brk strings.withoutLeadingPathSeparator(file[bun.fs.FileSystem.instance.top_level_dir.len..]); - } else { - break :brk file; - } - }; - - if (!strings.eql(junit.current_file, filename)) { - while (junit.suite_stack.items.len > 0 and !junit.suite_stack.items[junit.suite_stack.items.len - 1].is_file_suite) { - junit.endTestSuite() catch bun.outOfMemory(); - } - - if (junit.current_file.len > 0) { - junit.endTestSuite() catch bun.outOfMemory(); - } - - junit.beginTestSuite(filename) catch bun.outOfMemory(); - } - - var scopes_stack = std.BoundedArray(*jest.DescribeScope, 64).init(0) catch unreachable; - var parent_ = parent; - - while (parent_) |scope| { - scopes_stack.append(scope) catch break; - parent_ = scope.parent; - } - - const scopes: []*jest.DescribeScope = scopes_stack.slice(); - - // Replicate the JUnit reporting logic from the normal flow - var needed_suites = std.ArrayList(*jest.DescribeScope).init(bun.default_allocator); - defer needed_suites.deinit(); - - for (scopes, 0..) |_, i| { - const index = (scopes.len - 1) - i; - const scope = scopes[index]; - if (scope.label.len > 0) { - needed_suites.append(scope) catch bun.outOfMemory(); - } - } - - var current_suite_depth: u32 = 0; - if (junit.suite_stack.items.len > 0) { - for (junit.suite_stack.items) |suite_info| { - if (!suite_info.is_file_suite) { - current_suite_depth += 1; - } - } - } - - while (current_suite_depth > needed_suites.items.len) { - if (junit.suite_stack.items.len > 0 and !junit.suite_stack.items[junit.suite_stack.items.len - 1].is_file_suite) { - junit.endTestSuite() catch bun.outOfMemory(); - current_suite_depth -= 1; - } else { - break; - } - } - - var suites_to_close: u32 = 0; - var suite_index: usize = 0; - for (junit.suite_stack.items) |suite_info| { - if (suite_info.is_file_suite) continue; - - if (suite_index < needed_suites.items.len) { - const needed_scope = needed_suites.items[suite_index]; - if (!strings.eql(suite_info.name, needed_scope.label)) { - suites_to_close = @as(u32, @intCast(current_suite_depth)) - @as(u32, @intCast(suite_index)); - break; - } - } else { - suites_to_close = @as(u32, @intCast(current_suite_depth)) - @as(u32, @intCast(suite_index)); - break; - } - suite_index += 1; - } - - while (suites_to_close > 0) { - if (junit.suite_stack.items.len > 0 and !junit.suite_stack.items[junit.suite_stack.items.len - 1].is_file_suite) { - junit.endTestSuite() catch bun.outOfMemory(); - current_suite_depth -= 1; - suites_to_close -= 1; - } else { - break; - } - } - - var describe_suite_index: usize = 0; - for (junit.suite_stack.items) |suite_info| { - if (!suite_info.is_file_suite) { - describe_suite_index += 1; - } - } - - while (describe_suite_index < needed_suites.items.len) { - const scope = needed_suites.items[describe_suite_index]; - junit.beginTestSuiteWithLine(scope.label, scope.line_number, false) catch bun.outOfMemory(); - describe_suite_index += 1; - } - - var arena = std.heap.ArenaAllocator.init(bun.default_allocator); - defer arena.deinit(); - var stack_fallback = std.heap.stackFallback(4096, arena.allocator()); - const allocator = stack_fallback.get(); - var concatenated_describe_scopes = std.ArrayList(u8).init(allocator); - - { - const initial_length = concatenated_describe_scopes.items.len; - for (scopes) |scope| { - if (scope.label.len > 0) { - if (initial_length != concatenated_describe_scopes.items.len) { - concatenated_describe_scopes.appendSlice(" > ") catch bun.outOfMemory(); - } - - escapeXml(scope.label, concatenated_describe_scopes.writer()) catch bun.outOfMemory(); - } - } - } - - const display_label = if (label.len > 0) label else "test"; - junit.writeTestCase(status, filename, display_label, concatenated_describe_scopes.items, assertions, elapsed_ns, line_number) catch bun.outOfMemory(); - }, - } - } - return; - } var scopes_stack = std.BoundedArray(*jest.DescribeScope, 64).init(0) catch unreachable; var parent_ = parent; @@ -791,8 +667,13 @@ pub const CommandLineReporter = struct { } const scopes: []*jest.DescribeScope = scopes_stack.slice(); - const display_label = if (label.len > 0) label else "test"; + + // In quiet mode (CLAUDECODE=1), only print failures to console + // but still handle JUnit reporting normally below + if (bun.getRuntimeFeatureFlag(.CLAUDECODE) and status != .fail) { + // Skip console output but continue to JUnit reporting + } else { const color_code = comptime if (skip) "" else ""; @@ -836,6 +717,7 @@ pub const CommandLineReporter = struct { } writer.writeAll("\n") catch unreachable; + } if (file_reporter) |reporter| { switch (reporter) { @@ -960,6 +842,27 @@ pub const CommandLineReporter = struct { pub inline fn summary(this: *CommandLineReporter) *TestRunner.Summary { return &this.jest.summary; } + + pub fn printFilenameIfNeeded(this: *CommandLineReporter) void { + if (this.filename_printed_for_current_file) return; + + if (this.current_file_title) |file_title| { + const file_prefix = this.current_file_prefix orelse ""; + + if (this.current_repeat_info) |repeat_info| { + if (repeat_info.count > 1) { + Output.prettyErrorln("\n{s}{s}: (run #{d})\n", .{ file_prefix, file_title, repeat_info.index + 1 }); + } else { + Output.prettyErrorln("\n{s}{s}:\n", .{ file_prefix, file_title }); + } + } else { + Output.prettyErrorln("\n{s}{s}:\n", .{ file_prefix, file_title }); + } + + Output.flush(); + this.filename_printed_for_current_file = true; + } + } pub fn handleTestPass(cb: *TestRunner.Callback, id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void { const writer = Output.errorWriterBuffered(); @@ -982,6 +885,11 @@ pub const CommandLineReporter = struct { defer Output.flush(); var this: *CommandLineReporter = @fieldParentPtr("callback", cb); + // In CLAUDECODE mode, print filename before first failure + if (bun.getRuntimeFeatureFlag(.CLAUDECODE)) { + this.printFilenameIfNeeded(); + } + // when the tests fail, we want to repeat the failures at the end // so that you can see them better when there are lots of tests that ran const initial_length = this.failures_to_repeat_buf.items.len; @@ -1979,12 +1887,25 @@ pub const TestCommand = struct { vm.onUnhandledRejection = jest.TestRunnerTask.onUnhandledRejection; while (repeat_index < repeat_count) : (repeat_index += 1) { - if (repeat_count > 1) { - Output.prettyErrorln("\n{s}{s}: (run #{d})\n", .{ file_prefix, file_title, repeat_index + 1 }); + // Store filename info for CLAUDECODE mode + reporter.current_file_title = file_title; + reporter.current_file_prefix = file_prefix; + reporter.current_repeat_info = if (repeat_count > 1) .{ .count = repeat_count, .index = repeat_index } else null; + reporter.filename_printed_for_current_file = false; + + if (bun.getRuntimeFeatureFlag(.CLAUDECODE)) { + // In CLAUDECODE mode, don't print filename immediately + // It will be printed by printFilenameIfNeeded when first failure occurs } else { - Output.prettyErrorln("\n{s}{s}:\n", .{ file_prefix, file_title }); + // Normal mode - print filename immediately + if (repeat_count > 1) { + Output.prettyErrorln("\n{s}{s}: (run #{d})\n", .{ file_prefix, file_title, repeat_index + 1 }); + } else { + Output.prettyErrorln("\n{s}{s}:\n", .{ file_prefix, file_title }); + } + Output.flush(); + reporter.filename_printed_for_current_file = true; } - Output.flush(); var promise = try vm.loadEntryPointForTestRunner(file_path); reporter.summary().files += 1; From 42520b38278141e8e0fb2430c3076a7e917fd951 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 17 Jul 2025 15:44:27 +0000 Subject: [PATCH 3/9] [autofix.ci] apply automated fixes --- src/cli/test_command.zig | 91 +++++++++++++-------------- test/cli/test/claudecode-flag.test.ts | 21 +++---- 2 files changed, 52 insertions(+), 60 deletions(-) diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 2bea748d1fb..7b9d314034d 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -101,7 +101,7 @@ fn writeTestStatusLine(comptime status: @Type(.enum_literal), writer: anytype) v if (bun.getRuntimeFeatureFlag(.CLAUDECODE) and status != .fail) { return; } - + if (Output.enable_ansi_colors_stderr) writer.print(fmtStatusTextLine(status, true), .{}) catch unreachable else @@ -619,7 +619,7 @@ pub const CommandLineReporter = struct { todos_to_repeat_buf: std.ArrayListUnmanaged(u8) = .{}, file_reporter: ?FileReporter = null, - + // Track current file info for CLAUDECODE mode current_file_title: ?string = null, current_file_prefix: ?string = null, @@ -668,55 +668,54 @@ pub const CommandLineReporter = struct { const scopes: []*jest.DescribeScope = scopes_stack.slice(); const display_label = if (label.len > 0) label else "test"; - + // In quiet mode (CLAUDECODE=1), only print failures to console // but still handle JUnit reporting normally below if (bun.getRuntimeFeatureFlag(.CLAUDECODE) and status != .fail) { // Skip console output but continue to JUnit reporting } else { - - const color_code = comptime if (skip) "" else ""; - - if (Output.enable_ansi_colors_stderr) { - for (scopes, 0..) |_, i| { - const index = (scopes.len - 1) - i; - const scope = scopes[index]; - if (scope.label.len == 0) continue; - writer.writeAll(" ") catch unreachable; - - writer.print(comptime Output.prettyFmt("" ++ color_code, true), .{}) catch unreachable; - writer.writeAll(scope.label) catch unreachable; - writer.print(comptime Output.prettyFmt("", true), .{}) catch unreachable; - writer.writeAll(" >") catch unreachable; - } - } else { - for (scopes, 0..) |_, i| { - const index = (scopes.len - 1) - i; - const scope = scopes[index]; - if (scope.label.len == 0) continue; - writer.writeAll(" ") catch unreachable; - writer.writeAll(scope.label) catch unreachable; - writer.writeAll(" >") catch unreachable; + const color_code = comptime if (skip) "" else ""; + + if (Output.enable_ansi_colors_stderr) { + for (scopes, 0..) |_, i| { + const index = (scopes.len - 1) - i; + const scope = scopes[index]; + if (scope.label.len == 0) continue; + writer.writeAll(" ") catch unreachable; + + writer.print(comptime Output.prettyFmt("" ++ color_code, true), .{}) catch unreachable; + writer.writeAll(scope.label) catch unreachable; + writer.print(comptime Output.prettyFmt("", true), .{}) catch unreachable; + writer.writeAll(" >") catch unreachable; + } + } else { + for (scopes, 0..) |_, i| { + const index = (scopes.len - 1) - i; + const scope = scopes[index]; + if (scope.label.len == 0) continue; + writer.writeAll(" ") catch unreachable; + writer.writeAll(scope.label) catch unreachable; + writer.writeAll(" >") catch unreachable; + } } - } - const line_color_code = if (comptime skip) "" else ""; + const line_color_code = if (comptime skip) "" else ""; - if (Output.enable_ansi_colors_stderr) - writer.print(comptime Output.prettyFmt(line_color_code ++ " {s}", true), .{display_label}) catch unreachable - else - writer.print(comptime Output.prettyFmt(" {s}", false), .{display_label}) catch unreachable; + if (Output.enable_ansi_colors_stderr) + writer.print(comptime Output.prettyFmt(line_color_code ++ " {s}", true), .{display_label}) catch unreachable + else + writer.print(comptime Output.prettyFmt(" {s}", false), .{display_label}) catch unreachable; - if (elapsed_ns > (std.time.ns_per_us * 10)) { - writer.print(" {any}", .{ - Output.ElapsedFormatter{ - .colors = Output.enable_ansi_colors_stderr, - .duration_ns = elapsed_ns, - }, - }) catch unreachable; - } + if (elapsed_ns > (std.time.ns_per_us * 10)) { + writer.print(" {any}", .{ + Output.ElapsedFormatter{ + .colors = Output.enable_ansi_colors_stderr, + .duration_ns = elapsed_ns, + }, + }) catch unreachable; + } - writer.writeAll("\n") catch unreachable; + writer.writeAll("\n") catch unreachable; } if (file_reporter) |reporter| { @@ -842,13 +841,13 @@ pub const CommandLineReporter = struct { pub inline fn summary(this: *CommandLineReporter) *TestRunner.Summary { return &this.jest.summary; } - + pub fn printFilenameIfNeeded(this: *CommandLineReporter) void { if (this.filename_printed_for_current_file) return; - + if (this.current_file_title) |file_title| { const file_prefix = this.current_file_prefix orelse ""; - + if (this.current_repeat_info) |repeat_info| { if (repeat_info.count > 1) { Output.prettyErrorln("\n{s}{s}: (run #{d})\n", .{ file_prefix, file_title, repeat_info.index + 1 }); @@ -858,7 +857,7 @@ pub const CommandLineReporter = struct { } else { Output.prettyErrorln("\n{s}{s}:\n", .{ file_prefix, file_title }); } - + Output.flush(); this.filename_printed_for_current_file = true; } @@ -1892,7 +1891,7 @@ pub const TestCommand = struct { reporter.current_file_prefix = file_prefix; reporter.current_repeat_info = if (repeat_count > 1) .{ .count = repeat_count, .index = repeat_index } else null; reporter.filename_printed_for_current_file = false; - + if (bun.getRuntimeFeatureFlag(.CLAUDECODE)) { // In CLAUDECODE mode, don't print filename immediately // It will be printed by printFilenameIfNeeded when first failure occurs diff --git a/test/cli/test/claudecode-flag.test.ts b/test/cli/test/claudecode-flag.test.ts index 7bf855b3f0d..6d2e7ac140d 100644 --- a/test/cli/test/claudecode-flag.test.ts +++ b/test/cli/test/claudecode-flag.test.ts @@ -1,6 +1,6 @@ -import { test, expect } from "bun:test"; -import { bunEnv, bunExe, tempDirWithFiles, normalizeBunSnapshot } from "harness"; import { spawnSync } from "bun"; +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, normalizeBunSnapshot, tempDirWithFiles } from "harness"; test("CLAUDECODE=0 shows normal test output", async () => { const dir = tempDirWithFiles("claudecode-test-normal", { @@ -29,10 +29,7 @@ test("CLAUDECODE=0 shows normal test output", async () => { cwd: dir, }); - const [stdout, stderr] = await Promise.all([ - new Response(proc.stdout).text(), - new Response(proc.stderr).text(), - ]); + const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]); const output = stderr + stdout; const normalized = normalizeBunSnapshot(output, dir); @@ -67,10 +64,7 @@ test("CLAUDECODE=1 shows quiet test output (only failures)", async () => { cwd: dir, }); - const [stdout, stderr] = await Promise.all([ - new Response(proc.stdout).text(), - new Response(proc.stderr).text(), - ]); + const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]); const output = stderr + stdout; const normalized = normalizeBunSnapshot(output, dir); @@ -120,10 +114,9 @@ test("CLAUDECODE=1 vs CLAUDECODE=0 comparison", async () => { const normalOutput = result1.stderr.toString() + result1.stdout.toString(); const quietOutput = result2.stderr.toString() + result2.stdout.toString(); - // Normal output should contain pass/skip/todo indicators expect(normalOutput).toContain("(pass)"); // pass indicator - expect(normalOutput).toContain("(skip)"); // skip indicator + expect(normalOutput).toContain("(skip)"); // skip indicator expect(normalOutput).toContain("(todo)"); // todo indicator // Quiet output should NOT contain pass/skip/todo indicators (only failures) @@ -135,8 +128,8 @@ test("CLAUDECODE=1 vs CLAUDECODE=0 comparison", async () => { expect(normalOutput).toContain("2 pass"); expect(normalOutput).toContain("1 skip"); expect(normalOutput).toContain("1 todo"); - + expect(quietOutput).toContain("2 pass"); expect(quietOutput).toContain("1 skip"); expect(quietOutput).toContain("1 todo"); -}); \ No newline at end of file +}); From 405fee89871931ce7d40cf3ee3d52c5645ebfffb Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 17 Jul 2025 17:59:27 -0700 Subject: [PATCH 4/9] a --- src/cli/test_command.zig | 155 +++++++++--------- src/feature_flags.zig | 1 - src/output.zig | 49 ++++++ .../claudecode-flag.test.ts.snap | 60 ++++++- test/cli/test/claudecode-flag.test.ts | 76 +++++---- 5 files changed, 229 insertions(+), 112 deletions(-) diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 7b9d314034d..fd4c92e1035 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -97,8 +97,8 @@ fn fmtStatusTextLine(comptime status: @Type(.enum_literal), comptime emoji_or_co } fn writeTestStatusLine(comptime status: @Type(.enum_literal), writer: anytype) void { - // In quiet mode (CLAUDECODE=1), only print failures - if (bun.getRuntimeFeatureFlag(.CLAUDECODE) and status != .fail) { + // When using AI agents, only print failures + if (Output.isAIAgent() and status != .fail) { return; } @@ -607,6 +607,55 @@ pub const JunitReporter = struct { } }; +const CurrentFile = struct { + title: string = "", + prefix: string = "", + repeat_info: struct { + count: u32 = 0, + index: u32 = 0, + } = .{}, + has_printed_filename: bool = false, + + pub fn set(this: *CurrentFile, title: string, prefix: string, repeat_count: u32, repeat_index: u32) void { + if (Output.isAIAgent()) { + this.freeAndClear(); + this.title = bun.default_allocator.dupe(u8, title) catch bun.outOfMemory(); + this.prefix = bun.default_allocator.dupe(u8, prefix) catch bun.outOfMemory(); + this.repeat_info.count = repeat_count; + this.repeat_info.index = repeat_index; + this.has_printed_filename = false; + return; + } + + print(title, prefix, repeat_count, repeat_index); + } + + fn freeAndClear(this: *CurrentFile) void { + bun.default_allocator.free(this.title); + bun.default_allocator.free(this.prefix); + } + + fn print(title: string, prefix: string, repeat_count: u32, repeat_index: u32) void { + if (repeat_count > 0) { + if (repeat_count > 1) { + Output.prettyErrorln("\n{s}{s}: (run #{d})\n", .{ prefix, title, repeat_index + 1 }); + } else { + Output.prettyErrorln("\n{s}{s}:\n", .{ prefix, title }); + } + } else { + Output.prettyErrorln("\n{s}{s}:\n", .{ prefix, title }); + } + + Output.flush(); + } + + pub fn printIfNeeded(this: *CurrentFile) void { + if (this.has_printed_filename) return; + this.has_printed_filename = true; + print(this.title, this.prefix, this.repeat_info.count, this.repeat_info.index); + } +}; + pub const CommandLineReporter = struct { jest: TestRunner, callback: TestRunner.Callback, @@ -620,14 +669,7 @@ pub const CommandLineReporter = struct { file_reporter: ?FileReporter = null, - // Track current file info for CLAUDECODE mode - current_file_title: ?string = null, - current_file_prefix: ?string = null, - current_repeat_info: ?struct { - count: u32, - index: u32, - } = null, - filename_printed_for_current_file: bool = false, + current_file: CurrentFile = .{}, pub const FileReporter = union(enum) { junit: *JunitReporter, @@ -669,11 +711,8 @@ pub const CommandLineReporter = struct { const scopes: []*jest.DescribeScope = scopes_stack.slice(); const display_label = if (label.len > 0) label else "test"; - // In quiet mode (CLAUDECODE=1), only print failures to console - // but still handle JUnit reporting normally below - if (bun.getRuntimeFeatureFlag(.CLAUDECODE) and status != .fail) { - // Skip console output but continue to JUnit reporting - } else { + // Quieter output when claude code is in use. + if (!Output.isAIAgent() or status == .fail) { const color_code = comptime if (skip) "" else ""; if (Output.enable_ansi_colors_stderr) { @@ -842,27 +881,6 @@ pub const CommandLineReporter = struct { return &this.jest.summary; } - pub fn printFilenameIfNeeded(this: *CommandLineReporter) void { - if (this.filename_printed_for_current_file) return; - - if (this.current_file_title) |file_title| { - const file_prefix = this.current_file_prefix orelse ""; - - if (this.current_repeat_info) |repeat_info| { - if (repeat_info.count > 1) { - Output.prettyErrorln("\n{s}{s}: (run #{d})\n", .{ file_prefix, file_title, repeat_info.index + 1 }); - } else { - Output.prettyErrorln("\n{s}{s}:\n", .{ file_prefix, file_title }); - } - } else { - Output.prettyErrorln("\n{s}{s}:\n", .{ file_prefix, file_title }); - } - - Output.flush(); - this.filename_printed_for_current_file = true; - } - } - pub fn handleTestPass(cb: *TestRunner.Callback, id: Test.ID, file: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void { const writer = Output.errorWriterBuffered(); defer Output.flush(); @@ -884,10 +902,7 @@ pub const CommandLineReporter = struct { defer Output.flush(); var this: *CommandLineReporter = @fieldParentPtr("callback", cb); - // In CLAUDECODE mode, print filename before first failure - if (bun.getRuntimeFeatureFlag(.CLAUDECODE)) { - this.printFilenameIfNeeded(); - } + this.current_file.printIfNeeded(); // when the tests fail, we want to repeat the failures at the end // so that you can see them better when there are lots of tests that ran @@ -1352,9 +1367,11 @@ pub const TestCommand = struct { pub fn exec(ctx: Command.Context) !void { Output.is_github_action = Output.isGithubAction(); - // print the version so you know its doing stuff if it takes a sec - Output.prettyln("bun test v" ++ Global.package_json_version_with_sha ++ "", .{}); - Output.flush(); + if (!Output.isAIAgent()) { + // print the version so you know its doing stuff if it takes a sec + Output.prettyln("bun test v" ++ Global.package_json_version_with_sha ++ "", .{}); + Output.flush(); + } var env_loader = brk: { const map = try ctx.allocator.create(DotEnv.Map); @@ -1616,16 +1633,24 @@ pub const TestCommand = struct { if (test_files.len == 0) { failed_to_find_any_tests = true; - if (ctx.positionals.len == 0) { - Output.prettyErrorln( - \\No tests found! - \\Tests need ".test", "_test_", ".spec" or "_spec_" in the filename (ex: "MyApp.test.ts") - \\ - , .{}); + // "bun test" - positionals[0] == "test" + // Therefore positionals starts at [1]. + if (ctx.positionals.len < 2) { + if (Output.isAIAgent()) { + // Be very clear to ai. + Output.errGeneric("0 test files matching **.{{test,spec,_test_,_spec_}}.{{js,ts,jsx,tsx}} found in cwd {}", .{bun.fmt.quote(bun.fs.FileSystem.instance.top_level_dir)}); + } else { + // Be friendlier to humans. + Output.prettyErrorln( + \\No tests found! + \\ + \\Tests need ".test", "_test_", ".spec" or "_spec_" in the filename (ex: "MyApp.test.ts") + \\ + , .{}); + } } else { Output.prettyErrorln("The following filters did not match any test files:", .{}); var has_file_like: ?usize = null; - Output.prettyError(" ", .{}); for (ctx.positionals[1..], 1..) |filter, i| { Output.prettyError(" {s}", .{filter}); @@ -1656,10 +1681,12 @@ pub const TestCommand = struct { , .{ ctx.positionals[i], ctx.positionals[i] }); } } - Output.prettyError( - \\ - \\Learn more about the test runner: https://bun.com/docs/cli/test - , .{}); + if (!Output.isAIAgent()) { + Output.prettyError( + \\ + \\Learn more about bun test: https://bun.com/docs/cli/test + , .{}); + } } else { Output.prettyError("\n", .{}); @@ -1886,25 +1913,7 @@ pub const TestCommand = struct { vm.onUnhandledRejection = jest.TestRunnerTask.onUnhandledRejection; while (repeat_index < repeat_count) : (repeat_index += 1) { - // Store filename info for CLAUDECODE mode - reporter.current_file_title = file_title; - reporter.current_file_prefix = file_prefix; - reporter.current_repeat_info = if (repeat_count > 1) .{ .count = repeat_count, .index = repeat_index } else null; - reporter.filename_printed_for_current_file = false; - - if (bun.getRuntimeFeatureFlag(.CLAUDECODE)) { - // In CLAUDECODE mode, don't print filename immediately - // It will be printed by printFilenameIfNeeded when first failure occurs - } else { - // Normal mode - print filename immediately - if (repeat_count > 1) { - Output.prettyErrorln("\n{s}{s}: (run #{d})\n", .{ file_prefix, file_title, repeat_index + 1 }); - } else { - Output.prettyErrorln("\n{s}{s}:\n", .{ file_prefix, file_title }); - } - Output.flush(); - reporter.filename_printed_for_current_file = true; - } + reporter.current_file.set(file_title, file_prefix, repeat_count, repeat_index); var promise = try vm.loadEntryPointForTestRunner(file_path); reporter.summary().files += 1; diff --git a/src/feature_flags.zig b/src/feature_flags.zig index 52b078061d1..8521372fcd6 100644 --- a/src/feature_flags.zig +++ b/src/feature_flags.zig @@ -36,7 +36,6 @@ pub const RuntimeFeatureFlag = enum { BUN_INTERNAL_BUNX_INSTALL, BUN_NO_CODESIGN_MACHO_BINARY, BUN_TRACE, - CLAUDECODE, NODE_NO_WARNINGS, }; diff --git a/src/output.zig b/src/output.zig index 8f82c7be1a8..f2ea332e817 100644 --- a/src/output.zig +++ b/src/output.zig @@ -462,6 +462,55 @@ pub fn isGithubAction() bool { return false; } +pub fn isAIAgent() bool { + const get_is_agent = struct { + var value = false; + fn evaluate() bool { + if (bun.getenvZ("IS_CODE_AGENT")) |env| { + return strings.eqlComptime(env, "1"); + } + + if (isVerbose()) { + return false; + } + + // Claude Code. + if (bun.getenvTruthy("CLAUDECODE")) { + return true; + } + + // Replit. + if (bun.getenvTruthy("REPL_ID")) { + return true; + } + + // TODO: add environment variable for Gemini + // Gemini does not appear to add any environment variables to identify it. + + // TODO: add environment variable for Codex + // codex does not appear to add any environment variables to identify it. + + // TODO: add environment variable for Cursor Background Agents + // cursor does not appear to add any environment variables to identify it. + + return false; + } + + fn setValue() void { + value = evaluate(); + } + + var once = std.once(setValue); + + pub fn isEnabled() bool { + once.call(); + return value; + } + }; + + return get_is_agent.isEnabled(); +} + pub fn isVerbose() bool { // Set by Github Actions when a workflow is run using debug mode. if (bun.getenvZ("RUNNER_DEBUG")) |value| { diff --git a/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap b/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap index 06cc7ff5ff9..6f02b710124 100644 --- a/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap +++ b/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap @@ -1,5 +1,61 @@ // Bun Snapshot v1, https://bun.sh/docs/test/snapshots -exports[`CLAUDECODE=0 shows normal test output 1`] = `"bun test ()"`; +exports[`CLAUDECODE=1 shows quiet test output (only failures) 1`] = ` +"4 | test("passing test", () => { +5 | expect(1).toBe(1); +6 | }); +7 | +8 | test("failing test", () => { +9 | expect(1).toBe(2); + ^ +error: expect(received).toBe(expected) -exports[`CLAUDECODE=1 shows quiet test output (only failures) 1`] = `"bun test ()"`; +Expected: 2 +Received: 1 + at (file:NN:NN) + +test2.test.js: +(fail) failing test + + 1 pass + 1 skip + 1 todo + 1 fail + 2 expect() calls +Ran 4 tests across 1 file." +`; + +exports[`CLAUDECODE=1 vs CLAUDECODE=0 comparison: normal 1`] = ` +"test3.test.js: +(pass) passing test +(pass) another passing test +(skip) skipped test +(todo) todo test + + 2 pass + 1 skip + 1 todo + 0 fail + 2 expect() calls +Ran 4 tests across 1 file. +bun test ()" +`; + +exports[`CLAUDECODE=1 vs CLAUDECODE=0 comparison: quiet 1`] = ` +"2 pass + 1 skip + 1 todo + 0 fail + 2 expect() calls +Ran 4 tests across 1 file." +`; + +exports[`CLAUDECODE flag handles no test files found: no-tests-normal 1`] = ` +"No tests found! +Tests need ".test", "_test_", ".spec" or "_spec_" in the filename (ex: "MyApp.test.ts") + +Learn more about bun test: https://bun.com/docs/cli/test +bun test ()" +`; + +exports[`CLAUDECODE flag handles no test files found: no-tests-quiet 1`] = `"error: 0 test files matching **.{test,spec,_test_,_spec_}.{js,ts,jsx,tsx} found in cwd """`; diff --git a/test/cli/test/claudecode-flag.test.ts b/test/cli/test/claudecode-flag.test.ts index 6d2e7ac140d..76be32a1c37 100644 --- a/test/cli/test/claudecode-flag.test.ts +++ b/test/cli/test/claudecode-flag.test.ts @@ -2,41 +2,6 @@ import { spawnSync } from "bun"; import { expect, test } from "bun:test"; import { bunEnv, bunExe, normalizeBunSnapshot, tempDirWithFiles } from "harness"; -test("CLAUDECODE=0 shows normal test output", async () => { - const dir = tempDirWithFiles("claudecode-test-normal", { - "test1.test.js": ` - import { test, expect } from "bun:test"; - - test("passing test", () => { - expect(1).toBe(1); - }); - - test("failing test", () => { - expect(1).toBe(2); - }); - - test.skip("skipped test", () => { - expect(1).toBe(1); - }); - - test.todo("todo test"); - `, - }); - - await using proc = Bun.spawn({ - cmd: [bunExe(), "test", "test1.test.js"], - env: { ...bunEnv, CLAUDECODE: "0" }, - cwd: dir, - }); - - const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]); - - const output = stderr + stdout; - const normalized = normalizeBunSnapshot(output, dir); - - expect(normalized).toMatchSnapshot(); -}); - test("CLAUDECODE=1 shows quiet test output (only failures)", async () => { const dir = tempDirWithFiles("claudecode-test-quiet", { "test2.test.js": ` @@ -62,9 +27,11 @@ test("CLAUDECODE=1 shows quiet test output (only failures)", async () => { cmd: [bunExe(), "test", "test2.test.js"], env: { ...bunEnv, CLAUDECODE: "1" }, cwd: dir, + stderr: "pipe", + stdout: "pipe", }); - const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]); + const [stdout, stderr] = await Promise.all([proc.stdout.text(), proc.stderr.text()]); const output = stderr + stdout; const normalized = normalizeBunSnapshot(output, dir); @@ -132,4 +99,41 @@ test("CLAUDECODE=1 vs CLAUDECODE=0 comparison", async () => { expect(quietOutput).toContain("2 pass"); expect(quietOutput).toContain("1 skip"); expect(quietOutput).toContain("1 todo"); + + expect(normalizeBunSnapshot(normalOutput, dir)).toMatchSnapshot("normal"); + expect(normalizeBunSnapshot(quietOutput, dir)).toMatchSnapshot("quiet"); +}); + +test("CLAUDECODE flag handles no test files found", () => { + const dir = tempDirWithFiles("empty-project", { + "package.json": `{ + "name": "empty-project", + "version": "1.0.0" + }`, + "src/index.js": `console.log("hello world");`, + }); + + // Run with CLAUDECODE=0 (normal output) - no test files + const result1 = spawnSync({ + cmd: [bunExe(), "test"], + env: { ...bunEnv, CLAUDECODE: "0" }, + cwd: dir, + stderr: "pipe", + stdout: "pipe", + }); + + // Run with CLAUDECODE=1 (quiet output) - no test files + const result2 = spawnSync({ + cmd: [bunExe(), "test"], + env: { ...bunEnv, CLAUDECODE: "1" }, + cwd: dir, + stderr: "pipe", + stdout: "pipe", + }); + + const normalOutput = result1.stderr.toString() + result1.stdout.toString(); + const quietOutput = result2.stderr.toString() + result2.stdout.toString(); + + expect(normalizeBunSnapshot(normalOutput, dir)).toMatchSnapshot("no-tests-normal"); + expect(normalizeBunSnapshot(quietOutput, dir)).toMatchSnapshot("no-tests-quiet"); }); From e5d09bde413eb93ea643571c7a304286d4ec9299 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 17 Jul 2025 19:05:16 -0700 Subject: [PATCH 5/9] Fix --- src/bun.js/test/jest.zig | 62 +++++++++++++++++++ src/cli/test_command.zig | 55 +--------------- .../claudecode-flag.test.ts.snap | 6 +- 3 files changed, 67 insertions(+), 56 deletions(-) diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 4c2649a48d7..bbf0244942e 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -39,8 +39,61 @@ pub const Tag = enum(u3) { skipped_because_label, }; const debug = Output.scoped(.jest, false); + var max_test_id_for_debugger: u32 = 0; + +const CurrentFile = struct { + title: string = "", + prefix: string = "", + repeat_info: struct { + count: u32 = 0, + index: u32 = 0, + } = .{}, + has_printed_filename: bool = false, + + pub fn set(this: *CurrentFile, title: string, prefix: string, repeat_count: u32, repeat_index: u32) void { + if (Output.isAIAgent()) { + this.freeAndClear(); + this.title = bun.default_allocator.dupe(u8, title) catch bun.outOfMemory(); + this.prefix = bun.default_allocator.dupe(u8, prefix) catch bun.outOfMemory(); + this.repeat_info.count = repeat_count; + this.repeat_info.index = repeat_index; + this.has_printed_filename = false; + return; + } + + this.has_printed_filename = true; + print(title, prefix, repeat_count, repeat_index); + } + + fn freeAndClear(this: *CurrentFile) void { + bun.default_allocator.free(this.title); + bun.default_allocator.free(this.prefix); + } + + fn print(title: string, prefix: string, repeat_count: u32, repeat_index: u32) void { + if (repeat_count > 0) { + if (repeat_count > 1) { + Output.prettyErrorln("\n{s}{s}: (run #{d})\n", .{ prefix, title, repeat_index + 1 }); + } else { + Output.prettyErrorln("\n{s}{s}:\n", .{ prefix, title }); + } + } else { + Output.prettyErrorln("\n{s}{s}:\n", .{ prefix, title }); + } + + Output.flush(); + } + + pub fn printIfNeeded(this: *CurrentFile) void { + if (this.has_printed_filename) return; + this.has_printed_filename = true; + print(this.title, this.prefix, this.repeat_info.count, this.repeat_info.index); + } +}; + pub const TestRunner = struct { + current_file: CurrentFile = CurrentFile{}, tests: TestRunner.Test.List = .{}, log: *logger.Log, files: File.List = .{}, @@ -1325,6 +1378,10 @@ pub const TestRunnerTask = struct { deduped = true; } else { if (is_unhandled and Jest.runner != null) { + if (Output.isAIAgent()) { + Jest.runner.?.current_file.printIfNeeded(); + } + Output.prettyErrorln( \\ \\# Unhandled error between tests @@ -1333,7 +1390,12 @@ pub const TestRunnerTask = struct { , .{}); Output.flush(); + } else if (!is_unhandled and Jest.runner != null) { + if (Output.isAIAgent()) { + Jest.runner.?.current_file.printIfNeeded(); + } } + jsc_vm.runErrorHandlerWithDedupe(rejection, jsc_vm.onUnhandledRejectionExceptionList); if (is_unhandled and Jest.runner != null) { Output.prettyError("-------------------------------\n\n", .{}); diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index fd4c92e1035..6ca57d97c79 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -607,55 +607,6 @@ pub const JunitReporter = struct { } }; -const CurrentFile = struct { - title: string = "", - prefix: string = "", - repeat_info: struct { - count: u32 = 0, - index: u32 = 0, - } = .{}, - has_printed_filename: bool = false, - - pub fn set(this: *CurrentFile, title: string, prefix: string, repeat_count: u32, repeat_index: u32) void { - if (Output.isAIAgent()) { - this.freeAndClear(); - this.title = bun.default_allocator.dupe(u8, title) catch bun.outOfMemory(); - this.prefix = bun.default_allocator.dupe(u8, prefix) catch bun.outOfMemory(); - this.repeat_info.count = repeat_count; - this.repeat_info.index = repeat_index; - this.has_printed_filename = false; - return; - } - - print(title, prefix, repeat_count, repeat_index); - } - - fn freeAndClear(this: *CurrentFile) void { - bun.default_allocator.free(this.title); - bun.default_allocator.free(this.prefix); - } - - fn print(title: string, prefix: string, repeat_count: u32, repeat_index: u32) void { - if (repeat_count > 0) { - if (repeat_count > 1) { - Output.prettyErrorln("\n{s}{s}: (run #{d})\n", .{ prefix, title, repeat_index + 1 }); - } else { - Output.prettyErrorln("\n{s}{s}:\n", .{ prefix, title }); - } - } else { - Output.prettyErrorln("\n{s}{s}:\n", .{ prefix, title }); - } - - Output.flush(); - } - - pub fn printIfNeeded(this: *CurrentFile) void { - if (this.has_printed_filename) return; - this.has_printed_filename = true; - print(this.title, this.prefix, this.repeat_info.count, this.repeat_info.index); - } -}; - pub const CommandLineReporter = struct { jest: TestRunner, callback: TestRunner.Callback, @@ -669,8 +620,6 @@ pub const CommandLineReporter = struct { file_reporter: ?FileReporter = null, - current_file: CurrentFile = .{}, - pub const FileReporter = union(enum) { junit: *JunitReporter, }; @@ -902,7 +851,7 @@ pub const CommandLineReporter = struct { defer Output.flush(); var this: *CommandLineReporter = @fieldParentPtr("callback", cb); - this.current_file.printIfNeeded(); + this.jest.current_file.printIfNeeded(); // when the tests fail, we want to repeat the failures at the end // so that you can see them better when there are lots of tests that ran @@ -1913,7 +1862,7 @@ pub const TestCommand = struct { vm.onUnhandledRejection = jest.TestRunnerTask.onUnhandledRejection; while (repeat_index < repeat_count) : (repeat_index += 1) { - reporter.current_file.set(file_title, file_prefix, repeat_count, repeat_index); + reporter.jest.current_file.set(file_title, file_prefix, repeat_count, repeat_index); var promise = try vm.loadEntryPointForTestRunner(file_path); reporter.summary().files += 1; diff --git a/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap b/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap index 6f02b710124..7205472c0d8 100644 --- a/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap +++ b/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap @@ -1,7 +1,8 @@ // Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`CLAUDECODE=1 shows quiet test output (only failures) 1`] = ` -"4 | test("passing test", () => { +"test2.test.js: +4 | test("passing test", () => { 5 | expect(1).toBe(1); 6 | }); 7 | @@ -13,8 +14,6 @@ error: expect(received).toBe(expected) Expected: 2 Received: 1 at (file:NN:NN) - -test2.test.js: (fail) failing test 1 pass @@ -52,6 +51,7 @@ Ran 4 tests across 1 file." exports[`CLAUDECODE flag handles no test files found: no-tests-normal 1`] = ` "No tests found! + Tests need ".test", "_test_", ".spec" or "_spec_" in the filename (ex: "MyApp.test.ts") Learn more about bun test: https://bun.com/docs/cli/test From bdcf21f2204510f49a09d71472006354ba4c7740 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 17 Jul 2025 19:25:36 -0700 Subject: [PATCH 6/9] Update test_command.zig --- src/cli/test_command.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 6ca57d97c79..8054d04ec50 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -1541,7 +1541,7 @@ pub const TestCommand = struct { const write_snapshots_success = try jest.Jest.runner.?.snapshots.writeInlineSnapshots(); try jest.Jest.runner.?.snapshots.writeSnapshotFile(); var coverage_options = ctx.test_options.coverage; - if (reporter.summary().pass > 20) { + if (reporter.summary().pass > 20 and !Output.isAIAgent()) { if (reporter.summary().skip > 0) { Output.prettyError("\n{d} tests skipped:\n", .{reporter.summary().skip}); Output.flush(); From 91bd1f81cef1d7e26aa5d6ab050d6fcf73b6a81d Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 17 Jul 2025 19:56:07 -0700 Subject: [PATCH 7/9] okay done --- src/cli/test_command.zig | 8 +++----- src/output.zig | 4 +++- .../test/__snapshots__/claudecode-flag.test.ts.snap | 12 +++++++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 8054d04ec50..48a4e50a117 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -1316,11 +1316,9 @@ pub const TestCommand = struct { pub fn exec(ctx: Command.Context) !void { Output.is_github_action = Output.isGithubAction(); - if (!Output.isAIAgent()) { - // print the version so you know its doing stuff if it takes a sec - Output.prettyln("bun test v" ++ Global.package_json_version_with_sha ++ "", .{}); - Output.flush(); - } + // print the version so you know its doing stuff if it takes a sec + Output.prettyln("bun test v" ++ Global.package_json_version_with_sha ++ "", .{}); + Output.flush(); var env_loader = brk: { const map = try ctx.allocator.create(DotEnv.Map); diff --git a/src/output.zig b/src/output.zig index f2ea332e817..5f1b1c93789 100644 --- a/src/output.zig +++ b/src/output.zig @@ -457,7 +457,9 @@ pub inline fn isEmojiEnabled() bool { pub fn isGithubAction() bool { if (bun.getenvZ("GITHUB_ACTIONS")) |value| { - return strings.eqlComptime(value, "true"); + return strings.eqlComptime(value, "true") and + // Do not print github annotations for AI agents because that wastes the context window. + !isAIAgent(); } return false; } diff --git a/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap b/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap index 7205472c0d8..01ad4bfa555 100644 --- a/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap +++ b/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap @@ -21,7 +21,8 @@ Received: 1 1 todo 1 fail 2 expect() calls -Ran 4 tests across 1 file." +Ran 4 tests across 1 file. +bun test ()" `; exports[`CLAUDECODE=1 vs CLAUDECODE=0 comparison: normal 1`] = ` @@ -46,7 +47,8 @@ exports[`CLAUDECODE=1 vs CLAUDECODE=0 comparison: quiet 1`] = ` 1 todo 0 fail 2 expect() calls -Ran 4 tests across 1 file." +Ran 4 tests across 1 file. +bun test ()" `; exports[`CLAUDECODE flag handles no test files found: no-tests-normal 1`] = ` @@ -58,4 +60,8 @@ Learn more about bun test: https://bun.com/docs/cli/test bun test ()" `; -exports[`CLAUDECODE flag handles no test files found: no-tests-quiet 1`] = `"error: 0 test files matching **.{test,spec,_test_,_spec_}.{js,ts,jsx,tsx} found in cwd """`; +exports[`CLAUDECODE flag handles no test files found: no-tests-quiet 1`] = ` +"error: 0 test files matching **.{test,spec,_test_,_spec_}.{js,ts,jsx,tsx} found in cwd "" + +bun test ()" +`; From 6eef9cf8f10523eeaf395de002f572c1b3f88ec2 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 17 Jul 2025 20:31:27 -0700 Subject: [PATCH 8/9] jk one more --- src/cli/test_command.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 48a4e50a117..cd7a89feb84 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -1585,7 +1585,7 @@ pub const TestCommand = struct { if (ctx.positionals.len < 2) { if (Output.isAIAgent()) { // Be very clear to ai. - Output.errGeneric("0 test files matching **.{{test,spec,_test_,_spec_}}.{{js,ts,jsx,tsx}} found in cwd {}", .{bun.fmt.quote(bun.fs.FileSystem.instance.top_level_dir)}); + Output.errGeneric("0 test files matching **{{.test,.spec,_test_,_spec_}}.{{js,ts,jsx,tsx}} in --cwd={}", .{bun.fmt.quote(bun.fs.FileSystem.instance.top_level_dir)}); } else { // Be friendlier to humans. Output.prettyErrorln( From 25d749afe7123f5cc877cd2ba854ffae3eefdd28 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 17 Jul 2025 20:31:41 -0700 Subject: [PATCH 9/9] Update claudecode-flag.test.ts.snap --- test/cli/test/__snapshots__/claudecode-flag.test.ts.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap b/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap index 01ad4bfa555..ce36ec955e3 100644 --- a/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap +++ b/test/cli/test/__snapshots__/claudecode-flag.test.ts.snap @@ -61,7 +61,7 @@ bun test ()" `; exports[`CLAUDECODE flag handles no test files found: no-tests-quiet 1`] = ` -"error: 0 test files matching **.{test,spec,_test_,_spec_}.{js,ts,jsx,tsx} found in cwd "" +"error: 0 test files matching **{.test,.spec,_test_,_spec_}.{js,ts,jsx,tsx} in --cwd="" bun test ()" `;