Skip to content

feat(test): implement onTestFinished hook for bun:test#24038

Merged
Jarred-Sumner merged 12 commits into
mainfrom
claude/implement-on-test-finished
Oct 25, 2025
Merged

feat(test): implement onTestFinished hook for bun:test#24038
Jarred-Sumner merged 12 commits into
mainfrom
claude/implement-on-test-finished

Conversation

@robobun

@robobun robobun commented Oct 24, 2025

Copy link
Copy Markdown
Collaborator

Summary

Implements onTestFinished() for bun:test, which runs after all afterEach hooks have completed.

Implementation

  • Added onTestFinished export to the test module in jest.zig
  • Modified genericHook in bun_test.zig to handle onTestFinished as a special case that:
    • Can only be called inside a test (not in describe blocks or preload)
    • Appends hooks at the very end of the execution sequence
  • Added comprehensive tests covering basic ordering, multiple callbacks, async callbacks, and interaction with other hooks

Execution Order

When called inside a test:

  1. Test body executes
  2. afterAll hooks (if added inside the test)
  3. afterEach hooks
  4. onTestFinished hooks ✨

Test Plan

  • ✅ All new tests pass with bun bd test
  • ✅ Tests correctly fail with USE_SYSTEM_BUN=1 (feature not in released version)
  • ✅ Verifies correct ordering with afterEach, afterAll, and multiple onTestFinished calls
  • ✅ Tests async onTestFinished callbacks

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

This adds the onTestFinished() function to bun:test, which runs after
all afterEach hooks have completed. This allows cleanup code that
needs to run at the very end of a test.

Execution order:
1. Test body
2. afterAll hooks (if added inside test)
3. afterEach hooks
4. onTestFinished hooks

onTestFinished can only be called inside a test, not in describe blocks
or during preload. Multiple onTestFinished hooks can be registered and
they execute in the order they were added.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@robobun

robobun commented Oct 24, 2025

Copy link
Copy Markdown
Collaborator Author
Updated 4:33 PM PT - Oct 24th, 2025

@pfgithub, your commit c17b1d9 has 3 failures in Build #30174 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 24038

That installs a local version of the PR into your bun-24038 executable, so you can run:

bun-24038 --bun

Claude Bot and others added 9 commits October 24, 2025 19:44
Cleaner code structure using a switch statement to determine the
append point based on the hook tag, rather than conditional checks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Simplified the loop logic to break directly to the test_entry when found,
and fallback to active_entry when not found, matching the original behavior
exactly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Remove redundant outer if-check by moving the error handling into
the switch statement's else branch. Cleaner control flow.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add TypeScript type definition for onTestFinished function in bun:test
module with documentation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add onTestFinished to the test lifecycle hooks documentation table.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Replace manual for-loop assertions with cleaner expect().toEqual() calls
in onTestFinished tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Increment the initial object size from 19 to 20 to account for the
new onTestFinished export.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Remove unnecessary output.push("test 2") and check full output array
instead of using slice.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

@pfgithub pfgithub left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

wording is a bit confusing for the docs. pending CI.

@pfgithub pfgithub marked this pull request as ready for review October 24, 2025 21:16
@pfgithub pfgithub requested a review from alii as a code owner October 24, 2025 21:16
@coderabbitai

coderabbitai Bot commented Oct 24, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a new onTestFinished lifecycle hook across docs, types, runtime, and tests; updates mock API usage in docs to use jest.fn() instead of mock().

Changes

Cohort / File(s) Summary
Documentation
docs/cli/test.md
Updated lifecycle hooks table (added onTestFinished) and replaced mock() example with jest.fn() usage.
Type Definitions
packages/bun-types/test.d.ts
Added onTestFinished(...) declaration to bun:test typings (note: duplicate insertion present in patch).
Runtime: Test Execution
src/bun.js/test/bun_test.zig
Added preload/collection guards for onTestFinished, refactored execution append-point logic with switch over hook tag, and appends new ExecutionEntry for onTestFinished.
Runtime: Jest Module
src/bun.js/test/jest.zig
Increased test module allocation size and exposed onTestFinished binding on the Jest test module.
Tests
test/js/bun/test/test-on-test-finished.test.ts
Added tests covering ordering, multiple callbacks, async behavior, concurrent-test errors, and interactions with afterEach/afterAll.

