diff --git a/packages/client/src/auth-get-session-envelope.test.ts b/packages/client/src/auth-get-session-envelope.test.ts index 78724bbff50..96e852bd9e7 100644 --- a/packages/client/src/auth-get-session-envelope.test.ts +++ b/packages/client/src/auth-get-session-envelope.test.ts @@ -45,8 +45,42 @@ // `data.token` is absent from the normalized body too, so a regression back // to `data.data?.token` cannot pass by accident, and the "enveloping it // would have fixed refreshToken" reading stays refuted in code. +// - `⑥ the cold start stays outside every clocked window` — pins WHERE the +// first scenario's one-time cost is paid (next section). +// +// ## [#20327] Why one throwaway scenario runs at MODULE SCOPE +// +// The first scenario in a worker pays a one-time cost no later one sees: +// better-auth's lazily imported module graph, sql.js's WASM compile, and the +// first-use costs of the sync and the sign-up. The phases are measured in +// `auth-login-register-envelope.test.ts`, which carries the same arrangement +// and the same fix. Here, idle on 4 vCPU at `c74de10a9`, that cost put the +// first case at 1035 ms against 110-330 ms for the six after it. On a loaded +// `Test Core` shard (PR #20325's run) the first case crossed vitest's 5000 ms +// `testTimeout` while every other case passed. Twenty-four CPU-bound busy +// loops on this box reproduce that exact signature on the first case. +// +// So the cost is now paid by a module-scope `await`, during COLLECTION, which +// no vitest clock covers: `@vitest/runner@4.1.11` wraps hooks and test bodies +// in `withTimeout(...)` and awaits the file import bare. This is the repo's +// convention: "clocked windows measure behaviour, never loading" (AGENTS.md, +// Build & Test; `check:test-source-alias`). The warm-up is the file's own +// `signedIn()`, the arrangement five of the seven cases run, so no list of +// loads can drift from what the cases really pay. +// +// ⛔ It shares nothing a case asserts on. Its engine and manager are its own +// and are closed before any case starts. Every case still builds a fresh +// engine, a fresh `AuthManager` and a fresh sign-up. What it leaves warm is +// process-level: the module registry, sql.js's compiled WASM and the JIT, +// which the first case used to leave to every later case. +// +// ⛔ Do not move it into a hook, and do not answer a recurrence by raising a +// timeout: vitest clocks a hook exactly as it clocks a test body, and a wider +// window only moves the cliff to a heavier shard. import { describe, it, expect, afterEach } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; import { ObjectQL } from '@objectstack/objectql'; import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm'; import { AuthManager } from '@objectstack/plugin-auth'; @@ -250,12 +284,21 @@ const principalFor = async (manager: AuthManager, token: string | undefined) => return session?.user?.id ?? null; }; -afterEach(async () => { +/** Close every engine a scenario opened: after each case, and after the warm-up. */ +const closeEngines = async (): Promise => { while (engines.length) { const engine = engines.pop(); await (engine as unknown as { close?: () => Promise })?.close?.().catch(() => {}); } -}); +}; + +// [#20327] The first scenario's one-time cost, paid during COLLECTION, which +// no vitest clock covers (header, last section). ⛔ It stays at module scope, +// and `⑥` below pins that. +await signedIn(); +await closeEngines(); + +afterEach(closeEngines); describe('[#16760] /get-session is lifted into the SessionResponse envelope it declares', () => { describe('① me() delivers the envelope it declares', () => { @@ -427,4 +470,21 @@ describe('[#16760] /get-session is lifted into the SessionResponse envelope it d expect(typeof res.data.session?.token).toBe('string'); }); }); + + describe('⑥ the cold start stays outside every clocked window', () => { + it('pays the first scenario at module scope, and no hook carries it', () => { + // [#20327] Read off this file's own text, so "do not move it into a + // hook" is an assertion rather than a sentence nobody reads. + const code = readFileSync(fileURLToPath(import.meta.url), 'utf8'); + + // Exactly one warm-up call, and it opens its own line at column 0. So it + // sits in no function body, which is what "paid during collection" + // reduces to. Comment lines start with `//` and cannot match. + expect(code.match(/^await signedIn\(\);$/gm) ?? []).toHaveLength(1); + + // ⛔ No `before*` hook may come back to carry it: vitest clocks a hook + // with `hookTimeout` exactly as it clocks a test body with `testTimeout`. + expect(code).not.toMatch(/^\s*before(All|Each)\s*\(/m); + }); + }); }); diff --git a/packages/client/src/auth-rotated-session-token.test.ts b/packages/client/src/auth-rotated-session-token.test.ts index 6eb528daede..3a232a01488 100644 --- a/packages/client/src/auth-rotated-session-token.test.ts +++ b/packages/client/src/auth-rotated-session-token.test.ts @@ -41,8 +41,51 @@ // that header in the shared `fetch` wrapper instead of on the rotating routes // would rewrite the stored credential on an ordinary write — and every // assertion in ① and ② would stay green. +// - `④ the cold start stays outside every clocked window` — pins WHERE the +// first scenario's one-time cost is paid (next section). +// +// ## [#20327] Why one throwaway scenario runs at MODULE SCOPE +// +// The first scenario in a worker pays a one-time cost no later one sees: +// better-auth's lazily imported module graph, sql.js's WASM compile, and the +// first-use costs of the sync and the sign-up. The phases are measured in +// `auth-login-register-envelope.test.ts`, which carries the same arrangement +// and the same fix. Every case here used to carry an explicit `60_000` +// timeout, which WIDENED the first case's window around that cost instead of +// moving the cost out of it: every budget a cost is moved into can be +// exhausted by a heavier shard (`check:test-source-alias`, clocked-window +// rule). +// +// So the cost is now paid by a module-scope `await`, during COLLECTION, which +// no vitest clock covers: `@vitest/runner@4.1.11` wraps hooks and test bodies +// in `withTimeout(...)` and awaits the file import bare. This is the repo's +// convention: "clocked windows measure behaviour, never loading" (AGENTS.md, +// Build & Test). The warm-up is the file's own `signedIn()`, the arrangement +// every case runs, so no list of loads can drift from what the cases pay. With +// the cost moved out, the `60_000`s are gone and every case runs on the +// default `testTimeout`. +// +// ⚠ What a window holds now is behaviour, and ① holds the most: the card's +// whole probe, a sign-up plus five more calls that each check a password or a +// TOTP code. It takes about 700 ms on 4 vCPU with the box otherwise quiet, and +// a warm-up that ran the WHOLE probe left it no faster than this one does, so +// none of that is loading. Under sixteen CPU-bound busy loops it took 4.1-4.2 +// s. Under twenty-four, the load that times out every one of these suites' +// cold first cases, it reached the 5000 ms budget on its own work. If CI ever +// reds it, the lever is that case's own work, ⛔ not a timeout. +// +// ⛔ It shares nothing a case asserts on. Its engine and manager are its own +// and are destroyed before any case starts. Every case still builds a fresh +// engine, a fresh `AuthManager` and a fresh sign-up. What it leaves warm is +// process-level: the module registry, sql.js's compiled WASM and the JIT, +// which the first case used to leave to every later case. +// +// ⛔ Do not move it into a hook, and do not answer a recurrence by raising a +// timeout: vitest clocks a hook exactly as it clocks a test body. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; import { createHmac } from 'node:crypto'; import { ObjectQL } from '@objectstack/objectql'; import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm'; @@ -261,12 +304,8 @@ const signedCredentialFor = async ( return String(signed); }; -beforeEach(() => { - vi.spyOn(console, 'warn').mockImplementation(() => {}); - vi.spyOn(console, 'error').mockImplementation(() => {}); -}); -afterEach(async () => { - vi.restoreAllMocks(); +/** Destroy every engine a scenario opened: after each case, and after the warm-up. */ +const closeEngines = async (): Promise => { while (engines.length) { const e = engines.pop(); try { @@ -275,6 +314,21 @@ afterEach(async () => { /* noop */ } } +}; + +// [#20327] The first scenario's one-time cost, paid during COLLECTION, which +// no vitest clock covers (header, last section). ⛔ It stays at module scope, +// and `④` below pins that. +await signedIn(); +await closeEngines(); + +beforeEach(() => { + vi.spyOn(console, 'warn').mockImplementation(() => {}); + vi.spyOn(console, 'error').mockImplementation(() => {}); +}); +afterEach(async () => { + vi.restoreAllMocks(); + await closeEngines(); }); // ─────────────────────────────────────────────────────────────────────────── @@ -335,7 +389,7 @@ describe("#16534 ① the card's own probe, with the manual re-set deleted", () = // status code: after the whole sequence the client is still holding a // credential that resolves to the same principal. expect(await principalForStoredToken(manager, client)).toBe(userId); - }, 60_000); + }); }); // ─────────────────────────────────────────────────────────────────────────── @@ -358,7 +412,7 @@ describe('#16534 ② one assertion per rotating route — all three', () => { expect(await principalForStoredToken(manager, client)).toBe(userId); // …and the one it replaced is genuinely gone. expect(await principalFor(manager, before)).toBeNull(); - }, 60_000); + }); it('changePassword WITHOUT revokeOtherSessions rotates nothing and stores nothing', async () => { // The other half of the same route: `token` is `null` there, and a client @@ -374,7 +428,7 @@ describe('#16534 ② one assertion per rotating route — all three', () => { expect(result.token).toBeNull(); expect(storedToken(client)).toBe(before); expect(await principalForStoredToken(manager, client)).not.toBeNull(); - }, 60_000); + }); it('twoFactor.verifyTotp on the enrolment lane — the body echoes the LIVE token', async () => { const { engine, manager, client, email } = await signedIn(); @@ -390,7 +444,7 @@ describe('#16534 ② one assertion per rotating route — all three', () => { // The row behind the replaced value was deleted, so asserting only "the // stored token changed" would not have been enough. expect(await principalFor(manager, before)).toBeNull(); - }, 60_000); + }); it('twoFactor.disable — the credential arrives ONLY in the `set-auth-token` header', async () => { // The route triage singled out: it answers `{ status: true }`, so an @@ -410,7 +464,7 @@ describe('#16534 ② one assertion per rotating route — all three', () => { expect(storedToken(client)).not.toBe(before); expect(await principalForStoredToken(manager, client)).toBe(userId); expect(await principalFor(manager, before)).toBeNull(); - }, 60_000); + }); }); // ─────────────────────────────────────────────────────────────────────────── @@ -439,7 +493,7 @@ describe('#16534 ③ the negative control — a NON-rotating route changes nothi // Still the same live session at the end of it — the invariant is "did not // move", not "was emptied". expect(await principalForStoredToken(manager, client)).not.toBeNull(); - }, 60_000); + }); it("verifyBackupCode's already-logged-in lane leaves the stored credential byte-identical", async () => { // `/two-factor/verify-backup-code` shares `AuthTwoFactorVerificationResult` @@ -472,5 +526,35 @@ describe('#16534 ③ the negative control — a NON-rotating route changes nothi expect(result.token).not.toBe(signed); expect(storedToken(bearerClient), 'verifyBackupCode moved the stored credential').toBe(signed); expect(await principalForStoredToken(manager, bearerClient)).toBe(userId); - }, 60_000); + }); +}); + +// ─────────────────────────────────────────────────────────────────────────── +describe('#16534 ④ the cold start stays outside every clocked window', () => { + it('pays the first scenario at module scope, no hook carries it, and no case widens its clock', () => { + // [#20327] Read off this file's own text, so "do not move it into a + // hook" is an assertion rather than a sentence nobody reads. + const code = readFileSync(fileURLToPath(import.meta.url), 'utf8'); + + // Exactly one warm-up call, and it opens its own line at column 0. So it + // sits in no function body, which is what "paid during collection" + // reduces to. Comment lines start with `//` and cannot match. + expect(code.match(/^await signedIn\(\);$/gm) ?? []).toHaveLength(1); + + // ⛔ No `before*` hook may come back to carry it: vitest clocks a hook + // with `hookTimeout` exactly as it clocks a test body with `testTimeout`. + // This file's one hook is the console-spy `beforeEach`. Each hook is read + // up to the next column-0 `});`, so a hook indented inside a `describe` + // reads on to that block's close and cannot hide a scenario from this. + const hooks = [...code.matchAll(/^\s*before(?:All|Each)\s*\(([\s\S]*?)^\}\);$/gm)].map( + (m) => m[1], + ); + expect(hooks).toHaveLength(1); + for (const body of hooks) expect(body).not.toMatch(/\b(signedIn|arrange)\(/); + + // ⛔ And no case widens its own clock again. Every case here used to end + // `}, 60_000);`, which kept the cold start inside a wider window instead + // of moving it out. + expect(code).not.toMatch(/^\s*\},\s*[\d_]+\s*\);$/m); + }); }); diff --git a/packages/client/src/organization-invitation-resend-team-placement.test.ts b/packages/client/src/organization-invitation-resend-team-placement.test.ts index c4397f6cf48..fc8d67ebd71 100644 --- a/packages/client/src/organization-invitation-resend-team-placement.test.ts +++ b/packages/client/src/organization-invitation-resend-team-placement.test.ts @@ -54,9 +54,47 @@ * .test.ts) plus one team, deliberately not extracted: a shared harness that * one card's edit can reshape under another card's pins is the drift both * files exist to catch. + * + * ⑥ pins WHERE the first scenario's one-time cost is paid (next section). + * + * ## [#20327] Why one throwaway scenario runs at MODULE SCOPE + * + * The first scenario in a worker pays a one-time cost no later one sees: + * better-auth's lazily imported module graph, sql.js's WASM compile, and the + * first-use costs of the sync and the sign-up. The phases are measured in + * `auth-login-register-envelope.test.ts`, which carries the same arrangement + * and the same fix. Here, idle on 4 vCPU at `c74de10a9`, that cost put ① at + * 1245 ms against 307-411 ms for the four later cases that run `arrange()`. + * On the `Test Core` shard where a sibling suite went red (PR #20325's run), + * ① took 3599 ms here against 486-820 ms for the later cases that run + * `arrange()`. Under twenty-four CPU-bound busy loops on this box, the load + * that reproduces that red, ① timed out at vitest's 5000 ms `testTimeout` in + * every run. + * + * So the cost is now paid by a module-scope `await`, during COLLECTION, which + * no vitest clock covers: `@vitest/runner@4.1.11` wraps hooks and test bodies + * in `withTimeout(...)` and awaits the file import bare. This is the repo's + * convention: "clocked windows measure behaviour, never loading" (AGENTS.md, + * Build & Test; `check:test-source-alias`). The warm-up is the file's own + * `arrange()`, so no list of loads can drift from what the cases really pay. + * It needs the posture the `beforeAll` sets for the cases, so it holds that + * posture for itself and puts back what it found, even when it throws. + * + * ⛔ It shares nothing a case asserts on. Every case still builds a fresh + * engine, a fresh `AuthManager`, a fresh owner and a fresh organization. Its + * engine stays open, as every case's does (`arrange()` hands none back), so + * no case runs in a state no case ran in before: every case but the first + * already ran after an earlier scenario. What it leaves warm is + * process-level: the module registry, sql.js's compiled WASM and the JIT. + * + * ⛔ Do not move it into a hook, and do not answer a recurrence by raising a + * timeout: vitest clocks a hook exactly as it clocks a test body, and a wider + * window only moves the cliff to a heavier shard. */ import { describe, it, expect, expectTypeOf, vi, beforeAll, afterAll } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; import { ObjectQL } from '@objectstack/objectql'; import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm'; import { AuthManager } from '@objectstack/plugin-auth'; @@ -90,14 +128,17 @@ const IDENTITY_OBJECTS = Object.values( */ const PRIOR_POSTURE = process.env.OS_TENANCY_POSTURE; +/** Put back the posture this file found: after the warm-up, and after the file. */ +const restorePosture = (): void => { + if (PRIOR_POSTURE === undefined) delete process.env.OS_TENANCY_POSTURE; + else process.env.OS_TENANCY_POSTURE = PRIOR_POSTURE; +}; + beforeAll(() => { process.env.OS_TENANCY_POSTURE = 'isolated'; }); -afterAll(() => { - if (PRIOR_POSTURE === undefined) delete process.env.OS_TENANCY_POSTURE; - else process.env.OS_TENANCY_POSTURE = PRIOR_POSTURE; -}); +afterAll(restorePosture); let seq = 0; const nextEmail = (tag: string) => `os17274-${tag}-${++seq}-${Date.now()}@example.com`; @@ -151,6 +192,13 @@ async function arrange(): Promise { return { client, organizationId, teamId }; } +// [#20327] The first scenario's one-time cost, paid during COLLECTION, which +// no vitest clock covers (header, last section). It holds the posture the +// `beforeAll` above sets for the cases, and `.finally` puts back what it +// found. ⛔ It stays at module scope, and `⑥` below pins that. +process.env.OS_TENANCY_POSTURE = 'isolated'; +await arrange().finally(restorePosture); + /** What `invitations.list` answers for one invitation — the round-trip shape. */ type ListedInvitation = Awaited< ReturnType @@ -454,3 +502,32 @@ export async function resendTeamIdStaysDeclared17274(): Promise { // hands to `invite`: the round trip is a type-level fact, not a convention. expectTypeOf().toExtend(); } + +// ───────────────────────────────────────────────────────────────────────── +// ⑥ the cold start stays outside every clocked window +// ───────────────────────────────────────────────────────────────────────── + +describe('#17274 ⑥ the cold start stays outside every clocked window', () => { + it('pays the first scenario at module scope, and no hook carries it', () => { + // [#20327] Read off this file's own text, so "do not move it into a + // hook" is an assertion rather than a sentence nobody reads. + const code = readFileSync(fileURLToPath(import.meta.url), 'utf8'); + + // Exactly one warm-up call, and it opens its own line at column 0. So it + // sits in no function body, which is what "paid during collection" + // reduces to. Comment lines start with `//` or ` *` and cannot match. + expect(code.match(/^await arrange\(\)\.finally\(restorePosture\);$/gm) ?? []).toHaveLength(1); + + // ⛔ No `before*` hook may come back to carry it: vitest clocks a hook + // with `hookTimeout` exactly as it clocks a test body with `testTimeout`. + // This file's one hook is the posture `beforeAll`, which loads nothing. + // Each hook is read up to the next column-0 `});`, so a hook indented + // inside a `describe` reads on to that block's close and cannot hide a + // scenario from this. + const hooks = [...code.matchAll(/^\s*before(?:All|Each)\s*\(([\s\S]*?)^\}\);$/gm)].map( + (m) => m[1], + ); + expect(hooks).toHaveLength(1); + for (const body of hooks) expect(body).not.toMatch(/\barrange\(/); + }); +}); diff --git a/packages/client/src/organization-invite-role-default.test.ts b/packages/client/src/organization-invite-role-default.test.ts index 2aa56a7fbcf..809bd274c29 100644 --- a/packages/client/src/organization-invite-role-default.test.ts +++ b/packages/client/src/organization-invite-role-default.test.ts @@ -58,9 +58,47 @@ * caller, while the default costs no type change at all. ⑤ is a compile-time * assertion that `role` is still optional, so that route cannot be taken later * by accident. + * + * ⑥ pins WHERE the first scenario's one-time cost is paid (next section). + * + * ## [#20327] Why one throwaway scenario runs at MODULE SCOPE + * + * The first scenario in a worker pays a one-time cost no later one sees: + * better-auth's lazily imported module graph, sql.js's WASM compile, and the + * first-use costs of the sync and the sign-up. The phases are measured in + * `auth-login-register-envelope.test.ts`, which carries the same arrangement + * and the same fix. Here, idle on 4 vCPU at `c74de10a9`, that cost put ① at + * 968 ms against 310-336 ms for ② and ③, which run the same `arrange()`. + * On the `Test Core` shard where a sibling suite went red (PR #20325's run), + * ① took 2849 ms here against 497-820 ms for the later cases that run + * `arrange()`. Under twenty-four CPU-bound busy loops on this box, the load + * that reproduces that red, ① timed out at vitest's 5000 ms `testTimeout` in + * every run. + * + * So the cost is now paid by a module-scope `await`, during COLLECTION, which + * no vitest clock covers: `@vitest/runner@4.1.11` wraps hooks and test bodies + * in `withTimeout(...)` and awaits the file import bare. This is the repo's + * convention: "clocked windows measure behaviour, never loading" (AGENTS.md, + * Build & Test; `check:test-source-alias`). The warm-up is the file's own + * `arrange()`, so no list of loads can drift from what the cases really pay. + * It needs the posture the `beforeAll` sets for the cases, so it holds that + * posture for itself and puts back what it found, even when it throws. + * + * ⛔ It shares nothing a case asserts on. Every case still builds a fresh + * engine, a fresh `AuthManager`, a fresh owner and a fresh organization. Its + * engine stays open, as every case's does (`arrange()` hands none back), so + * no case runs in a state no case ran in before: every case but the first + * already ran after an earlier scenario. What it leaves warm is + * process-level: the module registry, sql.js's compiled WASM and the JIT. + * + * ⛔ Do not move it into a hook, and do not answer a recurrence by raising a + * timeout: vitest clocks a hook exactly as it clocks a test body, and a wider + * window only moves the cliff to a heavier shard. */ import { describe, it, expect, expectTypeOf, vi, beforeAll, afterAll } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; import { ObjectQL } from '@objectstack/objectql'; import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm'; import { AuthManager } from '@objectstack/plugin-auth'; @@ -96,14 +134,17 @@ const IDENTITY_OBJECTS = Object.values( */ const PRIOR_POSTURE = process.env.OS_TENANCY_POSTURE; +/** Put back the posture this file found: after the warm-up, and after the file. */ +const restorePosture = (): void => { + if (PRIOR_POSTURE === undefined) delete process.env.OS_TENANCY_POSTURE; + else process.env.OS_TENANCY_POSTURE = PRIOR_POSTURE; +}; + beforeAll(() => { process.env.OS_TENANCY_POSTURE = 'isolated'; }); -afterAll(() => { - if (PRIOR_POSTURE === undefined) delete process.env.OS_TENANCY_POSTURE; - else process.env.OS_TENANCY_POSTURE = PRIOR_POSTURE; -}); +afterAll(restorePosture); let seq = 0; const nextEmail = (tag: string) => `os16582-${tag}-${++seq}-${Date.now()}@example.com`; @@ -155,6 +196,13 @@ async function arrange(): Promise { return { client, organizationId }; } +// [#20327] The first scenario's one-time cost, paid during COLLECTION, which +// no vitest clock covers (header, last section). It holds the posture the +// `beforeAll` above sets for the cases, and `.finally` puts back what it +// found. ⛔ It stays at module scope, and `⑥` below pins that. +process.env.OS_TENANCY_POSTURE = 'isolated'; +await arrange().finally(restorePosture); + // ───────────────────────────────────────────────────────────────────────── // ① the card's call, against the real vendor schema // ───────────────────────────────────────────────────────────────────────── @@ -328,3 +376,32 @@ export async function inviteRoleStaysOptional16582(): Promise { Exclude >(); } + +// ───────────────────────────────────────────────────────────────────────── +// ⑥ the cold start stays outside every clocked window +// ───────────────────────────────────────────────────────────────────────── + +describe('#16582 ⑥ the cold start stays outside every clocked window', () => { + it('pays the first scenario at module scope, and no hook carries it', () => { + // [#20327] Read off this file's own text, so "do not move it into a + // hook" is an assertion rather than a sentence nobody reads. + const code = readFileSync(fileURLToPath(import.meta.url), 'utf8'); + + // Exactly one warm-up call, and it opens its own line at column 0. So it + // sits in no function body, which is what "paid during collection" + // reduces to. Comment lines start with `//` or ` *` and cannot match. + expect(code.match(/^await arrange\(\)\.finally\(restorePosture\);$/gm) ?? []).toHaveLength(1); + + // ⛔ No `before*` hook may come back to carry it: vitest clocks a hook + // with `hookTimeout` exactly as it clocks a test body with `testTimeout`. + // This file's one hook is the posture `beforeAll`, which loads nothing. + // Each hook is read up to the next column-0 `});`, so a hook indented + // inside a `describe` reads on to that block's close and cannot hide a + // scenario from this. + const hooks = [...code.matchAll(/^\s*before(?:All|Each)\s*\(([\s\S]*?)^\}\);$/gm)].map( + (m) => m[1], + ); + expect(hooks).toHaveLength(1); + for (const body of hooks) expect(body).not.toMatch(/\barrange\(/); + }); +});