@ultimat3/testing 27.11.0 → 27.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -205,8 +205,13 @@ there is no flag on the fixture and no code to silence.
205
205
 
206
206
  Each takes an optional `{ timeoutMs }` after the body — `unitTest(name, body, { timeoutMs: 30_000 })`,
207
207
  and `test` takes the same; `evalTest` reads it off its options — handed to `bun:test` as that
208
- test's own deadline (`TestOptions`). Absent is Bun's 5 s default. An `E2eDriver` receives it as its
209
- third argument. `unitTest`, `contractTest`, `liveTest` and `jobTest` hand their body the
208
+ test's own deadline (`TestOptions`), at this machine's pace. Absent is Bun's 5 s default, which
209
+ `x verify` paces the same way. An `E2eDriver` receives it as its third argument.
210
+
211
+ **`paced(ms)`** is a declared deadline at this machine's pace, for a bare `test(…, paced(90_000))`:
212
+ `ms` times `ULTIMATE_TEST_PACE`, which `x verify` measures and sets on every test run it starts —
213
+ 1 on a 4-core CI runner, more on a 2-core or slower box. Absent (a hand-run `bun test`) is 1; a
214
+ value that is not a number at or above 1 is `X_TEST_PACE_INVALID`. `unitTest`, `contractTest`, `liveTest` and `jobTest` hand their body the
210
215
  [fixture bag](#fixtures); `e2eTest` hands its own (`E2eFixtures`).
211
216
 
212
217
  **Registering one, `As of 2026-08-25`.** `@ultimat3/cli`'s `installE2eDriver({ page, baseUrl })` is
@@ -305,9 +310,15 @@ its own, so one shared store is enough.
305
310
  ## Parallel databases
306
311
 
307
312
  ```ts
308
- const db = await acquireWorkerDatabase({ adminUrl, migrate });
313
+ import { acquireWorkerDatabase } from '@ultimat3/testing';
314
+
315
+ // `migrate: (url) => …` applies the app's schema into the template once; omitted here.
316
+ await using db = await acquireWorkerDatabase({ adminUrl: Bun.env['TEST_DATABASE_URL'] });
309
317
  ```
310
318
 
319
+ The handle is async-disposable (`As of 2026-10-18`): `await using` drops the worker's database when
320
+ the scope ends, a failed test included; `db.drop()` is the same call spelled by hand.
321
+
311
322
  The first worker creates the template under a Postgres advisory lock and migrates it; every
312
323
  worker then clones it copy-on-write, under the same lock — a clone waits only 5 s for a session on
313
324
  the template to leave, and the next worker's migration holds one. With no Postgres configured it falls back to PGlite, so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/testing",
3
- "version": "27.11.0",
3
+ "version": "27.13.0",
4
4
  "description": "Test harness: cloned template DBs per worker, frozen clock, sealed network, 6 test types",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,20 +36,20 @@
36
36
  "test": "bun test"
37
37
  },
38
38
  "dependencies": {
39
- "@ultimat3/action": "27.11.0",
40
- "@ultimat3/cache": "27.11.0",
41
- "@ultimat3/core": "27.11.0",
42
- "@ultimat3/db": "27.11.0",
43
- "@ultimat3/entity": "27.11.0",
44
- "@ultimat3/http": "27.11.0",
45
- "@ultimat3/i18n": "27.11.0",
46
- "@ultimat3/jobs": "27.11.0",
47
- "@ultimat3/mail": "27.11.0",
48
- "@ultimat3/policy": "27.11.0",
49
- "@ultimat3/pwa": "27.11.0",
50
- "@ultimat3/query": "27.11.0",
51
- "@ultimat3/realtime": "27.11.0",
52
- "@ultimat3/render": "27.11.0",
53
- "@ultimat3/time": "27.11.0"
39
+ "@ultimat3/action": "27.13.0",
40
+ "@ultimat3/cache": "27.13.0",
41
+ "@ultimat3/core": "27.13.0",
42
+ "@ultimat3/db": "27.13.0",
43
+ "@ultimat3/entity": "27.13.0",
44
+ "@ultimat3/http": "27.13.0",
45
+ "@ultimat3/i18n": "27.13.0",
46
+ "@ultimat3/jobs": "27.13.0",
47
+ "@ultimat3/mail": "27.13.0",
48
+ "@ultimat3/policy": "27.13.0",
49
+ "@ultimat3/pwa": "27.13.0",
50
+ "@ultimat3/query": "27.13.0",
51
+ "@ultimat3/realtime": "27.13.0",
52
+ "@ultimat3/render": "27.13.0",
53
+ "@ultimat3/time": "27.13.0"
54
54
  }