Possibly related PRs

Suggested reviewers

  • alii
  • nektro
  • pfgithub

Pre-merge checks

✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "feat(test): implement onTestFinished hook for bun:test" clearly and directly summarizes the main change in the changeset. It uses conventional commit format (feat scope), is specific about the feature being added (onTestFinished hook), and indicates the target module (bun:test). The title is concise, avoids vague terminology, and provides sufficient context for teammates scanning the history to immediately understand the primary purpose of this PR.
Description Check ✅ Passed The pull request description comprehensively addresses both required template sections. The "What does this PR do?" requirement is satisfied through the Summary, Implementation, and Execution Order sections, which clearly explain the onTestFinished feature and its behavior. The "How did you verify your code works?" requirement is fulfilled by the Test Plan section, which documents the testing approach, verification steps, and successful outcomes. The description is well-organized, detailed, and goes beyond minimum requirements by explaining implementation details and hook execution order.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ab1395d and 371e0c3.

📒 Files selected for processing (5)
  • docs/cli/test.md (1 hunks)
  • packages/bun-types/test.d.ts (1 hunks)
  • src/bun.js/test/bun_test.zig (2 hunks)
  • src/bun.js/test/jest.zig (2 hunks)
  • test/js/bun/test/test-on-test-finished.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (11)
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/bun.js/test/jest.zig
  • src/bun.js/test/bun_test.zig
src/bun.js/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

src/bun.js/**/*.zig: In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS and re-export toJS/fromJS/fromJSDirect
Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions
Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors
Implement getters as get(this, globalObject) returning JSC.JSValue and matching the .classes.ts interface
Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern
Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)
For properties marked cache: true, use the generated Zig accessors (NameSetCached/GetCached) to work with GC-owned values
In finalize() for objects holding JS references, release them using .deref() before destroy

Files:

  • src/bun.js/test/jest.zig
  • src/bun.js/test/bun_test.zig
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

src/**/*.zig: Use private fields in Zig with the # prefix (e.g., struct { #foo: u32 };)
Prefer decl literals in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 };)
Prefer placing @import statements at the bottom of the Zig file (formatter may reorder automatically)
Prefer @import("bun") rather than @import("root").bun or @import("../bun.zig")

In Zig code, be careful with allocators and use defer for cleanup

Files:

  • src/bun.js/test/jest.zig
  • src/bun.js/test/bun_test.zig
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/bun/test/test-on-test-finished.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/bun/test/test-on-test-finished.test.ts
test/js/bun/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place Bun API tests under test/js/bun/, separated by category (e.g., test/js/bun/glob/)

Files:

  • test/js/bun/test/test-on-test-finished.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/bun/test/test-on-test-finished.test.ts
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}: Use bun:test for files ending with *.test.{ts,js,jsx,tsx,mjs,cjs}
Prefer concurrent tests (test.concurrent/describe.concurrent) over sequential when feasible
Organize tests with describe blocks to group related tests
Use utilities like describe.each, toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach) and track resources for cleanup

Files:

  • test/js/bun/test/test-on-test-finished.test.ts
test/**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

For large/repetitive strings, use Buffer.alloc(count, fill).toString() instead of "A".repeat(count)

Files:

  • test/js/bun/test/test-on-test-finished.test.ts
test/js/{bun,node}/**

📄 CodeRabbit inference engine (test/CLAUDE.md)

Organize unit tests by module under /test/js/bun/ and /test/js/node/

Files:

  • test/js/bun/test/test-on-test-finished.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Test files must be created under test/ and end with .test.ts or .test.tsx
In tests that open network ports, always use port: 0; do not hardcode port numbers or roll your own random port
Use normalizeBunSnapshot to normalize snapshot output instead of comparing raw stdout
Do not write tests that merely assert absence of strings like "panic" or "uncaught exception"
Use tempDir from "harness" to create temporary directories; do not use fs.mkdtempSync or tmpdirSync
When spawning processes in tests, assert output before asserting the exit code
Avoid shell commands in tests (e.g., find, grep); prefer Bun's Glob and built-in tools
Prefer snapshot tests over direct string equality assertions on stdout

Files:

  • test/js/bun/test/test-on-test-finished.test.ts
🧠 Learnings (12)
📓 Common learnings
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use utilities like `describe.each`, `toMatchSnapshot`, and lifecycle hooks (`beforeAll`, `beforeEach`, `afterEach`) and track resources for cleanup
📚 Learning: 2025-08-30T00:11:00.890Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/javascriptcore-class.mdc:0-0
Timestamp: 2025-08-30T00:11:00.890Z
Learning: Applies to **/*.zig : Wrap the Bun__<Type>__toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Applied to files:

  • src/bun.js/test/jest.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors

Applied to files:

  • src/bun.js/test/jest.zig
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use utilities like `describe.each`, `toMatchSnapshot`, and lifecycle hooks (`beforeAll`, `beforeEach`, `afterEach`) and track resources for cleanup

Applied to files:

  • test/js/bun/test/test-on-test-finished.test.ts
  • docs/cli/test.md
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : When spawning Bun in tests, use `bunExe()` and `bunEnv` from `harness`

Applied to files:

  • src/bun.js/test/bun_test.zig
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : Do not set explicit test timeouts; Bun already has timeouts

Applied to files:

  • src/bun.js/test/bun_test.zig
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/js/bun/**/*.{js,ts} : Place Bun API tests under test/js/bun/, separated by category (e.g., test/js/bun/glob/)

Applied to files:

  • src/bun.js/test/bun_test.zig
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun:test` for files ending with `*.test.{ts,js,jsx,tsx,mjs,cjs}`

Applied to files:

  • src/bun.js/test/bun_test.zig
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js,test/**/*-fixture.ts} : Use `using`/`await using` for resource cleanup with Bun APIs (e.g., `Bun.spawn`, `Bun.listen`, `Bun.serve`)

Applied to files:

  • src/bun.js/test/bun_test.zig
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/js/node/test/{parallel,sequential}/*.js : For `test/js/node/test/{parallel,sequential}/*.js` (no .test extension), run with `bun bd <file>` instead of `bun bd test <file>`

Applied to files:

  • src/bun.js/test/bun_test.zig
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/**/*.{js,ts} : Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test

Applied to files:

  • src/bun.js/test/bun_test.zig
📚 Learning: 2025-09-20T03:39:41.770Z
Learnt from: pfgithub
PR: oven-sh/bun#22534
File: test/regression/issue/21830.fixture.ts:14-63
Timestamp: 2025-09-20T03:39:41.770Z
Learning: Bun's test runner supports async describe callbacks, unlike Jest/Vitest where describe callbacks must be synchronous. The syntax `describe("name", async () => { ... })` is valid in Bun.

Applied to files:

  • docs/cli/test.md
🧬 Code graph analysis (1)
test/js/bun/test/test-on-test-finished.test.ts (1)
src/js/node/test.ts (1)
  • afterEach (135-139)
🔇 Additional comments (3)
src/bun.js/test/bun_test.zig (1)

92-101: Tail-append logic for onTestFinished looks correct (preserves registration order)

Finding the last entry from active_entry and inserting maintains FIFO for multiple registrations and ensures it runs after afterEach. Good.

If you want to double‑check interleaving with afterEach/afterAll added inside the test, consider a stress test with mixed sync/async hooks to assert final sequence.

src/bun.js/test/jest.zig (1)

166-166: Binding added and capacity bumped — LGTM

Exposes onTestFinished and adjusts object preallocation. Looks good.

Also applies to: 186-186

packages/bun-types/test.d.ts (1)

361-382: No duplicate declarations found - single onTestFinished export confirmed

Verification confirms there is exactly one onTestFinished function declaration in the type definitions at packages/bun-types/test.d.ts:379. All other matches in the repository are function calls in test files, not conflicting declarations.

