as-test 0.5.3 → 1.0.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/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);
@@ -30,44 +34,59 @@ if (!args.length) {
30
34
  else if (COMMANDS.includes(args[0])) {
31
35
  try {
32
36
  const command = args.shift();
33
- const commandArgs = resolveCommandArgs(_args, command ?? "");
34
- const featureToggles = resolveFeatureToggles(_args, command ?? "");
35
- const buildFeatureToggles = {
36
- tryAs: featureToggles.tryAs,
37
- coverage: featureToggles.coverage,
38
- };
39
- const runFlags = {
40
- snapshot: !flags.includes("--no-snapshot"),
41
- updateSnapshots: flags.includes("--update-snapshots"),
42
- clean: flags.includes("--clean"),
43
- showCoverage: flags.includes("--show-coverage"),
44
- verbose: flags.includes("--verbose"),
45
- coverage: featureToggles.coverage,
46
- };
47
- if (command === "build") {
48
- const modeTargets = resolveExecutionModes(configPath, selectedModes);
49
- runBuildModes(configPath, commandArgs, modeTargets, buildFeatureToggles).catch((error) => {
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
50
  printCliError(error);
51
51
  process.exit(1);
52
52
  });
53
53
  }
54
54
  else if (command === "run") {
55
- const modeTargets = resolveExecutionModes(configPath, selectedModes);
56
- runRuntimeModes(runFlags, configPath, commandArgs, modeTargets).catch((error) => {
55
+ executeRunCommand(_args, flags, configPath, selectedModes, {
56
+ resolveCommandArgs,
57
+ resolveListFlags,
58
+ resolveFeatureToggles,
59
+ resolveExecutionModes,
60
+ listExecutionPlan,
61
+ runRuntimeModes,
62
+ }).catch((error) => {
57
63
  printCliError(error);
58
64
  process.exit(1);
59
65
  });
60
66
  }
61
67
  else if (command === "test") {
62
- const modeTargets = resolveExecutionModes(configPath, selectedModes);
63
- runTestModes(runFlags, configPath, commandArgs, modeTargets, buildFeatureToggles).catch((error) => {
68
+ executeTestCommand(_args, flags, configPath, selectedModes, {
69
+ resolveCommandArgs,
70
+ resolveListFlags,
71
+ resolveFeatureToggles,
72
+ resolveExecutionModes,
73
+ listExecutionPlan,
74
+ runTestModes,
75
+ }).catch((error) => {
64
76
  printCliError(error);
65
77
  process.exit(1);
66
78
  });
67
79
  }
68
80
  else if (command === "init") {
69
- const commandTokens = resolveCommandTokens(_args, command ?? "");
70
- init(commandTokens).catch((error) => {
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) => {
71
90
  printCliError(error);
72
91
  process.exit(1);
73
92
  });
@@ -126,6 +145,12 @@ function info() {
126
145
  chalk.dim("<./dir>") +
127
146
  " " +
128
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");
129
154
  console.log("");
130
155
  console.log(chalk.bold("Flags:"));
131
156
  console.log(" " +
@@ -154,11 +179,11 @@ function info() {
154
179
  "Print all coverage points with line:column refs");
155
180
  console.log(" " +
156
181
  chalk.bold.blue("--enable <feature>") +
157
- " " +
182
+ " " +
158
183
  "Enable as-test feature (coverage|try-as)");
159
184
  console.log(" " +
160
185
  chalk.bold.blue("--disable <feature>") +
161
- " " +
186
+ " " +
162
187
  "Disable as-test feature (coverage|try-as)");
163
188
  console.log(" " +
164
189
  chalk.bold.blue("--verbose") +
@@ -168,6 +193,15 @@ function info() {
168
193
  chalk.bold.blue("--reporter <name|path>") +
169
194
  " " +
170
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");
171
205
  console.log("");
172
206
  console.log(chalk.dim("If your using this, consider dropping a star, it would help a lot!") + "\n");
173
207
  console.log("View the repo: " +
@@ -177,6 +211,102 @@ function info() {
177
211
  // chalk.blue("https://docs.jairus.dev/as-test"),
178
212
  // );
179
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
+ }
180
310
  function resolveConfigPath(rawArgs) {
181
311
  for (let i = 0; i < rawArgs.length; i++) {
182
312
  const arg = rawArgs[i];
@@ -277,6 +407,29 @@ function resolveFeatureToggles(rawArgs, command) {
277
407
  }
278
408
  return out;
279
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
+ }
280
433
  function applyFeatureToggle(out, rawFeature, enabled) {
281
434
  const key = rawFeature.trim().toLowerCase();
282
435
  if (key == "coverage") {
@@ -306,10 +459,7 @@ function resolveCommandTokens(rawArgs, command) {
306
459
  async function runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, modeName) {
307
460
  const files = await resolveSelectedFiles(configPath, selectors);
308
461
  if (!files.length) {
309
- const scope = selectors.length > 0
310
- ? selectors.join(", ")
311
- : "configured input patterns";
312
- throw new Error(`No test files matched: ${scope}`);
462
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
313
463
  }
314
464
  const reporterSession = await createRunReporter(configPath, undefined, modeName);
315
465
  const reporter = reporterSession.reporter;
@@ -323,9 +473,10 @@ async function runTestSequential(runFlags, configPath, selectors, buildFeatureTo
323
473
  });
324
474
  const results = [];
325
475
  let failed = false;
476
+ const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
326
477
  for (const file of files) {
327
478
  await build(configPath, [file], modeName, buildFeatureToggles);
328
- const artifactKey = path.basename(file).replace(/[^a-zA-Z0-9._-]/g, "_");
479
+ const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
329
480
  const result = await run(runFlags, configPath, [file], false, {
330
481
  reporter,
331
482
  emitRunStart: false,
@@ -381,10 +532,7 @@ async function runRuntimeModes(runFlags, configPath, selectors, modes) {
381
532
  async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal) {
382
533
  const files = await resolveSelectedFiles(configPath, selectors);
383
534
  if (!files.length) {
384
- const scope = selectors.length > 0
385
- ? selectors.join(", ")
386
- : "configured input patterns";
387
- throw new Error(`No test files matched: ${scope}`);
535
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
388
536
  }
389
537
  const reporterSession = await createRunReporter(configPath);
390
538
  const reporter = reporterSession.reporter;
@@ -409,6 +557,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
409
557
  failed: false,
410
558
  passed: false,
411
559
  }));
560
+ const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
412
561
  for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
413
562
  const file = files[fileIndex];
414
563
  const fileName = path.basename(file);
@@ -420,7 +569,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
420
569
  for (let i = 0; i < modes.length; i++) {
421
570
  const modeName = modes[i];
422
571
  try {
423
- const artifactKey = fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
572
+ const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
424
573
  const result = await run(runFlags, configPath, [file], false, {
425
574
  reporter: silentReporter,
426
575
  reporterKind: "default",
@@ -490,10 +639,7 @@ async function runTestModes(runFlags, configPath, selectors, modes, buildFeature
490
639
  async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal) {
491
640
  const files = await resolveSelectedFiles(configPath, selectors);
492
641
  if (!files.length) {
493
- const scope = selectors.length > 0
494
- ? selectors.join(", ")
495
- : "configured input patterns";
496
- throw new Error(`No test files matched: ${scope}`);
642
+ throw await buildNoTestFilesMatchedError(configPath, selectors);
497
643
  }
498
644
  const reporterSession = await createRunReporter(configPath);
499
645
  const reporter = reporterSession.reporter;
@@ -518,6 +664,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
518
664
  failed: false,
519
665
  passed: false,
520
666
  }));
667
+ const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
521
668
  for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
522
669
  const file = files[fileIndex];
523
670
  const fileName = path.basename(file);
@@ -530,7 +677,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
530
677
  const modeName = modes[i];
531
678
  try {
532
679
  await build(configPath, [file], modeName, buildFeatureToggles);
533
- const artifactKey = fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
680
+ const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
534
681
  const result = await run(runFlags, configPath, [file], false, {
535
682
  reporter: silentReporter,
536
683
  reporterKind: "default",
@@ -590,7 +737,9 @@ function renderMatrixFileResult(file, modes, results, modeTimes, liveMatrix, sho
590
737
  : chalk.bgBlackBright.white(" SKIP ");
591
738
  const avg = formatMatrixAverageTime(results);
592
739
  const timingText = showPerModeTimes ? modeTimes.join(",") : avg;
593
- const suffix = showPerModeTimes ? ` ${chalk.dim(`(${modes.join(",")})`)}` : "";
740
+ const suffix = showPerModeTimes
741
+ ? ` ${chalk.dim(`(${modes.join(",")})`)}`
742
+ : "";
594
743
  const line = `${badge} ${file} ${chalk.dim(timingText)}${suffix}`;
595
744
  if (liveMatrix)
596
745
  clearLiveLine();
@@ -616,7 +765,9 @@ function renderMatrixLiveLine(file, modes, modeTimes, showPerModeTimes) {
616
765
  if (!canRewriteStdout())
617
766
  return;
618
767
  const timingText = showPerModeTimes ? modeTimes.join(",") : "...";
619
- const suffix = showPerModeTimes ? ` ${chalk.dim(`(${modes.join(",")})`)}` : "";
768
+ const suffix = showPerModeTimes
769
+ ? ` ${chalk.dim(`(${modes.join(",")})`)}`
770
+ : "";
620
771
  const line = `${chalk.bgBlackBright.white(" .... ")} ${file} ${chalk.dim(timingText)}${suffix}`;
621
772
  process.stdout.write(`\r\x1b[2K${line}`);
622
773
  }
@@ -629,7 +780,9 @@ function formatMatrixAverageTime(results) {
629
780
  return "0.0ms";
630
781
  let total = 0;
631
782
  for (const result of results) {
632
- total += Number.isFinite(result.stats.time) ? Math.max(0, result.stats.time) : 0;
783
+ total += Number.isFinite(result.stats.time)
784
+ ? Math.max(0, result.stats.time)
785
+ : 0;
633
786
  }
634
787
  return `${(total / results.length).toFixed(1)}ms`;
635
788
  }
@@ -711,16 +864,105 @@ function resolveExecutionModes(configPath, selectedModes) {
711
864
  return [undefined];
712
865
  return configuredModes;
713
866
  }
714
- async function resolveSelectedFiles(configPath, selectors) {
867
+ async function resolveSelectedFiles(configPath, selectors, warn = true) {
715
868
  const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
716
- const config = loadConfig(resolvedConfigPath, true);
869
+ const config = loadConfig(resolvedConfigPath, warn);
717
870
  const patterns = resolveInputPatterns(config.input, selectors);
718
871
  const matches = await glob(patterns);
719
872
  const specs = matches.filter((file) => file.endsWith(".spec.ts"));
720
873
  return [...new Set(specs)].sort((a, b) => a.localeCompare(b));
721
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
+ }
722
962
  function resolveInputPatterns(configured, selectors) {
723
- const configuredInputs = Array.isArray(configured) ? configured : [configured];
963
+ const configuredInputs = Array.isArray(configured)
964
+ ? configured
965
+ : [configured];
724
966
  if (!selectors.length)
725
967
  return configuredInputs;
726
968
  const patterns = new Set();
@@ -770,6 +1012,121 @@ function isBareSuiteSelector(selector) {
770
1012
  function stripSuiteSuffix(selector) {
771
1013
  return selector.replace(/\.spec\.ts$/, "").replace(/\.ts$/, "");
772
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
+ }
773
1130
  function aggregateRunResults(results) {
774
1131
  const stats = {
775
1132
  passedFiles: 0,
@@ -821,7 +1178,8 @@ function aggregateRunResults(results) {
821
1178
  snapshotSummary.created += result.snapshotSummary.created;
822
1179
  snapshotSummary.updated += result.snapshotSummary.updated;
823
1180
  snapshotSummary.failed += result.snapshotSummary.failed;
824
- coverageSummary.enabled = coverageSummary.enabled || result.coverageSummary.enabled;
1181
+ coverageSummary.enabled =
1182
+ coverageSummary.enabled || result.coverageSummary.enabled;
825
1183
  coverageSummary.showPoints =
826
1184
  coverageSummary.showPoints || result.coverageSummary.showPoints;
827
1185
  for (const fileCoverage of result.coverageSummary.files) {