as-test 1.2.0 → 1.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Change Log
2
2
 
3
+ ## 2026-05-22 - v1.3.0
4
+
5
+ ### `features` config array + arbitrary `--enable` passthrough
6
+
7
+ - feat: new top-level `"features": ["try-as", "simd"]` array in `as-test.config.json` (and per-mode override). `try-as` is the only as-test-internal feature today and wires the `try-as/transform` + `AS_TEST_TRY_AS=1` build flags as before. Any other name in the array is passed through to `asc` as `--enable <name>` — so `"simd"`, `"threads"`, `"reference-types"`, `"gc"`, etc. now work without hand-editing `buildOptions.args`.
8
+ - feat: `ast test|run|build --enable <name>` and `--disable <name>` now accept arbitrary feature names. CLI flags override the config array (CLI `--disable simd` removes a config-listed feature; CLI `--enable simd` adds it). The known special-case `coverage` flag still routes to the dedicated top-level `coverage` config field rather than the features array. Both flags accept comma-separated lists too: `--enable try-as,coverage,simd` and `--disable try-as,coverage`. The same syntax is honored by `ast init`.
9
+ - feat: `ast init` interactive prompt now includes a multi-select "Features" step (↑/↓ to move, space to toggle, enter to confirm) with `coverage` and `try-as` options. `--enable`/`--disable` flags on `ast init` skip the prompt with explicit selections. The generated `as-test.config.json` writes `coverage` at the top level and `features` as a string array; when try-as is selected, `try-as` is also added to `devDependencies`.
10
+ - chore: schema validation rejects malformed shapes (object form, non-string array entries) with a fix hint pointing at the new shape.
11
+
12
+ ### `mode()` registration gate + `AS_TEST_MODE_NAME`
13
+
14
+ - feat: new `mode(matchers: string[], fn: () => void)` helper in the `as-test` runtime, plus an `AS_TEST_MODE_NAME: string` compile-time constant. Use `mode(["node:bindings"], () => { ... })` to gate suite/test registrations on the active mode name. Matcher semantics: positive entries OR; `!name` entries exclude; `[]` is a no-op; positive + negative entries combine as "any positive matches AND no negative matches."
15
+ - feat: build-side wiring — `build-core.ts` injects `AS_TEST_MODE_NAME=<mode>` into the asc env per-mode build, and the as-test transform rewrites the initializer of `AS_TEST_MODE_NAME` in `assembly/src/mode.ts` (an `afterParse` AST patch) so the value is baked into the wasm at compile time. Default value when no mode is selected is `"default"`.
16
+ - chore: bundled `assembly/__tests__/mode.spec.ts` exercises positive/negative/mixed matchers, empty matchers, and per-mode counter behaviour end-to-end against the project's own `node:bindings` / `node:wasi` modes.
17
+
18
+ ### Tooling
19
+
20
+ - chore: `build:transform` now runs `prettier -w ./transform/` after the TypeScript build so generated output stays formatted.
21
+
3
22
  ## 2026-05-20 - v1.2.0
4
23
 
5
24
  ### Directory-preserving artifact layout
@@ -180,6 +180,14 @@
180
180
  ],
181
181
  "default": false
182
182
  },
183
+ "features": {
184
+ "type": "array",
185
+ "description": "Enabled feature names. \"try-as\" wires up the try-as transform; any other name is passed through to asc as --enable <name> (e.g. \"simd\", \"threads\", \"reference-types\"). CLI --enable/--disable flags override this list. Coverage has its own top-level \"coverage\" field.",
186
+ "items": {
187
+ "type": "string"
188
+ },
189
+ "default": []
190
+ },
183
191
  "env": {
184
192
  "description": "Environment variables injected when building/running. Accepts a .env file path, an array of KEY=value strings, or an object map.",
185
193
  "oneOf": [
@@ -400,6 +408,13 @@
400
408
  }
401
409
  ]
402
410
  },
411
+ "features": {
412
+ "type": "array",
413
+ "description": "Mode-specific feature list. Replaces the base list entirely when set.",
414
+ "items": {
415
+ "type": "string"
416
+ }
417
+ },
403
418
  "fuzz": {
404
419
  "type": "object",
405
420
  "additionalProperties": false,
@@ -13,18 +13,17 @@ export class CoverPoint {
13
13
 
14
14
  export class Coverage {
15
15
  public all: CoverPoint[] = [];
16
- public allIndex: Map<string, i32> = new Map<string, i32>();
17
- public hashes: Map<string, CoverPoint> = new Map<string, CoverPoint>();
18
- public points: i32 = 0;
16
+ public byHash: Map<string, CoverPoint> = new Map<string, CoverPoint>();
17
+ public uncovered: i32 = 0;
19
18
  static SN: Coverage = new Coverage();
20
19
  }
21
20
 
22
21
  export function __REGISTER(point: CoverPoint): void {
23
- if (Coverage.SN.allIndex.has(point.hash)) return;
24
- Coverage.SN.points++;
25
- Coverage.SN.allIndex.set(point.hash, Coverage.SN.all.length);
26
- Coverage.SN.all.push(point);
27
- Coverage.SN.hashes.set(point.hash, point);
22
+ const cov = Coverage.SN;
23
+ if (cov.byHash.has(point.hash)) return;
24
+ cov.byHash.set(point.hash, point);
25
+ cov.all.push(point);
26
+ cov.uncovered++;
28
27
  }
29
28
 
30
29
  export function __REGISTER_RAW(
@@ -38,7 +37,8 @@ export function __REGISTER_RAW(
38
37
  scopeName: string = "",
39
38
  depth: i32 = 0,
40
39
  ): void {
41
- if (Coverage.SN.allIndex.has(hash)) return;
40
+ const cov = Coverage.SN;
41
+ if (cov.byHash.has(hash)) return;
42
42
  const point = new CoverPoint();
43
43
  point.file = file;
44
44
  point.hash = hash;
@@ -49,32 +49,28 @@ export function __REGISTER_RAW(
49
49
  point.scopeKind = scopeKind;
50
50
  point.scopeName = scopeName;
51
51
  point.depth = depth;
52
- Coverage.SN.points++;
53
- Coverage.SN.allIndex.set(hash, Coverage.SN.all.length);
54
- Coverage.SN.all.push(point);
55
- Coverage.SN.hashes.set(hash, point);
52
+ cov.byHash.set(hash, point);
53
+ cov.all.push(point);
54
+ cov.uncovered++;
56
55
  }
57
56
 
57
+ // Hot path: invoked at every instrumented point. After first hit, subsequent
58
+ // hits short-circuit on `executed` before any writes.
58
59
  export function __COVER(hash: string): void {
59
- if (Coverage.SN.allIndex.has(hash)) {
60
- const index = Coverage.SN.allIndex.get(hash);
61
- if (index < Coverage.SN.all.length) {
62
- unchecked(Coverage.SN.all[index]).executed = true;
63
- }
64
- }
65
- if (Coverage.SN.hashes.has(hash)) Coverage.SN.hashes.delete(hash);
66
- }
67
-
68
- export function __HASHES(): Map<string, CoverPoint> {
69
- return Coverage.SN.hashes;
60
+ const cov = Coverage.SN;
61
+ if (!cov.byHash.has(hash)) return;
62
+ const point = cov.byHash.get(hash);
63
+ if (point.executed) return;
64
+ point.executed = true;
65
+ cov.uncovered--;
70
66
  }
71
67
 
72
68
  export function __POINTS(): i32 {
73
- return Coverage.SN.points;
69
+ return Coverage.SN.all.length;
74
70
  }
75
71
 
76
72
  export function __UNCOVERED(): i32 {
77
- return Coverage.SN.hashes.size;
73
+ return Coverage.SN.uncovered;
78
74
  }
79
75
 
80
76
  export function __ALL_POINTS(): CoverPoint[] {
package/assembly/index.ts CHANGED
@@ -752,3 +752,5 @@ function formatTime(time: f64): string {
752
752
 
753
753
  return `${_us}μs`;
754
754
  }
755
+
756
+ export { mode, AS_TEST_MODE_NAME } from "./src/mode";
@@ -57,6 +57,41 @@ export class Expectation<T> extends Tests {
57
57
  return this;
58
58
  }
59
59
 
60
+ /**
61
+ * Asserts that a custom predicate holds. Accepts either a bool or a
62
+ * `() => bool` lambda. Useful when the verdict isn't expressible via the
63
+ * built-in matchers — e.g. delegating to a hand-written comparator.
64
+ *
65
+ * expect(x).where(x > 0 && x < 10);
66
+ * expect(actual).where((): bool => deepEqual(GLOBAL_A, GLOBAL_B));
67
+ *
68
+ * Chains with other matchers as an independent assertion:
69
+ *
70
+ * expect(7).toBe(7).where((): bool => isFresh());
71
+ *
72
+ * Note: AssemblyScript does not implement closures, so the lambda cannot
73
+ * capture local variables from the enclosing scope. Use the bool form when
74
+ * the predicate references locals, or refer to module-level values from
75
+ * inside the lambda.
76
+ */
77
+ where<W>(predicate: W, message: string = ""): Expectation<T> {
78
+ let passed: bool;
79
+ if (isFunction<W>()) {
80
+ passed = (predicate as () => bool)();
81
+ } else {
82
+ // @ts-ignore: W is a bool-compatible primitive in this branch
83
+ passed = predicate as bool;
84
+ }
85
+ this._resolve(
86
+ passed,
87
+ "where",
88
+ q(passed ? "true" : "false"),
89
+ q("true"),
90
+ message,
91
+ );
92
+ return this;
93
+ }
94
+
60
95
  private _resolve(
61
96
  passed: bool,
62
97
  instr: string,
@@ -74,14 +109,26 @@ export class Expectation<T> extends Tests {
74
109
  return;
75
110
  }
76
111
  const isFail = this._not ? passed : !passed;
77
- this.verdict = isFail ? "fail" : "ok";
78
- this.instr = instr;
79
- this.left = left;
80
- this.right = right;
81
112
  const resolvedMessage = message.length ? message : this._message;
82
- this.message = isFail ? resolvedMessage : "";
113
+ // When matchers chain, later ones must not overwrite an earlier failure's
114
+ // recorded state — otherwise a passing matcher after a failed one would
115
+ // flip the suite's verdict back to "ok". Each matcher still fires its own
116
+ // IPC failure independently below.
117
+ if (this.verdict != "fail") {
118
+ this.verdict = isFail ? "fail" : "ok";
119
+ this.instr = instr;
120
+ this.left = left;
121
+ this.right = right;
122
+ this.message = isFail ? resolvedMessage : "";
123
+ }
83
124
  if (isFail) {
84
- sendAssertionFailure(this._snapshotKey, instr, left, right, this.message);
125
+ sendAssertionFailure(
126
+ this._snapshotKey,
127
+ instr,
128
+ left,
129
+ right,
130
+ resolvedMessage,
131
+ );
85
132
  // @ts-ignore
86
133
  if (isDefined(AS_TEST_FUZZ)) {
87
134
  // @ts-ignore
@@ -95,7 +142,7 @@ export class Expectation<T> extends Tests {
95
142
  // @ts-ignore
96
143
  __as_test_fuzz_failure_right = right;
97
144
  // @ts-ignore
98
- __as_test_fuzz_failure_message = this.message;
145
+ __as_test_fuzz_failure_message = resolvedMessage;
99
146
  }
100
147
  }
101
148
  }
@@ -105,7 +152,7 @@ export class Expectation<T> extends Tests {
105
152
  /**
106
153
  * Tests if a == null
107
154
  */
108
- toBeNull(message: string = ""): void {
155
+ toBeNull(message: string = ""): Expectation<T> {
109
156
  const passed =
110
157
  (isNullable<T>() && changetype<usize>(this._left) == 0) ||
111
158
  (isInteger<T>() && nameof<T>() == "usize" && this._left == 0);
@@ -121,12 +168,13 @@ export class Expectation<T> extends Tests {
121
168
  ),
122
169
  message,
123
170
  );
171
+ return this;
124
172
  }
125
173
 
126
174
  /**
127
175
  * Tests if a > b
128
176
  */
129
- toBeGreaterThan(value: T, message: string = ""): void {
177
+ toBeGreaterThan(value: T, message: string = ""): Expectation<T> {
130
178
  if (!isInteger<T>() && !isFloat<T>())
131
179
  ERROR("toBeGreaterThan() can only be used on number types!");
132
180
 
@@ -144,12 +192,13 @@ export class Expectation<T> extends Tests {
144
192
  ),
145
193
  message,
146
194
  );
195
+ return this;
147
196
  }
148
197
 
149
198
  /**
150
199
  * Tests if a >= b
151
200
  */
152
- toBeGreaterOrEqualTo(value: T, message: string = ""): void {
201
+ toBeGreaterOrEqualTo(value: T, message: string = ""): Expectation<T> {
153
202
  if (!isInteger<T>() && !isFloat<T>())
154
203
  ERROR("toBeGreaterOrEqualTo() can only be used on number types!");
155
204
 
@@ -167,12 +216,13 @@ export class Expectation<T> extends Tests {
167
216
  ),
168
217
  message,
169
218
  );
219
+ return this;
170
220
  }
171
221
 
172
222
  /**
173
223
  * Tests if a < b
174
224
  */
175
- toBeLessThan(value: T, message: string = ""): void {
225
+ toBeLessThan(value: T, message: string = ""): Expectation<T> {
176
226
  if (!isInteger<T>() && !isFloat<T>())
177
227
  ERROR("toBeLessThan() can only be used on number types!");
178
228
 
@@ -190,12 +240,13 @@ export class Expectation<T> extends Tests {
190
240
  ),
191
241
  message,
192
242
  );
243
+ return this;
193
244
  }
194
245
 
195
246
  /**
196
247
  * Tests if a <= b
197
248
  */
198
- toBeLessThanOrEqualTo(value: T, message: string = ""): void {
249
+ toBeLessThanOrEqualTo(value: T, message: string = ""): Expectation<T> {
199
250
  if (!isInteger<T>() && !isFloat<T>())
200
251
  ERROR("toBeLessThanOrEqualTo() can only be used on number types!");
201
252
 
@@ -213,12 +264,13 @@ export class Expectation<T> extends Tests {
213
264
  ),
214
265
  message,
215
266
  );
267
+ return this;
216
268
  }
217
269
 
218
270
  /**
219
271
  * Tests if a is string
220
272
  */
221
- toBeString(message: string = ""): void {
273
+ toBeString(message: string = ""): Expectation<T> {
222
274
  this._resolve(
223
275
  isString<T>(),
224
276
  "toBeString",
@@ -226,12 +278,13 @@ export class Expectation<T> extends Tests {
226
278
  q("string"),
227
279
  message,
228
280
  );
281
+ return this;
229
282
  }
230
283
 
231
284
  /**
232
285
  * Tests if a is boolean
233
286
  */
234
- toBeBoolean(message: string = ""): void {
287
+ toBeBoolean(message: string = ""): Expectation<T> {
235
288
  this._resolve(
236
289
  isBoolean<T>(),
237
290
  "toBeBoolean",
@@ -239,12 +292,13 @@ export class Expectation<T> extends Tests {
239
292
  q("boolean"),
240
293
  message,
241
294
  );
295
+ return this;
242
296
  }
243
297
 
244
298
  /**
245
299
  * Tests if a is array
246
300
  */
247
- toBeArray(message: string = ""): void {
301
+ toBeArray(message: string = ""): Expectation<T> {
248
302
  this._resolve(
249
303
  isArray<T>(),
250
304
  "toBeArray",
@@ -252,12 +306,13 @@ export class Expectation<T> extends Tests {
252
306
  q("Array<any>"),
253
307
  message,
254
308
  );
309
+ return this;
255
310
  }
256
311
 
257
312
  /**
258
313
  * Tests if a is number
259
314
  */
260
- toBeNumber(message: string = ""): void {
315
+ toBeNumber(message: string = ""): Expectation<T> {
261
316
  this._resolve(
262
317
  isFloat<T>() || isInteger<T>(),
263
318
  "toBeNumber",
@@ -265,12 +320,13 @@ export class Expectation<T> extends Tests {
265
320
  q("number"),
266
321
  message,
267
322
  );
323
+ return this;
268
324
  }
269
325
 
270
326
  /**
271
327
  * Tests if a is integer
272
328
  */
273
- toBeInteger(message: string = ""): void {
329
+ toBeInteger(message: string = ""): Expectation<T> {
274
330
  this._resolve(
275
331
  isInteger<T>(),
276
332
  "toBeInteger",
@@ -278,12 +334,13 @@ export class Expectation<T> extends Tests {
278
334
  q("integer"),
279
335
  message,
280
336
  );
337
+ return this;
281
338
  }
282
339
 
283
340
  /**
284
341
  * Tests if a is float
285
342
  */
286
- toBeFloat(message: string = ""): void {
343
+ toBeFloat(message: string = ""): Expectation<T> {
287
344
  this._resolve(
288
345
  isFloat<T>(),
289
346
  "toBeFloat",
@@ -291,21 +348,23 @@ export class Expectation<T> extends Tests {
291
348
  q("float"),
292
349
  message,
293
350
  );
351
+ return this;
294
352
  }
295
353
 
296
354
  /**
297
355
  * Tests if a is finite
298
356
  */
299
- toBeFinite(message: string = ""): void {
357
+ toBeFinite(message: string = ""): Expectation<T> {
300
358
  // @ts-ignore
301
359
  const passed = (isFloat<T>() || isInteger<T>()) && isFinite(this._left);
302
360
  this._resolve(passed, "toBeFinite", q("Infinity"), q("Finite"), message);
361
+ return this;
303
362
  }
304
363
 
305
364
  /**
306
365
  * Tests if a value is truthy
307
366
  */
308
- toBeTruthy(message: string = ""): void {
367
+ toBeTruthy(message: string = ""): Expectation<T> {
309
368
  this._resolve(
310
369
  isTruthy<T>(this._left),
311
370
  "toBeTruthy",
@@ -313,12 +372,13 @@ export class Expectation<T> extends Tests {
313
372
  q("truthy"),
314
373
  message,
315
374
  );
375
+ return this;
316
376
  }
317
377
 
318
378
  /**
319
379
  * Tests if a value is falsy
320
380
  */
321
- toBeFalsy(message: string = ""): void {
381
+ toBeFalsy(message: string = ""): Expectation<T> {
322
382
  this._resolve(
323
383
  !isTruthy<T>(this._left),
324
384
  "toBeFalsy",
@@ -326,12 +386,14 @@ export class Expectation<T> extends Tests {
326
386
  q("falsy"),
327
387
  message,
328
388
  );
389
+ return this;
329
390
  }
330
391
 
331
392
  /**
332
393
  * Tests if a floating-point number is close to expected
333
394
  */
334
- toBeCloseTo(expected: T, precision: i32 = 2, message: string = ""): void {
395
+ // prettier-ignore
396
+ toBeCloseTo(expected: T, precision: i32 = 2, message: string = ""): Expectation<T> {
335
397
  if (!isFloat<T>() && !isInteger<T>())
336
398
  ERROR("toBeCloseTo() can only be used on number types!");
337
399
  const factor = Math.pow(10, precision as f64);
@@ -344,12 +406,13 @@ export class Expectation<T> extends Tests {
344
406
  visualize<T>(expected),
345
407
  message,
346
408
  );
409
+ return this;
347
410
  }
348
411
 
349
412
  /**
350
413
  * Tests if a string contains substring
351
414
  */
352
- toMatch(value: string, message: string = ""): void {
415
+ toMatch(value: string, message: string = ""): Expectation<T> {
353
416
  if (!isString<T>()) ERROR("toMatch() can only be used on string types!");
354
417
  // @ts-ignore
355
418
  const passed = this._left.indexOf(value) >= 0;
@@ -361,36 +424,39 @@ export class Expectation<T> extends Tests {
361
424
  q(value),
362
425
  message,
363
426
  );
427
+ return this;
364
428
  }
365
429
 
366
430
  /**
367
431
  * Tests if a string starts with the provided prefix.
368
432
  */
369
- toStartWith(value: string, message: string = ""): void {
433
+ toStartWith(value: string, message: string = ""): Expectation<T> {
370
434
  if (!isString<T>())
371
435
  ERROR("toStartWith() can only be used on string types!");
372
436
  // @ts-ignore
373
437
  const left = this._left as string;
374
438
  const passed = left.indexOf(value) == 0;
375
439
  this._resolve(passed, "toStartWith", q(left), q(value), message);
440
+ return this;
376
441
  }
377
442
 
378
443
  /**
379
444
  * Tests if a string ends with the provided suffix.
380
445
  */
381
- toEndWith(value: string, message: string = ""): void {
446
+ toEndWith(value: string, message: string = ""): Expectation<T> {
382
447
  if (!isString<T>()) ERROR("toEndWith() can only be used on string types!");
383
448
  // @ts-ignore
384
449
  const left = this._left as string;
385
450
  const idx = left.lastIndexOf(value);
386
451
  const passed = idx >= 0 && idx + value.length == left.length;
387
452
  this._resolve(passed, "toEndWith", q(left), q(value), message);
453
+ return this;
388
454
  }
389
455
 
390
456
  /**
391
457
  * Tests if an array has length x
392
458
  */
393
- toHaveLength(value: i32, message: string = ""): void {
459
+ toHaveLength(value: i32, message: string = ""): Expectation<T> {
394
460
  // @ts-ignore
395
461
  const leftLen = this._left.length as i32;
396
462
  // @ts-ignore
@@ -402,13 +468,14 @@ export class Expectation<T> extends Tests {
402
468
  value.toString(),
403
469
  message,
404
470
  );
471
+ return this;
405
472
  }
406
473
 
407
474
  /**
408
475
  * Tests if an array or string contains a value
409
476
  */
410
477
  // @ts-ignore
411
- toContain(value: valueof<T>, message: string = ""): void {
478
+ toContain(value: valueof<T>, message: string = ""): Expectation<T> {
412
479
  if (isString<T>()) {
413
480
  // @ts-ignore
414
481
  const left = this._left as string;
@@ -416,7 +483,7 @@ export class Expectation<T> extends Tests {
416
483
  const needle = value as string;
417
484
  const passed = left.indexOf(needle) >= 0;
418
485
  this._resolve(passed, "toContain", q(left), q(needle), message);
419
- return;
486
+ return this;
420
487
  }
421
488
 
422
489
  if (isArray<T>()) {
@@ -429,24 +496,25 @@ export class Expectation<T> extends Tests {
429
496
  JSON.stringify<valueof<T>>(value),
430
497
  message,
431
498
  );
432
- return;
499
+ return this;
433
500
  }
434
501
 
435
502
  ERROR("toContain() can only be used on string and array types!");
503
+ return this;
436
504
  }
437
505
 
438
506
  /**
439
507
  * Alias for toContain().
440
508
  */
441
509
  // @ts-ignore
442
- toContains(value: valueof<T>, message: string = ""): void {
443
- this.toContain(value, message);
510
+ toContains(value: valueof<T>, message: string = ""): Expectation<T> {
511
+ return this.toContain(value, message);
444
512
  }
445
513
 
446
514
  /**
447
515
  * Tests if serialized value matches stored snapshot.
448
516
  */
449
- toMatchSnapshot(name: string = "", message: string = ""): void {
517
+ toMatchSnapshot(name: string = "", message: string = ""): Expectation<T> {
450
518
  let key = name.length
451
519
  ? namedSnapshotKey(this._snapshotKey, name)
452
520
  : nextUnnamedSnapshotKey(this._snapshotKey);
@@ -454,6 +522,7 @@ export class Expectation<T> extends Tests {
454
522
  const actual = JSON.stringify<T>(this._left);
455
523
  const res = snapshotAssert(key, actual);
456
524
  this._resolve(res.ok, "toMatchSnapshot", actual, res.expected, message);
525
+ return this;
457
526
  }
458
527
 
459
528
  /**
@@ -466,7 +535,7 @@ export class Expectation<T> extends Tests {
466
535
  * `.toThrow()` on a non-function value records a failure that explains the
467
536
  * usage.
468
537
  */
469
- toThrow(message: string = ""): void {
538
+ toThrow(message: string = ""): Expectation<T> {
470
539
  // @ts-ignore
471
540
  if (!isDefined(AS_TEST_TRY_AS)) {
472
541
  if (!warnedToThrowDisabled) {
@@ -476,7 +545,7 @@ export class Expectation<T> extends Tests {
476
545
  warnedToThrowDisabled = true;
477
546
  }
478
547
  this._resolve(true, "toThrow", q("disabled"), q("disabled"), message);
479
- return;
548
+ return this;
480
549
  }
481
550
 
482
551
  if (!isFunction<T>()) {
@@ -489,7 +558,7 @@ export class Expectation<T> extends Tests {
489
558
  ? message
490
559
  : "toThrow() requires a function: expect((): void => { ... }).toThrow()",
491
560
  );
492
- return;
561
+ return this;
493
562
  }
494
563
 
495
564
  // try-as rewrites the throw inside the callback to bump
@@ -515,12 +584,13 @@ export class Expectation<T> extends Tests {
515
584
  q("throws"),
516
585
  message,
517
586
  );
587
+ return this;
518
588
  }
519
589
 
520
590
  /**
521
591
  * Tests for equality
522
592
  */
523
- toBe(equals: T, message: string = ""): void {
593
+ toBe(equals: T, message: string = ""): Expectation<T> {
524
594
  const passed = this._left === equals;
525
595
 
526
596
  this._resolve(
@@ -530,12 +600,13 @@ export class Expectation<T> extends Tests {
530
600
  JSON.stringify<T>(equals),
531
601
  message,
532
602
  );
603
+ return this;
533
604
  }
534
605
 
535
606
  /**
536
607
  * Tests for deep equality
537
608
  */
538
- toEqual(equals: T, message: string = ""): void {
609
+ toEqual(equals: T, message: string = ""): Expectation<T> {
539
610
  const passed = valueEquals<T>(this._left, equals, false);
540
611
  this._resolve(
541
612
  passed,
@@ -544,12 +615,13 @@ export class Expectation<T> extends Tests {
544
615
  JSON.stringify<T>(equals),
545
616
  message,
546
617
  );
618
+ return this;
547
619
  }
548
620
 
549
621
  /**
550
622
  * Tests for strict deep equality
551
623
  */
552
- toStrictEqual(equals: T, message: string = ""): void {
624
+ toStrictEqual(equals: T, message: string = ""): Expectation<T> {
553
625
  const passed = valueEquals<T>(this._left, equals, true);
554
626
  this._resolve(
555
627
  passed,
@@ -558,6 +630,7 @@ export class Expectation<T> extends Tests {
558
630
  JSON.stringify<T>(equals),
559
631
  message,
560
632
  );
633
+ return this;
561
634
  }
562
635
  }
563
636
 
@@ -0,0 +1,55 @@
1
+ // The current mode name is patched at build time by as-test's transform
2
+ // (transform/src/index.ts) using the AS_TEST_MODE_NAME env var that
3
+ // build-core.ts injects per-mode build. When unset, defaults to "default".
4
+ export const AS_TEST_MODE_NAME: string = "default";
5
+
6
+ function modeMatches(matchers: string[], current: string): bool {
7
+ if (matchers.length == 0) return false;
8
+ let sawPositive = false;
9
+ let positiveHit = false;
10
+ for (let i = 0; i < matchers.length; i++) {
11
+ const m = matchers[i];
12
+ if (m.length == 0) continue;
13
+ if (m.charCodeAt(0) == 33 /* '!' */) {
14
+ if (m.substring(1) == current) return false;
15
+ } else {
16
+ sawPositive = true;
17
+ if (m == current) positiveHit = true;
18
+ }
19
+ }
20
+ return sawPositive ? positiveHit : true;
21
+ }
22
+
23
+ /**
24
+ * Gate a block of suite/test registrations on the current execution mode.
25
+ *
26
+ * The current mode is the name under `modes.<name>` in `as-test.config.json`
27
+ * (or `"default"` when running the base config). Comparisons are by exact
28
+ * string match.
29
+ *
30
+ * Matcher semantics:
31
+ * - `["a"]` runs when the current mode equals `"a"`.
32
+ * - `["a", "b"]` runs when the current mode is in `{a, b}` (positive OR).
33
+ * - `["!a"]` runs when the current mode is NOT `"a"`.
34
+ * - `["!a", "!b"]` runs when the current mode is neither `{a, b}` (AND).
35
+ * - Mixed `["a", "!b"]` runs when any positive matches AND no negative does.
36
+ * - `[]` and empty entries are skipped (no-op).
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * mode(["simd"], () => {
41
+ * describe("vectorised path", () => { ... });
42
+ * });
43
+ *
44
+ * mode(["simd", "swar"], () => {
45
+ * describe("fast paths", () => { ... });
46
+ * });
47
+ *
48
+ * mode(["!naive"], () => {
49
+ * describe("anything but naive", () => { ... });
50
+ * });
51
+ * ```
52
+ */
53
+ export function mode(matchers: string[], fn: () => void): void {
54
+ if (modeMatches(matchers, AS_TEST_MODE_NAME)) fn();
55
+ }