Comment thread docs/cli/test.md
Comment on lines +260 to 267
| Hook | Description |
| ---------------- | -------------------------------------------------------- |
| `beforeAll` | Runs once before all tests. |
| `beforeEach` | Runs before each test. |
| `afterEach` | Runs after each test. |
| `afterAll` | Runs once after all tests. |
| `onTestFinished` | Runs after a test finishes, including after `afterEach`. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Document exception: onTestFinished isn’t allowed in preload or concurrent tests

Right below, the docs say hooks can be defined in a preloaded file; that excludes onTestFinished. Add a note after the table.

 | `afterAll`       | Runs once after all tests.                              |
 | `onTestFinished` | Runs after a test finishes, including after `afterEach`. |
+
+> Note
+> `onTestFinished` can only be registered while a test is executing. It is not allowed in `describe` blocks, cannot be used during `--preload`, and is not supported inside `test.concurrent` tests.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| Hook | Description |
| ---------------- | -------------------------------------------------------- |
| `beforeAll` | Runs once before all tests. |
| `beforeEach` | Runs before each test. |
| `afterEach` | Runs after each test. |
| `afterAll` | Runs once after all tests. |
| `onTestFinished` | Runs after a test finishes, including after `afterEach`. |
| Hook | Description |
| ---------------- | -------------------------------------------------------- |
| `beforeAll` | Runs once before all tests. |
| `beforeEach` | Runs before each test. |
| `afterEach` | Runs after each test. |
| `afterAll` | Runs once after all tests. |
| `onTestFinished` | Runs after a test finishes, including after `afterEach`. |
> Note
> `onTestFinished` can only be registered while a test is executing. It is not allowed in `describe` blocks, cannot be used during `--preload`, and is not supported inside `test.concurrent` tests.
🤖 Prompt for AI Agents
In docs/cli/test.md around lines 260 to 267, the hooks table lists
onTestFinished but the docs later say hooks can be defined in a preloaded file —
add a short note immediately after this table stating that onTestFinished is not
permitted in preload files and is also not supported for tests run with the
concurrent option; update wording to explicitly exclude onTestFinished from
preload and concurrent contexts.

Comment on lines +361 to +382
/**
* Runs a function after a test finishes, including after all afterEach hooks.
*
* This is useful for cleanup tasks that need to run at the very end of a test,
* after all other hooks have completed.
*
* Can only be called inside a test, not in describe blocks.
*
* @example
* test("my test", () => {
* onTestFinished(() => {
* // This runs after all afterEach hooks
* console.log("Test finished!");
* });
* });
*
* @param fn the function to run
*/
export function onTestFinished(
fn: (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void),
options?: HookOptions,
): void;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Align JSDoc with runtime: add preload and concurrent-test constraints

Runtime throws in preload and when called inside a concurrent test; types should say so. Update the comment accordingly.

   /**
-   * Runs a function after a test finishes, including after all afterEach hooks.
+   * Runs a function after a test finishes, including after all afterEach hooks.
    *
    * This is useful for cleanup tasks that need to run at the very end of a test,
    * after all other hooks have completed.
    *
-   * Can only be called inside a test, not in describe blocks.
+   * Can only be called while a test is executing. Not allowed in `describe` blocks,
+   * not allowed during `--preload`, and not supported inside `test.concurrent` tests.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Runs a function after a test finishes, including after all afterEach hooks.
*
* This is useful for cleanup tasks that need to run at the very end of a test,
* after all other hooks have completed.
*
* Can only be called inside a test, not in describe blocks.
*
* @example
* test("my test", () => {
* onTestFinished(() => {
* // This runs after all afterEach hooks
* console.log("Test finished!");
* });
* });
*
* @param fn the function to run
*/
export function onTestFinished(
fn: (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void),
options?: HookOptions,
): void;
/**
* Runs a function after a test finishes, including after all afterEach hooks.
*
* This is useful for cleanup tasks that need to run at the very end of a test,
* after all other hooks have completed.
*
* Can only be called while a test is executing. Not allowed in `describe` blocks,
* not allowed during `--preload`, and not supported inside `test.concurrent` tests.
*
* @example
* test("my test", () => {
* onTestFinished(() => {
* // This runs after all afterEach hooks
* console.log("Test finished!");
* });
* });
*
* @param fn the function to run
*/
export function onTestFinished(
fn: (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void),
options?: HookOptions,
): void;
🤖 Prompt for AI Agents
In packages/bun-types/test.d.ts around lines 361 to 382, the JSDoc for
onTestFinished is out of sync with runtime behavior: the runtime throws if
called from a preload script or from inside a concurrent test. Update the
comment to explicitly state those constraints (cannot be used in preload and
cannot be used inside concurrent tests), and mention that runtime will throw in
those cases; keep existing examples and param description but add brief notes
about the preload and concurrent-test restrictions.

