What version of Effect is running?
effect@4.0.0-rc.112
@effect/vitest@4.0.0-rc.112
vitest@4.1.11
Bun 1.4.1
What steps can reproduce the bug?
Create lifecycle.test.ts:
import { it } from "@effect/vitest"
import { Effect } from "effect"
import { expect } from "vitest"
const events: Array<string> = []
const resource = Effect.acquireRelease(
Effect.sync(() => {
events.push("acquired")
}),
() =>
Effect.gen(function*() {
events.push("release:start")
yield* Effect.promise(
() => new Promise((resolve) => setTimeout(resolve, 250))
)
events.push("release:end")
})
)
it.effect(
"times out",
() =>
Effect.gen(function*() {
yield* resource
return yield* Effect.never
}),
{ timeout: 75 }
)
it.effect("runs after the timed-out test", () =>
Effect.sync(() => {
events.push("next:start")
expect(events).toContain("release:end")
})
)
Run it sequentially:
bunx vitest run lifecycle.test.ts --maxWorkers=1
The second test fails because Vitest starts it after the finalizer begins but before the finalizer completes.
The finalizer deliberately uses a real delayed Promise instead of Effect.sleep, because @effect/vitest supplies TestClock.
What is the expected behavior?
When Vitest times out an it.effect test, the Effect fiber should be interrupted and its finalizers should finish before Vitest advances to the next test.
The observed event order should be:
acquired
release:start
release:end
next:start
What do you see instead?
The second test fails with:
actual: [ 'acquired', 'release:start', 'next:start' ]
release:end has not occurred before Vitest advances.
Finalizers do finish before Vitest advances when the Effect completes through ordinary success, typed failure, defect, or assertion failure. The problem is specific to the timeout path in this reproduction.
Additional information
Programmatic run cancellation appears to have the same lifecycle behavior: the Effect finalizer starts, but Vitest shutdown can complete before the finalizer finishes. I can provide a separate reproduction for cancellation if that should be tracked independently.
I searched the existing Effect and Vitest issues and did not find one covering this exact finalizer-completion behavior.
A potentially relevant implementation reference is Effect PR #8135: its Rstest adapter explicitly awaits the interrupted fiber after a test timeout.
What version of Effect is running?
What steps can reproduce the bug?
Create
lifecycle.test.ts:Run it sequentially:
The second test fails because Vitest starts it after the finalizer begins but before the finalizer completes.
The finalizer deliberately uses a real delayed Promise instead of
Effect.sleep, because@effect/vitestsuppliesTestClock.What is the expected behavior?
When Vitest times out an
it.effecttest, the Effect fiber should be interrupted and its finalizers should finish before Vitest advances to the next test.The observed event order should be:
What do you see instead?
The second test fails with:
release:endhas not occurred before Vitest advances.Finalizers do finish before Vitest advances when the Effect completes through ordinary success, typed failure, defect, or assertion failure. The problem is specific to the timeout path in this reproduction.
Additional information
Programmatic run cancellation appears to have the same lifecycle behavior: the Effect finalizer starts, but Vitest shutdown can complete before the finalizer finishes. I can provide a separate reproduction for cancellation if that should be tracked independently.
I searched the existing Effect and Vitest issues and did not find one covering this exact finalizer-completion behavior.
A potentially relevant implementation reference is Effect PR #8135: its Rstest adapter explicitly awaits the interrupted fiber after a test timeout.