@ultimat3/testing 19.2.0 → 19.3.1

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.
Files changed (3) hide show
  1. package/CLAUDE.md +2 -0
  2. package/package.json +12 -12
  3. package/src/matchers.ts +31 -17
package/CLAUDE.md CHANGED
@@ -27,6 +27,8 @@ is its own entry point and not part of the barrel.
27
27
  | A FIXED interval, and never a curve | doubling the gap makes the last look land long after the state changed, and the caller's deadline is the contract. There is exactly one backoff curve in the framework (`@ultimat3/core`'s `backoff.ts`) and this is deliberately not a second one. A zero or negative interval is REFUSED: the failure mode is a test that never fails — it hangs, and CI reports a runner timeout with no assertion in it |
28
28
  | A matcher that may throw a coded error MUST NOT be `async` | measured against Bun 1.4.0: an `async` matcher has any error it throws REPLACED by bun's own `Matcher \`x\` returned a promise that rejected`, so the code, the cause and the fix are gone before a reader sees them. `X_TEST_SCHEMA_EXPECTED` and `X_TEST_JOB_EXPECTED` were declared, registered, titled — and unreachable by any caller for their whole life, because nothing asserted them. The shape is a SYNCHRONOUS prologue that validates the receiver (`assertStandardSchema`, `assertJobDeclaration`, `assertVisibilityProbe`) and a promise returned after it. `matcher-receiver.test.ts` pins all four |
29
29
  | Wrong receiver throws, wrong value returns | a matcher handed a page where a locator belongs is not FALSE, it is unanswerable — `pass: false` would read as "the element was hidden". A wrong-shaped receiver is a coded throw; a wrong value is `result(false, …)` |
30
+ | A matcher MESSAGE is a thunk, and never `JSON.stringify` | `result(pass, () => …)` — `expect.extend` reads `message()` only on the wrong verdict, and the string used to be built EAGERLY. `JSON.stringify` refuses a BigInt and throws on anything cyclic, so `expect(schema).toRejectInput({ n: 1n })` — a schema that DID reject — came back as ``Matcher `toRejectInput` returned a promise that rejected``, with the real answer gone, on the PASSING path. Three matchers quoted their input that way. `renderCauseValue` (`@ultimat3/core`) is the renderer: it quotes what JSON can quote and names the shape of what it cannot, so the message still reads `expected the schema to reject {"id":"ok"}` |
31
+ | Every message thunk needs a test that PROVOKES it | a thunk is a function, so an unread message is an UNCOVERED function and `bun run scripts/coverage-gate.ts --package testing` is what says so — the eleven thunks landed with two of them read and took the package from 95.62% to 94.03%. That number is the useful half: `.not` on a `pass: false` never asks for the message, so `expect({ paths: {} }).not.toMatchOpenApi(…)` passes with the type guard deleted, and the same test reads as covering the matcher. `messageOf()` in `matchers.test.ts` awaits the FAILING side and asserts the sentence, whichever way bun delivers it |
30
32
  | Test names | the filename picks the step; `testName(type, name)` on the outer `describe` puts that type on every failure line under it. Never on the inner `test` too — the prefix would print twice |
31
33
  | `test` here is `fixtureTest`, and it takes no timeout | `(name, body)`, nothing else (`fixtures.ts:248`, exported as `test` by `index.ts:107`) — `bunTest` is called with two arguments, so bun's own third one is unreachable and no case can ask for more than the default 5s. Slow work goes in `beforeAll(fn, 60_000)`, which is `bun:test`'s own re-export and does take one; that is where both generated island tests and `examples/dummy/apps/web/app/settings/settings.island.test.ts` build their chunk, once, for every case to share — a Babel pass plus a browser bundle is seconds. A case that needs 60s of its own is work sitting in the wrong place |
32
34
  | Injection | `SqlRunner` and `connect` are parameters, so unit tests need no server |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/testing",