Comment thread src/bun.js/test/bun_test.zig
Change the error message for onTestFinished when called inside a
concurrent test to suggest using test.serial or removing test.concurrent,
which is more helpful than the generic message.

Also add tests to verify the error message works correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 371e0c3 and 5cd0d51.

📒 Files selected for processing (2)
  • src/bun.js/test/bun_test.zig (2 hunks)
  • test/js/bun/test/test-on-test-finished.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (11)
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/bun/test/test-on-test-finished.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/bun/test/test-on-test-finished.test.ts
test/js/bun/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place Bun API tests under test/js/bun/, separated by category (e.g., test/js/bun/glob/)

Files:

  • test/js/bun/test/test-on-test-finished.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/bun/test/test-on-test-finished.test.ts
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}: Use bun:test for files ending with *.test.{ts,js,jsx,tsx,mjs,cjs}
Prefer concurrent tests (test.concurrent/describe.concurrent) over sequential when feasible
Organize tests with describe blocks to group related tests
Use utilities like describe.each, toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach) and track resources for cleanup

Files:

  • test/js/bun/test/test-on-test-finished.test.ts
test/**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

For large/repetitive strings, use Buffer.alloc(count, fill).toString() instead of "A".repeat(count)

Files:

  • test/js/bun/test/test-on-test-finished.test.ts
test/js/{bun,node}/**

📄 CodeRabbit inference engine (test/CLAUDE.md)

Organize unit tests by module under /test/js/bun/ and /test/js/node/

Files:

  • test/js/bun/test/test-on-test-finished.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Test files must be created under test/ and end with .test.ts or .test.tsx
In tests that open network ports, always use port: 0; do not hardcode port numbers or roll your own random port
Use normalizeBunSnapshot to normalize snapshot output instead of comparing raw stdout
Do not write tests that merely assert absence of strings like "panic" or "uncaught exception"
Use tempDir from "harness" to create temporary directories; do not use fs.mkdtempSync or tmpdirSync
When spawning processes in tests, assert output before asserting the exit code
Avoid shell commands in tests (e.g., find, grep); prefer Bun's Glob and built-in tools
Prefer snapshot tests over direct string equality assertions on stdout

Files:

  • test/js/bun/test/test-on-test-finished.test.ts
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Files:

  • src/bun.js/test/bun_test.zig
src/bun.js/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

src/bun.js/**/*.zig: In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS and re-export toJS/fromJS/fromJSDirect
Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions
Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors
Implement getters as get(this, globalObject) returning JSC.JSValue and matching the .classes.ts interface
Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern
Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)
For properties marked cache: true, use the generated Zig accessors (NameSetCached/GetCached) to work with GC-owned values
In finalize() for objects holding JS references, release them using .deref() before destroy

Files:

  • src/bun.js/test/bun_test.zig
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

When adding debug logs in Zig, create a scoped logger and log via Bun APIs: const log = bun.Output.scoped(.${SCOPE}, .hidden); then log("...", .{})

src/**/*.zig: Use private fields in Zig with the # prefix (e.g., struct { #foo: u32 };)
Prefer decl literals in Zig (e.g., const decl: Decl = .{ .binding = 0, .value = 0 };)
Prefer placing @import statements at the bottom of the Zig file (formatter may reorder automatically)
Prefer @import("bun") rather than @import("root").bun or @import("../bun.zig")

In Zig code, be careful with allocators and use defer for cleanup

Files:

  • src/bun.js/test/bun_test.zig
