as-test 1.5.1 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Change Log
2
2
 
3
+ ## 2026-06-01 - v1.5.2
4
+
5
+ ### Selectors resolve folders, files, and globs consistently
6
+
7
+ - feat: positional selectors for `ast test`/`ast run`/`ast build` now resolve through a single shared resolver (`cli/selectors.ts:resolveSpecFiles`), replacing three drifting private copies of `resolveInputPatterns` (in `build-core`, `run-core`, and `index`). Three input shapes are supported:
8
+ - **Bare folders/files/globs** (no leading `./`) resolve against the configured input root(s) — the static prefix of each `input` glob, e.g. `assembly/__tests__` — searched recursively, and fall back to the cwd only if nothing matched there: `ast test rfc/` → `<root>/**/rfc/**/*.spec.ts`; `ast test foo` → `<root>/**/foo.spec.ts`; `ast test 'rfc/*.spec.ts'` → `<root>/**/rfc/*.spec.ts` (the user's glob appended verbatim). A bare path shorthand like `nested/array` is tried as a cwd path first, then anchored to the test folder.
9
+ - **`./`-prefixed** selectors (and absolute / `~` paths) are cwd-relative only; on a miss we emit a `did you mean "rfc/*.spec.ts"` hint pointing at the test-folder form when that would have matched.
10
+ - feat: a bare selector that matches under more than one configured input root is flagged with a `WARN` (it still runs everything that matched), and a selector that matches nothing emits a `WARN` naming where it looked. Warnings are deduped by text across the orchestrator + per-file build/run passes (`emitSelectorWarnings`), so each prints once per invocation. Folder selectors (`rfc/`) and `,`-joined bare names (`a,b`) are recognized; selectors with an internal path separator (e.g. the orchestrator's own `assembly/__tests__/foo.spec.ts`) are still treated as direct cwd paths, preserving existing per-file dispatch.
11
+
3
12
  ## 2026-05-30 - v1.5.1
4
13
 
5
14
  ### An early-exiting runtime now fails instead of warning
@@ -22,6 +22,7 @@ import {
22
22
  } from "../util.js";
23
23
  import { persistCrashRecord } from "../crash-store.js";
24
24
  import { BuildWorkerPool } from "../build-worker-pool.js";
25
+ import { resolveSpecFiles, emitSelectorWarnings } from "../selectors.js";
25
26
  const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
26
27
  export const buildRecorderStorage = new AsyncLocalStorage();
27
28
  export class BuildFailureError extends Error {
@@ -67,14 +68,9 @@ export async function build(
67
68
  const pkgRunner = getPkgRunner();
68
69
  const sourceInputPatterns =
69
70
  overrides.kind === "fuzz" ? config.fuzz.input : config.input;
70
- const inputPatterns = resolveInputPatterns(sourceInputPatterns, selectors);
71
- const includePatterns = inputPatterns.filter((p) => !p.startsWith("!"));
72
- const ignorePatterns = inputPatterns
73
- .filter((p) => p.startsWith("!"))
74
- .map((p) => p.slice(1));
75
- const inputFiles = (
76
- await glob(includePatterns, { ignore: ignorePatterns })
77
- ).sort((a, b) => a.localeCompare(b));
71
+ const { files: inputFiles, warnings: selectorWarnings } =
72
+ await resolveSpecFiles(sourceInputPatterns, selectors);
73
+ emitSelectorWarnings(selectorWarnings);
78
74
  await assertNoArtifactCollisions(sourceInputPatterns);
79
75
  warnOnUnknownModeReferences(inputFiles, loadedConfig.modes ?? {});
80
76
  const coverageEnabled = resolveCoverageEnabled(
@@ -540,61 +536,6 @@ async function assertNoArtifactCollisions(configured) {
540
536
  seen.set(artifact, file);
541
537
  }
542
538
  }
543
- function resolveInputPatterns(configured, selectors) {
544
- const configuredInputs = Array.isArray(configured)
545
- ? configured
546
- : [configured];
547
- if (!selectors.length) return configuredInputs;
548
- const patterns = new Set();
549
- for (const selector of expandSelectors(selectors)) {
550
- if (!selector) continue;
551
- if (isBareSuiteSelector(selector)) {
552
- const base = stripSuiteSuffix(selector);
553
- for (const configuredInput of configuredInputs) {
554
- patterns.add(
555
- path.join(path.dirname(configuredInput), `${base}.spec.ts`),
556
- );
557
- }
558
- continue;
559
- }
560
- patterns.add(selector);
561
- }
562
- return [...patterns];
563
- }
564
- function expandSelectors(selectors) {
565
- const expanded = [];
566
- for (const selector of selectors) {
567
- if (!selector) continue;
568
- if (!shouldSplitSelector(selector)) {
569
- expanded.push(selector);
570
- continue;
571
- }
572
- for (const token of selector.split(",")) {
573
- const trimmed = token.trim();
574
- if (!trimmed.length) continue;
575
- expanded.push(trimmed);
576
- }
577
- }
578
- return expanded;
579
- }
580
- function shouldSplitSelector(selector) {
581
- return (
582
- selector.includes(",") &&
583
- !selector.includes("/") &&
584
- !selector.includes("\\") &&
585
- !/[*?[\]{}]/.test(selector)
586
- );
587
- }
588
- function isBareSuiteSelector(selector) {
589
- return (
590
- !selector.includes("/") &&
591
- !selector.includes("\\") &&
592
- !/[*?[\]{}]/.test(selector)
593
- );
594
- }
595
- function stripSuiteSuffix(selector) {
596
- return selector.replace(/\.spec\.ts$/, "").replace(/\.ts$/, "");
597
- }
598
539
  function ensureDeps(config) {
599
540
  if (config.buildOptions.target == "wasi") {
600
541
  if (!resolveWasiShim()) {
@@ -1,6 +1,5 @@
1
1
  import chalk from "chalk";
2
2
  import { spawn } from "child_process";
3
- import { glob } from "glob";
4
3
  import { minimatch } from "minimatch";
5
4
  import { Channel, MessageType } from "../wipc.js";
6
5
  import {
@@ -21,6 +20,7 @@ import { PassThrough } from "stream";
21
20
  import { buildWebRunnerSource } from "./web-runner-source.js";
22
21
  import { PersistentWebSessionHost } from "./web-session.js";
23
22
  import { build } from "./build-core.js";
23
+ import { resolveSpecFiles, emitSelectorWarnings } from "../selectors.js";
24
24
  import { createReporter as createDefaultReporter } from "../reporters/default.js";
25
25
  import { createTapReporter } from "../reporters/tap.js";
26
26
  import { persistCrashRecord } from "../crash-store.js";
@@ -733,14 +733,9 @@ export async function run(
733
733
  }
734
734
  }
735
735
  }
736
- const inputPatterns = resolveInputPatterns(config.input, selectors);
737
- const includePatterns = inputPatterns.filter((p) => !p.startsWith("!"));
738
- const ignorePatterns = inputPatterns
739
- .filter((p) => p.startsWith("!"))
740
- .map((p) => p.slice(1));
741
- const inputFiles = (
742
- await glob(includePatterns, { ignore: ignorePatterns })
743
- ).sort((a, b) => a.localeCompare(b));
736
+ const { files: inputFiles, warnings: selectorWarnings } =
737
+ await resolveSpecFiles(config.input, selectors);
738
+ emitSelectorWarnings(selectorWarnings);
744
739
  const snapshotEnabled = flags.snapshot !== false;
745
740
  const createSnapshots = Boolean(flags.createSnapshots);
746
741
  const overwriteSnapshots = Boolean(flags.overwriteSnapshots);
@@ -1364,61 +1359,6 @@ function runtimeNameFromCommand(command) {
1364
1359
  const token = command.trim().split(/\s+/)[0];
1365
1360
  return token && token.length ? token : "runtime";
1366
1361
  }
1367
- function resolveInputPatterns(configured, selectors) {
1368
- const configuredInputs = Array.isArray(configured)
1369
- ? configured
1370
- : [configured];
1371
- if (!selectors.length) return configuredInputs;
1372
- const patterns = new Set();
1373
- for (const selector of expandSelectors(selectors)) {
1374
- if (!selector) continue;
1375
- if (isBareSuiteSelector(selector)) {
1376
- const base = stripSuiteSuffix(selector);
1377
- for (const configuredInput of configuredInputs) {
1378
- patterns.add(
1379
- path.join(path.dirname(configuredInput), `${base}.spec.ts`),
1380
- );
1381
- }
1382
- continue;
1383
- }
1384
- patterns.add(selector);
1385
- }
1386
- return [...patterns];
1387
- }
1388
- function expandSelectors(selectors) {
1389
- const expanded = [];
1390
- for (const selector of selectors) {
1391
- if (!selector) continue;
1392
- if (!shouldSplitSelector(selector)) {
1393
- expanded.push(selector);
1394
- continue;
1395
- }
1396
- for (const token of selector.split(",")) {
1397
- const trimmed = token.trim();
1398
- if (!trimmed.length) continue;
1399
- expanded.push(trimmed);
1400
- }
1401
- }
1402
- return expanded;
1403
- }
1404
- function shouldSplitSelector(selector) {
1405
- return (
1406
- selector.includes(",") &&
1407
- !selector.includes("/") &&
1408
- !selector.includes("\\") &&
1409
- !/[*?[\]{}]/.test(selector)
1410
- );
1411
- }
1412
- function isBareSuiteSelector(selector) {
1413
- return (
1414
- !selector.includes("/") &&
1415
- !selector.includes("\\") &&
1416
- !/[*?[\]{}]/.test(selector)
1417
- );
1418
- }
1419
- function stripSuiteSuffix(selector) {
1420
- return selector.replace(/\.spec\.ts$/, "").replace(/\.ts$/, "");
1421
- }
1422
1362
  function normalizeReport(raw) {
1423
1363
  if (Array.isArray(raw)) {
1424
1364
  return {
package/bin/index.js CHANGED
@@ -41,6 +41,7 @@ import { BuildWorkerPool } from "./build-worker-pool.js";
41
41
  import { PersistentWebSessionHost } from "./commands/web-session.js";
42
42
  import { buildRecorderStorage } from "./commands/build-core.js";
43
43
  import { DependencyGraph } from "./dependency-graph.js";
44
+ import { resolveSpecFiles, emitSelectorWarnings } from "./selectors.js";
44
45
  const _args = process.argv.slice(2);
45
46
  const flags = [];
46
47
  const args = [];
@@ -3855,9 +3856,9 @@ async function resolveSelectedFiles(configPath, selectors, warn = true) {
3855
3856
  const resolvedConfigPath =
3856
3857
  configPath ?? path.join(process.cwd(), "./as-test.config.json");
3857
3858
  const config = loadConfig(resolvedConfigPath, warn);
3858
- const patterns = resolveInputPatterns(config.input, selectors);
3859
- const matches = await glob(patterns);
3860
- const specs = matches.filter((file) => file.endsWith(".spec.ts"));
3859
+ const { files, warnings } = await resolveSpecFiles(config.input, selectors);
3860
+ if (warn) emitSelectorWarnings(warnings);
3861
+ const specs = files.filter((file) => file.endsWith(".spec.ts"));
3861
3862
  return [...new Set(specs)].sort((a, b) => a.localeCompare(b));
3862
3863
  }
3863
3864
  async function resolveSelectedFuzzFiles(
@@ -3996,27 +3997,6 @@ function levenshteinDistance(left, right) {
3996
3997
  }
3997
3998
  return matrix[left.length][right.length];
3998
3999
  }
3999
- function resolveInputPatterns(configured, selectors) {
4000
- const configuredInputs = Array.isArray(configured)
4001
- ? configured
4002
- : [configured];
4003
- if (!selectors.length) return configuredInputs;
4004
- const patterns = new Set();
4005
- for (const selector of expandSelectors(selectors)) {
4006
- if (!selector) continue;
4007
- if (isBareSuiteSelector(selector)) {
4008
- const base = stripSuiteSuffix(selector);
4009
- for (const configuredInput of configuredInputs) {
4010
- patterns.add(
4011
- path.join(path.dirname(configuredInput), `${base}.spec.ts`),
4012
- );
4013
- }
4014
- continue;
4015
- }
4016
- patterns.add(selector);
4017
- }
4018
- return [...patterns];
4019
- }
4020
4000
  function resolveFuzzPatterns(configured, selectors) {
4021
4001
  const configuredInputs = Array.isArray(configured)
4022
4002
  ? configured
@@ -0,0 +1,208 @@
1
+ import { glob } from "glob";
2
+ import chalk from "chalk";
3
+ import * as path from "path";
4
+ // Selector resolution runs in several places per command (the orchestrator,
5
+ // then build/run cores per file); dedupe by text so a warning prints once per
6
+ // process regardless of how many resolvers see the same selector.
7
+ const reportedSelectorWarnings = new Set();
8
+ export function emitSelectorWarnings(warnings) {
9
+ for (const warning of warnings) {
10
+ if (reportedSelectorWarnings.has(warning)) continue;
11
+ reportedSelectorWarnings.add(warning);
12
+ process.stderr.write(`${chalk.yellow.bold("WARN")} ${warning}\n`);
13
+ }
14
+ }
15
+ const GLOB_MAGIC = /[*?[\]{}]/;
16
+ function hasGlobMagic(selector) {
17
+ return GLOB_MAGIC.test(selector);
18
+ }
19
+ function endsWithSlash(selector) {
20
+ return /[\\/]$/.test(selector);
21
+ }
22
+ function stripTrailingSlash(selector) {
23
+ return selector.replace(/[\\/]+$/, "");
24
+ }
25
+ function stripSuiteSuffix(selector) {
26
+ return selector.replace(/\.spec\.ts$/, "").replace(/\.ts$/, "");
27
+ }
28
+ function isCwdRelative(selector) {
29
+ return (
30
+ selector.startsWith("./") ||
31
+ selector.startsWith("../") ||
32
+ selector.startsWith(".\\") ||
33
+ selector.startsWith("..\\") ||
34
+ selector.startsWith("/") ||
35
+ selector.startsWith("~") ||
36
+ path.isAbsolute(selector)
37
+ );
38
+ }
39
+ // A selector with a path separator that is not merely a single trailing slash
40
+ // (e.g. `assembly/__tests__/foo.spec.ts`, passed verbatim by the orchestrator)
41
+ // is treated as a direct cwd-relative path rather than a test-folder alias.
42
+ function hasInternalSlash(selector) {
43
+ return /[\\/]/.test(stripTrailingSlash(selector));
44
+ }
45
+ // The longest leading run of path segments containing no glob magic — the
46
+ // static "test folder" of an input pattern (`assembly/__tests__/**/*.spec.ts`
47
+ // -> `assembly/__tests__`).
48
+ function globBase(pattern) {
49
+ const segments = pattern.split("/");
50
+ const base = [];
51
+ for (const segment of segments) {
52
+ if (hasGlobMagic(segment)) break;
53
+ base.push(segment);
54
+ }
55
+ return base.join("/") || ".";
56
+ }
57
+ function uniqueInputRoots(configuredInputs) {
58
+ const roots = new Set();
59
+ for (const pattern of configuredInputs) {
60
+ if (pattern.startsWith("!")) continue;
61
+ roots.add(globBase(pattern));
62
+ }
63
+ return [...roots];
64
+ }
65
+ // Turn a cwd-relative selector into the spec glob(s) it stands for.
66
+ function cwdPatterns(selector) {
67
+ if (endsWithSlash(selector)) {
68
+ return [`${stripTrailingSlash(selector)}/**/*.spec.ts`];
69
+ }
70
+ if (/\.ts$/.test(selector)) return [selector];
71
+ return [`${stripSuiteSuffix(selector)}.spec.ts`];
72
+ }
73
+ // Turn a bare selector into the spec glob(s) it stands for, anchored to a
74
+ // configured input root and searched recursively beneath it. A selector that
75
+ // already carries glob magic (`rfc/*.spec.ts`, `*.spec.ts`) is appended
76
+ // verbatim so the user's pattern controls the match; a plain folder/name has
77
+ // the spec suffix supplied.
78
+ function barePatterns(root, selector) {
79
+ if (hasGlobMagic(selector)) {
80
+ return [`${root}/**/${selector}`];
81
+ }
82
+ if (endsWithSlash(selector)) {
83
+ return [`${root}/**/${stripTrailingSlash(selector)}/**/*.spec.ts`];
84
+ }
85
+ return [`${root}/**/${stripSuiteSuffix(selector)}.spec.ts`];
86
+ }
87
+ // Split comma-joined bare selectors (`a,b,c`) while leaving paths and globs
88
+ // (which can legitimately contain commas, e.g. `{a,b}`) intact.
89
+ function expandSelectors(selectors) {
90
+ const expanded = [];
91
+ for (const selector of selectors) {
92
+ if (!selector) continue;
93
+ if (
94
+ selector.includes(",") &&
95
+ !hasInternalSlash(selector) &&
96
+ !endsWithSlash(selector) &&
97
+ !hasGlobMagic(selector)
98
+ ) {
99
+ for (const token of selector.split(",")) {
100
+ const trimmed = token.trim();
101
+ if (trimmed.length) expanded.push(trimmed);
102
+ }
103
+ continue;
104
+ }
105
+ expanded.push(selector);
106
+ }
107
+ return expanded;
108
+ }
109
+ async function globFiles(patterns) {
110
+ return glob(patterns);
111
+ }
112
+ async function resolveSelector(selector, inputRoots) {
113
+ const warnings = [];
114
+ const isGlob = hasGlobMagic(selector);
115
+ // Explicit cwd-relative selector (`./`, `../`, absolute, `~`) — resolve from
116
+ // the cwd only. A glob is matched verbatim; a plain path gets the spec suffix.
117
+ if (isCwdRelative(selector)) {
118
+ const files = await globFiles(isGlob ? [selector] : cwdPatterns(selector));
119
+ if (!files.length) {
120
+ const bare = selector.replace(/^\.[\\/]/, "");
121
+ let suggestion = null;
122
+ for (const root of inputRoots) {
123
+ const inRoot = await globFiles(barePatterns(root, bare));
124
+ if (inRoot.length) {
125
+ suggestion = bare;
126
+ break;
127
+ }
128
+ }
129
+ warnings.push(
130
+ suggestion
131
+ ? `"${selector}" not found relative to the current directory — did you mean "${suggestion}" (searches the configured test folder)?`
132
+ : `"${selector}" not found relative to the current directory`,
133
+ );
134
+ }
135
+ return { files, warnings };
136
+ }
137
+ // A plain path with an internal separator (e.g. the orchestrator's own
138
+ // `assembly/__tests__/foo.spec.ts`) resolves from the cwd verbatim. Globs
139
+ // skip this and fall through to test-folder anchoring below.
140
+ if (!isGlob && hasInternalSlash(selector)) {
141
+ const direct = await globFiles(cwdPatterns(selector));
142
+ if (direct.length) return { files: direct, warnings };
143
+ // Fall through to test-folder resolution for user shorthands like
144
+ // `nested/array` that aren't a real cwd path.
145
+ }
146
+ // Bare name/folder or relative glob — configured input root(s) first.
147
+ const perRoot = [];
148
+ for (const root of inputRoots) {
149
+ const files = await globFiles(barePatterns(root, selector));
150
+ if (files.length) perRoot.push({ root, files });
151
+ }
152
+ if (perRoot.length) {
153
+ if (perRoot.length > 1) {
154
+ warnings.push(
155
+ `selector "${selector}" matched specs under ${perRoot.length} input roots (${perRoot
156
+ .map((entry) => entry.root)
157
+ .join(", ")}) — running all of them`,
158
+ );
159
+ }
160
+ return { files: perRoot.flatMap((entry) => entry.files), warnings };
161
+ }
162
+ // Fall back to the cwd before giving up.
163
+ const cwdFiles = await globFiles(
164
+ isGlob ? [selector] : cwdPatterns(`./${selector}`),
165
+ );
166
+ if (cwdFiles.length) {
167
+ return { files: cwdFiles, warnings };
168
+ }
169
+ warnings.push(
170
+ inputRoots.length
171
+ ? `no spec files matched "${selector}" in ${inputRoots.join(", ")} or the current directory`
172
+ : `no spec files matched "${selector}"`,
173
+ );
174
+ return { files: [], warnings };
175
+ }
176
+ // Resolve configured input patterns + positional selectors into the concrete
177
+ // set of spec files to act on, along with any human-readable warnings. With no
178
+ // selectors this is just the configured globs (honoring `!`-negations); with
179
+ // selectors the per-selector rules above apply and config negations are
180
+ // intentionally bypassed so an explicit pick always wins.
181
+ export async function resolveSpecFiles(configured, selectors) {
182
+ const configuredInputs = Array.isArray(configured)
183
+ ? configured
184
+ : [configured];
185
+ if (!selectors.length) {
186
+ const include = configuredInputs.filter((p) => !p.startsWith("!"));
187
+ const ignore = configuredInputs
188
+ .filter((p) => p.startsWith("!"))
189
+ .map((p) => p.slice(1));
190
+ const files = (await glob(include, { ignore })).sort((a, b) =>
191
+ a.localeCompare(b),
192
+ );
193
+ return { files, warnings: [] };
194
+ }
195
+ const inputRoots = uniqueInputRoots(configuredInputs);
196
+ const files = new Set();
197
+ const warnings = [];
198
+ for (const selector of expandSelectors(selectors)) {
199
+ if (!selector) continue;
200
+ const resolved = await resolveSelector(selector, inputRoots);
201
+ for (const file of resolved.files) files.add(file);
202
+ warnings.push(...resolved.warnings);
203
+ }
204
+ return {
205
+ files: [...files].sort((a, b) => a.localeCompare(b)),
206
+ warnings,
207
+ };
208
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "as-test",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "author": "Jairus Tanaka",
5
5
  "repository": {
6
6
  "type": "git",