Skip to content

Commit 2eb3873

Browse files
committed
Dedup test-realm lifecycle hooks + raise hook timeout for contention
The non-live suite intermittently failed on cache-miss runs (comms-xwqm): under a full-parallel `bun run check` the starved event loop stretched real-in-process-realm setup/teardown past bun's default 5s HOOK timeout — independent of the per-test body timeout the gap-replay test already raised — so the failure landed on whichever test the slow hook wrapped, explaining the moving test name. Extract one shared `registerRealmHooks(start, assign)` + `REALM_HOOK_TIMEOUT_MS` (packages/testing/realm-hooks.ts). The byte-identical realm beforeEach/afterEach boilerplate in adapter.test.ts and bot-dm-guard.test.ts, the same-shaped fixture hooks in http.test.ts, and the contract suite's factory/dispose hooks all route through it. The `assign` callback hands the instance back to each file's own `let` binding, so the ~170 `realm.`/`fixture.` call sites stay untouched. contract.ts (async factory / Effect dispose) just imports the timeout constant. This is headroom, not a full cure: the two infinite-long-poll teardown tests (gap-replay comms-4au, scope-close-interrupt spj3.8) can still exceed even 30s under aggressive contention. That residual is a fiber/scope lifecycle problem (server.stop(true) is not the blocker — an isolated probe resolves it in 0.2ms with a handler stuck forever), tracked for the Effect-Scope-based realm rework in comms-4lz5 / comms-30hq.
1 parent 5fcba76 commit 2eb3873

5 files changed

Lines changed: 54 additions & 23 deletions

File tree

‎packages/testing/contract.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
UnknownIdentity,
3333
} from '@commy/core/ports'
3434
import { Duration, Effect, Exit, Option, Queue, type Scope, Stream } from 'effect'
35+
import { REALM_HOOK_TIMEOUT_MS } from './realm-hooks.ts'
3536

3637
export interface ContractEnv {
3738
/**
@@ -110,9 +111,9 @@ export const runAgentCommsContract = (label: string, factory: ContractFactory):
110111

111112
beforeEach(async () => {
112113
env = await factory()
113-
})
114+
}, REALM_HOOK_TIMEOUT_MS)
114115

115-
afterEach(() => Effect.runPromise(env.dispose()))
116+
afterEach(() => Effect.runPromise(env.dispose()), REALM_HOOK_TIMEOUT_MS)
116117

117118
test('publisher.post returns a MessageRef whose channel matches the destination', () =>
118119
Effect.runPromise(

‎packages/testing/realm-hooks.ts‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { afterEach, beforeEach } from 'bun:test'
2+
3+
/**
4+
* bun:test lifecycle hooks keep their own 5s timeout, independent of any
5+
* per-test timeout. In isolation a real in-process realm starts and stops in a
6+
* few ms, but under a full-parallel `bun run check` the starved event loop can
7+
* stretch that setup/teardown past 5s — and then the HOOK, not the test body,
8+
* times out, landing the failure on whichever test the hook happened to be
9+
* wrapping (comms-xwqm). 30s gives that contention wide headroom while still
10+
* failing if a hook genuinely wedges.
11+
*
12+
* This is headroom, not a cure-all: the long-poll teardown tests (the
13+
* gap-replay / scope-close-interrupt cases that drive an infinite server
14+
* handler and rely on AbortSignal unwinding a forked drain fiber) can still
15+
* exceed even this under aggressive contention. That residual is a fiber/scope
16+
* lifecycle problem, not a too-small-number problem, and is tracked for the
17+
* Effect-Scope-based realm rework (comms-4lz5 / comms-30hq) — running the whole
18+
* test, realm acquisition included, inside one scope with guaranteed finalizers
19+
* removes the leftover-fiber-starves-teardown failure mode structurally.
20+
*/
21+
export const REALM_HOOK_TIMEOUT_MS = 30_000
22+
23+
/**
24+
* Register per-test realm lifecycle on the bun:test hooks — a fresh instance
25+
* before each test, stopped after — with the contention-proof timeout above.
26+
* The caller keeps its own directly-referenced binding via `assign`, so
27+
* existing `realm.` / `fixture.` call sites stay untouched.
28+
*/
29+
export const registerRealmHooks = <T extends { readonly stop: () => Promise<void> }>(
30+
start: () => T,
31+
assign: (instance: T) => void,
32+
): void => {
33+
let instance: T
34+
beforeEach(() => {
35+
instance = start()
36+
assign(instance)
37+
}, REALM_HOOK_TIMEOUT_MS)
38+
afterEach(() => instance.stop(), REALM_HOOK_TIMEOUT_MS)
39+
}

‎packages/zulip/adapter.test.ts‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { afterEach, beforeEach, expect, test } from 'bun:test'
1+
import { expect, test } from 'bun:test'
22
import type { ChannelRef, Identity, InboundEvent, MessageRef } from '@commy/core/ports'
33
import {
44
DirectoryError,
@@ -17,6 +17,7 @@ import {
1717
UnknownChannel,
1818
type UnknownIdentity,
1919
} from '@commy/core/ports'
20+
import { registerRealmHooks } from '@commy/testing/realm-hooks'
2021
import { FetchHttpClient, HttpClient } from '@effect/platform'
2122
import { Cause, Duration, Effect, Exit, Option, Queue, Redacted, type Scope, Stream } from 'effect'
2223
import type { ZulipAdapter, ZulipAdapterConfig } from './adapter.ts'
@@ -27,12 +28,8 @@ import { startTestRealm } from './test-server.ts'
2728

2829
let realm: TestRealm
2930

30-
beforeEach(() => {
31-
realm = startTestRealm()
32-
})
33-
34-
afterEach(async () => {
35-
await realm.stop()
31+
registerRealmHooks(startTestRealm, (next) => {
32+
realm = next
3633
})
3734

3835
const HERMES = {

‎packages/zulip/bot-dm-guard.test.ts‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { afterEach, beforeEach, expect, test } from 'bun:test'
1+
import { expect, test } from 'bun:test'
2+
import { registerRealmHooks } from '@commy/testing/realm-hooks'
23
import { FetchHttpClient, HttpClient } from '@effect/platform'
34
import { Cause, Effect, Exit, Option, Schema } from 'effect'
45
import { BotToBotDirectMessageError, type RecipientDirectory, wrapBotHttp } from './bot-dm-guard.ts'
@@ -9,12 +10,8 @@ import { ZulipUserRef } from './user-ref.ts'
910

1011
let realm: TestRealm
1112

12-
beforeEach(() => {
13-
realm = startTestRealm()
14-
})
15-
16-
afterEach(async () => {
17-
await realm.stop()
13+
registerRealmHooks(startTestRealm, (next) => {
14+
realm = next
1815
})
1916

2017
const SELF_ID = ZulipUserRef(100)

‎packages/zulip/http.test.ts‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { afterEach, beforeEach, expect, test } from 'bun:test'
1+
import { expect, test } from 'bun:test'
2+
import { registerRealmHooks } from '@commy/testing/realm-hooks'
23
import { FetchHttpClient, HttpClient } from '@effect/platform'
34
import {
45
Cause,
@@ -115,12 +116,8 @@ const startFixture = (): Fixture => {
115116

116117
let fixture: Fixture
117118

118-
beforeEach(() => {
119-
fixture = startFixture()
120-
})
121-
122-
afterEach(async () => {
123-
await fixture.stop()
119+
registerRealmHooks(startFixture, (next) => {
120+
fixture = next
124121
})
125122

126123
const successSchema = Schema.Struct({ result: Schema.Literal('success') })

0 commit comments

Comments
 (0)