55
55
  }
@@ -7,6 +7,7 @@ import type { CdpConnection } from './cdp-connection';
7
7
  import type { E2eTab } from './cdp-e2e-page';
8
8
  import { cdpE2eTab } from './cdp-e2e-page';
9
9
  import { CdpCallFailedError, CdpTimeoutError } from './cdp-errors';
10
+ import { isNetworkProbe, networkAgrees } from './cdp-network-probe';
10
11
  import { offlineScripts } from './cdp-offline-script';
11
12
 
12
13
  export interface E2eSession {
@@ -92,6 +93,8 @@ export async function cdpE2eSession(options: CdpE2eSessionOptions): Promise<E2eS
92
93
  });
93
94
  connection.on('Network.requestWillBeSent', (params) => {
94
95
  const request = params['request'];
96
+ // The switch's own proof, not the app's traffic: a test that counts requests never sees it.
97
+ if (isNetworkProbe(field(request, 'url') ?? '')) return;
95
98
  requests.push(`${field(request, 'method') ?? '?'} ${field(request, 'url') ?? ''}`);
96
99
  });
97
100
  connection.on('Target.attachedToTarget', (params) => {
@@ -232,13 +235,20 @@ export async function cdpE2eSession(options: CdpE2eSessionOptions): Promise<E2eS
232
235
  const confirmed = async (online: boolean): Promise<void> => {
233
236
  const deadline = performance.now() + options.loadTimeoutMs;
234
237
  const open = [...pageSessions].filter((session) => sessions.has(session));
238
+ // `navigator.onLine` first, then a real request: Chrome tells the page before its network
239
+ // service applies the cut, so the first alone let a `fetch` through (`cdp-network-probe.ts`).
235
240
  const answers = await Promise.all(
236
- open.map(async (session) => [session, await pageReads(session, online, deadline)] as const),
241
+ open.map(async (session) => {
242
+ const reads =
243
+ (await pageReads(session, online, deadline)) &&
244
+ (await networkAgrees(send, session, online, deadline));
245
+ return [session, reads] as const;
246
+ }),
237
247
  );
238
248
  const waiting = answers.filter(([, reads]) => !reads).map(([session]) => session);
239
249
  if (waiting.length > 0) {
240
250
  throw new CdpTimeoutError({
241
- method: `${ON_LINE} === ${String(online)} in page session ${waiting.join(', ')}`,
251
+ method: `${ON_LINE} === ${String(online)} and a request ${online ? 'reaching' : 'refused'} in page session ${waiting.join(', ')}`,
242
252
  timeoutMs: options.loadTimeoutMs,
243
253
  });
244
254
  }
@@ -0,0 +1,80 @@
1
+ // One responsibility: proving a page's NETWORK agrees with the offline switch, by sending a request.
2
+ // `navigator.onLine` is not that proof: Chrome tells the renderer before the network service applies
3
+ // the condition (two pipes, no order between them), so a `fetch` on the next line went through under
4
+ // a cut the page already read as offline — `browser-harness.e2e.test.ts`, CI run 39223207154.
5
+
6
+ import type { CdpResult } from './cdp-connection';
7
+
8
+ type Send = (
9
+ method: string,
10
+ params: Record<string, unknown>,
11
+ session: string,
12
+ deadlineMs?: number,
13
+ ) => Promise<CdpResult>;
14
+
15
+ /** The probe's path: under `/_x/`, which no app route owns, so the app answers it 404 and does nothing. */
16
+ export const NETWORK_PROBE_PATH = '/_x/network-probe';
17
+
18
+ /**
19
+ * `HEAD`, because the framework's service worker answers GET only — a GET could be served from its
20
+ * cache or its offline document, and read as `reached` under a cut forever. `none` on a page that is
21
+ * not on http(s) (`about:blank`): it has no origin to ask, and nothing on it can fetch either.
22
+ */
23
+ export const NETWORK_PROBE = `(async () => {
24
+ if (location.protocol !== 'http:' && location.protocol !== 'https:') return 'none';
25
+ try {
26
+ await fetch('${NETWORK_PROBE_PATH}', { method: 'HEAD', cache: 'no-store' });
27
+ return 'reached';
28
+ } catch {
29
+ return 'refused';
30
+ }
31
+ })()`;
32
+
33
+ /** Whether a logged request URL is a probe — kept out of the session's request log. */
34
+ export const isNetworkProbe = (url: string): boolean => {
35
+ try {
36
+ return new URL(url).pathname === NETWORK_PROBE_PATH;
37
+ } catch {
38
+ return false;
39
+ }
40
+ };
41
+
42
+ const probeValue = (answer: CdpResult): unknown => {
43
+ const result = answer.result?.['result'];
44
+ return typeof result === 'object' && result !== null
45
+ ? (result as Record<string, unknown>)['value']
46
+ : undefined;
47
+ };
48
+
49
+ /**
50
+ * Probes one page until a request from it is refused (`online` false) or reaches the server
51
+ * (`online` true), and answers `false` at `deadline`. Each probe is itself a round trip to the
52
+ * network, so the loop paces itself — no sleep. A probe refused by a navigation is asked again.
53
+ */
54
+ export async function networkAgrees(
55
+ send: Send,
56
+ session: string,
57
+ online: boolean,
58
+ deadline: number,
59
+ ): Promise<boolean> {
60
+ const wanted = online ? 'reached' : 'refused';
61
+ for (;;) {
62
+ const remaining = deadline - performance.now();
63
+ if (remaining <= 0) return false;
64
+ try {
65
+ const answer = await send(
66
+ 'Runtime.evaluate',
67
+ { expression: NETWORK_PROBE, returnByValue: true, awaitPromise: true },
68
+ session,
69
+ remaining,
70
+ );
71
+ const value = probeValue(answer);
72
+ if (value === wanted || value === 'none') return true;
73
+ } catch {
74
+ // A navigation destroyed the context, or the call ran out of time: the deadline decides.
75
+ }
76
+ // One macrotask between probes: a refusal that settles as a microtask would otherwise starve
77
+ // every timer and socket in the process until the deadline.
78
+ await new Promise<void>((resolve) => setImmediate(resolve));
79
+ }
80
+ }
package/src/e2e-driver.ts CHANGED
@@ -9,6 +9,7 @@ import { FixtureUnavailableError } from './errors';
9
9
  import { withFailureContext } from './failure-context';
10
10
  import { unavailableFixture } from './fixture-drivers';
11
11
  import { defineFixtures } from './fixtures';
12
+ import { paced } from './test-pace';
12
13
  import type { E2eBody, E2eFixtures, PageLike } from './test-types';
13
14
  import { resetE2eDriver, useE2eDriver } from './test-types';
14
15
 
@@ -107,7 +108,7 @@ export function installE2eDriver(options: E2eDriverOptions): () => void {
107
108
  throw withFailureContext(error);
108
109
  }
109
110
  };
110
- bunTest(name, run, test?.timeoutMs);
111
+ bunTest(name, run, test?.timeoutMs === undefined ? undefined : paced(test.timeoutMs));
111
112
  });