3
- "version": "19.2.0",
3
+ "version": "19.3.1",
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",
@@ -33,16 +33,16 @@
33
33
  "test": "bun test"
34
34
  },
35
35
  "dependencies": {
36
- "@ultimat3/cache": "19.2.0",
37
- "@ultimat3/core": "19.2.0",
38
- "@ultimat3/db": "19.2.0",
39
- "@ultimat3/entity": "19.2.0",
40
- "@ultimat3/i18n": "19.2.0",
41
- "@ultimat3/jobs": "19.2.0",
42
- "@ultimat3/mail": "19.2.0",
43
- "@ultimat3/policy": "19.2.0",
44
- "@ultimat3/query": "19.2.0",
45
- "@ultimat3/realtime": "19.2.0",
46
- "@ultimat3/time": "19.2.0"
36
+ "@ultimat3/cache": "19.3.1",
37
+ "@ultimat3/core": "19.3.1",
38
+ "@ultimat3/db": "19.3.1",
39
+ "@ultimat3/entity": "19.3.1",
40
+ "@ultimat3/i18n": "19.3.1",
41
+ "@ultimat3/jobs": "19.3.1",
42
+ "@ultimat3/mail": "19.3.1",
43
+ "@ultimat3/policy": "19.3.1",
44
+ "@ultimat3/query": "19.3.1",
45
+ "@ultimat3/realtime": "19.3.1",
46
+ "@ultimat3/time": "19.3.1"
47
47
  }
48
48
  }
package/src/matchers.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import type { ExpectExtendMatchers } from 'bun:test';
6
6
  import { expect } from 'bun:test';
7
- import { describeValue, isUltimateError, stringField } from '@ultimat3/core';
7
+ import { describeValue, isUltimateError, renderCauseValue, stringField } from '@ultimat3/core';
8
8
  import { TestJobExpectedError, TestSchemaExpectedError } from './errors';
9
9
  import type { MatcherResult } from './matcher-result';
10
10
  import type { UltimateMatchers } from './matcher-surface';
@@ -144,10 +144,15 @@ export async function recordSteps(job: unknown, input: unknown = {}): Promise<re
144
144
  return names;
145
145
  }
146
146
 
147
- const result = (pass: boolean, message: string): MatcherResult => ({
148
- pass,
149
- message: () => message,
150
- });
147
+ /**
148
+ * A THUNK, never a built string. `expect.extend` reads `message()` only when the verdict is the
149
+ * wrong one, and building the sentence eagerly made three matchers raise from inside the assertion
150
+ * library on values they were answering correctly: `JSON.stringify` refuses a BigInt and throws on
151
+ * anything cyclic, so `expect(schema).toRejectInput({ n: 1n })` — a schema that DID reject — came
152
+ * back as `Matcher \`toRejectInput\` returned a promise that rejected`, with the real answer gone.
153
+ * Deferred, a value that cannot be rendered costs nothing on the path that passes.
154
+ */
155
+ const result = (pass: boolean, message: () => string): MatcherResult => ({ pass, message });
151
156
 
152
157
  const isOpenApiLike = (value: unknown): value is OpenApiLike =>
153
158
  isRecord(value) &&
@@ -221,11 +226,12 @@ const implementations: ExpectExtendMatchers<UltimateMatchers<unknown>> = {
221
226
  if (actual === undefined) {
222
227
  return result(
223
228
  false,
224
- `expected an UltimateError (an X_ code with a cause and a fix), received ${describeValue(received)}`,
229
+ () =>
230
+ `expected an UltimateError (an X_ code with a cause and a fix), received ${describeValue(received)}`,
225
231
  );
226
232
  }
227
- if (code === undefined) return result(true, `expected not to be an UltimateError`);
228
- return result(actual === code, `expected error code ${code}, received ${actual}`);
233
+ if (code === undefined) return result(true, () => 'expected not to be an UltimateError');
234
+ return result(actual === code, () => `expected error code ${code}, received ${actual}`);
229
235
  },
230
236
 
