@nestia/e2e 14.0.0 → 14.0.2

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.
@@ -14,18 +14,31 @@ import { json_equal_to } from "./internal/json_equal_to";
14
14
  * maintain currying patterns for advanced composition. All provide detailed
15
15
  * error messages for debugging failed assertions.
16
16
  *
17
+ * Processing cost: Scalar assertions use constant bookkeeping; JSON assertions
18
+ * traverse the expected structure, index compares the shared page prefix,
19
+ * search scans the dataset once per sample and sort checks adjacent results.
20
+ * External callbacks own their computation costs.
21
+ *
17
22
  * @author Jeongho Nam - https://github.com/samchon
18
23
  * @example
19
- * ```typescript
20
24
  * // Basic condition testing
21
- * TestValidator.predicate("user should be authenticated", user.isAuthenticated);
25
+ * TestValidator.predicate(
26
+ * "user should be authenticated",
27
+ * user.isAuthenticated,
28
+ * );
22
29
  *
23
30
  * // Equality validation
24
31
  * TestValidator.equals("API response should match expected", x, y);
25
32
  *
26
33
  * // Error validation
27
- * TestValidator.error("should throw on invalid input", () => assertInput(""));
28
- * ```;
34
+ * TestValidator.error("should throw on invalid input", () =>
35
+ * assertInput(""),
36
+ * );
37
+ *
38
+ * @evidence contracts/common.md#principled-implementation Each validator runs a check and throws an `Error` whose message begins with `Bug on <title>`, returning a promise only when its input is asynchronous, so the same function serves synchronous and asynchronous tests; the comparison helpers read values through their JSON form, which is the form an HTTP API delivers.
39
+ * @evidence contracts/common.md#clear-and-simple-design One function per assertion kind, sharing the JSON comparison (`json_equal_to`) and two small module helpers (`is_promise` and `is_sorted`), with the curried forms only where a request must be prepared once and run twice.
40
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Assertions compare the values they are given; none consults a fixture, an entity name, or an environment.
41
+ * @evidence contracts/common.md#meaningful-documentation The namespace prose lists the assertion families and the error message convention, with examples.
29
42
  */
30
43
  export namespace TestValidator {
31
44
  /**
@@ -35,19 +48,33 @@ export namespace TestValidator {
35
48
  * boolean, and asynchronous functions returning Promise<boolean>. The return
36
49
  * type is automatically inferred based on the input type.
37
50
  *
51
+ * Processing cost: The condition is evaluated once and asynchronous
52
+ * settlement adds constant bookkeeping without polling or repeating the
53
+ * condition.
54
+ *
55
+ * Reuse: Each invocation must evaluate its supplied task or request to
56
+ * observe current behavior; prior results cannot replace these effectful
57
+ * checks.
58
+ *
59
+ * Resource ownership: The returned promise retains invocation-local state
60
+ * until settlement. The callback owns its external resources, and this
61
+ * validator provides no cancellation or timeout.
62
+ *
38
63
  * @example
39
- * ```typescript
40
64
  * // Synchronous boolean
41
65
  * TestValidator.predicate("user should exist", user !== null);
42
66
  *
43
67
  * // Synchronous function
44
- * TestValidator.predicate("array should be empty", () => arr.length === 0);
68
+ * TestValidator.predicate(
69
+ * "array should be empty",
70
+ * () => arr.length === 0,
71
+ * );
45
72
  *
46
73
  * // Asynchronous function
47
- * await TestValidator.predicate("database should be connected",
48
- * async () => await db.ping()
74
+ * await TestValidator.predicate(
75
+ * "database should be connected",
76
+ * async () => await db.ping(),
49
77
  * );
50
- * ```;
51
78
  *
52
79
  * @param title - Descriptive title used in error messages when validation
53
80
  * fails
@@ -55,6 +82,10 @@ export namespace TestValidator {
55
82
  * function)
56
83
  * @returns Void or Promise<void> based on the input type
57
84
  * @throws Error with descriptive message when condition is not satisfied
85
+ * @evidence contracts/common.md#principled-implementation A boolean, a function returning a boolean, or a function returning a promise of a boolean is evaluated, and only the exact value `true` passes; a false result throws the same `Error` in all three forms, and the asynchronous form rejects with it.
86
+ * @evidence contracts/common.md#clear-and-simple-design One function whose three branches differ only in when the value is available.
87
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The check is on the value the caller gives, with no truthiness coercion.
88
+ * @evidence contracts/common.md#meaningful-documentation The comment documents the three forms, the parameters, the return type rule, and the thrown error, with examples.
58
89
  */
59
90
  export function predicate<
60
91
  T extends boolean | (() => boolean) | (() => Promise<boolean>),
@@ -83,7 +114,7 @@ export namespace TestValidator {
83
114
  output
84
115
  .then((flag) => {
85
116
  if (flag === true) resolve();
86
- else reject(message());
117
+ else reject(new Error(message()));
87
118
  })
88
119
  .catch(reject);
89
120
  }) as any;
@@ -92,30 +123,45 @@ export namespace TestValidator {
92
123
  /**
93
124
  * Validates deep equality between two values using JSON comparison.
94
125
  *
95
- * Performs recursive comparison of objects and arrays. Supports an optional
126
+ * Performs recursive comparison of objects and arrays through their JSON
127
+ * form. The comparison walks the keys of the first value, so a key that only
128
+ * the second value has is ignored, which is why the second value may be a
129
+ * subtype of the first. Keys whose value in the first value is `undefined`
130
+ * are skipped, and arrays must have the same length. Supports an optional
96
131
  * exception filter to ignore specific keys during comparison. Useful for
97
132
  * validating API responses, data transformations, and object state changes.
98
133
  *
134
+ * Processing cost: The shared JSON walker traverses the first value and its
135
+ * corresponding second-value positions once, with recursion and accessor
136
+ * output proportional to visited structure; serialization occurs only on
137
+ * failure.
138
+ *
99
139
  * @example
100
- * ```typescript
101
140
  * // Basic equality
102
- * TestValidator.equals("response should match expected", expectedUser, actualUser);
141
+ * TestValidator.equals(
142
+ * "response should match expected",
143
+ * expectedUser,
144
+ * actualUser,
145
+ * );
103
146
  *
104
147
  * // Ignore timestamps in comparison
105
- * TestValidator.equals("user data should match", expectedUser, actualUser,
106
- * (key) => key === "updatedAt"
148
+ * TestValidator.equals(
149
+ * "user data should match",
150
+ * expectedUser,
151
+ * actualUser,
152
+ * (key) => key === "updatedAt",
107
153
  * );
108
154
  *
109
155
  * // Validate API response structure
110
- * TestValidator.equals("API response structure",
156
+ * TestValidator.equals(
157
+ * "API response structure",
158
+ * { id: 1, name: "John" },
111
159
  * { id: 1, name: "John" },
112
- * { id: 1, name: "John" }
113
160
  * );
114
161
  *
115
162
  * // Type-safe nullable comparisons
116
163
  * const nullableData: { name: string } | null = getData();
117
164
  * TestValidator.equals("nullable check", nullableData, null);
118
- * ```;
119
165
  *
120
166
  * @param title - Descriptive title used in error messages when values differ
121
167
  * @param X - The first value to compare
@@ -123,6 +169,10 @@ export namespace TestValidator {
123
169
  * @param exception - Optional filter function to exclude specific keys from
124
170
  * comparison
125
171
  * @throws Error with detailed diff information when values are not equal
172
+ * @evidence contracts/common.md#principled-implementation Both values are compared through `json_equal_to`, which walks the keys of the first value, so the second value may carry extra keys and the typing `Y extends X` states that; the differing accessors are listed in the error with both values serialized.
173
+ * @evidence contracts/common.md#clear-and-simple-design A thin wrapper over the shared comparison that adds the message.
174
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The exception filter is the caller's; the function contains no key name of its own.
175
+ * @evidence contracts/common.md#meaningful-documentation The comment states the JSON form, the key-walking rule with its consequence for extra keys, the array length rule, and the exception filter, with examples.
126
176
  */
127
177
  export function equals<X, Y extends X = X>(
128
178
  title: string,
@@ -145,27 +195,42 @@ export namespace TestValidator {
145
195
  * Validates deep inequality between two values using JSON comparison.
146
196
  *
147
197
  * Performs recursive comparison of objects and arrays to ensure they are NOT
148
- * equal. Supports an optional exception filter to ignore specific keys during
149
- * comparison. Useful for validating that data has changed, objects are
198
+ * equal, by the same rules as {@link equals}: only the keys of the first value
199
+ * are compared, so a key that only the second value has does not make them
200
+ * different. Supports an optional exception filter to ignore specific keys
201
+ * during comparison. Useful for validating that data has changed, objects are
150
202
  * different, or mutations have occurred.
151
203
  *
204
+ * Processing cost: The shared JSON walker visits the first value and
205
+ * corresponding positions once; a failure additionally serializes both values
206
+ * for diagnostics. Space follows visited structure and reported differences.
207
+ *
152
208
  * @example
153
- * ```typescript
154
209
  * // Basic inequality
155
- * TestValidator.notEquals("user should be different after update", originalUser, updatedUser);
210
+ * TestValidator.notEquals(
211
+ * "user should be different after update",
212
+ * originalUser,
213
+ * updatedUser,
214
+ * );
156
215
  *
157
216
  * // Ignore timestamps in comparison
158
- * TestValidator.notEquals("user data should differ", originalUser, modifiedUser,
159
- * (key) => key === "updatedAt"
217
+ * TestValidator.notEquals(
218
+ * "user data should differ",
219
+ * originalUser,
220
+ * modifiedUser,
221
+ * (key) => key === "updatedAt",
160
222
  * );
161
223
  *
162
224
  * // Validate state changes
163
- * TestValidator.notEquals("state should have changed", initialState, currentState);
225
+ * TestValidator.notEquals(
226
+ * "state should have changed",
227
+ * initialState,
228
+ * currentState,
229
+ * );
164
230
  *
165
231
  * // Type-safe nullable comparisons
166
232
  * const mutableData: { count: number } | null = getMutableData();
167
233
  * TestValidator.notEquals("should have changed", mutableData, null);
168
- * ```;
169
234
  *
170
235
  * @param title - Descriptive title used in error messages when values are
171
236
  * equal
@@ -174,6 +239,10 @@ export namespace TestValidator {
174
239
  * @param exception - Optional filter function to exclude specific keys from
175
240
  * comparison
176
241
  * @throws Error when values are equal (indicating validation failure)
242
+ * @evidence contracts/common.md#principled-implementation It runs the same comparison as `equals` and fails when the comparison finds no difference, so two values differing only by extra keys of the second are considered equal and fail.
243
+ * @evidence contracts/common.md#clear-and-simple-design A thin wrapper over the shared comparison with the opposite outcome.
244
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The exception filter is the caller's; the function contains no key name of its own.
245
+ * @evidence contracts/common.md#meaningful-documentation The comment states that the rules are those of `equals`, and gives examples.
177
246
  */
178
247
  export function notEquals<X, Y extends X = X>(
179
248
  title: string,
@@ -199,47 +268,61 @@ export namespace TestValidator {
199
268
  * successfully without throwing an error or rejecting, this validator will
200
269
  * throw an exception. Supports both synchronous and asynchronous functions.
201
270
  *
271
+ * Processing cost: The task is invoked once and its settlement is classified
272
+ * with constant bookkeeping; no retry or repeated execution occurs.
273
+ *
274
+ * Reuse: Each invocation must evaluate its supplied task or request to
275
+ * observe current behavior; prior results cannot replace these effectful
276
+ * checks.
277
+ *
278
+ * Resource ownership: The returned promise retains invocation-local state
279
+ * until settlement. The callback owns its external resources, and this
280
+ * validator provides no cancellation or timeout.
281
+ *
202
282
  * @example
203
- * ```typescript
204
283
  * // Synchronous error validation
205
- * TestValidator.error("should reject invalid email",
206
- * () => validateEmail("invalid-email")
284
+ * TestValidator.error("should reject invalid email", () =>
285
+ * validateEmail("invalid-email"),
207
286
  * );
208
287
  *
209
288
  * // Asynchronous error validation
210
- * await TestValidator.error("should reject unauthorized access",
211
- * async () => await api.functional.getSecretData()
289
+ * await TestValidator.error(
290
+ * "should reject unauthorized access",
291
+ * async () => await api.functional.getSecretData(),
212
292
  * );
213
293
  *
214
294
  * // Validate input validation
215
- * TestValidator.error("should throw on empty string",
216
- * () => processRequiredInput("")
295
+ * TestValidator.error("should throw on empty string", () =>
296
+ * processRequiredInput(""),
217
297
  * );
218
- * ```;
219
298
  *
220
299
  * @param title - Descriptive title used in error messages when no error
221
300
  * occurs
222
301
  * @param task - The function that should throw an error or reject
223
302
  * @returns Void or Promise<void> based on the input type
224
303
  * @throws Error when the task function does not throw an error or reject
304
+ * @evidence contracts/common.md#principled-implementation The task is run inside a `try` so that only its own throw counts, a promise is awaited and a rejection counts as the expected error, and a task that neither throws nor rejects raises the failure outside the `try`, so the failure cannot be swallowed.
305
+ * @evidence contracts/common.md#clear-and-simple-design One function with a synchronous and an asynchronous path selected by the returned value.
306
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Any error satisfies it, as documented, and no error type is special-cased.
307
+ * @evidence contracts/common.md#meaningful-documentation The comment states that an exception is required, the return type rule, and the thrown error, with examples.
225
308
  */
226
309
  export function error<T>(
227
310
  title: string,
228
311
  task: () => T,
229
312
  ): T extends Promise<any> ? Promise<void> : void {
230
313
  const message = () => `Bug on ${title}: exception must be thrown.`;
314
+ // only the task's own throw is caught: the failure below must escape
315
+ let output: T;
231
316
  try {
232
- const output: T = task();
233
- if (is_promise(output))
234
- return new Promise<void>((resolve, reject) =>
235
- output
236
- .catch(() => resolve())
237
- .then(() => reject(new Error(message()))),
238
- ) as any;
239
- else throw new Error(message());
317
+ output = task();
240
318
  } catch {
241
319
  return undefined as any;
242
320
  }
321
+ if (is_promise(output))
322
+ return new Promise<void>((resolve, reject) =>
323
+ output.catch(() => resolve()).then(() => reject(new Error(message()))),
324
+ ) as any;
325
+ throw new Error(message());
243
326
  }
244
327
 
245
328
  /**
@@ -249,23 +332,40 @@ export namespace TestValidator {
249
332
  * function throws an HttpError with one of the specified status codes. Useful
250
333
  * for testing API endpoints, authentication, and authorization logic.
251
334
  *
335
+ * Processing cost: The task is invoked once. Classification reads the thrown
336
+ * shape once and scans the expected status list until a match, using O(S)
337
+ * worst-case status comparisons and constant auxiliary state.
338
+ *
339
+ * Reuse: Each invocation must evaluate its supplied task or request to
340
+ * observe current behavior; prior results cannot replace these effectful
341
+ * checks.
342
+ *
343
+ * Resource ownership: The returned promise retains invocation-local state
344
+ * until settlement. The callback owns its external resources, and this
345
+ * validator provides no cancellation or timeout.
346
+ *
252
347
  * @example
253
- * ```typescript
254
348
  * // Validate 401 Unauthorized
255
- * await TestValidator.httpError("should return 401 for invalid token", 401,
256
- * async () => await api.functional.getProtectedResource("invalid-token")
349
+ * await TestValidator.httpError(
350
+ * "should return 401 for invalid token",
351
+ * 401,
352
+ * async () =>
353
+ * await api.functional.getProtectedResource("invalid-token"),
257
354
  * );
258
355
  *
259
356
  * // Validate multiple possible error codes
260
- * await TestValidator.httpError("should return client error", [400, 404, 422],
261
- * async () => await api.functional.updateNonexistentResource(data)
357
+ * await TestValidator.httpError(
358
+ * "should return client error",
359
+ * [400, 404, 422],
360
+ * async () => await api.functional.updateNonexistentResource(data),
262
361
  * );
263
362
  *
264
363
  * // Validate server errors
265
- * TestValidator.httpError("should handle server errors", [500, 502, 503],
266
- * () => callFaultyEndpoint()
364
+ * TestValidator.httpError(
365
+ * "should handle server errors",
366
+ * [500, 502, 503],
367
+ * () => callFaultyEndpoint(),
267
368
  * );
268
- * ```;
269
369
  *
270
370
  * @param title - Descriptive title used in error messages
271
371
  * @param status - Expected status code(s), can be a single number or array
@@ -273,6 +373,10 @@ export namespace TestValidator {
273
373
  * @returns Void or Promise<void> based on the input type
274
374
  * @throws Error when function doesn't throw HttpError or status code doesn't
275
375
  * match
376
+ * @evidence contracts/common.md#principled-implementation The thrown value must expose a constructor named `HttpError` and one expected numeric status, matched by name because generated SDKs define their own classes. Missing or throwing properties make classification fail instead of escaping the expectation. The returned promise chain propagates rejection-handler failures without leaving a separately bridged promise pending.
377
+ * @evidence contracts/common.md#clear-and-simple-design One predicate reads the thrown value once and is shared by the synchronous and asynchronous paths; asynchronous settlement uses the task's own promise chain.
378
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The check follows the documented error shape, not a specific SDK or status list.
379
+ * @evidence contracts/common.md#meaningful-documentation The comment documents the status argument, the two task kinds, and the thrown error, with examples.
276
380
  */
277
381
  export function httpError<T>(
278
382
  title: string,
@@ -288,29 +392,36 @@ export namespace TestValidator {
288
392
  : `Bug on ${title}: status code must be ${status.join(
289
393
  " or ",
290
394
  )}, but succeeded.`;
291
- const predicate = (exp: any): Error | null =>
292
- typeof exp === "object" &&
293
- exp.constructor.name === "HttpError" &&
294
- status.some((val) => val === exp.status)
395
+ const predicate = (exp: any): Error | null => {
396
+ let actual: number | undefined;
397
+ try {
398
+ if (
399
+ typeof exp === "object" &&
400
+ exp !== null &&
401
+ exp.constructor?.name === "HttpError"
402
+ ) {
403
+ const value: unknown = exp.status;
404
+ if (typeof value === "number") actual = value;
405
+ }
406
+ } catch {
407
+ // A thrown value with unreadable properties cannot establish an
408
+ // HTTP status; it fails the same expectation as any other value.
409
+ }
410
+ return actual !== undefined && status.some((val) => val === actual)
295
411
  ? null
296
- : new Error(
297
- message(
298
- typeof exp === "object" && exp.constructor.name === "HttpError"
299
- ? exp.status
300
- : undefined,
301
- ),
302
- );
412
+ : new Error(message(actual));
413
+ };
303
414
  try {
304
415
  const output: T = task();
305
416
  if (is_promise(output))
306
- return new Promise<void>((resolve, reject) =>
307
- output
308
- .catch((exp) => {
309
- const res: Error | null = predicate(exp);
310
- if (res) reject(res);
311
- else resolve();
312
- })
313
- .then(() => reject(new Error(message()))),
417
+ return output.then(
418
+ () => {
419
+ throw new Error(message());
420
+ },
421
+ (exp) => {
422
+ const res: Error | null = predicate(exp);
423
+ if (res) throw res;
424
+ },
314
425
  ) as any;
315
426
  else throw new Error(message());
316
427
  } catch (exp) {
@@ -328,28 +439,55 @@ export namespace TestValidator {
328
439
  * sequence. Commonly used for testing database queries, search results, and
329
440
  * any paginated data APIs.
330
441
  *
442
+ * A page holds the first entities, so only the common prefix of the two lists
443
+ * is compared. An empty result against expected entities, or the reverse,
444
+ * fails.
445
+ *
446
+ * Processing cost: Only the common prefix of P entities is sliced, converted
447
+ * to id strings and compared, giving O(P) entries and temporary storage;
448
+ * string conversion and comparison add their text costs.
449
+ *
331
450
  * @example
332
- * ```typescript
333
451
  * // Test article pagination
334
- * const expectedArticles = await db.articles.findAll({ order: 'created_at DESC' });
335
- * const actualArticles = await api.functional.getArticles({ page: 1, limit: 10 });
452
+ * const expectedArticles = await db.articles.findAll({
453
+ * order: "created_at DESC",
454
+ * });
455
+ * const actualArticles = await api.functional.getArticles({
456
+ * page: 1,
457
+ * limit: 10,
458
+ * });
336
459
  *
337
- * TestValidator.index("article pagination order", expectedArticles, actualArticles,
338
- * true // enable trace logging
460
+ * TestValidator.index(
461
+ * "article pagination order",
462
+ * expectedArticles,
463
+ * actualArticles,
464
+ * true, // enable trace logging
339
465
  * );
340
466
  *
341
467
  * // Test user search results
342
- * const manuallyFilteredUsers = allUsers.filter(u => u.name.includes("John"));
343
- * const apiSearchResults = await api.functional.searchUsers({ query: "John" });
468
+ * const manuallyFilteredUsers = allUsers.filter((u) =>
469
+ * u.name.includes("John"),
470
+ * );
471
+ * const apiSearchResults = await api.functional.searchUsers({
472
+ * query: "John",
473
+ * });
344
474
  *
345
- * TestValidator.index("user search results", manuallyFilteredUsers, apiSearchResults);
346
- * ```;
475
+ * TestValidator.index(
476
+ * "user search results",
477
+ * manuallyFilteredUsers,
478
+ * apiSearchResults,
479
+ * );
347
480
  *
348
481
  * @param title - Descriptive title used in error messages when order differs
349
482
  * @param expected - The expected entities in correct order
350
483
  * @param gotten - The actual entities returned by the API
351
484
  * @param trace - Optional flag to enable debug logging (default: false)
352
485
  * @throws Error when entity order differs between expected and actual results
486
+ * over their common prefix, or when exactly one of them is empty
487
+ * @evidence contracts/common.md#principled-implementation A page holds the first entities of the expected list, so the two id lists are compared over their common prefix, and one side being empty while the other is not is a difference because there is no prefix to compare; ids are compared as strings.
488
+ * @evidence contracts/common.md#clear-and-simple-design One function over the id lists produced by a private helper, with an optional trace flag.
489
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Only ids are compared, with no entity type or field named in the function.
490
+ * @evidence contracts/common.md#meaningful-documentation The comment states the prefix rule, the empty rule, the trace flag, and the thrown error, with examples.
353
491
  */
354
492
  export const index = <X extends IEntity<any>, Y extends X = X>(
355
493
  title: string,
@@ -357,12 +495,21 @@ export namespace TestValidator {
357
495
  gotten: Y[],
358
496
  trace: boolean = false,
359
497
  ): void => {
498
+ // a page holds the first entities, so only the common prefix is compared,
499
+ // but an empty side against a non-empty one has no prefix to prove
500
+ if ((expected.length === 0) !== (gotten.length === 0)) {
501
+ if (trace === true)
502
+ console.log({
503
+ expected: get_ids(expected),
504
+ gotten: get_ids(gotten),
505
+ });
506
+ throw new Error(
507
+ `Bug on ${title}: result of the index is different with manual aggregation.`,
508
+ );
509
+ }
360
510
  const length: number = Math.min(expected.length, gotten.length);
361
- expected = expected.slice(0, length);
362
- gotten = gotten.slice(0, length);
363
-
364
- const xIds: string[] = get_ids(expected).slice(0, length);
365
- const yIds: string[] = get_ids(gotten).slice(0, length);
511
+ const xIds: string[] = get_ids(expected.slice(0, length));
512
+ const yIds: string[] = get_ids(gotten.slice(0, length));
366
513
 
367
514
  const equals: boolean = xIds.every((x, i) => x === yIds[i]);
368
515
  if (equals === true) return;
@@ -385,15 +532,27 @@ export namespace TestValidator {
385
532
  * API, and compares results. Validates that search APIs return the correct
386
533
  * subset of data matching the search criteria.
387
534
  *
535
+ * Processing cost: For K sampled entities from N entries, sparse sampling
536
+ * costs O(K), each reference filter scans N entries and each returned page
537
+ * prefix is compared once. Calls are sequential and one sample result is live
538
+ * at a time.
539
+ *
540
+ * Reuse: Each invocation must evaluate its supplied task or request to
541
+ * observe current behavior; prior results cannot replace these effectful
542
+ * checks.
543
+ *
544
+ * Resource ownership: The returned promise retains invocation-local state
545
+ * until settlement. The callback owns its external resources, and this
546
+ * validator provides no cancellation or timeout.
547
+ *
388
548
  * @example
389
- * ```typescript
390
549
  * // Test article search functionality with exact matching
391
550
  * const allArticles = await db.articles.findAll();
392
551
  * const searchValidator = TestValidator.search(
393
552
  * "article search API",
394
553
  * (req) => api.searchArticles(req),
395
554
  * allArticles,
396
- * 5 // test with 5 random samples
555
+ * 5, // test with 5 random samples
397
556
  * );
398
557
  *
399
558
  * // Test exact match search
@@ -401,7 +560,7 @@ export namespace TestValidator {
401
560
  * fields: ["title"],
402
561
  * values: (article) => [article.title], // full title for exact match
403
562
  * filter: (article, [title]) => article.title === title, // exact match
404
- * request: ([title]) => ({ search: { title } })
563
+ * request: ([title]) => ({ search: { title } }),
405
564
  * });
406
565
  *
407
566
  * // Test partial match search with includes
@@ -409,7 +568,7 @@ export namespace TestValidator {
409
568
  * fields: ["content"],
410
569
  * values: (article) => [article.content.substring(0, 20)], // partial content
411
570
  * filter: (article, [keyword]) => article.content.includes(keyword),
412
- * request: ([keyword]) => ({ q: keyword })
571
+ * request: ([keyword]) => ({ q: keyword }),
413
572
  * });
414
573
  *
415
574
  * // Test multi-field search with exact matching
@@ -418,9 +577,8 @@ export namespace TestValidator {
418
577
  * values: (article) => [article.writer, article.title],
419
578
  * filter: (article, [writer, title]) =>
420
579
  * article.writer === writer && article.title === title,
421
- * request: ([writer, title]) => ({ search: { writer, title } })
580
+ * request: ([writer, title]) => ({ search: { writer, title } }),
422
581
  * });
423
- * ```;
424
582
  *
425
583
  * @param title - Descriptive title used in error messages when search fails
426
584
  * @param getter - API function that performs the search
@@ -428,6 +586,10 @@ export namespace TestValidator {
428
586
  * @param sampleCount - Number of random samples to test (default: 1)
429
587
  * @returns A function that accepts search configuration properties
430
588
  * @throws Error when API search results don't match manual filtering results
589
+ * @evidence contracts/common.md#principled-implementation A sample of entities is drawn, the search values are read from each, the expected set is computed by a manual filter over the whole dataset, the API is called with the request built from the values, and `index` compares the two lists, so the API is checked against a filter the test author wrote independently.
590
+ * @evidence contracts/common.md#clear-and-simple-design The function is curried so the dataset and the API are given once and each search configuration is run against them, and the comparison is delegated to `index`.
591
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The sample, values, filter, and request all come from the caller's configuration.
592
+ * @evidence contracts/common.md#meaningful-documentation The comment documents the steps, the parameters, and the thrown error, with examples.
431
593
  */
432
594
  export const search =
433
595
  <Entity extends IEntity<any>, Request>(
@@ -465,6 +627,10 @@ export namespace TestValidator {
465
627
  * @template Values - Tuple type representing the search values extracted from
466
628
  * entities
467
629
  * @template Request - Type of the API request object
630
+ * @evidence contracts/common.md#principled-implementation The record supplies what the check needs from a test author: the field names for the message and the three functions that extract values, filter, and build the request.
631
+ * @evidence contracts/common.md#clear-and-simple-design A four-member record with the functions as methods.
632
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts It is a type and adds no runtime behavior.
633
+ * @evidence contracts/common.md#meaningful-documentation The comment names the role of the record and each member documents itself.
468
634
  */
469
635
  export interface ISearchProps<
470
636
  Entity extends IEntity<any>,
@@ -479,6 +645,10 @@ export namespace TestValidator {
479
645
  *
480
646
  * @param entity - The entity to extract search values from
481
647
  * @returns Tuple of values used for searching
648
+ * @evidence contracts/common.md#principled-implementation The search values are read from a sampled entity, so the search is exercised with values that occur in the dataset.
649
+ * @evidence contracts/common.md#clear-and-simple-design One method with one entity argument.
650
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts It is caller code.
651
+ * @evidence contracts/common.md#meaningful-documentation The comment documents the parameter and the return value.
482
652
  */
483
653
  values(entity: Entity): Values;
484
654
 
@@ -488,6 +658,10 @@ export namespace TestValidator {
488
658
  * @param entity - Entity to test against criteria
489
659
  * @param values - Search values to match against
490
660
  * @returns True if entity matches the search criteria
661
+ * @evidence contracts/common.md#principled-implementation The predicate decides whether an entity matches the values, which is the manual reference against which the API is compared.
662
+ * @evidence contracts/common.md#clear-and-simple-design One predicate over an entity and the values.
663
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts It is caller code.
664
+ * @evidence contracts/common.md#meaningful-documentation The comment documents the parameters and the return value.
491
665
  */
492
666
  filter(entity: Entity, values: Values): boolean;
493
667
 
@@ -496,6 +670,10 @@ export namespace TestValidator {
496
670
  *
497
671
  * @param values - Search values to include in request
498
672
  * @returns Request object for the search API
673
+ * @evidence contracts/common.md#principled-implementation The function builds the API request from the values, so the same values drive the manual filter and the call.
674
+ * @evidence contracts/common.md#clear-and-simple-design One method from values to the request.
675
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts It is caller code.
676
+ * @evidence contracts/common.md#meaningful-documentation The comment documents the parameter and the return value.
499
677
  */
500
678
  request(values: Values): Request;
501
679
  }
@@ -508,15 +686,24 @@ export namespace TestValidator {
508
686
  * ascending/descending order, and optional filtering. Provides detailed error
509
687
  * reporting for sorting failures.
510
688
  *
689
+ * Processing cost: After one getter call, an optional filter scans N results
690
+ * and sortedness checks at most N minus one adjacent pairs. Signed field
691
+ * construction costs O(F); filter output costs O(N) space when requested.
692
+ *
693
+ * Reuse: Each invocation must evaluate its supplied task or request to
694
+ * observe current behavior; prior results cannot replace these effectful
695
+ * checks.
696
+ *
697
+ * Resource ownership: The returned promise retains invocation-local state
698
+ * until settlement. The callback owns its external resources, and this
699
+ * validator provides no cancellation or timeout.
700
+ *
511
701
  * @example
512
- * ```typescript
513
702
  * // Test single field sorting with GaffComparator
514
703
  * const sortValidator = TestValidator.sort(
515
704
  * "article sorting",
516
- * (sortable) => api.getArticles({ sort: sortable })
517
- * )("created_at")(
518
- * GaffComparator.dates((a) => a.created_at)
519
- * );
705
+ * (sortable) => api.getArticles({ sort: sortable }),
706
+ * )("created_at")(GaffComparator.dates((a) => a.created_at));
520
707
  *
521
708
  * await sortValidator("+"); // ascending
522
709
  * await sortValidator("-"); // descending
@@ -524,10 +711,10 @@ export namespace TestValidator {
524
711
  * // Test multi-field sorting with GaffComparator
525
712
  * const userSortValidator = TestValidator.sort(
526
713
  * "user sorting",
527
- * (sortable) => api.getUsers({ sort: sortable })
714
+ * (sortable) => api.getUsers({ sort: sortable }),
528
715
  * )("lastName", "firstName")(
529
716
  * GaffComparator.strings((user) => [user.lastName, user.firstName]),
530
- * (user) => user.isActive // only test active users
717
+ * (user) => user.isActive, // only test active users
531
718
  * );
532
719
  *
533
720
  * await userSortValidator("+", true); // ascending with trace logging
@@ -535,20 +722,24 @@ export namespace TestValidator {
535
722
  * // Custom comparator for complex logic
536
723
  * const customSortValidator = TestValidator.sort(
537
724
  * "custom sorting",
538
- * (sortable) => api.getProducts({ sort: sortable })
539
- * )("price", "rating")(
540
- * (a, b) => {
541
- * const priceDiff = a.price - b.price;
542
- * return priceDiff !== 0 ? priceDiff : b.rating - a.rating; // price asc, rating desc
543
- * }
544
- * );
545
- * ```;
725
+ * (sortable) => api.getProducts({ sort: sortable }),
726
+ * )(
727
+ * "price",
728
+ * "rating",
729
+ * )((a, b) => {
730
+ * const priceDiff = a.price - b.price;
731
+ * return priceDiff !== 0 ? priceDiff : b.rating - a.rating; // price asc, rating desc
732
+ * });
546
733
  *
547
734
  * @param title - Descriptive title used in error messages when sorting fails
548
735
  * @param getter - API function that fetches sorted data
549
736
  * @returns A currying function chain: field names, comparator, then direction
550
737
  * @throws Error when API results are not properly sorted according to
551
738
  * specification
739
+ * @evidence contracts/common.md#principled-implementation The API is called with the signed field names, its result is optionally filtered, and the comparator (reversed for the descending direction) is applied to adjacent pairs, so any adjacent pair out of order fails; checking adjacent pairs suffices for a total preorder.
740
+ * @evidence contracts/common.md#clear-and-simple-design The function is curried in three steps so the request, the comparator, and the direction can be given at the moments each is known.
741
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The order is judged by the caller's comparator, with no field or direction special-cased.
742
+ * @evidence contracts/common.md#meaningful-documentation The comment documents each stage, the parameters, and the thrown error, with examples.
552
743
  */
553
744
  export const sort =
554
745
  <
@@ -594,14 +785,18 @@ export namespace TestValidator {
594
785
  * prefixed with '+' for ascending order or '-' for descending order.
595
786
  *
596
787
  * @example
597
- * ```typescript
598
- * type UserSortable = TestValidator.Sortable<"name" | "email" | "created_at">;
788
+ * type UserSortable = TestValidator.Sortable<
789
+ * "name" | "email" | "created_at"
790
+ * >;
599
791
  * // Results in: Array<"-name" | "+name" | "-email" | "+email" | "-created_at" | "+created_at">
600
792
  *
601
793
  * const userSort: UserSortable = ["+name", "-created_at"];
602
- * ```;
603
794
  *
604
795
  * @template Literal - String literal type representing available field names
796
+ * @evidence contracts/common.md#principled-implementation A sortable is an array of field names with a leading `+` or `-`, the notation the API under test receives.
797
+ * @evidence contracts/common.md#clear-and-simple-design A single template-literal type.
798
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts It is a type and adds no runtime behavior.
799
+ * @evidence contracts/common.md#meaningful-documentation The comment states the notation.
605
800
  */
606
801
  export type Sortable<Literal extends string> = Array<
607
802
  `-${Literal}` | `+${Literal}`