112
113
  return () => {
113
114
  // Both halves, because both were installed. Putting the DECLARATION back — rather than
package/src/e2e-page.ts CHANGED
@@ -62,6 +62,21 @@ const readField = (raw: unknown, key: string): unknown => {
62
62
 
63
63
  const TITLE = '(() => JSON.stringify({ title: document.title }))()';
64
64
 
65
+ /**
66
+ * Where the server's SECOND flush begins: every hole a streamed page reveals is written as one
67
+ * chunk opening with this tag (`revealChunk` in `@ultimat3/render`'s `render-stream.ts`, which the
68
+ * barrel does not export). Everything before it is the first flush — the head and the shell.
69
+ */
70
+ export const REVEAL_MARK = '<template data-x-hole=';
71
+
72
+ /**
73
+ * How long the reader waits for the rest of a flush that arrived in pieces. One server flush is
74
+ * ONE `enqueue`, but the transport may hand it to the page as several chunks — a read of the first
75
+ * network chunk alone cut the shell mid-tag and failed `toContain` on a header it really sent.
76
+ * A pause this long ends the read when no hole has been revealed; a reveal ends it at once.
77
+ */
78
+ export const FLUSH_IDLE_MS = 250;
79
+
65
80
  /**
66
81
  * The first flush, read by a `fetch` INSIDE the page after the navigation, because neither
67
82
  * `ScrapePage` nor `CdpPageLike` exposes a response body — puppeteer's `page.on('response')` is
@@ -70,15 +85,37 @@ const TITLE = '(() => JSON.stringify({ title: document.title }))()';
70
85
  * The cost, stated rather than hidden: this is a SECOND request to the same route, so what it
71
86
  * measures is that route's streaming behaviour and not the byte-for-byte first chunk the open
72
87
  * document received. It runs in the page, so it carries the page's cookies and its origin — a
73
- * `fetch` from the test process would carry neither. The reader is CANCELLED after that chunk: a
74
- * streamed response nobody pulls stays open until its last hole fills, holding one of the page's
75
- * six connections to its origin for the rest of the test.
88
+ * `fetch` from the test process would carry neither. Chunks are joined until the first reveal
89
+ * (cut there) or a pause of `FLUSH_IDLE_MS`, and the reader is then CANCELLED: a streamed response
90
+ * nobody pulls stays open until its last hole fills, holding one of the page's six connections to
91
+ * its origin for the rest of the test.
76
92
  */
77
93
  const firstFlushExpression = (url: string): string =>
78
94
  `(() => fetch(${JSON.stringify(url)}, { credentials: 'same-origin' })
79
- .then((response) => { const reader = response.body.getReader(); return reader.read()
80
- .then((chunk) => { reader.cancel().catch(() => {}); return chunk; }); })
81
- .then((chunk) => JSON.stringify({ html: new TextDecoder().decode(chunk.value || new Uint8Array()) })))()`;
95
+ .then((response) => {
96
+ const reader = response.body.getReader();
97
+ const decoder = new TextDecoder();
98
+ const mark = ${JSON.stringify(REVEAL_MARK)};
99
+ let html = '';
100
+ const finish = () => {
101
+ reader.cancel().catch(() => {});
102
+ const cut = html.indexOf(mark);
103
+ return JSON.stringify({ html: cut === -1 ? html : html.slice(0, cut) });
104
+ };
105
+ const pull = (first) => {
106
+ const read = reader.read();
107
+ const next = first ? read : Promise.race([
108
+ read,
109
+ new Promise((resolve) => setTimeout(() => resolve(null), ${FLUSH_IDLE_MS})),
110
+ ]);
111
+ return next.then((chunk) => {
112
+ if (chunk === null || chunk.done) return finish();
113
+ html += decoder.decode(chunk.value, { stream: true });
114
+ return html.includes(mark) ? finish() : pull(false);
115
+ });
116
+ };
117
+ return pull(true);
118
+ }))()`;
82
119
 
83
120
  /**
84
121
  * The adapter. Every member re-reads the live page: a `PageLike` handed to a test outlives every
package/src/errors.ts CHANGED
@@ -48,6 +48,7 @@ export const TESTING_ERROR_CODES = [
48
48
  'X_TEST_NUMBER_EXPECTED',
49
49
  'X_TEST_ISLAND_STATE_UNKNOWN',
50
50
  'X_TEST_APP_NOT_BOOTED',
51
+ 'X_TEST_PACE_INVALID',
51
52
  ] as const;
52
53
 
53
54
  export type TestingErrorCode = (typeof TESTING_ERROR_CODES)[number];
@@ -88,6 +89,7 @@ export const TESTING_ERROR_TITLES: Readonly<Record<TestingErrorCode, string>> =
88
89
  X_TEST_NUMBER_EXPECTED: 'a matcher expected a finite number and got something else',
89
90
  X_TEST_ISLAND_STATE_UNKNOWN: 'an island state manifest declares no state with that id',
90
91
  X_TEST_APP_NOT_BOOTED: 'describeApp handle read before boot',
92
+ X_TEST_PACE_INVALID: 'ULTIMATE_TEST_PACE is not a number at or above 1',
91
93
  };
92
94
 
93
95
  // Titles must be registered for `format()` to render the contract's first line. Every code above is
package/src/fixtures.ts CHANGED
@@ -18,6 +18,7 @@ import type { TestMail } from './fixture-mail';
18
18
  import type { TestNetwork } from './fixture-network';
19
19
  import type { TestPush } from './fixture-push';
20
20
  import type { TestStatements } from './fixture-statements';
21
+ import { paced } from './test-pace';
21
22
  import type { PageLike, TestOptions } from './test-types';
22
23
 
23
24
  /** Built once per test, on first use. */
@@ -257,8 +258,13 @@ export async function runWithFixtures(body: FixtureRunBody): Promise<void> {
257
258
  * must not be the reason the next file inherits a queue.
258
259
  *
259
260
  * `options.timeoutMs` is the test's own deadline, the same key the six typed helpers take: a
260
- * fixture that builds something slow (an island, a browser) is the test that needs one.
261
+ * fixture that builds something slow (an island, a browser) is the test that needs one. Read at
262
+ * this machine's pace (`paced`), as the typed helpers read theirs.
261
263
  */
262
264
  export function fixtureTest(name: string, body: FixtureBody, options?: TestOptions): void {
263
- bunTest(name, () => runWithFixtures(body), options?.timeoutMs);
265
+ bunTest(
266
+ name,
267
+ () => runWithFixtures(body),
268
+ options?.timeoutMs === undefined ? undefined : paced(options.timeoutMs),
269
+ );
264
270
  }
package/src/index.ts CHANGED
@@ -340,7 +340,12 @@ export {
340
340
  } from './sealed-network';
341
341
  export type { SharedExamples } from './shared-examples';
342
342
  export { behavesLike, sharedExamples } from './shared-examples';
343
- export type { SqlRunner, TemplateDbConfig, WorkerDatabase } from './template-db';
343
+ export type {
344
+ DisposableWorkerDatabase,
345
+ SqlRunner,
346
+ TemplateDbConfig,
347
+ WorkerDatabase,
348
+ } from './template-db';
344
349
  export {
345
350
  acquireWorkerDatabase,
346
351
  cloneSql,
@@ -353,6 +358,7 @@ export {
353
358
  urlFor,
354
359
  workerId,
355
360
  } from './template-db';
361
+ export { paced, TEST_PACE_ENV, TestPaceInvalidError, testPace } from './test-pace';
356
362
  export type {
357
363
  E2eBody,
358
364
  E2eDriver,
@@ -30,6 +30,13 @@ export interface WorkerDatabase {
30
30
  drop(): Promise<void>;
31
31
  }
32
32
 
33
+ /**
34
+ * What `acquireWorkerDatabase` answers: the handle plus `drop()` as its async disposer, so
35
+ * `await using db = await acquireWorkerDatabase()` releases it however the scope exits. A separate
36
+ * type so a hand-built `WorkerDatabase` (a fake `acquire`) owes no disposer.
37
+ */
38
+ export type DisposableWorkerDatabase = WorkerDatabase & AsyncDisposable;
39
+
33
40
  export const DEFAULT_TEMPLATE = 'ultimate_test_template';
34
41
 
35
42
  /**
@@ -109,7 +116,7 @@ const defaultConnect = (url: string): SqlRunner => {
109
116
  export async function acquireWorkerDatabase(
110
117
  config: TemplateDbConfig = {},
111
118
  deps: Partial<TemplateDbDeps> = {},
112
- ): Promise<WorkerDatabase> {
119
+ ): Promise<DisposableWorkerDatabase> {
113
120
  const env = deps.env ?? (Bun.env as Readonly<Record<string, string | undefined>>);
114
121
  const connect = deps.connect ?? defaultConnect;
115
122
  const template = config.templateName ?? DEFAULT_TEMPLATE;
@@ -118,13 +125,13 @@ export async function acquireWorkerDatabase(
118
125
  const adminUrl = config.adminUrl ?? env['TEST_DATABASE_URL'] ?? env['DATABASE_URL'];
119
126
 
120
127
  if (adminUrl === undefined || adminUrl.length === 0) {
121
- return {
128
+ return disposable({
122
129
  kind: 'pglite',
123
130
  worker,
124
131
  database,
125
132
  url: `pglite://memory/${database}`,
126
133
  drop: async () => undefined,
127
- };
134
+ });
128
135
  }
