as-test 1.4.1 → 1.5.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.
@@ -1,6 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { spawn } from "child_process";
3
3
  import { glob } from "glob";
4
+ import { minimatch } from "minimatch";
4
5
  import { Channel, MessageType } from "../wipc.js";
5
6
  import {
6
7
  applyMode,
@@ -686,10 +687,60 @@ export async function run(
686
687
  const loadedConfig = loadConfig(resolvedConfigPath);
687
688
  const mode = applyMode(loadedConfig, options.modeName);
688
689
  const config = mode.config;
690
+ // Mode-level exclusion: when a mode override adds `!`-prefixed negations
691
+ // beyond the top-level config, honor them even when the orchestrator passes
692
+ // a single-file selector (`resolveInputPatterns` drops the configured input
693
+ // in that case). Top-level negations are intentionally not enforced here so
694
+ // explicit selectors can still override them (e.g. picking a __tmp_* spec).
695
+ if (options.modeName && selectors.length > 0) {
696
+ const topInput = Array.isArray(loadedConfig.input)
697
+ ? loadedConfig.input
698
+ : [loadedConfig.input];
699
+ const modeInput = Array.isArray(config.input)
700
+ ? config.input
701
+ : [config.input];
702
+ const topNegations = new Set(topInput.filter((p) => p.startsWith("!")));
703
+ const modeOnlyNegations = modeInput
704
+ .filter((p) => p.startsWith("!") && !topNegations.has(p))
705
+ .map((p) => p.slice(1));
706
+ if (modeOnlyNegations.length > 0) {
707
+ const cwd = process.cwd();
708
+ const allExcluded = selectors.every((sel) => {
709
+ const abs = path.isAbsolute(sel) ? sel : path.resolve(cwd, sel);
710
+ const rel = path.relative(cwd, abs).split(path.sep).join("/");
711
+ return modeOnlyNegations.some((pat) =>
712
+ minimatch(rel, pat, { dot: true, matchBase: true }),
713
+ );
714
+ });
715
+ if (allExcluded) {
716
+ return {
717
+ failed: false,
718
+ stats: collectRunStats([]),
719
+ buildTime: 0,
720
+ snapshotSummary: { matched: 0, created: 0, updated: 0, failed: 0 },
721
+ coverageSummary: {
722
+ enabled: false,
723
+ showPoints: false,
724
+ total: 0,
725
+ covered: 0,
726
+ uncovered: 0,
727
+ percent: 100,
728
+ files: [],
729
+ },
730
+ reports: [],
731
+ logSummary: { count: 0, file: null, groups: [], text: "" },
732
+ };
733
+ }
734
+ }
735
+ }
689
736
  const inputPatterns = resolveInputPatterns(config.input, selectors);
690
- const inputFiles = (await glob(inputPatterns)).sort((a, b) =>
691
- a.localeCompare(b),
692
- );
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));
693
744
  const snapshotEnabled = flags.snapshot !== false;
694
745
  const createSnapshots = Boolean(flags.createSnapshots);
695
746
  const overwriteSnapshots = Boolean(flags.overwriteSnapshots);
@@ -798,7 +849,20 @@ export async function run(
798
849
  token.replace(/<name>/g, fileBase).replace(/<file>/g, fileToken),
799
850
  ),
800
851
  };
