as-test 1.1.4 → 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,5 +1,29 @@
1
1
  # Change Log
2
2
 
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
19
+
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.
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.
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.
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)`.
26
+
3
27
  ## 2026-05-14 - v1.1.4
4
28
 
5
29
  - feat: add `coverage.mode` (`project` or `all`) plus `coverage.dependencies` package allowlisting so dependency coverage can include normal or pnpm-installed packages without raw path globs.
@@ -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))