129
136
 
130
137
  const admin = connect(adminUrl);
@@ -160,7 +167,7 @@ export async function acquireWorkerDatabase(
160
167
  }
161
168
  await admin.close();
162
169
 
163
- return {
170
+ return disposable({
164
171
  kind: 'postgres',
165
172
  worker,
166
173
  database,
@@ -170,7 +177,12 @@ export async function acquireWorkerDatabase(
170
177
  await cleanup.exec(dropSql(database));
171
178
  await cleanup.close();
172
179
  },
173
- };
180
+ });
181
+ }
182
+
183
+ /** The handle with its `drop` as the async disposer, so `await using` is the one release spelling. */
184
+ function disposable(db: WorkerDatabase): DisposableWorkerDatabase {
185
+ return { ...db, [Symbol.asyncDispose]: () => db.drop() };
174
186
  }
175
187
 
176
188
  /**
@@ -0,0 +1,36 @@
1
+ // How much slower this machine runs than the one a test's declared deadline was measured on.
2
+ // `x verify` measures it (`packages/cli/src/verify-pace.ts`) and hands it to every test run it
3
+ // starts as `ULTIMATE_TEST_PACE`; a test's own timeout is read at that pace, so a 2-core 1.8 GHz
4
+ // box is held to the same bound a CI runner is instead of failing on a number set for the runner.
5
+
6
+ import { UltimateError } from '@ultimat3/core';
7
+
8
+ /** Set by `x verify` on each test run it starts. Absent — a hand-run `bun test` — is pace 1. */
9
+ export const TEST_PACE_ENV = 'ULTIMATE_TEST_PACE';
10
+
11
+ /** A pace that does not parse, or one below 1: a machine is never read as faster than the bound. */
12
+ export class TestPaceInvalidError extends UltimateError {
13
+ constructor(value: string) {
14
+ super({
15
+ code: 'X_TEST_PACE_INVALID',
16
+ cause: `${TEST_PACE_ENV}=${JSON.stringify(value)} is not a number at or above 1`,
17
+ fix: `env -u ${TEST_PACE_ENV} bun test <file> # the gate measures the pace and sets it on each test run itself; unset is pace 1`,
18
+ });
19
+ }
20
+ }
21
+
22
+ /** The pace this process was started at. */
23
+ export function testPace(env: Readonly<Record<string, string | undefined>> = Bun.env): number {
24
+ const raw = env[TEST_PACE_ENV];
25
+ if (raw === undefined || raw.trim() === '') return 1;
26
+ const pace = Number(raw);
27
+ if (!Number.isFinite(pace) || pace < 1) throw new TestPaceInvalidError(raw);
28
+ return pace;
29
+ }
30
+
31
+ /**
32
+ * A declared test deadline at this machine's pace: `test(name, body, paced(90_000))`. The number
33
+ * stays the one measured on a fast machine; only a slower one stretches it.
34
+ */
35
+ export const paced = (ms: number, env?: Readonly<Record<string, string | undefined>>): number =>
36
+ Math.round(ms * testPace(env));
package/src/test-types.ts CHANGED
@@ -6,6 +6,7 @@ import { test } from 'bun:test';
6
6
  import { TestEvalThresholdError } from './errors';