801
- const runCommandForLog = formatInvocation(invocation);
852
+ // A managed web-session run never spawns the standalone runner, so its
853
+ // repro must re-run the managed CLI command (which renders the same panel
854
+ // UI) rather than `default.web.js <wasm>`, a different web stack.
855
+ const runCommandForLog = webSession
856
+ ? formatInvocation({
857
+ command: execPath,
858
+ args: [
859
+ process.argv[1] ?? "",
860
+ "run",
861
+ resolveSpecRelativePath(file, config.input),
862
+ ...(options.modeName ? ["--mode", options.modeName] : []),
863
+ ],
864
+ })
865
+ : formatInvocation(invocation);
802
866
  const snapshotStore = new SnapshotStore(
803
867
  file,
804
868
  config.snapshotDir,
@@ -1991,7 +2055,7 @@ async function runProcess(
1991
2055
  if (reportStream.sawChunkStart && !reportStream.sawChunkEnd) {
1992
2056
  reportStream.chunkFramesReceived++;
1993
2057
  reportStream.chunkBytesReceived += data.length;
1994
- reportStream.chunks.push(data.toString("utf8"));
2058
+ reportStream.chunks.push(Buffer.from(data));
1995
2059
  return;
1996
2060
  }
1997
2061
  try {
@@ -2041,7 +2105,9 @@ async function runProcess(
2041
2105
  parseError =
2042
2106
  parseError ?? "missing report:end marker for chunked report payload";
2043
2107
  } else {
2044
- const chunkedPayload = reportStream.chunks.join("");
2108
+ const chunkedPayload = Buffer.concat(reportStream.chunks).toString(
2109
+ "utf8",
2110
+ );
2045
2111
  try {
2046
2112
  report = JSON.parse(chunkedPayload);
2047
2113
  parseError = null;
@@ -2099,14 +2165,19 @@ async function runProcess(
2099
2165
  runtimeEvents,
2100
2166
  );
2101
2167
  if (synthesized) {
2102
- reporter.onWarning?.({
2103
- message:
2104
- "runtime report payload missing; reconstructed result from streamed lifecycle events",
2105
- });
2106
- if (code !== 0 || hasMeaningfulRuntimeOutput(stderrBuffer)) {
2168
+ const exitedEarly = !runtimeEvents.sawFileEnd;
2169
+ if (
2170
+ exitedEarly ||
2171
+ code !== 0 ||
2172
+ hasMeaningfulRuntimeOutput(stderrBuffer)
2173
+ ) {
2107
2174
  const errorParts = [];
2108
2175
  if (code !== 0) {
2109
2176
  errorParts.push(`child process exited with code ${code}`);
2177
+ } else if (exitedEarly) {
2178
+ errorParts.push(
2179
+ "test runtime exited before reporting file completion",
2180
+ );
2110
2181
  }
2111
2182
  const stderrText = normalizeRuntimeOutput(stderrBuffer);
2112
2183
  if (stderrText.length) {
@@ -2135,12 +2206,18 @@ async function runProcess(
2135
2206
  modeName,
2136
2207
  code !== 0
2137
2208
  ? `test runtime failed with exit code ${code}`
2138
- : "test runtime wrote to stderr",
2209
+ : exitedEarly
2210
+ ? "test runtime exited before completing the test file"
2211
+ : "test runtime wrote to stderr",
2139
2212
  errorText,
2140
2213
  stdoutBuffer,
2141
2214
  stderrBuffer,
2142
2215
  );
2143
2216
  }
2217
+ reporter.onWarning?.({
2218
+ message:
2219
+ "runtime report payload missing; reconstructed result from streamed lifecycle events",
2220
+ });
2144
2221
  return synthesized;
2145
2222
  }
2146
2223
  const errorText = "missing report payload from test runtime";
@@ -2375,7 +2452,7 @@ async function runWebSessionProcess(
2375
2452
  if (reportStream.sawChunkStart && !reportStream.sawChunkEnd) {
2376
2453
  reportStream.chunkFramesReceived++;
2377
2454
  reportStream.chunkBytesReceived += data.length;
2378
- reportStream.chunks.push(data.toString("utf8"));
2455
+ reportStream.chunks.push(Buffer.from(data));
2379
2456
  return;
2380
2457
  }
2381
2458
  try {
@@ -2422,7 +2499,9 @@ async function runWebSessionProcess(
2422
2499
  parseError =
2423
2500
  parseError ?? "missing report:end marker for chunked report payload";
2424
2501
  } else {
2425
- const chunkedPayload = reportStream.chunks.join("");
2502
+ const chunkedPayload = Buffer.concat(reportStream.chunks).toString(
2503
+ "utf8",
2504
+ );
2426
2505
  try {
2427
2506
  report = JSON.parse(chunkedPayload);
2428
2507
  parseError = null;
@@ -2480,14 +2559,19 @@ async function runWebSessionProcess(
2480
2559
  runtimeEvents,
2481
2560
  );
2482
2561
  if (synthesized) {
2483
- reporter.onWarning?.({
2484
- message:
2485
- "runtime report payload missing; reconstructed result from streamed lifecycle events",
2486
- });
2487
- if (code !== 0 || hasMeaningfulRuntimeOutput(stderrBuffer)) {
2562
+ const exitedEarly = !runtimeEvents.sawFileEnd;
2563
+ if (
2564
+ exitedEarly ||
2565
+ code !== 0 ||
2566
+ hasMeaningfulRuntimeOutput(stderrBuffer)
2567
+ ) {
2488
2568
  const errorParts = [];
2489
2569
  if (code !== 0) {
2490
2570
  errorParts.push(`child process exited with code ${code}`);
2571
+ } else if (exitedEarly) {
2572
+ errorParts.push(
2573
+ "test runtime exited before reporting file completion",
2574
+ );
2491
2575
  }
2492
2576
  const stderrText = normalizeRuntimeOutput(stderrBuffer);
2493
2577
  if (stderrText.length) {
@@ -2499,10 +2583,35 @@ async function runWebSessionProcess(
2499
2583
  reportStream,
2500
2584
  runtimeEvents,
2501
2585
  );
2502
- reporter.onWarning?.({
2503
- message: `${errorParts.join("; ")}\n${diagnostics}`,
2586
+ errorParts.push(diagnostics);
2587
+ const errorText = errorParts.join("\n\n");
2588
+ persistCrashRecord(crashDir, {
2589
+ kind: "test",
2590
+ file: specFile,
2591
+ entryKey: crashEntryKey,
2592
+ mode: modeName ?? "default",
2593
+ error: errorText || "runtime reported an unknown error",
2594
+ stdout: stdoutBuffer,
2595
+ stderr: stderrBuffer,
2504
2596
  });
2597
+ return appendRuntimeFailureReport(
2598
+ synthesized,
2599
+ specFile,
2600
+ modeName,
2601
+ code !== 0
2602
+ ? `test runtime failed with exit code ${code}`
2603
+ : exitedEarly
2604
+ ? "test runtime exited before completing the test file"
2605
+ : "test runtime wrote to stderr",
2606
+ errorText,
2607
+ stdoutBuffer,
2608
+ stderrBuffer,
2609
+ );
2505
2610
  }
2611
+ reporter.onWarning?.({
2612
+ message:
2613
+ "runtime report payload missing; reconstructed result from streamed lifecycle events",
2614
+ });
2506
2615
  return synthesized;
2507
2616
  }
2508
2617
  const diagnostics = buildRuntimeReportDiagnostics(
@@ -16,6 +16,8 @@ export class PersistentWebSessionHost {
16
16
  this.serverSockets = new Set();
17
17
  this.wsSocket = null;
18
18
  this.wsBuffer = Buffer.alloc(0);
19
+ this.wsFragmentOpcode = 0;
20
+ this.wsFragments = [];
19
21
  this.ready = false;
20
22
  this.closed = false;
21
23
  this.currentJob = null;
@@ -334,14 +336,32 @@ export class PersistentWebSessionHost {
334
336
  payload = Buffer.from(payload);
335
337
  }
336
338
  this.wsBuffer = this.wsBuffer.subarray(offset + maskLength + length);
337
- if (opcode == 0x8) {
339
+ // Reassemble fragmented messages: a non-final data frame (0x1/0x2 with
340
+ // FIN=0) starts a sequence continued by 0x0 frames until FIN=1. Chromium
341
+ // fragments large (64KB) frames, so without this the wipc stream desyncs
342
+ // and the report payload is corrupted.
343
+ const fin = (first & 0x80) !== 0;
344
+ let effectiveOpcode = opcode;
345
+ if (opcode == 0x0) {
346
+ this.wsFragments.push(payload);
347
+ if (!fin) continue;
348
+ payload = Buffer.concat(this.wsFragments);
349
+ this.wsFragments = [];
350
+ effectiveOpcode = this.wsFragmentOpcode;
351
+ this.wsFragmentOpcode = 0;
352
+ } else if ((opcode == 0x1 || opcode == 0x2) && !fin) {
353
+ this.wsFragmentOpcode = opcode;
354
+ this.wsFragments = [payload];
355
+ continue;
356
+ }
357
+ if (effectiveOpcode == 0x8) {
338
358
  return;
339
359
  }
340
- if (opcode == 0x1) {
360
+ if (effectiveOpcode == 0x1) {
341
361
  this.onControl(payload.toString("utf8"));
342
362
  continue;
343
363
  }
344
- if (opcode == 0x2 && this.currentJob) {
364
+ if (effectiveOpcode == 0x2 && this.currentJob) {
345
365
  this.currentJob.onBinary(Buffer.from(payload));
346
366
  }
347
367
  }
@@ -1012,6 +1032,10 @@ async function instantiate(imports) {
1012
1032
  if (typeof helper.instantiate != "function") {
1013
1033
  throw new Error("bindings helper missing instantiate export");
1014
1034
  }
1035
+ // Stub before the helper runs: it reads imports.<module> while building its
1036
+ // import object, so a missing import must be filled here, not just in the
1037
+ // WebAssembly.instantiate wrapper (which runs too late for raw bindings).
1038
+ stubMissingImports(module, imports, ["wasi_snapshot_preview1"]);
1015
1039
  const instance = await captureInstantiateInstance(async () => {
1016
1040
  await helper.instantiate(module, imports);
1017
1041
  });
@@ -1026,11 +1050,31 @@ async function instantiate(imports) {
1026
1050
  }
1027
1051
  const binary = await fetchWasmBinary(wasmUrl);
1028
1052
  const module = new WebAssembly.Module(binary);
1053
+ stubMissingImports(module, imports, ["wasi_snapshot_preview1"]);
1029
1054
  const result = await WebAssembly.instantiate(module, imports);
1030
1055
  const wasmInstance = result instanceof WebAssembly.Instance ? result : result.instance;
1031
1056
  return decorateInstance(wasmInstance);
1032
1057
  }
1033
1058
 
1059
+ // Fills function imports the module declares but nothing provided with a no-op
1060
+ // stub, so instantiation never fails on a missing import (e.g. an unmocked
1061
+ // mockImport target with no host implementation). Mirrors the node runner.
1062
+ function stubMissingImports(module, importObject, skipModules) {
1063
+ const skip = new Set(skipModules || []);
1064
+ for (const entry of WebAssembly.Module.imports(module)) {
1065
+ if (entry.kind !== "function" || skip.has(entry.module)) continue;
1066
+ let mod = importObject[entry.module];
1067
+ if (!mod || typeof mod !== "object") {
1068
+ mod = {};
1069
+ importObject[entry.module] = mod;
1070
+ }
1071
+ if (!(entry.name in mod)) {
1072
+ mod[entry.name] = () => 0;
1073
+ }
1074
+ }
1075
+ return importObject;
1076
+ }
1077
+
1034
1078
  async function fetchWasmBinary(wasmUrl) {
1035
1079
  const response = await fetch(wasmUrl);
1036
1080
  if (!response.ok) throw new Error("failed to fetch wasm artifact: " + response.status);
@@ -1041,6 +1085,9 @@ async function captureInstantiateInstance(run) {
1041
1085
  const originalInstantiate = WebAssembly.instantiate.bind(WebAssembly);
1042
1086
  let captured = null;
1043
1087
  WebAssembly.instantiate = async (source, importObject) => {
1088
+ if (source instanceof WebAssembly.Module && importObject) {
1089
+ stubMissingImports(source, importObject, ["wasi_snapshot_preview1"]);
1090
+ }
1044
1091
  const result = await originalInstantiate(source, importObject);
1045
1092
  captured = result instanceof WebAssembly.Instance ? result : result.instance;
1046
1093
  return result;
package/bin/index.js CHANGED
@@ -2344,7 +2344,10 @@ async function runWatchLoop(
2344
2344
  if (byte === 0x03) {
2345
2345
  if (rawModeEnabled) stdin.setRawMode(false);
2346
2346
  closeAllWatchers();
2347
- process.exit(0);
2347
+ // Exit non-zero if the last run left anything failing, or if a run
2348
+ // is still in flight (an interrupted run counts as a failure), so
2349
+ // quitting a red watch session still fails CI / shell pipelines.
2350
+ process.exit(isRunning || failingSpecs.size ? 1 : 0);
2348
2351
  }
2349
2352
  if (isRunning) break;
2350
2353
  if (byte === 0x77 || byte === 0x57) {
@@ -2394,7 +2397,8 @@ async function runWatchLoop(
2394
2397
  process.on("SIGINT", () => {
2395
2398
  if (rawModeEnabled) stdin.setRawMode(false);
2396
2399
  closeAllWatchers();
2397
- process.exit(0);
2400
+ // Mirror the raw-mode ctrl+c path: a failing or in-flight run exits 1.
2401
+ process.exit(isRunning || failingSpecs.size ? 1 : 0);
2398
2402
  });
2399
2403
  // Keep the process alive
2400
2404
  await new Promise(() => {});
package/bin/wipc.js CHANGED
@@ -45,14 +45,52 @@ export class Channel {
45
45
  if (this.buffer.length < Channel.HEADER_SIZE) return;
46
46
  const type = this.buffer.readUInt8(4);
47
47
  const length = this.buffer.readUInt32LE(5);
48
+ // The magic can occur by chance inside ordinary stdout output (e.g. a
49
+ // test printing binary data or the literal string "WIPC"). A genuine
50
+ // frame always carries a known type and a bounded length; if either
51
+ // check fails the match is coincidental, so surface the magic bytes as
52
+ // passthrough and resume scanning past them rather than crash or stall.
53
+ if (!Channel.isKnownType(type) || length > Channel.MAX_FRAME_SIZE) {
54
+ this.resyncPastMagic();
55
+ continue;
56
+ }
48
57
  const frameSize = Channel.HEADER_SIZE + length;
49
58
  if (this.buffer.length < frameSize) return;
50
59
  const payload = this.buffer.subarray(Channel.HEADER_SIZE, frameSize);
60
+ if (type === MessageType.CALL) {
61
+ // CALL payloads are always JSON. If the bytes do not parse the magic
62
+ // was coincidental — do NOT consume the frame; treat the magic as
63
+ // output and resync so the rest flushes as passthrough.
64
+ let parsed;
65
+ try {
66
+ parsed = JSON.parse(payload.toString("utf8"));
67
+ } catch {
68
+ this.resyncPastMagic();
69
+ continue;
70
+ }
71
+ this.buffer = this.buffer.subarray(frameSize);
72
+ this.onCall(parsed);
73
+ continue;
74
+ }
51
75
  this.buffer = this.buffer.subarray(frameSize);
52
- this.handleFrame(type, payload);
76
+ this.dispatchFrame(type, payload);
53
77
  }
54
78
  }
55
- handleFrame(type, payload) {
79
+ static isKnownType(type) {
80
+ return (
81
+ type === MessageType.OPEN ||
82
+ type === MessageType.CLOSE ||
83
+ type === MessageType.CALL ||
84
+ type === MessageType.DATA
85
+ );
86
+ }
87
+ // A coincidental magic match: emit the magic bytes as ordinary output and
88
+ // advance past them so scanning can continue from the next byte.
89
+ resyncPastMagic() {
90
+ this.onPassthrough(this.buffer.subarray(0, Channel.MAGIC.length));
91
+ this.buffer = this.buffer.subarray(Channel.MAGIC.length);
92
+ }
93
+ dispatchFrame(type, payload) {
56
94
  switch (type) {
57
95
  case MessageType.OPEN:
58
96
  this.onOpen();
@@ -60,14 +98,9 @@ export class Channel {
60
98
  case MessageType.CLOSE:
61
99
  this.onClose();
62
100
  break;
63
- case MessageType.CALL:
64
- this.onCall(JSON.parse(payload.toString("utf8")));
65
- break;
66
101
  case MessageType.DATA:
67
102
  this.onDataMessage(payload);
68
103
  break;
69
- default:
70
- throw new Error(`Unknown frame type: ${type}`);
71
104
  }
72
105
  }
73
106
  onPassthrough(_data) {}
@@ -79,3 +112,9 @@ export class Channel {
79
112
  Channel.MAGIC = Buffer.from("WIPC");
80
113
  Channel.HEADER_SIZE = 9;
81
114
  Channel.MAGIC_PREFIX_MAX = Channel.MAGIC.length - 1;
115
+ // Upper bound on a single frame's declared payload length. Real frames are
116
+ // small JSON events or report chunks (<= 64 KiB); anything larger means the
117
+ // 4 magic bytes appeared by coincidence inside passthrough output. Without
118
+ // this bound a forged length would make us buffer (and swallow real frames)
119
+ // indefinitely. The margin over 64 KiB future-proofs larger report chunks.
120
+ Channel.MAX_FRAME_SIZE = 16 * 1024 * 1024;
@@ -200,7 +200,11 @@ async function instantiateRawInstance(wasmPath, helperPath, imports) {
200
200
  if (typeof helper.instantiate != "function") {
201
201
  throw new Error("bindings helper missing instantiate export");
202
202
  }
203
- const mergedImports = mergeImports(withNodeIo({}), imports);
203
+ const mergedImports = stubMissingImports(
204
+ module,
205
+ mergeImports(withNodeIo({}), imports),
206
+ ["wasi_snapshot_preview1"],
207
+ );
204
208
  const instance = await captureHelperInstance(async () => {
205
209
  await helper.instantiate(module, mergedImports);
206
210
  });
@@ -216,6 +220,7 @@ async function instantiateEsmInstance(wasmPath, helperPath, imports) {
216
220
  "esm bindings do not support custom imports in as-test/lib; pass {} or switch to raw bindings",
217
221
  );
218
222
  }
223
+ patchNodeIo();
219
224
  const instance = await captureHelperInstance(async () => {
220
225
  await import(`${pathToFileURL(helperPath).href}?t=${Date.now()}`);
221
226
  });
@@ -279,6 +284,8 @@ async function instantiateWebInstance(wasmPath, imports) {
279
284
  let ready = false;
280
285
  let wsSocket = null;
281
286
  let wsBuffer = Buffer.alloc(0);
287
+ let wsFragmentOpcode = 0;
288
+ let wsFragments = [];
282
289
  let stdinBuffer = Buffer.alloc(0);
283
290
  let browserProcess = null;
284
291
  let browserStderr = "";
@@ -420,15 +427,29 @@ async function instantiateWebInstance(wasmPath, imports) {
420
427
  payload = Buffer.from(payload);
421
428
  }
422
429
  wsBuffer = wsBuffer.subarray(offset + maskLength + length);
423
- if (opcode == 0x8) {
430
+ const fin = (first & 0x80) !== 0;
431
+ let effectiveOpcode = opcode;
432
+ if (opcode == 0x0) {
433
+ wsFragments.push(payload);
434
+ if (!fin) continue;
435
+ payload = Buffer.concat(wsFragments);
436
+ wsFragments = [];
437
+ effectiveOpcode = wsFragmentOpcode;
438
+ wsFragmentOpcode = 0;
439
+ } else if ((opcode == 0x1 || opcode == 0x2) && !fin) {
440
+ wsFragmentOpcode = opcode;
441
+ wsFragments = [payload];
442
+ continue;
443
+ }
444
+ if (effectiveOpcode == 0x8) {
424
445
  finish(0);
425
446
  return;
426
447
  }
427
- if (opcode == 0x1) {
448
+ if (effectiveOpcode == 0x1) {
428
449
  onControl(payload.toString("utf8"));
429
450
  continue;
430
451
  }
431
- if (opcode == 0x2) {
452
+ if (effectiveOpcode == 0x2) {
432
453
  process.stdout.write(payload);
433
454
  }
434
455
  }
@@ -626,20 +647,23 @@ function hasUserImports(imports) {
626
647
  return Object.keys(imports).length > 0;
627
648
  }
628
649
  function createWasmImports(module, imports) {
629
- const mergedImports = mergeImports(withNodeIo({}), imports);
630
- if (!mergedImports.env || typeof mergedImports.env != "object") {
631
- mergedImports.env = {};
632
- }
650
+ return stubMissingImports(module, mergeImports(withNodeIo({}), imports));
651
+ }
652
+ function stubMissingImports(module, imports, skipModules = []) {
653
+ const skip = new Set(skipModules);
654
+ const bag = imports;
633
655
  for (const entry of WebAssembly.Module.imports(module)) {
634
- if (
635
- entry.module == "env" &&
636
- entry.kind == "function" &&
637
- !(entry.name in mergedImports.env)
638
- ) {
639
- mergedImports.env[entry.name] = () => 0;
656
+ if (entry.kind != "function" || skip.has(entry.module)) continue;
657
+ let mod = bag[entry.module];
658
+ if (!mod || typeof mod != "object") {
659
+ mod = {};
660
+ bag[entry.module] = mod;
661
+ }
662
+ if (!(entry.name in mod)) {
663
+ mod[entry.name] = () => 0;
640
664
  }
641
665
  }
642
- return mergedImports;
666
+ return imports;
643
667
  }
644
668
  let patchedWasiWarning = false;
645
669
  function suppressExperimentalWasiWarning() {
@@ -1153,6 +1177,12 @@ async function captureHelperInstance(runHelper) {
1153
1177
  const originalInstantiate = WebAssembly.instantiate.bind(WebAssembly);
1154
1178
  let instance = null;
1155
1179
  WebAssembly.instantiate = async (source, importObject) => {
1180
+ if (source instanceof WebAssembly.Module && importObject) {
1181
+ stubMissingImports(source, importObject, [
1182
+ "env",
1183
+ "wasi_snapshot_preview1",
1184
+ ]);
1185
+ }
1156
1186
  const result = await originalInstantiate(source, importObject);
1157
1187
  if (result instanceof WebAssembly.Instance) {
1158
1188
  instance = result;
@@ -109,6 +109,10 @@ async function instantiate(imports) {
109
109
  if (typeof helper.instantiate != "function") {
110
110
  throw new Error("bindings helper missing instantiate export");
111
111
  }
112
+ // Stub before the helper runs: it reads imports.<module> while building its
113
+ // import object, so a missing import must be filled here, not just in the
114
+ // WebAssembly.instantiate wrapper (which runs too late for raw bindings).
115
+ stubMissingImports(module, imports, ["wasi_snapshot_preview1"]);
112
116
  const instance = await captureInstantiateInstance(async () => {
113
117
  await helper.instantiate(module, imports);
114
118
  });
@@ -125,11 +129,31 @@ async function instantiate(imports) {
125
129
  }
126
130
  const binary = await fetchWasmBinary(wasmUrl);
127
131
  const module = new WebAssembly.Module(binary);
132
+ stubMissingImports(module, imports, ["wasi_snapshot_preview1"]);
128
133
  const result = await WebAssembly.instantiate(module, imports);
129
134
  const instance = result instanceof WebAssembly.Instance ? result : result.instance;
130
135
  return decorateInstance(instance);
131
136
  }
132
137
 
138
+ // Fills function imports the module declares but nothing provided with a no-op
139
+ // stub, so instantiation never fails on a missing import (e.g. an unmocked
140
+ // mockImport target with no host implementation). Mirrors the node runner.
141
+ function stubMissingImports(module, importObject, skipModules) {
142
+ const skip = new Set(skipModules || []);
143
+ for (const entry of WebAssembly.Module.imports(module)) {
144
+ if (entry.kind !== "function" || skip.has(entry.module)) continue;
145
+ let mod = importObject[entry.module];
146
+ if (!mod || typeof mod !== "object") {
147
+ mod = {};
148
+ importObject[entry.module] = mod;
149
+ }
150
+ if (!(entry.name in mod)) {
151
+ mod[entry.name] = () => 0;
152
+ }
153
+ }
154
+ return importObject;
155
+ }
156
+
133
157
  async function fetchWasmBinary(wasmUrl) {
134
158
  const response = await fetch(wasmUrl);
135
159
  if (!response.ok) {
@@ -142,6 +166,9 @@ async function captureInstantiateInstance(run) {
142
166
  const originalInstantiate = WebAssembly.instantiate.bind(WebAssembly);
143
167
  let captured = null;
144
168
  WebAssembly.instantiate = async (source, importObject) => {
169
+ if (source instanceof WebAssembly.Module && importObject) {
170
+ stubMissingImports(source, importObject, ["wasi_snapshot_preview1"]);
171
+ }
145
172
  const result = await originalInstantiate(source, importObject);
146
173
  if (result instanceof WebAssembly.Instance) {
147
174
  captured = result;