feat(runner): add afterAll hook for describe.serial block cleanup - #78
Merged
Drownek merged 3 commits intoSep 13, 2026
Merged
Conversation
Registers on the SerialBlock itself (afterAllHooks), not the generic describe scope stack, since only a serial block has a single 'finished' moment to hang cleanup on - a plain describe/test gets a fresh bot per test and has no equivalent event. Throws if called outside describe.serial.
Fires once per block instance, after the last test that actually executed - covers a block that stopped early and skipped the rest, which afterEach/cleanup finalizers never see since the skipped tests' bodies never run. Skipped entirely if the block never got its player. Runs before the plugins' own afterEach and bots.close(), mirroring how plugin hooks already wrap the whole block. A throwing hook is logged and does not fail the block.
Drownek
approved these changes
Sep 13, 2026
Drownek
left a comment
Owner
There was a problem hiding this comment.
Nice addition, this fixes a real architectural pain point in describe.serial test suites.
What I checked:
- Guaranteed cleanup: this fixes the issue where an early test exit or failure would skip downstream test bodies, leaving server-side state (permissions, whitelists, economy data) dangling with no cleanup.
- Lifecycle & concurrency: hook execution in the finally block, scoped per bot instance with its own lastCtx, holds up fine under concurrency: N.
- LIFO ordering: reversing the hook array gets teardown order right, matching how resource unwinding normally works.
- Resilience: catching and logging throwing hooks stops a broken teardown from flipping an otherwise-passing test block.
- Graceful skip: correctly skips execution when the bot never connected (lastCtx === null).
- Registration guard: calling afterAll outside describe.serial throws immediately, so you find out at import time instead of during a run.
- Docs in api-reference.mdx and writing-tests.mdx are clear.
Tested locally with early test exits, behavior checks out.
LGTM, ready to merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
describe.serialhas no hook that's guaranteed to run once the block is done, whatever "done" turned out to mean.afterEach/ctx.cleanup()are per-test: once the block stops early (a failure, a timeout,invalidatePlayer), every remaining test is reported skipped and its body — and any cleanup registered inside it — never runs. There's no way to write "revoke this permission / release this resource no matter which test in the block failed."Fix
Adds
afterAll(hook), importable alongsidetest/describe/beforeEach/afterEach. Only valid insidedescribe.serial— a plaindescribe/testhas no single block-completion event to hang it on, and calling it outside a block throws immediately at registration time.It runs:
concurrency: 5runs it 5 times, once per bot)afterEachand before the bot disconnectsA throwing
afterAllhook is logged ([serial <block>] afterAll error: ...) and never flips the block's result, matching howctx.cleanup()finalizer errors are already handled.Changes
test-registry.ts:SerialBlock.afterAllHooks,afterAll()exporttest-runner.ts:runSerialBlockruns the block'safterAllHooks(LIFO, try/catch-logged) in itsfinally, gated onlastCtx(mirrors the existingplugins.afterEach(lastCtx)gate)runner.ts: re-exportsafterAlldocs/writing-tests.mdx,docs/api-reference.mdx: document the hookVerification
tsc --noEmitclean. No existing behavior touched outside therunSerialBlockfinallyblock and the registry's block construction.