231
237
  async toDenyPolicy(received: unknown, context: Readonly<Record<string, unknown>>) {
@@ -233,10 +239,13 @@ const implementations: ExpectExtendMatchers<UltimateMatchers<unknown>> = {
233
239
  if (allowed === undefined) {
234
240
  return result(
235
241
  false,
236
- 'expected a policy — an object with run() (@ultimat3/policy) or evaluate()',
242
+ () => 'expected a policy — an object with run() (@ultimat3/policy) or evaluate()',
237
243
  );
238
244
  }
239
- return result(!allowed, `expected the policy to deny ${JSON.stringify(context)}`);
245
+ // `renderCauseValue`, never `JSON.stringify`: the context is a value a test authored, and this
246
+ // matcher is asked about policies whose input holds a BigInt id or a back-reference. It quotes
247
+ // what JSON can quote and names the shape of what it cannot, instead of throwing over either.
248
+ return result(!allowed, () => `expected the policy to deny ${renderCauseValue(context)}`);
240
249
  },
241
250
 
242
251
  // Not `async` — see `assertStandardSchema`. The guard is the synchronous prologue; the wait is
@@ -246,7 +255,7 @@ const implementations: ExpectExtendMatchers<UltimateMatchers<unknown>> = {
246
255
  return recordSteps(job).then((names) =>
247
256
  result(
248
257
  JSON.stringify(names) === JSON.stringify(expected),
249
- `expected steps ${expected.join(' -> ')}, ran ${names.join(' -> ')}`,
258
+ () => `expected steps ${expected.join(' -> ')}, ran ${names.join(' -> ')}`,
250
259
  ),
251
260
  );
252
261
  },
@@ -255,13 +264,15 @@ const implementations: ExpectExtendMatchers<UltimateMatchers<unknown>> = {
255
264
  if (!isOpenApiLike(received)) {
256
265
  return result(
257
266
  false,
258
- 'expected an OpenAPI document — an object with operations: [{ operationId, required? }]',
267
+ () =>
268
+ 'expected an OpenAPI document — an object with operations: [{ operationId, required? }]',
259
269
  );
260
270
  }
261
271
  const broke = breakingChanges(committed, received);
262
272
  return result(
263
273
  broke.length === 0,
264
- `contract broke: ${broke.join('; ')} — bump the package version or restore the old shape`,
274
+ () =>
275
+ `contract broke: ${broke.join('; ')} — bump the package version or restore the old shape`,
265
276
  );
266
277
  },
267
278
 
@@ -269,23 +280,26 @@ const implementations: ExpectExtendMatchers<UltimateMatchers<unknown>> = {
269
280
  if (typeof received !== 'number') {
270
281
  return result(
271
282
  false,
272
- `expected a number to compare against the budget, got ${typeof received}`,
283
+ () => `expected a number to compare against the budget, got ${typeof received}`,
273
284
  );
274
285
  }
275
- return result(received <= limit, `expected ${received} to be within the budget of ${limit}`);
286
+ return result(
287
+ received <= limit,
288
+ () => `expected ${received} to be within the budget of ${limit}`,
289
+ );
276
290
  },
277
291
 
278
292
  toRejectInput(received: unknown, input: unknown) {
279
293
  const schema = assertStandardSchema(received);
280
294
  return hasIssues(schema, input).then((rejected) =>
281
- result(rejected, `expected the schema to reject ${JSON.stringify(input)}`),
295
+ result(rejected, () => `expected the schema to reject ${renderCauseValue(input)}`),
282
296
  );
283
297
  },
284
298
 
285
299
  toAcceptInput(received: unknown, input: unknown) {
286
300
  const schema = assertStandardSchema(received);
287
301
  return hasIssues(schema, input).then((rejected) =>
288
- result(!rejected, `expected the schema to accept ${JSON.stringify(input)}`),
302
+ result(!rejected, () => `expected the schema to accept ${renderCauseValue(input)}`),
289
303
  );
290
304
  },
291
305
  };