7
7
  import type { FixtureBody } from './fixtures';
8
8
  import { runWithFixtures } from './fixtures';
9
+ import { paced } from './test-pace';
9
10
 
10
11
  export const TEST_TYPES = ['unit', 'contract', 'live', 'job', 'e2e', 'eval'] as const;
11
12
 
@@ -21,7 +22,10 @@ export const testName = (type: TestType, name: string): string => `${type}${SEPA
21
22
  * outlive Bun's 5 s default, which is the one-way rule broken for a number (#589).
22
23
  */
23
24
  export interface TestOptions {
24
- /** This test's own deadline in milliseconds, handed to `bun:test`. Absent is Bun's default. */
25
+ /**
26
+ * This test's own deadline in milliseconds, handed to `bun:test` at this machine's pace
27
+ * (`paced`). Absent is the run's default, which `x verify` paces the same way.
28
+ */
25
29
  readonly timeoutMs?: number;
26
30
  }
27
31
 
@@ -34,7 +38,11 @@ export interface TestOptions {
34
38
  const typed =
35
39
  (type: TestType) =>
36
40
  (name: string, body: FixtureBody, options?: TestOptions): void => {
37
- test(testName(type, name), () => runWithFixtures(body), options?.timeoutMs);
41
+ test(
42
+ testName(type, name),
43
+ () => runWithFixtures(body),
44
+ options?.timeoutMs === undefined ? undefined : paced(options.timeoutMs),
45
+ );
38
46
  };
39
47
 
40
48
  /** Pure logic: no database, no clock, no network. The cheapest test that can fail for real. */
@@ -192,7 +200,7 @@ export function evalTest<TInput, TOutput>(
192
200
  throw new TestEvalThresholdError({ name, threshold: options.threshold, detail });
193
201
  }
194
202
  },
195
- options.timeoutMs,
203
+ options.timeoutMs === undefined ? undefined : paced(options.timeoutMs),
196
204
  );
197
205
  }
198
206