🧠 Learnings (8)
📓 Common learnings
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use utilities like `describe.each`, `toMatchSnapshot`, and lifecycle hooks (`beforeAll`, `beforeEach`, `afterEach`) and track resources for cleanup
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use utilities like `describe.each`, `toMatchSnapshot`, and lifecycle hooks (`beforeAll`, `beforeEach`, `afterEach`) and track resources for cleanup

Applied to files:

  • test/js/bun/test/test-on-test-finished.test.ts
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : When spawning Bun in tests, use `bunExe()` and `bunEnv` from `harness`

Applied to files:

  • src/bun.js/test/bun_test.zig
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js} : Do not set explicit test timeouts; Bun already has timeouts

Applied to files:

  • src/bun.js/test/bun_test.zig
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun:test` for files ending with `*.test.{ts,js,jsx,tsx,mjs,cjs}`

Applied to files:

  • src/bun.js/test/bun_test.zig
📚 Learning: 2025-08-30T00:12:56.803Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-08-30T00:12:56.803Z
Learning: Applies to test/js/bun/**/*.{js,ts} : Place Bun API tests under test/js/bun/, separated by category (e.g., test/js/bun/glob/)

Applied to files:

  • src/bun.js/test/bun_test.zig
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/{test/**/*.test.{ts,js,jsx,tsx,mjs,cjs},test/js/node/test/{parallel,sequential}/*.js,test/**/*-fixture.ts} : Use `using`/`await using` for resource cleanup with Bun APIs (e.g., `Bun.spawn`, `Bun.listen`, `Bun.serve`)

Applied to files:

  • src/bun.js/test/bun_test.zig
📚 Learning: 2025-10-12T02:22:34.373Z
Learnt from: CR
PR: oven-sh/bun#0
File: test/CLAUDE.md:0-0
Timestamp: 2025-10-12T02:22:34.373Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Prefer concurrent tests (`test.concurrent`/`describe.concurrent`) over sequential when feasible

Applied to files:

  • src/bun.js/test/bun_test.zig
🧬 Code graph analysis (1)
test/js/bun/test/test-on-test-finished.test.ts (1)
src/js/node/test.ts (1)
  • afterEach (135-139)
🔇 Additional comments (9)
src/bun.js/test/bun_test.zig (4)

59-61: LGTM!

The preload guard correctly prevents onTestFinished from being called during preload, with a clear error message directing users to call it inside a test.


70-72: LGTM!

The collection phase guard correctly prevents onTestFinished from being called outside of a test (e.g., at the top level or inside describe blocks), with an appropriate error message.


78-85: Well done addressing the previous feedback!

The error message now provides specific, actionable guidance for onTestFinished in concurrent tests, while maintaining appropriate messages for other hooks. This resolves the concern raised in the previous review.


87-112: LGTM!

The append point logic is well-structured and correctly implements the execution order:

  • afterAll/afterEach hooks are inserted after the test entry
  • onTestFinished hooks are appended at the very end of the sequence by traversing to the last entry
  • The linked list insertion properly maintains the execution chain

This ensures the declared execution order: test body → afterAll → afterEach → onTestFinished.

test/js/bun/test/test-on-test-finished.test.ts (5)

4-25: LGTM!

The basic ordering test correctly verifies that onTestFinished executes after all other hooks, including inner afterAll and afterEach. The use of a shared array to capture execution order is a clear and effective testing pattern.


28-48: LGTM!

This test correctly verifies that multiple onTestFinished callbacks execute in registration order and after all other hooks. Good coverage for a common use case.


51-69: LGTM!

This test properly verifies that async onTestFinished callbacks are awaited before proceeding to the next test. The minimal timeout keeps the test fast while still demonstrating async behavior.


72-92: LGTM!

This test correctly verifies error handling for onTestFinished in concurrent tests, with the error message matching the implementation. This addresses the concurrent test scenario mentioned in the previous review feedback.


95-116: LGTM!

This integration test provides comprehensive verification of the complete execution order when onTestFinished is used alongside all other lifecycle hooks. Good coverage for the most complex scenario.

Comment on lines +1 to +116
import { afterAll, afterEach, describe, expect, onTestFinished, test } from "bun:test";

// Test the basic ordering of onTestFinished
describe("onTestFinished ordering", () => {
const output: string[] = [];

afterEach(() => {
output.push("afterEach");
});

test("test 1", () => {
afterAll(() => {
output.push("inner afterAll");
});
onTestFinished(() => {
output.push("onTestFinished");
});
output.push("test 1");
});

test("test 2", () => {
// After test 2 starts, verify the order from test 1
expect(output).toEqual(["test 1", "inner afterAll", "afterEach", "onTestFinished"]);
});
});

// Test multiple onTestFinished calls
describe("multiple onTestFinished", () => {
const output: string[] = [];

afterEach(() => {
output.push("afterEach");
});

test("test with multiple onTestFinished", () => {
onTestFinished(() => {
output.push("onTestFinished 1");
});
onTestFinished(() => {
output.push("onTestFinished 2");
});
output.push("test");
});

test("verify order", () => {
expect(output).toEqual(["test", "afterEach", "onTestFinished 1", "onTestFinished 2"]);
});
});

// Test onTestFinished with async callbacks
describe("async onTestFinished", () => {
const output: string[] = [];

afterEach(() => {
output.push("afterEach");
});

test("async onTestFinished", async () => {
onTestFinished(async () => {
await new Promise(resolve => setTimeout(resolve, 1));
output.push("onTestFinished async");
});
output.push("test");
});

test("verify async order", () => {
expect(output).toEqual(["test", "afterEach", "onTestFinished async"]);
});
});

// Test that onTestFinished throws proper error in concurrent tests
describe("onTestFinished errors", () => {
test.concurrent("cannot be called in concurrent test 1", () => {
expect(() => {
onTestFinished(() => {
console.log("should not run");
});
}).toThrow(
"Cannot call onTestFinished() here. It cannot be called inside a concurrent test. Use test.serial or remove test.concurrent.",
);
});

test.concurrent("cannot be called in concurrent test 2", () => {
expect(() => {
onTestFinished(() => {
console.log("should not run");
});
}).toThrow(
"Cannot call onTestFinished() here. It cannot be called inside a concurrent test. Use test.serial or remove test.concurrent.",
);
});
});

// Test onTestFinished with afterEach and afterAll together
describe("onTestFinished with all hooks", () => {
const output: string[] = [];

afterEach(() => {
output.push("afterEach");
});

test("test with all hooks", () => {
afterAll(() => {
output.push("inner afterAll");
});
onTestFinished(() => {
output.push("onTestFinished");
});
output.push("test");
});

test("verify complete order", () => {
// Expected order: test body, inner afterAll, afterEach, onTestFinished
expect(output).toEqual(["test", "inner afterAll", "afterEach", "onTestFinished"]);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding negative tests for invalid contexts.

The test suite provides excellent coverage for valid usage and concurrent test errors. However, per the previous review feedback, consider adding tests that verify errors when onTestFinished is called in invalid contexts (top-level or inside describe blocks).

These tests would likely need to spawn separate test processes and assert on the error output, similar to how other framework-level error cases are tested in the Bun test suite.

Do you want me to help draft these negative test cases that spawn child processes to verify the error behavior for top-level and describe-level invocations?

🤖 Prompt for AI Agents
In test/js/bun/test/test-on-test-finished.test.ts lines 1-116, add negative
tests that spawn separate test processes to ensure onTestFinished throws when
called in invalid contexts (top-level and inside describe). For each case create
a temporary test file string that calls onTestFinished at top-level or inside a
describe, spawn the test runner (e.g., using child_process.spawnSync or
Bun.spawnSync) to run that file, assert the process exits non-zero and stderr
contains the expected error message about "Cannot call onTestFinished() here"
(matching the existing concurrent test error text), and clean up the temp file;
place these new tests in the same suite near the "onTestFinished errors" block.

@pfgithub pfgithub left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

pending CI

@Jarred-Sumner Jarred-Sumner merged commit a3f18b9 into main Oct 25, 2025
57 of 61 checks passed
@Jarred-Sumner Jarred-Sumner deleted the claude/implement-on-test-finished branch October 25, 2025 02:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants