as-test 1.1.5 → 1.1.6-patch.1

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,11 +1,28 @@
1
1
  # Change Log
2
2
 
3
- ## 2026-05-15 - v1.1.5
3
+ ## 2026-05-19 - v1.1.6-patch.1
4
+
5
+ ### Directory-preserving artifact layout
6
+
7
+ - feat: build artifacts, fuzz artifacts, snapshots, readable logs, coverage logs, and crash records now mirror the source tree under the configured input globs instead of being flattened into a single directory with a `____`-mangled disambiguator suffix. For `assembly/__tests__/nested/array.spec.ts` the artifact is `outDir/<mode>/nested/array.spec.wasm` (previously `outDir/<mode>/array.<mode>.<target>.assembly____tests____nested.wasm`). Sidecar `.js` / `.d.ts` files sit next to the wasm.
8
+ - feat: filename simplified to `<stem>.wasm` — the `.<mode>.<target>` suffix has been dropped since the mode is already a directory level and the target is implied by the mode config.
9
+ - feat: add `resolveGlobBase`, `resolveSpecRelativePath`, and `resolveArtifactPath` in `cli/util.ts` as the shared path helpers used by every code path that writes or looks up a per-spec artifact. Glob bases are computed component-wise (so `assembly/__tests` is not a prefix of `assembly/__tests__/foo.spec.ts`) and the longest matching base wins when multiple configured input patterns overlap. Patterns without a static prefix (e.g. `**/*.spec.ts`) fall back to the file's cwd-relative path.
10
+ - feat: add an up-front collision check in `build()` that throws a clear error naming both source files when two configured inputs would resolve to the same artifact path.
11
+ - fix: `ast test <one-spec>`, `ast run <one-spec>`, and `ast fuzz <one-spec>` no longer drop the disambiguator when only one of two same-basename files is being built. The build side and the runner side now compute the same path from the same configured input set, so the runner no longer reports `bindings artifact not found` after a single-file build.
12
+ - fix: build sites now `mkdir -p` the artifact's parent directory before invoking `asc` — pinned `assemblyscript@0.28.17`'s `-o` flag does not create parents and would otherwise ENOENT for any new nested directory.
13
+ - fix: `persistCrashRecord` now `mkdir -p`'s the entry's parent directory, supporting `/` in entry keys so nested specs and fuzz failures get their own crash files instead of clobbering by basename.
14
+ - fix: replace the hardcoded `/__tests__/` and `/__fuzz__/` markers in snapshot and readable-log path resolution with proper glob-base computation, so projects with custom input layouts (e.g. `tests/specs/**/*.spec.ts`) now nest correctly instead of silently falling back to basename-only paths.
15
+ - fix: clean break on snapshot file layout — the legacy `${base}.snap.json` and `${base}.${disambiguator}.snap.json` fallbacks have been removed. After upgrading, run `--create-snapshots` or `--overwrite-snapshots` once so snapshots are written at their new relative-path locations.
16
+ - chore: delete the duplicate-basename plumbing introduced by the patch immediately preceding this one. Every code path that used to thread `Set<string>` of duplicate basenames now reads paths directly. No back-compat for the old flat-with-suffix scheme — `ast clean` removes any orphan artifacts.
17
+
18
+ ## 2026-05-14 - v1.1.6
4
19
 
5
20
  - fix: coverage `mode` and `dependencies` filtering now correctly handles AssemblyScript-normalized `~lib/<pkg>/...` paths, which are the actual runtime paths emitted for `node_modules` imports.
6
21
  - fix: `ENTRY_FILE` injected by the transform now uses the full relative path instead of the basename, preventing snapshot key collisions between specs with the same filename in different directories; snapshot lookup normalizes the file prefix to maintain backward compatibility with existing `.snap` files.
7
22
  - fix: transform visitor and coverage instrumentation now resolve `NodeKind` values at runtime instead of relying on compile-time const enum inlining, so they remain correct across AssemblyScript versions.
8
23
  - fix: add a no-op `TupleType` case to the transform visitor so files using tuple types no longer throw during instrumentation.
24
+ - fix: coverage transform no longer wraps `return this` in constructors, preventing AS231 ("A class with a constructor explicitly returning something else than 'this' must be '@final'").
25
+ - fix: coverage transform preserves expression-body arrows instead of converting them to block bodies, preventing TS1140 ("Type argument expected") on typed arrow parameters such as `[1,2,3].map((x: i32) => x + 1)`.
9
26
 
10
27
  ## 2026-05-14 - v1.1.4
11
28
 
@@ -0,0 +1,13 @@
1
+ import { expect, fuzz, FuzzSeed } from "as-test";
2
+
3
+ fuzz(
4
+ "nested array.fuzz: index windows stay ordered",
5
+ (start: i32, end: i32): bool => {
6
+ expect(start <= end).toBe(true);
7
+ return end - start <= 64;
8
+ },
9
+ ).generate((seed: FuzzSeed, run: (start: i32, end: i32) => bool): void => {
10
+ const start = seed.i32({ min: 0, max: 64 });
11
+ const width = seed.i32({ min: 0, max: 64 });
12
+ run(start, start + width);
13
+ });
@@ -1,10 +1,10 @@
1
- import { existsSync } from "fs";
1
+ import { existsSync, mkdirSync } from "fs";
2
2
  import { glob } from "glob";
3
3
  import chalk from "chalk";
4
4
  import { spawn } from "child_process";
5
5
  import * as path from "path";
6
6
  import { createMemoryStream, main as ascMain, } from "assemblyscript/dist/asc.js";
7
- import { applyMode, getPkgRunner, loadConfig, tokenizeCommand, resolveProjectModule, } from "../util.js";
7
+ import { applyMode, getPkgRunner, loadConfig, resolveArtifactPath, resolveSpecRelativePath, tokenizeCommand, resolveProjectModule, } from "../util.js";
8
8
  import { persistCrashRecord } from "../crash-store.js";
9
9
  import { BuildWorkerPool } from "../build-worker-pool.js";
10
10
  const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
@@ -36,9 +36,10 @@ export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = [], mo
36
36
  ensureDeps(config);
37
37
  }
38
38
  const pkgRunner = getPkgRunner();
39
- const inputPatterns = resolveInputPatterns(config.input, selectors);
39
+ const sourceInputPatterns = overrides.kind === "fuzz" ? config.fuzz.input : config.input;
40
+ const inputPatterns = resolveInputPatterns(sourceInputPatterns, selectors);
40
41
  const inputFiles = (await glob(inputPatterns)).sort((a, b) => a.localeCompare(b));
41
- const duplicateSpecBasenames = resolveDuplicateBasenames(inputFiles);
42
+ await assertNoArtifactCollisions(sourceInputPatterns);
42
43
  const coverageEnabled = resolveCoverageEnabled(config.coverage, featureToggles.coverage);
43
44
  const buildEnv = {
44
45
  ...mode.env,
@@ -50,7 +51,7 @@ export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = [], mo
50
51
  !hasCustomBuildCommand(config)) {
51
52
  const pool = getSerialBuildWorkerPool();
52
53
  for (const file of inputFiles) {
53
- const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
54
+ const outFile = path.join(config.outDir, resolveArtifactPath(file, sourceInputPatterns));
54
55
  const invocation = getBuildCommand(config, pkgRunner, file, outFile, modeName, featureToggles);
55
56
  await pool.buildFileMode({
56
57
  configPath,
@@ -64,7 +65,8 @@ export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = [], mo
64
65
  return;
65
66
  }
66
67
  for (const file of inputFiles) {
67
- const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
68
+ const outFile = path.join(config.outDir, resolveArtifactPath(file, sourceInputPatterns));
69
+ mkdirSync(path.dirname(outFile), { recursive: true });
68
70
  const invocation = getBuildCommand(config, pkgRunner, file, outFile, modeName, featureToggles);
69
71
  try {
70
72
  await buildFile(invocation, buildEnv);
@@ -79,6 +81,7 @@ export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = [], mo
79
81
  kind,
80
82
  stage: "build",
81
83
  file,
84
+ entryKey: resolveSpecRelativePath(file, sourceInputPatterns).replace(/\.ts$/i, ""),
82
85
  mode: modeLabel,
83
86
  cwd: process.cwd(),
84
87
  buildCommand,
@@ -127,8 +130,8 @@ export async function getBuildInvocationPreview(configPath = DEFAULT_CONFIG_PATH
127
130
  if (overrides.args?.length) {
128
131
  config.buildOptions.args = [...config.buildOptions.args, ...overrides.args];
129
132
  }
130
- const duplicateSpecBasenames = resolveDuplicateBasenames([file]);
131
- const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
133
+ const sourceInputPatterns = overrides.kind === "fuzz" ? config.fuzz.input : config.input;
134
+ const outFile = path.join(config.outDir, resolveArtifactPath(file, sourceInputPatterns));
132
135
  return getBuildCommand(config, getPkgRunner(), file, outFile, modeName, featureToggles);
133
136
  }
134
137
  export async function getBuildReuseInfo(configPath = DEFAULT_CONFIG_PATH, file, modeName, featureToggles = {}, overrides = {}) {
@@ -145,8 +148,8 @@ export async function getBuildReuseInfo(configPath = DEFAULT_CONFIG_PATH, file,
145
148
  if (hasCustomBuildCommand(config)) {
146
149
  return null;
147
150
  }
148
- const duplicateSpecBasenames = resolveDuplicateBasenames([file]);
149
- const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
151
+ const sourceInputPatterns = overrides.kind === "fuzz" ? config.fuzz.input : config.input;
152
+ const outFile = path.join(config.outDir, resolveArtifactPath(file, sourceInputPatterns));
150
153
  const invocation = getBuildCommand(config, getPkgRunner(), file, outFile, modeName, featureToggles);
151
154
  const coverageEnabled = resolveCoverageEnabled(config.coverage, featureToggles.coverage);
152
155
  const buildEnv = {
@@ -245,46 +248,21 @@ function expandBuildCommand(template, file, outFile, target, modeName) {
245
248
  .replace(/<target>/g, target)
246
249
  .replace(/<mode>/g, modeName ?? "");
247
250
  }
248
- function resolveArtifactFileName(file, target, modeName, duplicateSpecBasenames = new Set()) {
249
- const base = path
250
- .basename(file)
251
- .replace(/\.spec\.ts$/, "")
252
- .replace(/\.ts$/, "");
253
- const legacy = !modeName
254
- ? `${path.basename(file).replace(".ts", ".wasm")}`
255
- : `${base}.${modeName}.${target}.wasm`;
256
- if (!duplicateSpecBasenames.has(path.basename(file))) {
257
- return legacy;
258
- }
259
- const disambiguator = resolveDisambiguator(file);
260
- if (!disambiguator.length) {
261
- return legacy;
262
- }
263
- const ext = path.extname(legacy);
264
- const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
265
- return `${stem}.${disambiguator}${ext}`;
266
- }
267
- function resolveDuplicateBasenames(files) {
268
- const counts = new Map();
251
+ async function assertNoArtifactCollisions(configured) {
252
+ const patterns = Array.isArray(configured) ? configured : [configured];
253
+ const files = await glob(patterns);
254
+ const seen = new Map();
269
255
  for (const file of files) {
270
- const base = path.basename(file);
271
- counts.set(base, (counts.get(base) ?? 0) + 1);
272
- }
273
- const duplicates = new Set();
274
- for (const [base, count] of counts) {
275
- if (count > 1)
276
- duplicates.add(base);
277
- }
278
- return duplicates;
279
- }
280
- function resolveDisambiguator(file) {
281
- const relDir = path.dirname(path.relative(process.cwd(), file));
282
- if (!relDir.length || relDir == ".")
283
- return "";
284
- return relDir
285
- .replace(/[\\/]+/g, "__")
286
- .replace(/[^A-Za-z0-9._-]/g, "_")
287
- .replace(/^_+|_+$/g, "");
256
+ const artifact = resolveArtifactPath(file, patterns);
257
+ const prev = seen.get(artifact);
258
+ if (prev != null && prev !== file) {
259
+ throw new Error(`Two input files resolve to the same artifact path "${artifact}":\n` +
260
+ ` - ${prev}\n` +
261
+ ` - ${file}\n` +
262
+ `Rename one of them or narrow the input patterns to disambiguate.`);
263
+ }
264
+ seen.set(artifact, file);
265
+ }
288
266
  }
289
267
  function resolveInputPatterns(configured, selectors) {
290
268
  const configuredInputs = Array.isArray(configured)
@@ -3,7 +3,7 @@ import * as path from "path";
3
3
  import { pathToFileURL } from "url";
4
4
  import { glob } from "glob";
5
5
  import { build } from "./build-core.js";
6
- import { applyMode, loadConfig } from "../util.js";
6
+ import { applyMode, loadConfig, resolveArtifactPath, resolveSpecRelativePath, } from "../util.js";
7
7
  import { persistCrashRecord } from "../crash-store.js";
8
8
  const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
9
9
  const MAGIC = Buffer.from("WIPC");
@@ -19,14 +19,13 @@ export async function fuzz(configPath = DEFAULT_CONFIG_PATH, selectors = [], mod
19
19
  if (!inputFiles.length) {
20
20
  throw new Error(`No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`);
21
21
  }
22
- const duplicateBasenames = resolveDuplicateBasenames(inputFiles);
23
22
  const results = [];
24
23
  for (const file of inputFiles) {
25
24
  const buildStartedAt = Date.now();
26
25
  await build(configPath, [file], modeName, { coverage: false }, { target: "bindings", args: ["--use", "AS_TEST_FUZZ=1"], kind: "fuzz" }, loadedConfig);
27
26
  const buildFinishedAt = Date.now();
28
27
  const buildTime = buildFinishedAt - buildStartedAt;
29
- results.push(await runFuzzTarget(file, activeConfig.outDir, duplicateBasenames, config, fuzzerSelectors, buildStartedAt, buildFinishedAt, buildTime, modeName));
28
+ results.push(await runFuzzTarget(file, activeConfig.outDir, config, fuzzerSelectors, buildStartedAt, buildFinishedAt, buildTime, modeName));
30
29
  }
31
30
  return results;
32
31
  }
@@ -70,9 +69,9 @@ function encodeRunsOverrideKind(kind) {
70
69
  return 4;
71
70
  }
72
71
  }
73
- async function runFuzzTarget(file, outDir, duplicateBasenames, config, fuzzerSelectors, buildStartedAt, buildFinishedAt, buildTime, modeName) {
72
+ async function runFuzzTarget(file, outDir, config, fuzzerSelectors, buildStartedAt, buildFinishedAt, buildTime, modeName) {
74
73
  const startedAt = Date.now();
75
- const artifact = resolveArtifactFileName(file, duplicateBasenames, modeName);
74
+ const artifact = resolveArtifactPath(file, config.input);
76
75
  const wasmPath = path.resolve(process.cwd(), outDir, artifact);
77
76
  const jsPath = resolveBindingsHelperPath(wasmPath);
78
77
  const helper = await import(pathToFileURL(jsPath).href + `?t=${Date.now()}`);
@@ -140,7 +139,7 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, fuzzerSel
140
139
  const crash = persistCrashRecord(config.crashDir, {
141
140
  kind: "fuzz",
142
141
  file,
143
- entryKey: buildFuzzCrashEntryKey(file, modeName ?? "default"),
142
+ entryKey: buildFuzzCrashEntryKey(file, config.input, modeName ?? "default"),
144
143
  mode: modeName ?? "default",
145
144
  seed: config.seed,
146
145
  error: crashMessage,
@@ -188,7 +187,7 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, fuzzerSel
188
187
  const crash = persistCrashRecord(config.crashDir, {
189
188
  kind: "fuzz",
190
189
  file,
191
- entryKey: buildFuzzCrashEntryKey(file, modeName ?? "default"),
190
+ entryKey: buildFuzzCrashEntryKey(file, config.input, modeName ?? "default"),
192
191
  mode: modeName ?? "default",
193
192
  seed: config.seed,
194
193
  error: `${reportParseError ? `invalid fuzz report payload: ${reportParseError}` : `missing fuzz report payload from ${path.basename(file)}`} (${diagnostics})`,
@@ -223,7 +222,7 @@ async function runFuzzTarget(file, outDir, duplicateBasenames, config, fuzzerSel
223
222
  const crash = persistCrashRecord(config.crashDir, {
224
223
  kind: "fuzz",
225
224
  file,
226
- entryKey: buildFuzzFailureEntryKey(file, fuzzer.name, modeName ?? "default"),
225
+ entryKey: buildFuzzFailureEntryKey(file, config.input, fuzzer.name, modeName ?? "default"),
227
226
  mode: modeName ?? "default",
228
227
  seed: firstFailureSeed,
229
228
  reproCommand: buildFuzzReproCommand(file, firstFailureSeed, modeName ?? "default", fuzzer.selector, 1),
@@ -285,11 +284,13 @@ function buildFuzzReproCommand(file, seed, modeName, fuzzer, runs) {
285
284
  const runsArg = typeof runs == "number" ? ` --runs ${runs}` : "";
286
285
  return `ast fuzz ${file}${modeArg}${fuzzerArg} --seed ${seed}${runsArg}`;
287
286
  }
288
- function buildFuzzFailureEntryKey(file, name, modeName) {
289
- return `${path.basename(file).replace(/\.ts$/, "")}.${sanitizeEntryName(modeName)}.${sanitizeEntryName(name)}`;
287
+ function buildFuzzFailureEntryKey(file, inputPatterns, name, modeName) {
288
+ const stem = resolveSpecRelativePath(file, inputPatterns).replace(/\.ts$/i, "");
289
+ return `${stem}.${sanitizeEntryName(modeName)}.${sanitizeEntryName(name)}`;
290
290
  }
291
- function buildFuzzCrashEntryKey(file, modeName) {
292
- return `${path.basename(file).replace(/\.ts$/, "")}.${sanitizeEntryName(modeName)}`;
291
+ function buildFuzzCrashEntryKey(file, inputPatterns, modeName) {
292
+ const stem = resolveSpecRelativePath(file, inputPatterns).replace(/\.ts$/i, "");
293
+ return `${stem}.${sanitizeEntryName(modeName)}`;
293
294
  }
294
295
  function sanitizeEntryName(name) {
295
296
  return (name
@@ -398,47 +399,6 @@ function resolveFuzzInputPatterns(configured, selectors) {
398
399
  }
399
400
  return [...patterns];
400
401
  }
401
- function resolveArtifactFileName(file, duplicateBasenames, modeName) {
402
- const base = path
403
- .basename(file)
404
- .replace(/\.spec\.ts$/, "")
405
- .replace(/\.ts$/, "");
406
- const legacy = !modeName
407
- ? `${path.basename(file).replace(".ts", ".wasm")}`
408
- : `${base}.${modeName}.bindings.wasm`;
409
- if (!duplicateBasenames.has(path.basename(file))) {
410
- return legacy;
411
- }
412
- const disambiguator = resolveDisambiguator(file);
413
- if (!disambiguator.length) {
414
- return legacy;
415
- }
416
- const ext = path.extname(legacy);
417
- const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
418
- return `${stem}.${disambiguator}${ext}`;
419
- }
420
- function resolveDuplicateBasenames(files) {
421
- const counts = new Map();
422
- for (const file of files) {
423
- const base = path.basename(file);
424
- counts.set(base, (counts.get(base) ?? 0) + 1);
425
- }
426
- const duplicates = new Set();
427
- for (const [base, count] of counts) {
428
- if (count > 1)
429
- duplicates.add(base);
430
- }
431
- return duplicates;
432
- }
433
- function resolveDisambiguator(file) {
434
- const relDir = path.dirname(path.relative(process.cwd(), file));
435
- if (!relDir.length || relDir == ".")
436
- return "";
437
- return relDir
438
- .replace(/[\\/]+/g, "__")
439
- .replace(/[^A-Za-z0-9._-]/g, "_")
440
- .replace(/^_+|_+$/g, "");
441
- }
442
402
  function resolveBindingsHelperPath(wasmPath) {
443
403
  const bindingsPath = wasmPath.replace(/\.wasm$/, ".bindings.js");
444
404
  if (existsSync(bindingsPath))
@@ -2,7 +2,7 @@ import chalk from "chalk";
2
2
  import { spawn } from "child_process";
3
3
  import { glob } from "glob";
4
4
  import { Channel, MessageType } from "../wipc.js";
5
- import { applyMode, formatSpecDisplayPath, formatTime, getExec, loadConfig, tokenizeCommand, } from "../util.js";
5
+ import { applyMode, formatSpecDisplayPath, formatTime, getExec, loadConfig, resolveArtifactPath, resolveSpecRelativePath, tokenizeCommand, } from "../util.js";
6
6
  import * as path from "path";
7
7
  import { pathToFileURL } from "url";
8
8
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -16,7 +16,7 @@ import { persistCrashRecord } from "../crash-store.js";
16
16
  import { describeCoveragePoint } from "../coverage-points.js";
17
17
  const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
18
18
  class SnapshotStore {
19
- constructor(specFile, snapshotDir, duplicateSpecBasenames = new Set()) {
19
+ constructor(specFile, snapshotDir, inputPatterns) {
20
20
  this.dirty = false;
21
21
  this.created = 0;
22
22
  this.updated = 0;
@@ -25,16 +25,16 @@ class SnapshotStore {
25
25
  this.warnedMissing = new Set();
26
26
  this.specBasename = path.basename(specFile);
27
27
  const dir = path.join(process.cwd(), snapshotDir);
28
- const relative = resolveArtifactRelativePath(specFile, "__tests__").replace(/\.ts$/, ".snap");
28
+ const relative = resolveSpecRelativePath(specFile, inputPatterns).replace(/\.ts$/i, ".snap");
29
29
  this.filePath = path.join(dir, relative);
30
- const sourcePath = resolveSnapshotSourcePath(specFile, dir, duplicateSpecBasenames, this.filePath) ?? null;
30
+ const sourcePath = existsSync(this.filePath) ? this.filePath : null;
31
31
  const loaded = sourcePath
32
32
  ? readSnapshotFile(sourcePath, specFile)
33
33
  : { data: {}, normalized: false, preamble: "" };
34
34
  this.data = loaded.data;
35
35
  this.preamble = loaded.preamble;
36
- this.existed = Boolean(sourcePath && existsSync(sourcePath));
37
- this.dirty = Boolean((sourcePath && sourcePath != this.filePath) || loaded.normalized);
36
+ this.existed = Boolean(sourcePath);
37
+ this.dirty = Boolean(loaded.normalized);
38
38
  }
39
39
  assert(key, actual, allowSnapshot, createSnapshots, overwriteSnapshots) {
40
40
  key = canonicalizeSnapshotKey(key);
@@ -81,21 +81,6 @@ class SnapshotStore {
81
81
  writeFileSync(this.filePath, formatSnapshotFile(this.data, this.filePath, this.existed ? this.preamble : defaultSnapshotPreamble()));
82
82
  }
83
83
  }
84
- function resolveSnapshotSourcePath(specFile, snapshotDir, duplicateSpecBasenames, preferredPath) {
85
- if (existsSync(preferredPath))
86
- return preferredPath;
87
- const base = path.basename(specFile, ".ts");
88
- const legacyFlat = path.join(snapshotDir, `${base}.snap.json`);
89
- if (existsSync(legacyFlat))
90
- return legacyFlat;
91
- const disambiguator = resolveDisambiguator(specFile, duplicateSpecBasenames);
92
- if (disambiguator.length) {
93
- const legacyDisambiguated = path.join(snapshotDir, `${base}.${disambiguator}.snap.json`);
94
- if (existsSync(legacyDisambiguated))
95
- return legacyDisambiguated;
96
- }
97
- return null;
98
- }
99
84
  function readSnapshotFile(filePath, specFile) {
100
85
  const raw = readFileSync(filePath, "utf8");
101
86
  if (filePath.endsWith(".json")) {
@@ -247,16 +232,11 @@ function trimSnapshotPreamble(lines) {
247
232
  end--;
248
233
  return lines.slice(0, end).join("\n");
249
234
  }
235
+ // Only the basename of the returned path matters — callers feed this into
236
+ // `path.basename(...)` to localize snapshot keys (strip the "${basename}::"
237
+ // prefix). The full path is therefore synthetic but stable.
250
238
  function resolveSnapshotSpecFile(filePath) {
251
- const normalized = filePath.replace(/\\/g, "/");
252
- const marker = "/snapshots/";
253
- const markerIndex = normalized.lastIndexOf(marker);
254
- const suffix = markerIndex >= 0
255
- ? normalized.slice(markerIndex + marker.length)
256
- : path.basename(normalized);
257
- const withoutMode = suffix.replace(/^default\//, "");
258
- const relative = withoutMode.replace(/\.snap$/, ".ts");
259
- return `assembly/__tests__/${relative}`;
239
+ return path.basename(filePath).replace(/\.snap$/, ".ts");
260
240
  }
261
241
  function localizeSnapshotKey(specFile, key) {
262
242
  const prefix = `${path.basename(specFile)}::`;
@@ -297,16 +277,8 @@ function canonicalizeSnapshotLocalKey(localKey) {
297
277
  }
298
278
  return localKey;
299
279
  }
300
- function resolveArtifactRelativePath(sourceFile, segment) {
301
- const normalized = sourceFile.replace(/\\/g, "/");
302
- const marker = `/${segment}/`;
303
- const index = normalized.lastIndexOf(marker);
304
- if (index >= 0)
305
- return normalized.slice(index + marker.length);
306
- return path.basename(normalized);
307
- }
308
- function writeReadableLog(logRoot, file, suites, modeName, buildCommand, runCommand, snapshotSummary) {
309
- const relative = resolveArtifactRelativePath(file, "__tests__").replace(/\.ts$/, ".log");
280
+ function writeReadableLog(logRoot, file, inputPatterns, suites, modeName, buildCommand, runCommand, snapshotSummary) {
281
+ const relative = resolveSpecRelativePath(file, inputPatterns).replace(/\.ts$/i, ".log");
310
282
  const filePath = path.join(logRoot, relative);
311
283
  const dir = path.dirname(filePath);
312
284
  if (!existsSync(dir))
@@ -562,7 +534,6 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
562
534
  const config = mode.config;
563
535
  const inputPatterns = resolveInputPatterns(config.input, selectors);
564
536
  const inputFiles = (await glob(inputPatterns)).sort((a, b) => a.localeCompare(b));
565
- const duplicateSpecBasenames = await resolveDuplicateSpecBasenames(config.input);
566
537
  const snapshotEnabled = flags.snapshot !== false;
567
538
  const createSnapshots = Boolean(flags.createSnapshots);
568
539
  const overwriteSnapshots = Boolean(flags.overwriteSnapshots);
@@ -623,7 +594,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
623
594
  try {
624
595
  for (let i = 0; i < inputFiles.length; i++) {
625
596
  const file = inputFiles[i];
626
- const outFile = path.join(config.outDir, resolveArtifactFileName(file, config.buildOptions.target, options.modeName, duplicateSpecBasenames));
597
+ const outFile = path.join(config.outDir, resolveArtifactPath(file, config.input));
627
598
  if (!existsSync(outFile)) {
628
599
  const buildStartedAt = Date.now();
629
600
  await build(resolvedConfigPath, [file], options.modeName, { coverage: flags.coverage }, {}, loadedConfig);
@@ -642,7 +613,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
642
613
  .map((token) => token.replace(/<name>/g, fileBase).replace(/<file>/g, fileToken)),
643
614
  };
644
615
  const runCommandForLog = formatInvocation(invocation);
645
- const snapshotStore = new SnapshotStore(file, config.snapshotDir, duplicateSpecBasenames);
616
+ const snapshotStore = new SnapshotStore(file, config.snapshotDir, config.input);
646
617
  let report;
647
618
  try {
648
619
  const runtimeEnv = {
@@ -655,9 +626,10 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
655
626
  ? { BROWSER: config.runOptions.runtime.browser.trim() }
656
627
  : {}),
657
628
  };
629
+ const crashEntryKey = resolveSpecRelativePath(file, config.input).replace(/\.ts$/i, "");
658
630
  report = webSession
659
- ? await runWebSessionProcess(webSession, file, config.fuzz.crashDir, options.modeName, snapshotStore, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, reporterKind == "tap", runtimeEnv)
660
- : await runProcess(invocation, file, config.fuzz.crashDir, options.modeName, snapshotStore, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, reporterKind == "tap", runtimeEnv);
631
+ ? await runWebSessionProcess(webSession, file, config.fuzz.crashDir, crashEntryKey, options.modeName, snapshotStore, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, reporterKind == "tap", runtimeEnv)
632
+ : await runProcess(invocation, file, config.fuzz.crashDir, crashEntryKey, options.modeName, snapshotStore, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, reporterKind == "tap", runtimeEnv);
661
633
  }
662
634
  catch (error) {
663
635
  const modeLabel = options.modeName ?? "default";
@@ -698,7 +670,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
698
670
  mkdirSync(logRoot, { recursive: true });
699
671
  }
700
672
  for (const report of reports) {
701
- writeReadableLog(logRoot, report.file, report.suites, options.modeName, options.buildCommandsByFile?.[report.file] ??
673
+ writeReadableLog(logRoot, report.file, config.input, report.suites, options.modeName, options.buildCommandsByFile?.[report.file] ??
702
674
  options.buildCommand ??
703
675
  "", report.runCommand, report.snapshotSummary);
704
676
  }
@@ -713,10 +685,9 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
713
685
  coverageDir != "none" &&
714
686
  coverageSummary.files.length > 0) {
715
687
  const resolvedCoverageDir = path.join(process.cwd(), coverageDir);
716
- if (!existsSync(resolvedCoverageDir)) {
717
- mkdirSync(resolvedCoverageDir, { recursive: true });
718
- }
719
- writeFileSync(path.join(resolvedCoverageDir, options.coverageFileName ?? "coverage.log.json"), JSON.stringify(coverageSummary, null, 2));
688
+ const coverageFilePath = path.join(resolvedCoverageDir, options.coverageFileName ?? "coverage.log.json");
689
+ mkdirSync(path.dirname(coverageFilePath), { recursive: true });
690
+ writeFileSync(coverageFilePath, JSON.stringify(coverageSummary, null, 2));
720
691
  }
721
692
  if (options.emitRunComplete !== false) {
722
693
  const totalModes = Math.max(options.modeSummaryTotal ?? 1, 1);
@@ -937,51 +908,6 @@ instantiate(imports)
937
908
  }
938
909
  return null;
939
910
  }
940
- function resolveArtifactFileName(file, target, modeName, duplicateSpecBasenames = new Set()) {
941
- const base = path
942
- .basename(file)
943
- .replace(/\.spec\.ts$/, "")
944
- .replace(/\.ts$/, "");
945
- const legacy = !modeName
946
- ? `${path.basename(file).replace(".ts", ".wasm")}`
947
- : `${base}.${modeName}.${target}.wasm`;
948
- if (!duplicateSpecBasenames.has(path.basename(file))) {
949
- return legacy;
950
- }
951
- const disambiguator = resolveDisambiguator(file, duplicateSpecBasenames);
952
- if (!disambiguator.length) {
953
- return legacy;
954
- }
955
- const ext = path.extname(legacy);
956
- const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
957
- return `${stem}.${disambiguator}${ext}`;
958
- }
959
- async function resolveDuplicateSpecBasenames(configured) {
960
- const patterns = Array.isArray(configured) ? configured : [configured];
961
- const files = await glob(patterns);
962
- const counts = new Map();
963
- for (const file of files) {
964
- const base = path.basename(file);
965
- counts.set(base, (counts.get(base) ?? 0) + 1);
966
- }
967
- const duplicates = new Set();
968
- for (const [base, count] of counts) {
969
- if (count > 1)
970
- duplicates.add(base);
971
- }
972
- return duplicates;
973
- }
974
- function resolveDisambiguator(file, duplicateSpecBasenames) {
975
- if (!duplicateSpecBasenames.has(path.basename(file)))
976
- return "";
977
- const relDir = path.dirname(path.relative(process.cwd(), file));
978
- if (!relDir.length || relDir == ".")
979
- return "";
980
- return relDir
981
- .replace(/[\\/]+/g, "__")
982
- .replace(/[^A-Za-z0-9._-]/g, "_")
983
- .replace(/^_+|_+$/g, "");
984
- }
985
911
  function resolveRuntimeTargetEnv(target, wasmPath) {
986
912
  if (target == "bindings") {
987
913
  return resolveBindingsRuntimeEnv(wasmPath);
@@ -1602,7 +1528,7 @@ function compareCoveragePoints(a, b) {
1602
1528
  return a.type.localeCompare(b.type);
1603
1529
  return a.hash.localeCompare(b.hash);
1604
1530
  }
1605
- async function runProcess(invocation, specFile, crashDir, modeName, snapshots, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, tapMode = false, env = process.env) {
1531
+ async function runProcess(invocation, specFile, crashDir, crashEntryKey, modeName, snapshots, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, tapMode = false, env = process.env) {
1606
1532
  const child = spawn(invocation.command, invocation.args, {
1607
1533
  stdio: ["pipe", "pipe", "pipe"],
1608
1534
  shell: false,
@@ -1803,6 +1729,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1803
1729
  persistCrashRecord(crashDir, {
1804
1730
  kind: "test",
1805
1731
  file: specFile,
1732
+ entryKey: crashEntryKey,
1806
1733
  mode: modeName ?? "default",
1807
1734
  error: errorText,
1808
1735
  stdout: stdoutBuffer,
@@ -1845,6 +1772,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1845
1772
  persistCrashRecord(crashDir, {
1846
1773
  kind: "test",
1847
1774
  file: specFile,
1775
+ entryKey: crashEntryKey,
1848
1776
  mode: modeName ?? "default",
1849
1777
  error: fullError,
1850
1778
  stdout: stdoutBuffer,
@@ -1873,6 +1801,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1873
1801
  persistCrashRecord(crashDir, {
1874
1802
  kind: "test",
1875
1803
  file: specFile,
1804
+ entryKey: crashEntryKey,
1876
1805
  mode: modeName ?? "default",
1877
1806
  error: errorText || "runtime reported an unknown error",
1878
1807
  stdout: stdoutBuffer,
@@ -1890,6 +1819,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1890
1819
  persistCrashRecord(crashDir, {
1891
1820
  kind: "test",
1892
1821
  file: specFile,
1822
+ entryKey: crashEntryKey,
1893
1823
  mode: modeName ?? "default",
1894
1824
  error: fullError,
1895
1825
  stdout: stdoutBuffer,
@@ -1910,6 +1840,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1910
1840
  persistCrashRecord(crashDir, {
1911
1841
  kind: "test",
1912
1842
  file: specFile,
1843
+ entryKey: crashEntryKey,
1913
1844
  mode: modeName ?? "default",
1914
1845
  error: errorText || "runtime reported an unknown error",
1915
1846
  stdout: stdoutBuffer,
@@ -1921,7 +1852,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1921
1852
  }
1922
1853
  return report;
1923
1854
  }
1924
- async function runWebSessionProcess(session, specFile, crashDir, modeName, snapshots, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, tapMode = false, env = process.env) {
1855
+ async function runWebSessionProcess(session, specFile, crashDir, crashEntryKey, modeName, snapshots, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, tapMode = false, env = process.env) {
1925
1856
  const input = new PassThrough();
1926
1857
  const output = new PassThrough();
1927
1858
  let report = null;
@@ -2145,6 +2076,7 @@ async function runWebSessionProcess(session, specFile, crashDir, modeName, snaps
2145
2076
  persistCrashRecord(crashDir, {
2146
2077
  kind: "test",
2147
2078
  file: specFile,
2079
+ entryKey: crashEntryKey,
2148
2080
  mode: modeName ?? "default",
2149
2081
  error: fullError,
2150
2082
  stdout: stdoutBuffer,
@@ -2179,6 +2111,7 @@ async function runWebSessionProcess(session, specFile, crashDir, modeName, snaps
2179
2111
  persistCrashRecord(crashDir, {
2180
2112
  kind: "test",
2181
2113
  file: specFile,
2114
+ entryKey: crashEntryKey,
2182
2115
  mode: modeName ?? "default",
2183
2116
  error: fullError,
2184
2117
  stdout: stdoutBuffer,
@@ -56,6 +56,10 @@ export class PersistentWebSessionHost {
56
56
  ? env.AS_TEST_HELPER_PATH
57
57
  : null;
58
58
  const jobId = String(this.nextJobId++);
59
+ // URL paths use only the basename — that is safe because each job has
60
+ // exactly one wasm and one helper, scoped by the jobId path component.
61
+ // Nested directory artifacts (e.g. nested/array.spec.wasm) are read
62
+ // from disk via `wasmPath`/`helperPath`, not via these URLs.
59
63
  const browserEnv = {
60
64
  ...env,
61
65
  AS_TEST_WASM_PATH: `/job/${jobId}/${path.basename(wasmPath)}`,
@@ -5,9 +5,9 @@ export function persistCrashRecord(rootDir, record) {
5
5
  ? record.entryKey
6
6
  : crashEntryKey(record.file);
7
7
  const dir = path.resolve(process.cwd(), rootDir);
8
- mkdirSync(dir, { recursive: true });
9
8
  const jsonPath = path.join(dir, `${entry}.json`);
10
9
  const logPath = path.join(dir, `${entry}.log`);
10
+ mkdirSync(path.dirname(jsonPath), { recursive: true });
11
11
  const payload = {
12
12
  timestamp: new Date().toISOString(),
13
13
  ...record,
package/bin/index.js CHANGED
@@ -10,7 +10,7 @@ import { executeInitCommand } from "./commands/init.js";
10
10
  import { executeDoctorCommand } from "./commands/doctor.js";
11
11
  import { executeCleanCommand } from "./commands/clean.js";
12
12
  import { fuzz } from "./commands/fuzz-core.js";
13
- import { applyMode, formatTime, formatSpecDisplayPath, getDefaultModeNames, getCliVersion, loadConfig, resolveModeNames, } from "./util.js";
13
+ import { applyMode, formatTime, formatSpecDisplayPath, getDefaultModeNames, getCliVersion, loadConfig, resolveArtifactPath, resolveModeNames, resolveSpecRelativePath, } from "./util.js";
14
14
  import * as path from "path";
15
15
  import { spawnSync } from "child_process";
16
16
  import { glob } from "glob";
@@ -1140,7 +1140,7 @@ async function runTestSequential(runFlags, configPath, selectors, suiteSelectors
1140
1140
  const results = [];
1141
1141
  let failed = false;
1142
1142
  const buildIntervals = [];
1143
- const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1143
+ const inputPatterns = await loadInputPatterns(configPath);
1144
1144
  for (const file of files) {
1145
1145
  const buildStartedAt = Date.now();
1146
1146
  let result;
@@ -1148,7 +1148,7 @@ async function runTestSequential(runFlags, configPath, selectors, suiteSelectors
1148
1148
  await build(configPath, [file], modeName, buildFeatureToggles);
1149
1149
  buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1150
1150
  const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
1151
- const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
1151
+ const artifactKey = resolveArtifactStem(file, inputPatterns);
1152
1152
  result = await run(runFlags, configPath, [file], false, {
1153
1153
  reporter,
1154
1154
  webSession,
@@ -1156,7 +1156,7 @@ async function runTestSequential(runFlags, configPath, selectors, suiteSelectors
1156
1156
  emitRunStart: false,
1157
1157
  emitRunComplete: false,
1158
1158
  logFileName: `test.${artifactKey}.log.json`,
1159
- coverageFileName: `coverage.${artifactKey}.log.json`,
1159
+ coverageFileName: `${artifactKey}.log.json`,
1160
1160
  buildCommand: formatBuildInvocation(buildInvocation),
1161
1161
  modeName,
1162
1162
  });
@@ -1351,7 +1351,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, suiteSelectors,
1351
1351
  failed: false,
1352
1352
  passed: false,
1353
1353
  }));
1354
- const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1354
+ const inputPatterns = await loadInputPatterns(configPath);
1355
1355
  const buildIntervals = [];
1356
1356
  for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
1357
1357
  const file = files[fileIndex];
@@ -1365,7 +1365,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, suiteSelectors,
1365
1365
  const modeName = modes[i];
1366
1366
  try {
1367
1367
  const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, {});
1368
- const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
1368
+ const artifactKey = resolveArtifactStem(file, inputPatterns);
1369
1369
  const result = await run(runFlags, configPath, [file], false, {
1370
1370
  reporter: silentReporter,
1371
1371
  reporterKind: "default",
@@ -1373,7 +1373,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, suiteSelectors,
1373
1373
  emitRunStart: false,
1374
1374
  emitRunComplete: false,
1375
1375
  logFileName: `run.${artifactKey}.log.json`,
1376
- coverageFileName: `coverage.${artifactKey}.log.json`,
1376
+ coverageFileName: `${artifactKey}.log.json`,
1377
1377
  buildCommand: formatBuildInvocation(buildInvocation),
1378
1378
  modeName,
1379
1379
  });
@@ -1531,7 +1531,7 @@ async function runTestMatrix(runFlags, configPath, selectors, suiteSelectors, fu
1531
1531
  failed: false,
1532
1532
  passed: false,
1533
1533
  }));
1534
- const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1534
+ const inputPatterns = await loadInputPatterns(configPath);
1535
1535
  const buildIntervals = [];
1536
1536
  for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
1537
1537
  const file = files[fileIndex];
@@ -1554,14 +1554,14 @@ async function runTestMatrix(runFlags, configPath, selectors, suiteSelectors, fu
1554
1554
  });
1555
1555
  buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1556
1556
  const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
1557
- const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
1557
+ const artifactKey = resolveArtifactStem(file, inputPatterns);
1558
1558
  result = await run(runFlags, configPath, [file], false, {
1559
1559
  reporter: silentReporter,
1560
1560
  reporterKind: "default",
1561
1561
  emitRunStart: false,
1562
1562
  emitRunComplete: false,
1563
1563
  logFileName: `test.${artifactKey}.log.json`,
1564
- coverageFileName: `coverage.${artifactKey}.log.json`,
1564
+ coverageFileName: `${artifactKey}.log.json`,
1565
1565
  buildCommand: formatBuildInvocation(buildInvocation),
1566
1566
  modeName,
1567
1567
  });
@@ -1740,7 +1740,7 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, suiteSe
1740
1740
  const silentReporter = {};
1741
1741
  const modeLabels = modes.map((modeName) => modeName ?? "default");
1742
1742
  const showPerModeTimes = Boolean(runFlags.verbose);
1743
- const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1743
+ const inputPatterns = await loadInputPatterns(configPath);
1744
1744
  const ordered = new Array(files.length);
1745
1745
  const useQueueDisplay = reporterSession.reporterKind == "default";
1746
1746
  const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
@@ -1769,7 +1769,7 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, suiteSe
1769
1769
  });
1770
1770
  buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1771
1771
  const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, {});
1772
- const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
1772
+ const artifactKey = resolveArtifactStem(file, inputPatterns);
1773
1773
  result = await run(runFlags, configPath, [file], false, {
1774
1774
  reporter: silentReporter,
1775
1775
  reporterKind: "default",
@@ -1777,7 +1777,7 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, suiteSe
1777
1777
  emitRunStart: false,
1778
1778
  emitRunComplete: false,
1779
1779
  logFileName: `run.${artifactKey}.log.json`,
1780
- coverageFileName: `coverage.${artifactKey}.log.json`,
1780
+ coverageFileName: `${artifactKey}.log.json`,
1781
1781
  buildCommand: formatBuildInvocation(buildInvocation),
1782
1782
  modeName,
1783
1783
  });
@@ -1853,7 +1853,7 @@ async function runTestSingleParallel(runFlags, configPath, selectors, suiteSelec
1853
1853
  snapshotEnabled,
1854
1854
  createSnapshots: runFlags.createSnapshots,
1855
1855
  });
1856
- const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1856
+ const inputPatterns = await loadInputPatterns(configPath);
1857
1857
  const results = new Array(files.length);
1858
1858
  const useQueueDisplay = reporterSession.reporterKind == "default";
1859
1859
  const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
@@ -1881,14 +1881,14 @@ async function runTestSingleParallel(runFlags, configPath, selectors, suiteSelec
1881
1881
  });
1882
1882
  buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1883
1883
  const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
1884
- const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
1884
+ const artifactKey = resolveArtifactStem(file, inputPatterns);
1885
1885
  result = await run({ ...runFlags, clean: true }, configPath, [file], false, {
1886
1886
  reporter: buffered?.reporter,
1887
1887
  reporterKind: buffered?.reporterKind,
1888
1888
  suiteSelectors,
1889
1889
  emitRunComplete: false,
1890
1890
  logFileName: `test.${artifactKey}.log.json`,
1891
- coverageFileName: `coverage.${artifactKey}.log.json`,
1891
+ coverageFileName: `${artifactKey}.log.json`,
1892
1892
  buildCommand: formatBuildInvocation(buildInvocation),
1893
1893
  modeName,
1894
1894
  });
@@ -1967,7 +1967,7 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, suiteSelec
1967
1967
  const silentReporter = {};
1968
1968
  const modeLabels = modes.map((modeName) => modeName ?? "default");
1969
1969
  const showPerModeTimes = Boolean(runFlags.verbose);
1970
- const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
1970
+ const inputPatterns = await loadInputPatterns(configPath);
1971
1971
  const ordered = new Array(files.length);
1972
1972
  const useQueueDisplay = reporterSession.reporterKind == "default";
1973
1973
  const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
@@ -1996,7 +1996,7 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, suiteSelec
1996
1996
  });
1997
1997
  buildIntervals.push({ start: buildStartedAt, end: Date.now() });
1998
1998
  const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
1999
- const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
1999
+ const artifactKey = resolveArtifactStem(file, inputPatterns);
2000
2000
  result = await run(runFlags, configPath, [file], false, {
2001
2001
  reporter: silentReporter,
2002
2002
  reporterKind: "default",
@@ -2004,7 +2004,7 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, suiteSelec
2004
2004
  emitRunStart: false,
2005
2005
  emitRunComplete: false,
2006
2006
  logFileName: `test.${artifactKey}.log.json`,
2007
- coverageFileName: `coverage.${artifactKey}.log.json`,
2007
+ coverageFileName: `${artifactKey}.log.json`,
2008
2008
  buildCommand: formatBuildInvocation(buildInvocation),
2009
2009
  modeName,
2010
2010
  });
@@ -2707,57 +2707,19 @@ function isBareSuiteSelector(selector) {
2707
2707
  function stripSuiteSuffix(selector) {
2708
2708
  return selector.replace(/\.spec\.ts$/, "").replace(/\.ts$/, "");
2709
2709
  }
2710
- function resolveDuplicateSpecBasenames(files) {
2711
- const counts = new Map();
2712
- for (const file of files) {
2713
- const base = path.basename(file);
2714
- counts.set(base, (counts.get(base) ?? 0) + 1);
2715
- }
2716
- const duplicates = new Set();
2717
- for (const [base, count] of counts) {
2718
- if (count > 1)
2719
- duplicates.add(base);
2720
- }
2721
- return duplicates;
2722
- }
2723
- function resolvePerFileArtifactKey(file, duplicateSpecBasenames) {
2724
- const base = path.basename(file);
2725
- let raw = base;
2726
- if (duplicateSpecBasenames.has(base)) {
2727
- const disambiguator = resolvePerFileDisambiguator(file);
2728
- if (disambiguator.length) {
2729
- raw = `${base}.${disambiguator}`;
2730
- }
2731
- }
2732
- return raw.replace(/[^a-zA-Z0-9._-]/g, "_");
2733
- }
2734
- function resolvePerFileDisambiguator(file) {
2735
- const relDir = path.dirname(path.relative(process.cwd(), file));
2736
- if (!relDir.length || relDir == ".")
2737
- return "";
2738
- return relDir
2739
- .replace(/[\\/]+/g, "__")
2740
- .replace(/[^A-Za-z0-9._-]/g, "_")
2741
- .replace(/^_+|_+$/g, "");
2742
- }
2743
- function resolveArtifactFileNameForPreview(file, target, modeName, duplicateSpecBasenames) {
2744
- const base = path
2745
- .basename(file)
2746
- .replace(/\.spec\.ts$/, "")
2747
- .replace(/\.ts$/, "");
2748
- const legacy = !modeName
2749
- ? `${path.basename(file).replace(".ts", ".wasm")}`
2750
- : `${base}.${modeName}.${target}.wasm`;
2751
- if (!duplicateSpecBasenames.has(path.basename(file))) {
2752
- return legacy;
2753
- }
2754
- const disambiguator = resolvePerFileDisambiguator(file);
2755
- if (!disambiguator.length) {
2756
- return legacy;
2757
- }
2758
- const ext = path.extname(legacy);
2759
- const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
2760
- return `${stem}.${disambiguator}${ext}`;
2710
+ // Returns the spec relative path (under the configured input base) with the
2711
+ // trailing ".ts" stripped, suitable for use as a stable per-file key for
2712
+ // coverage and log filenames.
2713
+ function resolveArtifactStem(file, inputPatterns) {
2714
+ return resolveSpecRelativePath(file, inputPatterns).replace(/\.ts$/i, "");
2715
+ }
2716
+ async function loadInputPatterns(configPath) {
2717
+ const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
2718
+ return loadConfig(resolvedConfigPath, false).input;
2719
+ }
2720
+ async function loadFuzzInputPatterns(configPath) {
2721
+ const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
2722
+ return loadConfig(resolvedConfigPath, false).fuzz.input;
2761
2723
  }
2762
2724
  async function ensureWebBrowsersReady(configPath, modes, browserOverride) {
2763
2725
  const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
@@ -3235,8 +3197,8 @@ async function listExecutionPlan(command, configPath, selectors, modes, listFlag
3235
3197
  ? `No fuzz files matched: ${scope}`
3236
3198
  : `No test files matched: ${scope}`);
3237
3199
  }
3238
- const duplicateSpecBasenames = resolveDuplicateSpecBasenames(specFiles);
3239
- const duplicateFuzzBasenames = resolveDuplicateSpecBasenames(fuzzFiles);
3200
+ const inputPatterns = await loadInputPatterns(configPath);
3201
+ const fuzzInputPatterns = await loadFuzzInputPatterns(configPath);
3240
3202
  if (specFiles.length) {
3241
3203
  process.stdout.write(chalk.bold("Resolved files:\n"));
3242
3204
  for (const file of specFiles) {
@@ -3284,21 +3246,21 @@ async function listExecutionPlan(command, configPath, selectors, modes, listFlag
3284
3246
  if (specFiles.length) {
3285
3247
  process.stdout.write(" artifacts:\n");
3286
3248
  for (const file of specFiles) {
3287
- const artifactName = resolveArtifactFileNameForPreview(file, active.buildOptions.target, modeName, duplicateSpecBasenames);
3249
+ const artifactName = resolveArtifactPath(file, inputPatterns);
3288
3250
  process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
3289
3251
  }
3290
3252
  }
3291
3253
  if (fuzzFiles.length && command == "test") {
3292
3254
  process.stdout.write(" fuzz artifacts:\n");
3293
3255
  for (const file of fuzzFiles) {
3294
- const artifactName = resolveArtifactFileNameForPreview(file, "bindings", modeName, duplicateFuzzBasenames);
3256
+ const artifactName = resolveArtifactPath(file, fuzzInputPatterns);
3295
3257
  process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
3296
3258
  }
3297
3259
  }
3298
3260
  else if (command == "fuzz") {
3299
3261
  process.stdout.write(" artifacts:\n");
3300
3262
  for (const file of fuzzFiles) {
3301
- const artifactName = resolveArtifactFileNameForPreview(file, "bindings", modeName, duplicateFuzzBasenames);
3263
+ const artifactName = resolveArtifactPath(file, fuzzInputPatterns);
3302
3264
  process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
3303
3265
  }
3304
3266
  }
package/bin/util.js CHANGED
@@ -1398,3 +1398,83 @@ export function resolveProjectModule(specifier) {
1398
1398
  }
1399
1399
  return null;
1400
1400
  }
1401
+ // picomatch-compatible glob metacharacters; first occurrence marks the start
1402
+ // of the dynamic part of a pattern and everything before it is the static base.
1403
+ const GLOB_META_RE = /[*?[\](){}!|+@]/;
1404
+ // Longest non-glob prefix of a single pattern, returned with native separators.
1405
+ // Examples:
1406
+ // "assembly/__tests__/**/*.spec.ts" -> "assembly/__tests__"
1407
+ // "**/*.spec.ts" -> ""
1408
+ // "assembly/foo.spec.ts" -> "assembly" (no glob → use dirname)
1409
+ // "/abs/path/**/*.ts" -> "/abs/path"
1410
+ export function resolveGlobBase(pattern) {
1411
+ const normalized = pattern.replace(/\\/g, "/");
1412
+ const metaIdx = normalized.search(GLOB_META_RE);
1413
+ let base;
1414
+ if (metaIdx < 0) {
1415
+ const dir = dirname(normalized);
1416
+ base = dir == "." ? "" : dir;
1417
+ }
1418
+ else {
1419
+ const slice = normalized.slice(0, metaIdx);
1420
+ const lastSlash = slice.lastIndexOf("/");
1421
+ base = lastSlash < 0 ? "" : slice.slice(0, lastSlash);
1422
+ }
1423
+ if (!base.length)
1424
+ return "";
1425
+ return base.split("/").join(sep);
1426
+ }
1427
+ // Strip the most-specific matching configured input base off `file`, returning
1428
+ // the path relative to that base (with native separators). If no base matches,
1429
+ // returns the basename of the file. Comparison is component-wise — so
1430
+ // "assembly/__tests" is not a prefix of "assembly/__tests__/foo.spec.ts".
1431
+ export function resolveSpecRelativePath(file, inputPatterns) {
1432
+ const patterns = Array.isArray(inputPatterns)
1433
+ ? inputPatterns
1434
+ : [inputPatterns];
1435
+ const absFile = resolve(process.cwd(), file);
1436
+ const fileComponents = toComponents(absFile);
1437
+ let bestBaseAbs = null;
1438
+ let bestLength = -1;
1439
+ for (const pattern of patterns) {
1440
+ const base = resolveGlobBase(pattern);
1441
+ const absBase = base.length
1442
+ ? resolve(process.cwd(), base)
1443
+ : resolve(process.cwd());
1444
+ const baseComponents = toComponents(absBase);
1445
+ if (!isComponentPrefix(baseComponents, fileComponents))
1446
+ continue;
1447
+ if (baseComponents.length > bestLength) {
1448
+ bestBaseAbs = absBase;
1449
+ bestLength = baseComponents.length;
1450
+ }
1451
+ }
1452
+ if (bestBaseAbs == null)
1453
+ return basename(file);
1454
+ const rel = relative(bestBaseAbs, absFile);
1455
+ return rel.length ? rel : basename(file);
1456
+ }
1457
+ // Compute the artifact path (relative to outDir) for a given spec/fuzz source
1458
+ // file. Strips ".ts" only, keeping ".spec" / ".fuzz" suffixes so spec and
1459
+ // fuzz artifacts can coexist in the same outDir.
1460
+ // assembly/__tests__/array.spec.ts -> "array.spec.wasm"
1461
+ // assembly/__tests__/nested/array.spec.ts -> "nested/array.spec.wasm"
1462
+ // assembly/__fuzz__/nested/array.fuzz.ts -> "nested/array.fuzz.wasm"
1463
+ export function resolveArtifactPath(file, inputPatterns) {
1464
+ const rel = resolveSpecRelativePath(file, inputPatterns);
1465
+ return rel.replace(/\.ts$/i, ".wasm");
1466
+ }
1467
+ function toComponents(absPath) {
1468
+ return absPath
1469
+ .split(/[\\/]+/)
1470
+ .filter((segment, idx) => segment.length || idx == 0);
1471
+ }
1472
+ function isComponentPrefix(prefix, full) {
1473
+ if (prefix.length > full.length)
1474
+ return false;
1475
+ for (let i = 0; i < prefix.length; i++) {
1476
+ if (prefix[i] !== full[i])
1477
+ return false;
1478
+ }
1479
+ return true;
1480
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "as-test",
3
- "version": "1.1.5",
3
+ "version": "1.1.6-patch.1",
4
4
  "author": "Jairus Tanaka",
5
5
  "repository": {
6
6
  "type": "git",
@@ -298,10 +298,8 @@ export class CoverageTransform extends Visitor {
298
298
  node.body.statements.unshift(coverStmt);
299
299
  }
300
300
  else if (node.body instanceof ExpressionStatement) {
301
- const expression = node.body.expression;
302
- node.body = Node.createBlockStatement([Node.createReturnStatement(expression, expression.range)], expression.range);
303
- const bodyBlock = node.body;
304
- bodyBlock.statements.unshift(coverStmt);
301
+ const exprBody = node.body;
302
+ exprBody.expression = createCoverExpression(point.hash, exprBody.expression, node);
305
303
  }
306
304
  this.withScope(point, () => {
307
305
  this.visit(node.name, node);
@@ -445,6 +443,8 @@ export class CoverageTransform extends Visitor {
445
443
  super.visitReturnStatement(node);
446
444
  if (!node.value || isBuiltinCallExpression(node.value))
447
445
  return;
446
+ if (node.value.kind === NodeKind.This)
447
+ return;
448
448
  const path = node.range.source.normalizedPath;
449
449
  const point = this.createPoint(path, node.value, "Return");
450
450
  const replacer = new RangeTransform(node);