as-test 0.5.2 → 0.5.4

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/bin/index.js CHANGED
@@ -1,15 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import chalk from "chalk";
3
- import { build } from "./build.js";
4
- import { createRunReporter, run } from "./run.js";
5
- import { init } from "./init.js";
6
- import { getCliVersion, loadConfig, resolveModeNames } from "./util.js";
3
+ import { build } from "./commands/build.js";
4
+ import { createRunReporter, run } from "./commands/run.js";
5
+ import { executeBuildCommand } from "./commands/build.js";
6
+ import { executeRunCommand } from "./commands/run.js";
7
+ import { executeTestCommand } from "./commands/test.js";
8
+ import { executeInitCommand } from "./commands/init.js";
9
+ import { executeDoctorCommand } from "./commands/doctor.js";
10
+ import { applyMode, getCliVersion, loadConfig, resolveModeNames, } from "./util.js";
7
11
  import * as path from "path";
8
12
  import { glob } from "glob";
9
13
  const _args = process.argv.slice(2);
10
14
  const flags = [];
11
15
  const args = [];
12
- const COMMANDS = ["run", "build", "test", "init"];
16
+ const COMMANDS = ["run", "build", "test", "init", "doctor"];
13
17
  const version = getCliVersion();
14
18
  const configPath = resolveConfigPath(_args);
15
19
  const selectedModes = resolveModeNames(_args);
@@ -28,39 +32,69 @@ if (!args.length) {
28
32
  }
29
33
  }
30
34
  else if (COMMANDS.includes(args[0])) {
31
- const command = args.shift();
32
- const commandArgs = resolveCommandArgs(_args, command ?? "");
33
- const runFlags = {
34
- snapshot: !flags.includes("--no-snapshot"),
35
- updateSnapshots: flags.includes("--update-snapshots"),
36
- clean: flags.includes("--clean"),
37
- showCoverage: flags.includes("--show-coverage"),
38
- verbose: flags.includes("--verbose"),
39
- };
40
- if (command === "build") {
41
- runBuildModes(configPath, commandArgs, selectedModes).catch((error) => {
42
- printCliError(error);
43
- process.exit(1);
44
- });
45
- }
46
- else if (command === "run") {
47
- runRuntimeModes(runFlags, configPath, commandArgs, selectedModes).catch((error) => {
48
- printCliError(error);
49
- process.exit(1);
50
- });
51
- }
52
- else if (command === "test") {
53
- runTestModes(runFlags, configPath, commandArgs, selectedModes).catch((error) => {
54
- printCliError(error);
55
- process.exit(1);
56
- });
35
+ try {
36
+ const command = args.shift();
37
+ const normalizedCommand = command ?? "";
38
+ if (shouldShowCommandHelp(_args, normalizedCommand)) {
39
+ printCommandHelp(normalizedCommand);
40
+ }
41
+ else if (command === "build") {
42
+ executeBuildCommand(_args, configPath, selectedModes, {
43
+ resolveCommandArgs,
44
+ resolveListFlags,
45
+ resolveFeatureToggles,
46
+ resolveExecutionModes,
47
+ listExecutionPlan,
48
+ runBuildModes,
49
+ }).catch((error) => {
50
+ printCliError(error);
51
+ process.exit(1);
52
+ });
53
+ }
54
+ else if (command === "run") {
55
+ executeRunCommand(_args, flags, configPath, selectedModes, {
56
+ resolveCommandArgs,
57
+ resolveListFlags,
58
+ resolveFeatureToggles,
59
+ resolveExecutionModes,
60
+ listExecutionPlan,
61
+ runRuntimeModes,
62
+ }).catch((error) => {
63
+ printCliError(error);
64
+ process.exit(1);
65
+ });
66
+ }
67
+ else if (command === "test") {
68
+ executeTestCommand(_args, flags, configPath, selectedModes, {
69
+ resolveCommandArgs,
70
+ resolveListFlags,
71
+ resolveFeatureToggles,
72
+ resolveExecutionModes,
73
+ listExecutionPlan,
74
+ runTestModes,
75
+ }).catch((error) => {
76
+ printCliError(error);
77
+ process.exit(1);
78
+ });
79
+ }
80
+ else if (command === "init") {
81
+ executeInitCommand(_args, {
82
+ resolveCommandTokens,
83
+ }).catch((error) => {
84
+ printCliError(error);
85
+ process.exit(1);
86
+ });
87
+ }
88
+ else if (command === "doctor") {
89
+ executeDoctorCommand(configPath, selectedModes).catch((error) => {
90
+ printCliError(error);
91
+ process.exit(1);
92
+ });
93
+ }
57
94
  }
58
- else if (command === "init") {
59
- const commandTokens = resolveCommandTokens(_args, command ?? "");
60
- init(commandTokens).catch((error) => {
61
- printCliError(error);
62
- process.exit(1);
63
- });
95
+ catch (error) {
96
+ printCliError(error);
97
+ process.exit(1);
64
98
  }
65
99
  }
66
100
  else {
@@ -111,6 +145,12 @@ function info() {
111
145
  chalk.dim("<./dir>") +
112
146
  " " +
113
147
  "Initialize an empty testing template");
148
+ console.log(" " +
149
+ chalk.bold.magentaBright("doctor") +
150
+ " " +
151
+ chalk.dim("<--mode x>") +
152
+ " " +
153
+ "Validate environment/config/runtime setup");
114
154
  console.log("");
115
155
  console.log(chalk.bold("Flags:"));
116
156
  console.log(" " +
@@ -137,6 +177,14 @@ function info() {
137
177
  chalk.bold.blue("--show-coverage") +
138
178
  " " +
139
179
  "Print all coverage points with line:column refs");
180
+ console.log(" " +
181
+ chalk.bold.blue("--enable <feature>") +
182
+ " " +
183
+ "Enable as-test feature (coverage|try-as)");
184
+ console.log(" " +
185
+ chalk.bold.blue("--disable <feature>") +
186
+ " " +
187
+ "Disable as-test feature (coverage|try-as)");
140
188
  console.log(" " +
141
189
  chalk.bold.blue("--verbose") +
142
190
  " " +
@@ -145,6 +193,15 @@ function info() {
145
193
  chalk.bold.blue("--reporter <name|path>") +
146
194
  " " +
147
195
  "Use built-in reporter (default|tap) or custom module path");
196
+ console.log(" " +
197
+ chalk.bold.blue("--list") +
198
+ " " +
199
+ "Preview resolved files/modes/artifacts without running");
200
+ console.log(" " +
201
+ chalk.bold.blue("--list-modes") +
202
+ " " +
203
+ "Preview configured and selected mode names");
204
+ console.log(" " + chalk.bold.blue("--help, -h") + " Show help");
148
205
  console.log("");
149
206
  console.log(chalk.dim("If your using this, consider dropping a star, it would help a lot!") + "\n");
150
207
  console.log("View the repo: " +
@@ -154,6 +211,102 @@ function info() {
154
211
  // chalk.blue("https://docs.jairus.dev/as-test"),
155
212
  // );
156
213
  }
214
+ function isHelpFlag(value) {
215
+ return value == "--help" || value == "-h";
216
+ }
217
+ function shouldShowCommandHelp(rawArgs, command) {
218
+ if (!command.length)
219
+ return false;
220
+ const commandIndex = rawArgs.indexOf(command);
221
+ if (commandIndex == -1)
222
+ return false;
223
+ for (let i = 0; i < rawArgs.length; i++) {
224
+ if (i == commandIndex)
225
+ continue;
226
+ if (!isHelpFlag(rawArgs[i]))
227
+ continue;
228
+ return true;
229
+ }
230
+ return false;
231
+ }
232
+ function printCommandHelp(command) {
233
+ if (command == "build") {
234
+ process.stdout.write(chalk.bold("Usage: ast build [selectors...] [flags]\n\n"));
235
+ process.stdout.write("Compile selected specs into wasm artifacts.\n\n");
236
+ process.stdout.write(chalk.bold("Flags:\n"));
237
+ process.stdout.write(" --config <path> Use a specific config file\n");
238
+ process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
239
+ process.stdout.write(" --enable <feature> Enable build feature (coverage|try-as)\n");
240
+ process.stdout.write(" --disable <feature> Disable build feature (coverage|try-as)\n");
241
+ process.stdout.write(" --list Preview resolved files/artifacts without building\n");
242
+ process.stdout.write(" --list-modes Preview configured and selected mode names\n");
243
+ process.stdout.write(" --help, -h Show this help\n");
244
+ return;
245
+ }
246
+ if (command == "run") {
247
+ process.stdout.write(chalk.bold("Usage: ast run [selectors...] [flags]\n\n"));
248
+ process.stdout.write("Run compiled specs with the configured runtime.\n\n");
249
+ process.stdout.write(chalk.bold("Flags:\n"));
250
+ process.stdout.write(" --config <path> Use a specific config file\n");
251
+ process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
252
+ process.stdout.write(" --update-snapshots Create/update snapshot files on mismatch\n");
253
+ process.stdout.write(" --no-snapshot Disable snapshot assertions for this run\n");
254
+ process.stdout.write(" --show-coverage Print uncovered coverage point details\n");
255
+ process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
256
+ process.stdout.write(" --disable <feature> Disable feature (coverage|try-as)\n");
257
+ process.stdout.write(" --reporter <name|path> Use built-in reporter (default|tap) or custom module path\n");
258
+ process.stdout.write(" --tap Shortcut for --reporter tap\n");
259
+ process.stdout.write(" --verbose Keep expanded suite/test lines and live updates\n");
260
+ process.stdout.write(" --clean Disable in-place TTY updates; print final lines only\n");
261
+ process.stdout.write(" --list Preview resolved files/artifacts/runtime without running\n");
262
+ process.stdout.write(" --list-modes Preview configured and selected mode names\n");
263
+ process.stdout.write(" --help, -h Show this help\n");
264
+ return;
265
+ }
266
+ if (command == "test") {
267
+ process.stdout.write(chalk.bold("Usage: ast test [selectors...] [flags]\n\n"));
268
+ process.stdout.write("Build selected specs, run them, and print a final summary.\n\n");
269
+ process.stdout.write(chalk.bold("Flags:\n"));
270
+ process.stdout.write(" --config <path> Use a specific config file\n");
271
+ process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
272
+ process.stdout.write(" --update-snapshots Create/update snapshot files on mismatch\n");
273
+ process.stdout.write(" --no-snapshot Disable snapshot assertions for this run\n");
274
+ process.stdout.write(" --show-coverage Print uncovered coverage point details\n");
275
+ process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
276
+ process.stdout.write(" --disable <feature> Disable feature (coverage|try-as)\n");
277
+ process.stdout.write(" --reporter <name|path> Use built-in reporter (default|tap) or custom module path\n");
278
+ process.stdout.write(" --tap Shortcut for --reporter tap\n");
279
+ process.stdout.write(" --verbose Keep expanded suite/test lines and live updates\n");
280
+ process.stdout.write(" --clean Disable in-place TTY updates; print final lines only\n");
281
+ process.stdout.write(" --list Preview resolved files/artifacts/runtime without running\n");
282
+ process.stdout.write(" --list-modes Preview configured and selected mode names\n");
283
+ process.stdout.write(" --help, -h Show this help\n");
284
+ return;
285
+ }
286
+ if (command == "init") {
287
+ process.stdout.write(chalk.bold("Usage: ast init [dir] [flags]\n\n"));
288
+ process.stdout.write("Initialize as-test config, default runners, and example specs.\n\n");
289
+ process.stdout.write(chalk.bold("Flags:\n"));
290
+ process.stdout.write(" --target <wasi|bindings> Set build target\n");
291
+ process.stdout.write(" --example <minimal|full|none> Set example template\n");
292
+ process.stdout.write(" --install Install dependencies after scaffolding\n");
293
+ process.stdout.write(" --yes, -y Non-interactive setup with defaults\n");
294
+ process.stdout.write(" --force Overwrite managed files\n");
295
+ process.stdout.write(" --dir <path> Target output directory\n");
296
+ process.stdout.write(" --help, -h Show this help\n");
297
+ return;
298
+ }
299
+ if (command == "doctor") {
300
+ process.stdout.write(chalk.bold("Usage: ast doctor [flags]\n\n"));
301
+ process.stdout.write("Validate config, dependencies, runtime command, and spec discovery.\n\n");
302
+ process.stdout.write(chalk.bold("Flags:\n"));
303
+ process.stdout.write(" --config <path> Use a specific config file\n");
304
+ process.stdout.write(" --mode <name[,name...]> Run checks for one or multiple named modes\n");
305
+ process.stdout.write(" --help, -h Show this help\n");
306
+ return;
307
+ }
308
+ info();
309
+ }
157
310
  function resolveConfigPath(rawArgs) {
158
311
  for (let i = 0; i < rawArgs.length; i++) {
159
312
  const arg = rawArgs[i];
@@ -208,6 +361,13 @@ function resolveCommandArgs(rawArgs, command) {
208
361
  if (arg == "--tap") {
209
362
  continue;
210
363
  }
364
+ if (arg == "--enable" || arg == "--disable") {
365
+ i++;
366
+ continue;
367
+ }
368
+ if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) {
369
+ continue;
370
+ }
211
371
  if (arg.startsWith("-")) {
212
372
  continue;
213
373
  }
@@ -215,6 +375,73 @@ function resolveCommandArgs(rawArgs, command) {
215
375
  }
216
376
  return values;
217
377
  }
378
+ function resolveFeatureToggles(rawArgs, command) {
379
+ if (command !== "build" && command !== "run" && command !== "test")
380
+ return {};
381
+ const out = {};
382
+ let seenCommand = false;
383
+ for (let i = 0; i < rawArgs.length; i++) {
384
+ const arg = rawArgs[i];
385
+ if (!seenCommand) {
386
+ if (arg == command)
387
+ seenCommand = true;
388
+ continue;
389
+ }
390
+ if (arg == "--enable" || arg == "--disable") {
391
+ const enabled = arg == "--enable";
392
+ const next = rawArgs[i + 1];
393
+ if (next && !next.startsWith("-")) {
394
+ applyFeatureToggle(out, next, enabled);
395
+ i++;
396
+ }
397
+ continue;
398
+ }
399
+ if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) {
400
+ const enabled = arg.startsWith("--enable=");
401
+ const eq = arg.indexOf("=");
402
+ const value = arg.slice(eq + 1).trim();
403
+ if (value.length) {
404
+ applyFeatureToggle(out, value, enabled);
405
+ }
406
+ }
407
+ }
408
+ return out;
409
+ }
410
+ function resolveListFlags(rawArgs, command) {
411
+ const out = {
412
+ list: false,
413
+ listModes: false,
414
+ };
415
+ if (command !== "build" && command !== "run" && command !== "test") {
416
+ return out;
417
+ }
418
+ let seenCommand = false;
419
+ for (let i = 0; i < rawArgs.length; i++) {
420
+ const arg = rawArgs[i];
421
+ if (!seenCommand) {
422
+ if (arg == command)
423
+ seenCommand = true;
424
+ continue;
425
+ }
426
+ if (arg == "--list")
427
+ out.list = true;
428
+ if (arg == "--list-modes")
429
+ out.listModes = true;
430
+ }
431
+ return out;
432
+ }
433
+ function applyFeatureToggle(out, rawFeature, enabled) {
434
+ const key = rawFeature.trim().toLowerCase();
435
+ if (key == "coverage") {
436
+ out.coverage = enabled;
437
+ return;
438
+ }
439
+ if (key == "try-as" || key == "try_as" || key == "tryas") {
440
+ out.tryAs = enabled;
441
+ return;
442
+ }
443
+ throw new Error(`unknown feature "${rawFeature}". Supported features: coverage, try-as`);
444
+ }
218
445
  function resolveCommandTokens(rawArgs, command) {
219
446
  const values = [];
220
447
  let seenCommand = false;
@@ -229,13 +456,10 @@ function resolveCommandTokens(rawArgs, command) {
229
456
  }
230
457
  return values;
231
458
  }
232
- async function runTestSequential(runFlags, configPath, selectors, modeName) {
459
+ async function runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, modeName) {
233
460
  const files = await resolveSelectedFiles(configPath, selectors);
234
461
  if (!files.length) {
235
- const scope = selectors.length > 0
236
- ? selectors.join(", ")
237
- : "configured input patterns";
238
- throw new Error(`No test files matched: ${scope}`);
462
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
239
463
  }
240
464
  const reporterSession = await createRunReporter(configPath, undefined, modeName);
241
465
  const reporter = reporterSession.reporter;
@@ -249,9 +473,10 @@ async function runTestSequential(runFlags, configPath, selectors, modeName) {
249
473
  });
250
474
  const results = [];
251
475
  let failed = false;
476
+ const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
252
477
  for (const file of files) {
253
- await build(configPath, [file], modeName);
254
- const artifactKey = path.basename(file).replace(/[^a-zA-Z0-9._-]/g, "_");
478
+ await build(configPath, [file], modeName, buildFeatureToggles);
479
+ const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
255
480
  const result = await run(runFlags, configPath, [file], false, {
256
481
  reporter,
257
482
  emitRunStart: false,
@@ -265,6 +490,7 @@ async function runTestSequential(runFlags, configPath, selectors, modeName) {
265
490
  failed = true;
266
491
  }
267
492
  const summary = aggregateRunResults(results);
493
+ summary.stats = applyConfiguredFileTotalToStats(summary.stats, fileSummaryTotal);
268
494
  reporter.onRunComplete?.({
269
495
  clean: runFlags.clean,
270
496
  snapshotEnabled,
@@ -273,51 +499,474 @@ async function runTestSequential(runFlags, configPath, selectors, modeName) {
273
499
  coverageSummary: summary.coverageSummary,
274
500
  stats: summary.stats,
275
501
  reports: summary.reports,
502
+ modeSummary: buildSingleModeSummary(summary.stats, summary.snapshotSummary, modeSummaryTotal),
276
503
  });
277
504
  return failed;
278
505
  }
279
- async function runBuildModes(configPath, selectors, modes) {
280
- const targets = modes.length ? modes : [undefined];
281
- for (const modeName of targets) {
282
- await build(configPath, selectors, modeName);
506
+ async function runBuildModes(configPath, selectors, modes, buildFeatureToggles) {
507
+ for (const modeName of modes) {
508
+ await build(configPath, selectors, modeName, buildFeatureToggles);
283
509
  }
284
510
  }
285
511
  async function runRuntimeModes(runFlags, configPath, selectors, modes) {
286
- const targets = modes.length ? modes : [undefined];
512
+ const modeSummaryTotal = resolveConfiguredModeTotal(configPath);
513
+ const fileSummaryTotal = await resolveConfiguredFileTotal(configPath);
514
+ if (modes.length > 1) {
515
+ const failed = await runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal);
516
+ process.exit(failed ? 1 : 0);
517
+ return;
518
+ }
287
519
  let failed = false;
288
- for (const modeName of targets) {
520
+ for (const modeName of modes) {
289
521
  const result = await run(runFlags, configPath, selectors, false, {
290
522
  modeName,
523
+ modeSummaryTotal,
524
+ modeSummaryExecuted: 1,
525
+ fileSummaryTotal,
291
526
  });
292
527
  if (result.failed)
293
528
  failed = true;
294
529
  }
295
530
  process.exit(failed ? 1 : 0);
296
531
  }
297
- async function runTestModes(runFlags, configPath, selectors, modes) {
298
- const targets = modes.length ? modes : [undefined];
532
+ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal) {
533
+ const files = await resolveSelectedFiles(configPath, selectors);
534
+ if (!files.length) {
535
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
536
+ }
537
+ const reporterSession = await createRunReporter(configPath);
538
+ const reporter = reporterSession.reporter;
539
+ const snapshotEnabled = runFlags.snapshot !== false;
540
+ reporter.onRunStart?.({
541
+ runtimeName: reporterSession.runtimeName,
542
+ clean: runFlags.clean,
543
+ verbose: runFlags.verbose,
544
+ snapshotEnabled,
545
+ updateSnapshots: runFlags.updateSnapshots,
546
+ });
547
+ const silentReporter = {};
548
+ const allResults = [];
549
+ const modeLabels = modes.map((modeName) => modeName ?? "default");
550
+ const showPerModeTimes = Boolean(runFlags.verbose);
551
+ const liveMatrix = reporterSession.reporterKind == "default" && canRewriteStdout();
552
+ const modeState = modes.map(() => ({
553
+ failed: false,
554
+ passed: false,
555
+ }));
556
+ const fileState = files.map(() => ({
557
+ failed: false,
558
+ passed: false,
559
+ }));
560
+ const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
561
+ for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
562
+ const file = files[fileIndex];
563
+ const fileName = path.basename(file);
564
+ const fileResults = [];
565
+ const modeTimes = modes.map(() => "...");
566
+ if (liveMatrix) {
567
+ renderMatrixLiveLine(fileName, modeLabels, modeTimes, showPerModeTimes);
568
+ }
569
+ for (let i = 0; i < modes.length; i++) {
570
+ const modeName = modes[i];
571
+ try {
572
+ const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
573
+ const result = await run(runFlags, configPath, [file], false, {
574
+ reporter: silentReporter,
575
+ reporterKind: "default",
576
+ emitRunStart: false,
577
+ emitRunComplete: false,
578
+ logFileName: `run.${artifactKey}.log.json`,
579
+ coverageFileName: `coverage.${artifactKey}.log.json`,
580
+ modeName,
581
+ });
582
+ modeTimes[i] = formatMatrixModeTime(result.stats.time);
583
+ if (liveMatrix) {
584
+ renderMatrixLiveLine(fileName, modeLabels, modeTimes, showPerModeTimes);
585
+ }
586
+ if (result.failed) {
587
+ modeState[i].failed = true;
588
+ }
589
+ else if (result.stats.passedFiles > 0) {
590
+ modeState[i].passed = true;
591
+ }
592
+ fileResults.push(result);
593
+ allResults.push(result);
594
+ }
595
+ catch (error) {
596
+ clearLiveLine();
597
+ throw error;
598
+ }
599
+ }
600
+ renderMatrixFileResult(fileName, modeLabels, fileResults, modeTimes, liveMatrix, showPerModeTimes);
601
+ const verdict = resolveMatrixVerdict(fileResults);
602
+ if (verdict == "fail") {
603
+ fileState[fileIndex].failed = true;
604
+ }
605
+ else if (verdict == "ok") {
606
+ fileState[fileIndex].passed = true;
607
+ }
608
+ }
609
+ const summary = aggregateRunResults(allResults);
610
+ summary.stats = applyMatrixFileSummaryToStats(summary.stats, fileState, fileSummaryTotal);
611
+ reporter.onRunComplete?.({
612
+ clean: runFlags.clean,
613
+ snapshotEnabled,
614
+ showCoverage: runFlags.showCoverage,
615
+ snapshotSummary: summary.snapshotSummary,
616
+ coverageSummary: summary.coverageSummary,
617
+ stats: summary.stats,
618
+ reports: summary.reports,
619
+ modeSummary: buildModeSummary(modeState, modeSummaryTotal),
620
+ });
621
+ return allResults.some((result) => result.failed);
622
+ }
623
+ async function runTestModes(runFlags, configPath, selectors, modes, buildFeatureToggles) {
624
+ const modeSummaryTotal = resolveConfiguredModeTotal(configPath);
625
+ const fileSummaryTotal = await resolveConfiguredFileTotal(configPath);
626
+ if (modes.length > 1) {
627
+ const failed = await runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal);
628
+ process.exit(failed ? 1 : 0);
629
+ return;
630
+ }
299
631
  let failed = false;
300
- for (const modeName of targets) {
301
- const modeFailed = await runTestSequential(runFlags, configPath, selectors, modeName);
632
+ for (const modeName of modes) {
633
+ const modeFailed = await runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, modeName);
302
634
  if (modeFailed)
303
635
  failed = true;
304
636
  }
305
637
  process.exit(failed ? 1 : 0);
306
638
  }
307
- async function resolveSelectedFiles(configPath, selectors) {
639
+ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal) {
640
+ const files = await resolveSelectedFiles(configPath, selectors);
641
+ if (!files.length) {
642
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
643
+ }
644
+ const reporterSession = await createRunReporter(configPath);
645
+ const reporter = reporterSession.reporter;
646
+ const snapshotEnabled = runFlags.snapshot !== false;
647
+ reporter.onRunStart?.({
648
+ runtimeName: reporterSession.runtimeName,
649
+ clean: runFlags.clean,
650
+ verbose: runFlags.verbose,
651
+ snapshotEnabled,
652
+ updateSnapshots: runFlags.updateSnapshots,
653
+ });
654
+ const silentReporter = {};
655
+ const allResults = [];
656
+ const modeLabels = modes.map((modeName) => modeName ?? "default");
657
+ const showPerModeTimes = Boolean(runFlags.verbose);
658
+ const liveMatrix = reporterSession.reporterKind == "default" && canRewriteStdout();
659
+ const modeState = modes.map(() => ({
660
+ failed: false,
661
+ passed: false,
662
+ }));
663
+ const fileState = files.map(() => ({
664
+ failed: false,
665
+ passed: false,
666
+ }));
667
+ const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
668
+ for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
669
+ const file = files[fileIndex];
670
+ const fileName = path.basename(file);
671
+ const fileResults = [];
672
+ const modeTimes = modes.map(() => "...");
673
+ if (liveMatrix) {
674
+ renderMatrixLiveLine(fileName, modeLabels, modeTimes, showPerModeTimes);
675
+ }
676
+ for (let i = 0; i < modes.length; i++) {
677
+ const modeName = modes[i];
678
+ try {
679
+ await build(configPath, [file], modeName, buildFeatureToggles);
680
+ const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
681
+ const result = await run(runFlags, configPath, [file], false, {
682
+ reporter: silentReporter,
683
+ reporterKind: "default",
684
+ emitRunStart: false,
685
+ emitRunComplete: false,
686
+ logFileName: `test.${artifactKey}.log.json`,
687
+ coverageFileName: `coverage.${artifactKey}.log.json`,
688
+ modeName,
689
+ });
690
+ modeTimes[i] = formatMatrixModeTime(result.stats.time);
691
+ if (liveMatrix) {
692
+ renderMatrixLiveLine(fileName, modeLabels, modeTimes, showPerModeTimes);
693
+ }
694
+ if (result.failed) {
695
+ modeState[i].failed = true;
696
+ }
697
+ else if (result.stats.passedFiles > 0) {
698
+ modeState[i].passed = true;
699
+ }
700
+ fileResults.push(result);
701
+ allResults.push(result);
702
+ }
703
+ catch (error) {
704
+ clearLiveLine();
705
+ throw error;
706
+ }
707
+ }
708
+ renderMatrixFileResult(fileName, modeLabels, fileResults, modeTimes, liveMatrix, showPerModeTimes);
709
+ const verdict = resolveMatrixVerdict(fileResults);
710
+ if (verdict == "fail") {
711
+ fileState[fileIndex].failed = true;
712
+ }
713
+ else if (verdict == "ok") {
714
+ fileState[fileIndex].passed = true;
715
+ }
716
+ }
717
+ const summary = aggregateRunResults(allResults);
718
+ summary.stats = applyMatrixFileSummaryToStats(summary.stats, fileState, fileSummaryTotal);
719
+ reporter.onRunComplete?.({
720
+ clean: runFlags.clean,
721
+ snapshotEnabled,
722
+ showCoverage: runFlags.showCoverage,
723
+ snapshotSummary: summary.snapshotSummary,
724
+ coverageSummary: summary.coverageSummary,
725
+ stats: summary.stats,
726
+ reports: summary.reports,
727
+ modeSummary: buildModeSummary(modeState, modeSummaryTotal),
728
+ });
729
+ return allResults.some((result) => result.failed);
730
+ }
731
+ function renderMatrixFileResult(file, modes, results, modeTimes, liveMatrix, showPerModeTimes) {
732
+ const verdict = resolveMatrixVerdict(results);
733
+ const badge = verdict == "fail"
734
+ ? chalk.bgRed.white(" FAIL ")
735
+ : verdict == "ok"
736
+ ? chalk.bgGreenBright.black(" PASS ")
737
+ : chalk.bgBlackBright.white(" SKIP ");
738
+ const avg = formatMatrixAverageTime(results);
739
+ const timingText = showPerModeTimes ? modeTimes.join(",") : avg;
740
+ const suffix = showPerModeTimes
741
+ ? ` ${chalk.dim(`(${modes.join(",")})`)}`
742
+ : "";
743
+ const line = `${badge} ${file} ${chalk.dim(timingText)}${suffix}`;
744
+ if (liveMatrix)
745
+ clearLiveLine();
746
+ process.stdout.write(line + "\n");
747
+ }
748
+ function resolveMatrixVerdict(results) {
749
+ if (results.some((result) => result.failed))
750
+ return "fail";
751
+ const hasPass = results.some((result) => result.stats.passedFiles > 0);
752
+ if (hasPass)
753
+ return "ok";
754
+ return "skip";
755
+ }
756
+ function canRewriteStdout() {
757
+ return Boolean(process.stdout.isTTY);
758
+ }
759
+ function clearLiveLine() {
760
+ if (!canRewriteStdout())
761
+ return;
762
+ process.stdout.write("\r\x1b[2K");
763
+ }
764
+ function renderMatrixLiveLine(file, modes, modeTimes, showPerModeTimes) {
765
+ if (!canRewriteStdout())
766
+ return;
767
+ const timingText = showPerModeTimes ? modeTimes.join(",") : "...";
768
+ const suffix = showPerModeTimes
769
+ ? ` ${chalk.dim(`(${modes.join(",")})`)}`
770
+ : "";
771
+ const line = `${chalk.bgBlackBright.white(" .... ")} ${file} ${chalk.dim(timingText)}${suffix}`;
772
+ process.stdout.write(`\r\x1b[2K${line}`);
773
+ }
774
+ function formatMatrixModeTime(ms) {
775
+ const safeMs = Number.isFinite(ms) ? Math.max(0, ms) : 0;
776
+ return `${safeMs.toFixed(1)}ms`;
777
+ }
778
+ function formatMatrixAverageTime(results) {
779
+ if (!results.length)
780
+ return "0.0ms";
781
+ let total = 0;
782
+ for (const result of results) {
783
+ total += Number.isFinite(result.stats.time)
784
+ ? Math.max(0, result.stats.time)
785
+ : 0;
786
+ }
787
+ return `${(total / results.length).toFixed(1)}ms`;
788
+ }
789
+ function buildModeSummary(modeState, totalModes) {
790
+ const total = Math.max(totalModes, modeState.length, 1);
791
+ let skipped = Math.max(0, total - modeState.length);
792
+ let failed = 0;
793
+ for (const mode of modeState) {
794
+ if (mode.failed) {
795
+ failed++;
796
+ }
797
+ else if (!mode.passed) {
798
+ skipped++;
799
+ }
800
+ }
801
+ return {
802
+ failed,
803
+ skipped,
804
+ total,
805
+ };
806
+ }
807
+ function buildSingleModeSummary(stats, snapshotSummary, totalModes) {
808
+ const total = Math.max(totalModes, 1);
809
+ const failed = stats.failedFiles > 0 || snapshotSummary.failed > 0 ? 1 : 0;
810
+ const skippedInExecuted = failed ? 0 : stats.passedFiles > 0 ? 0 : 1;
811
+ return {
812
+ failed,
813
+ skipped: Math.max(0, total - 1) + skippedInExecuted,
814
+ total,
815
+ };
816
+ }
817
+ function applyConfiguredFileTotalToStats(stats, fileSummaryTotal) {
818
+ const total = Math.max(fileSummaryTotal, 0);
819
+ const executed = stats.failedFiles + stats.passedFiles + stats.skippedFiles;
820
+ const unexecuted = Math.max(0, total - executed);
821
+ return {
822
+ ...stats,
823
+ skippedFiles: stats.skippedFiles + unexecuted,
824
+ };
825
+ }
826
+ function applyMatrixFileSummaryToStats(stats, fileState, fileSummaryTotal) {
827
+ let failedFiles = 0;
828
+ let passedFiles = 0;
829
+ let skippedFiles = 0;
830
+ for (const file of fileState) {
831
+ if (file.failed)
832
+ failedFiles++;
833
+ else if (file.passed)
834
+ passedFiles++;
835
+ else
836
+ skippedFiles++;
837
+ }
838
+ const total = Math.max(fileSummaryTotal, fileState.length, 0);
839
+ const unexecuted = Math.max(0, total - fileState.length);
840
+ return {
841
+ ...stats,
842
+ failedFiles,
843
+ passedFiles,
844
+ skippedFiles: skippedFiles + unexecuted,
845
+ };
846
+ }
847
+ function resolveConfiguredModeTotal(configPath) {
308
848
  const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
309
- const config = loadConfig(resolvedConfigPath, true);
849
+ const config = loadConfig(resolvedConfigPath, false);
850
+ const configuredModes = Object.keys(config.modes).length;
851
+ return configuredModes || 1;
852
+ }
853
+ async function resolveConfiguredFileTotal(configPath) {
854
+ const files = await resolveSelectedFiles(configPath, []);
855
+ return files.length;
856
+ }
857
+ function resolveExecutionModes(configPath, selectedModes) {
858
+ if (selectedModes.length)
859
+ return selectedModes;
860
+ const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
861
+ const config = loadConfig(resolvedConfigPath, false);
862
+ const configuredModes = Object.keys(config.modes);
863
+ if (!configuredModes.length)
864
+ return [undefined];
865
+ return configuredModes;
866
+ }
867
+ async function resolveSelectedFiles(configPath, selectors, warn = true) {
868
+ const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
869
+ const config = loadConfig(resolvedConfigPath, warn);
310
870
  const patterns = resolveInputPatterns(config.input, selectors);
311
871
  const matches = await glob(patterns);
312
872
  const specs = matches.filter((file) => file.endsWith(".spec.ts"));
313
873
  return [...new Set(specs)].sort((a, b) => a.localeCompare(b));
314
874
  }
875
+ async function buildNoTestFilesMatchedError(configPath, selectors) {
876
+ const scope = selectors.length > 0 ? selectors.join(", ") : "configured input patterns";
877
+ const lines = [`No test files matched: ${scope}`];
878
+ const configuredFiles = await resolveSelectedFiles(configPath, [], false);
879
+ if (!selectors.length) {
880
+ lines.push('No specs were discovered from configured input patterns. Check "input" in config or run "ast doctor".');
881
+ return new Error(lines.join("\n"));
882
+ }
883
+ const suggestions = suggestClosestSuites(selectors, configuredFiles);
884
+ if (suggestions.length) {
885
+ lines.push(`Closest suite names: ${suggestions.join(", ")}`);
886
+ }
887
+ if (configuredFiles.length) {
888
+ const sample = configuredFiles
889
+ .slice(0, 5)
890
+ .map((file) => path.basename(file))
891
+ .join(", ");
892
+ lines.push(`Configured specs (${configuredFiles.length}): ${sample}${configuredFiles.length > 5 ? ", ..." : ""}`);
893
+ }
894
+ else {
895
+ lines.push('No specs were discovered from configured input patterns. Check "input" in config.');
896
+ }
897
+ lines.push('Run "ast test --list" to inspect resolved files.');
898
+ return new Error(lines.join("\n"));
899
+ }
900
+ function suggestClosestSuites(selectors, files) {
901
+ const suites = [
902
+ ...new Set(files.map((file) => stripSuiteSuffix(path.basename(file)))),
903
+ ];
904
+ if (!suites.length)
905
+ return [];
906
+ const out = new Set();
907
+ for (const selector of expandSelectors(selectors)) {
908
+ if (!isBareSuiteSelector(selector))
909
+ continue;
910
+ const query = stripSuiteSuffix(path.basename(selector));
911
+ const closest = resolveClosestSuiteName(query, suites);
912
+ if (closest)
913
+ out.add(closest);
914
+ }
915
+ return [...out].slice(0, 3);
916
+ }
917
+ function resolveClosestSuiteName(value, candidates) {
918
+ if (!value.length)
919
+ return null;
920
+ let best = null;
921
+ let bestDistance = Number.POSITIVE_INFINITY;
922
+ const lowered = value.toLowerCase();
923
+ for (const candidate of candidates) {
924
+ if (candidate == value)
925
+ return null;
926
+ const normalized = candidate.toLowerCase();
927
+ if (normalized.startsWith(lowered) || normalized.includes(lowered)) {
928
+ return candidate;
929
+ }
930
+ const distance = levenshteinDistance(lowered, normalized);
931
+ if (distance < bestDistance) {
932
+ bestDistance = distance;
933
+ best = candidate;
934
+ }
935
+ }
936
+ if (best && bestDistance <= 3)
937
+ return best;
938
+ return null;
939
+ }
940
+ function levenshteinDistance(left, right) {
941
+ if (left == right)
942
+ return 0;
943
+ if (!left.length)
944
+ return right.length;
945
+ if (!right.length)
946
+ return left.length;
947
+ const matrix = [];
948
+ for (let i = 0; i <= left.length; i++) {
949
+ matrix[i] = [i];
950
+ }
951
+ for (let j = 0; j <= right.length; j++) {
952
+ matrix[0][j] = j;
953
+ }
954
+ for (let i = 1; i <= left.length; i++) {
955
+ for (let j = 1; j <= right.length; j++) {
956
+ const cost = left[i - 1] == right[j - 1] ? 0 : 1;
957
+ matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
958
+ }
959
+ }
960
+ return matrix[left.length][right.length];
961
+ }
315
962
  function resolveInputPatterns(configured, selectors) {
316
- const configuredInputs = Array.isArray(configured) ? configured : [configured];
963
+ const configuredInputs = Array.isArray(configured)
964
+ ? configured
965
+ : [configured];
317
966
  if (!selectors.length)
318
967
  return configuredInputs;
319
968
  const patterns = new Set();
320
- for (const selector of selectors) {
969
+ for (const selector of expandSelectors(selectors)) {
321
970
  if (!selector)
322
971
  continue;
323
972
  if (isBareSuiteSelector(selector)) {
@@ -331,6 +980,30 @@ function resolveInputPatterns(configured, selectors) {
331
980
  }
332
981
  return [...patterns];
333
982
  }
983
+ function expandSelectors(selectors) {
984
+ const expanded = [];
985
+ for (const selector of selectors) {
986
+ if (!selector)
987
+ continue;
988
+ if (!shouldSplitSelector(selector)) {
989
+ expanded.push(selector);
990
+ continue;
991
+ }
992
+ for (const token of selector.split(",")) {
993
+ const trimmed = token.trim();
994
+ if (!trimmed.length)
995
+ continue;
996
+ expanded.push(trimmed);
997
+ }
998
+ }
999
+ return expanded;
1000
+ }
1001
+ function shouldSplitSelector(selector) {
1002
+ return (selector.includes(",") &&
1003
+ !selector.includes("/") &&
1004
+ !selector.includes("\\") &&
1005
+ !/[*?[\]{}]/.test(selector));
1006
+ }
334
1007
  function isBareSuiteSelector(selector) {
335
1008
  return (!selector.includes("/") &&
336
1009
  !selector.includes("\\") &&
@@ -339,6 +1012,121 @@ function isBareSuiteSelector(selector) {
339
1012
  function stripSuiteSuffix(selector) {
340
1013
  return selector.replace(/\.spec\.ts$/, "").replace(/\.ts$/, "");
341
1014
  }
1015
+ function resolveDuplicateSpecBasenames(files) {
1016
+ const counts = new Map();
1017
+ for (const file of files) {
1018
+ const base = path.basename(file);
1019
+ counts.set(base, (counts.get(base) ?? 0) + 1);
1020
+ }
1021
+ const duplicates = new Set();
1022
+ for (const [base, count] of counts) {
1023
+ if (count > 1)
1024
+ duplicates.add(base);
1025
+ }
1026
+ return duplicates;
1027
+ }
1028
+ function resolvePerFileArtifactKey(file, duplicateSpecBasenames) {
1029
+ const base = path.basename(file);
1030
+ let raw = base;
1031
+ if (duplicateSpecBasenames.has(base)) {
1032
+ const disambiguator = resolvePerFileDisambiguator(file);
1033
+ if (disambiguator.length) {
1034
+ raw = `${base}.${disambiguator}`;
1035
+ }
1036
+ }
1037
+ return raw.replace(/[^a-zA-Z0-9._-]/g, "_");
1038
+ }
1039
+ function resolvePerFileDisambiguator(file) {
1040
+ const relDir = path.dirname(path.relative(process.cwd(), file));
1041
+ if (!relDir.length || relDir == ".")
1042
+ return "";
1043
+ return relDir
1044
+ .replace(/[\\/]+/g, "__")
1045
+ .replace(/[^A-Za-z0-9._-]/g, "_")
1046
+ .replace(/^_+|_+$/g, "");
1047
+ }
1048
+ function resolveArtifactFileNameForPreview(file, target, modeName, duplicateSpecBasenames) {
1049
+ const base = path
1050
+ .basename(file)
1051
+ .replace(/\.spec\.ts$/, "")
1052
+ .replace(/\.ts$/, "");
1053
+ const legacy = !modeName
1054
+ ? `${path.basename(file).replace(".ts", ".wasm")}`
1055
+ : `${base}.${modeName}.${target}.wasm`;
1056
+ if (!duplicateSpecBasenames.has(path.basename(file))) {
1057
+ return legacy;
1058
+ }
1059
+ const disambiguator = resolvePerFileDisambiguator(file);
1060
+ if (!disambiguator.length) {
1061
+ return legacy;
1062
+ }
1063
+ const ext = path.extname(legacy);
1064
+ const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
1065
+ return `${stem}.${disambiguator}${ext}`;
1066
+ }
1067
+ async function listExecutionPlan(command, configPath, selectors, modes, listFlags) {
1068
+ const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
1069
+ const config = loadConfig(resolvedConfigPath, true);
1070
+ const configuredModes = Object.keys(config.modes);
1071
+ const configuredModeLabels = configuredModes.length
1072
+ ? configuredModes
1073
+ : ["default"];
1074
+ const selectedModeLabels = modes.map((modeName) => modeName ?? "default");
1075
+ const unknownModes = modes.filter((modeName) => Boolean(modeName && !configuredModes.includes(modeName)));
1076
+ if (unknownModes.length) {
1077
+ throw new Error(`unknown mode "${unknownModes[0]}". Available modes: ${configuredModes.join(", ") || "(none)"}`);
1078
+ }
1079
+ process.stdout.write(chalk.bold.blueBright("as-test plan") + "\n");
1080
+ process.stdout.write(chalk.dim(`command: ${command}`) + "\n");
1081
+ process.stdout.write(chalk.dim(`config: ${resolvedConfigPath}`) + "\n");
1082
+ process.stdout.write(chalk.dim(`selectors: ${selectors.length ? selectors.join(", ") : "(configured input patterns)"}`) + "\n\n");
1083
+ if (listFlags.listModes) {
1084
+ process.stdout.write(chalk.bold("Configured modes:\n"));
1085
+ for (const modeName of configuredModeLabels) {
1086
+ process.stdout.write(` - ${modeName}\n`);
1087
+ }
1088
+ process.stdout.write(chalk.bold("\nSelected modes:\n"));
1089
+ for (const modeName of selectedModeLabels) {
1090
+ process.stdout.write(` - ${modeName}\n`);
1091
+ }
1092
+ process.stdout.write("\n");
1093
+ }
1094
+ if (!listFlags.list)
1095
+ return;
1096
+ const files = await resolveSelectedFiles(configPath, selectors);
1097
+ if (!files.length) {
1098
+ const scope = selectors.length > 0 ? selectors.join(", ") : "configured input patterns";
1099
+ throw new Error(`No test files matched: ${scope}`);
1100
+ }
1101
+ const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1102
+ process.stdout.write(chalk.bold("Resolved files:\n"));
1103
+ for (const file of files) {
1104
+ process.stdout.write(` - ${file}\n`);
1105
+ }
1106
+ process.stdout.write("\n");
1107
+ for (const modeName of modes) {
1108
+ const applied = applyMode(config, modeName);
1109
+ const active = applied.config;
1110
+ const modeLabel = modeName ?? "default";
1111
+ process.stdout.write(chalk.bold(`Mode: ${modeLabel}\n`));
1112
+ process.stdout.write(` target: ${active.buildOptions.target}\n`);
1113
+ process.stdout.write(` outDir: ${active.outDir}\n`);
1114
+ if (command != "build") {
1115
+ process.stdout.write(` runtime: ${active.runOptions.runtime.cmd}\n`);
1116
+ }
1117
+ const envOverrides = modeName
1118
+ ? (config.modes[modeName]?.env ?? {})
1119
+ : config.env;
1120
+ const envKeys = Object.keys(envOverrides);
1121
+ process.stdout.write(` env overrides: ${envKeys.length}${envKeys.length ? ` (${envKeys.join(", ")})` : ""}\n`);
1122
+ process.stdout.write(" artifacts:\n");
1123
+ for (const file of files) {
1124
+ const artifactName = resolveArtifactFileNameForPreview(file, active.buildOptions.target, modeName, duplicateSpecBasenames);
1125
+ process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
1126
+ }
1127
+ process.stdout.write("\n");
1128
+ }
1129
+ }
342
1130
  function aggregateRunResults(results) {
343
1131
  const stats = {
344
1132
  passedFiles: 0,
@@ -390,7 +1178,8 @@ function aggregateRunResults(results) {
390
1178
  snapshotSummary.created += result.snapshotSummary.created;
391
1179
  snapshotSummary.updated += result.snapshotSummary.updated;
392
1180
  snapshotSummary.failed += result.snapshotSummary.failed;
393
- coverageSummary.enabled = coverageSummary.enabled || result.coverageSummary.enabled;
1181
+ coverageSummary.enabled =
1182
+ coverageSummary.enabled || result.coverageSummary.enabled;
394
1183
  coverageSummary.showPoints =
395
1184
  coverageSummary.showPoints || result.coverageSummary.showPoints;
396
1185
  for (const fileCoverage of result.coverageSummary.files) {