as-test 1.0.15 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,10 @@ import { applyMode, formatTime, getExec, loadConfig, tokenizeCommand, } from "..
6
6
  import * as path from "path";
7
7
  import { pathToFileURL } from "url";
8
8
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
9
- import { buildWebRunnerHooksSource, buildWebRunnerSource, } from "./web-runner-source.js";
9
+ import { PassThrough } from "stream";
10
+ import { buildWebRunnerSource } from "./web-runner-source.js";
11
+ import { PersistentWebSessionHost } from "./web-session.js";
12
+ import { build } from "./build-core.js";
10
13
  import { createReporter as createDefaultReporter } from "../reporters/default.js";
11
14
  import { createTapReporter } from "../reporters/tap.js";
12
15
  import { persistCrashRecord } from "../crash-store.js";
@@ -597,59 +600,82 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
597
600
  updated: 0,
598
601
  failed: 0,
599
602
  };
600
- for (let i = 0; i < inputFiles.length; i++) {
601
- const file = inputFiles[i];
602
- const outFile = path.join(config.outDir, resolveArtifactFileName(file, config.buildOptions.target, options.modeName, duplicateSpecBasenames));
603
- const fileBase = file
604
- .slice(file.lastIndexOf("/") + 1)
605
- .replace(".ts", "")
606
- .replace(".spec", "");
607
- const fileToken = config.buildOptions.target == "bindings" &&
608
- !runtimeTokens.some((token) => token.includes("<file>"))
609
- ? resolveBindingsHelperPath(outFile)
610
- : outFile;
611
- const invocation = {
612
- command: execPath,
613
- args: runtimeTokens
614
- .slice(1)
615
- .map((token) => token.replace(/<name>/g, fileBase).replace(/<file>/g, fileToken)),
616
- };
617
- const runCommandForLog = formatInvocation(invocation);
618
- const snapshotStore = new SnapshotStore(file, config.snapshotDir, duplicateSpecBasenames);
619
- let report;
620
- try {
621
- report = await runProcess(invocation, file, config.fuzz.crashDir, options.modeName, snapshotStore, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, reporterKind == "tap", {
622
- ...mode.env,
623
- ...config.runOptions.env,
603
+ let buildTime = 0;
604
+ const ownedWebSession = options.webSession === undefined &&
605
+ shouldUsePersistentHeadfulWebSession(config.buildOptions.target, runtimeCommand)
606
+ ? await PersistentWebSessionHost.start(false)
607
+ : null;
608
+ const webSession = options.webSession ?? ownedWebSession;
609
+ try {
610
+ for (let i = 0; i < inputFiles.length; i++) {
611
+ const file = inputFiles[i];
612
+ const outFile = path.join(config.outDir, resolveArtifactFileName(file, config.buildOptions.target, options.modeName, duplicateSpecBasenames));
613
+ if (!existsSync(outFile)) {
614
+ const buildStartedAt = Date.now();
615
+ await build(resolvedConfigPath, [file], options.modeName, { coverage: flags.coverage }, {}, loadedConfig);
616
+ buildTime += Date.now() - buildStartedAt;
617
+ }
618
+ const fileBase = file
619
+ .slice(file.lastIndexOf("/") + 1)
620
+ .replace(".ts", "")
621
+ .replace(".spec", "");
622
+ const fileToken = outFile;
623
+ const runtimeTargetEnv = resolveRuntimeTargetEnv(config.buildOptions.target, outFile);
624
+ const invocation = {
625
+ command: execPath,
626
+ args: runtimeTokens
627
+ .slice(1)
628
+ .map((token) => token.replace(/<name>/g, fileBase).replace(/<file>/g, fileToken)),
629
+ };
630
+ const runCommandForLog = formatInvocation(invocation);
631
+ const snapshotStore = new SnapshotStore(file, config.snapshotDir, duplicateSpecBasenames);
632
+ let report;
633
+ try {
634
+ const runtimeEnv = {
635
+ ...mode.env,
636
+ ...config.runOptions.env,
637
+ ...runtimeTargetEnv,
638
+ ...(process.env.BROWSER?.trim()
639
+ ? { BROWSER: process.env.BROWSER.trim() }
640
+ : config.runOptions.runtime.browser.trim()
641
+ ? { BROWSER: config.runOptions.runtime.browser.trim() }
642
+ : {}),
643
+ };
644
+ report = webSession
645
+ ? await runWebSessionProcess(webSession, file, config.fuzz.crashDir, options.modeName, snapshotStore, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, reporterKind == "tap", runtimeEnv)
646
+ : await runProcess(invocation, file, config.fuzz.crashDir, options.modeName, snapshotStore, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, reporterKind == "tap", runtimeEnv);
647
+ }
648
+ catch (error) {
649
+ const modeLabel = options.modeName ?? "default";
650
+ const details = error instanceof Error ? error.message : String(error);
651
+ throw new Error(`Failed to run ${path.basename(file)} in mode ${modeLabel} with ${details}`);
652
+ }
653
+ const normalized = normalizeReport(report);
654
+ const selectedSuites = options.suiteSelectors?.length
655
+ ? filterSelectedSuites(normalized.suites, options.suiteSelectors, file, options.modeName ?? "default")
656
+ : normalized.suites;
657
+ snapshotStore.flush();
658
+ snapshotSummary.matched += snapshotStore.matched;
659
+ snapshotSummary.created += snapshotStore.created;
660
+ snapshotSummary.updated += snapshotStore.updated;
661
+ snapshotSummary.failed += snapshotStore.failed;
662
+ reports.push({
663
+ file,
664
+ modeName: options.modeName ?? "default",
665
+ suites: selectedSuites,
666
+ coverage: normalized.coverage,
667
+ runCommand: runCommandForLog,
668
+ snapshotSummary: {
669
+ matched: snapshotStore.matched,
670
+ created: snapshotStore.created,
671
+ updated: snapshotStore.updated,
672
+ failed: snapshotStore.failed,
673
+ },
624
674
  });
625
675
  }
626
- catch (error) {
627
- const modeLabel = options.modeName ?? "default";
628
- const details = error instanceof Error ? error.message : String(error);
629
- throw new Error(`Failed to run ${path.basename(file)} in mode ${modeLabel} with ${details}`);
630
- }
631
- const normalized = normalizeReport(report);
632
- const selectedSuites = options.suiteSelectors?.length
633
- ? filterSelectedSuites(normalized.suites, options.suiteSelectors, file, options.modeName ?? "default")
634
- : normalized.suites;
635
- snapshotStore.flush();
636
- snapshotSummary.matched += snapshotStore.matched;
637
- snapshotSummary.created += snapshotStore.created;
638
- snapshotSummary.updated += snapshotStore.updated;
639
- snapshotSummary.failed += snapshotStore.failed;
640
- reports.push({
641
- file,
642
- modeName: options.modeName ?? "default",
643
- suites: selectedSuites,
644
- coverage: normalized.coverage,
645
- runCommand: runCommandForLog,
646
- snapshotSummary: {
647
- matched: snapshotStore.matched,
648
- created: snapshotStore.created,
649
- updated: snapshotStore.updated,
650
- failed: snapshotStore.failed,
651
- },
652
- });
676
+ }
677
+ finally {
678
+ await ownedWebSession?.close();
653
679
  }
654
680
  if (config.logs && config.logs != "none") {
655
681
  const logRoot = path.join(process.cwd(), config.logs);
@@ -686,7 +712,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
686
712
  clean: cleanOutput,
687
713
  snapshotEnabled,
688
714
  showCoverage,
689
- buildTime: 0,
715
+ buildTime,
690
716
  snapshotSummary,
691
717
  coverageSummary,
692
718
  stats,
@@ -705,7 +731,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
705
731
  }
706
732
  return {
707
733
  failed,
708
- buildTime: 0,
734
+ buildTime,
709
735
  stats,
710
736
  snapshotSummary,
711
737
  coverageSummary,
@@ -723,6 +749,9 @@ function resolveRuntimeCommand(runtimeRun, target, emitWarnings = true) {
723
749
  const normalized = resolveLegacyRuntime(targetDefaultAligned, target, emitWarnings);
724
750
  return fallbackToDefaultRuntime(normalized, target, emitWarnings);
725
751
  }
752
+ function shouldUsePersistentHeadfulWebSession(target, runtimeCommand) {
753
+ return target == "web" && !runtimeCommand.includes("--headless");
754
+ }
726
755
  function alignDefaultRuntimeToTarget(runtimeRun, target) {
727
756
  const fallback = getDefaultRuntimeFallback(target);
728
757
  if (!fallback)
@@ -817,19 +846,19 @@ function fallbackToDefaultRuntime(runtimeRun, target, emitWarnings) {
817
846
  function getDefaultRuntimeFallback(target) {
818
847
  if (target == "wasi") {
819
848
  return {
820
- command: "node ./.as-test/runners/default.wasi.js <file>",
849
+ command: "node ./.as-test/runners/default.wasi.js",
821
850
  scriptPath: "./.as-test/runners/default.wasi.js",
822
851
  };
823
852
  }
824
853
  if (target == "bindings") {
825
854
  return {
826
- command: "node ./.as-test/runners/default.bindings.js <file>",
855
+ command: "node ./.as-test/runners/default.bindings.js",
827
856
  scriptPath: "./.as-test/runners/default.bindings.js",
828
857
  };
829
858
  }
830
859
  if (target == "web") {
831
860
  return {
832
- command: "node ./.as-test/runners/default.web.js <file>",
861
+ command: "node ./.as-test/runners/default.web.js",
833
862
  scriptPath: "./.as-test/runners/default.web.js",
834
863
  };
835
864
  }
@@ -841,7 +870,6 @@ function ensureDefaultRuntimeRunner(target, emitWarnings) {
841
870
  return null;
842
871
  const resolvedScriptPath = path.join(process.cwd(), fallback.scriptPath);
843
872
  if (existsSync(resolvedScriptPath)) {
844
- ensureDefaultRuntimeHookFiles(target);
845
873
  return fallback;
846
874
  }
847
875
  const source = getDefaultRuntimeRunnerSource(target);
@@ -854,223 +882,37 @@ function ensureDefaultRuntimeRunner(target, emitWarnings) {
854
882
  if (emitWarnings) {
855
883
  process.stderr.write(chalk.dim(`runtime script missing; created ${fallback.scriptPath}\n`));
856
884
  }
857
- ensureDefaultRuntimeHookFiles(target);
858
885
  return fallback;
859
886
  }
860
- function ensureDefaultRuntimeHookFiles(target) {
861
- const hooks = getDefaultRuntimeRunnerHookFiles(target);
862
- for (const file of hooks) {
863
- if (existsSync(file.path))
864
- continue;
865
- if (!existsSync(path.dirname(file.path))) {
866
- mkdirSync(path.dirname(file.path), { recursive: true });
867
- }
868
- writeFileSync(file.path, file.source);
869
- }
870
- }
871
- function getDefaultRuntimeRunnerHookFiles(target) {
872
- if (target == "bindings") {
873
- return [
874
- {
875
- path: path.join(process.cwd(), "./.as-test/runners/default.bindings.hooks.js"),
876
- source: getDefaultBindingsRunnerHooksSource(),
877
- },
878
- ];
879
- }
880
- if (target == "web") {
881
- return [
882
- {
883
- path: path.join(process.cwd(), "./.as-test/runners/default.web.hooks.js"),
884
- source: buildWebRunnerHooksSource(),
885
- },
886
- ];
887
- }
888
- return [];
889
- }
890
887
  function getDefaultRuntimeRunnerSource(target) {
891
888
  if (target == "wasi") {
892
- return `import { readFileSync } from "fs";
893
- import { WASI } from "wasi";
894
-
895
- const originalEmitWarning = process.emitWarning.bind(process);
896
- process.emitWarning = ((warning, ...args) => {
897
- const type = typeof args[0] == "string" ? args[0] : "";
898
- const name = typeof warning?.name == "string" ? warning.name : type;
899
- const message =
900
- typeof warning == "string" ? warning : String(warning?.message ?? "");
901
- if (
902
- name == "ExperimentalWarning" &&
903
- message.includes("WASI is an experimental feature")
904
- ) {
905
- return;
906
- }
907
- return originalEmitWarning(warning, ...args);
908
- });
889
+ return `import { instantiate } from "as-test/lib";
909
890
 
910
- const wasmPath = process.argv[2];
911
- if (!wasmPath) {
912
- process.stderr.write("usage: node ./.as-test/runners/default.wasi.js <file.wasm>\\n");
913
- process.exit(1);
914
- }
915
-
916
- try {
917
- const wasi = new WASI({
918
- version: "preview1",
919
- args: [wasmPath],
920
- env: process.env,
921
- preopens: {},
922
- });
891
+ const imports = {};
923
892
 
924
- const binary = readFileSync(wasmPath);
925
- const module = new WebAssembly.Module(binary);
926
- const envImports = {
927
- __as_test_request_fuzz_config() {
928
- return 0;
929
- },
930
- };
931
- for (const entry of WebAssembly.Module.imports(module)) {
932
- if (entry.module == "env" && entry.kind == "function" && !(entry.name in envImports)) {
933
- envImports[entry.name] = () => 0;
934
- }
935
- }
936
- const instance = new WebAssembly.Instance(module, {
937
- env: envImports,
938
- wasi_snapshot_preview1: wasi.wasiImport,
893
+ instantiate(imports)
894
+ .then((instance) => {
895
+ instance.exports.start?.();
896
+ // Add extra startup logic here when needed.
897
+ })
898
+ .catch((error) => {
899
+ throw new Error("Failed to run WASI module: " + String(error));
939
900
  });
940
- wasi.start(instance);
941
- } catch (error) {
942
- process.stderr.write("failed to run WASI module: " + String(error) + "\\n");
943
- process.exit(1);
944
- }
945
901
  `;
946
902
  }
947
903
  if (target == "bindings") {
948
- return `import fs from "fs";
949
- import path from "path";
950
- import { pathToFileURL } from "url";
951
-
952
- const HOOKS_PATH = path.resolve(
953
- path.dirname(new URL(import.meta.url).pathname),
954
- "./default.bindings.hooks.js",
955
- );
956
-
957
- function readExact(length) {
958
- const out = Buffer.alloc(length);
959
- let offset = 0;
960
- while (offset < length) {
961
- let read = 0;
962
- try {
963
- read = fs.readSync(0, out, offset, length - offset, null);
964
- } catch (error) {
965
- if (error && error.code === "EAGAIN") {
966
- continue;
967
- }
968
- throw error;
969
- }
970
- if (!read) break;
971
- offset += read;
972
- }
973
- const view = out.subarray(0, offset);
974
- return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
975
- }
904
+ return `import { instantiate } from "as-test/lib";
976
905
 
977
- function writeRaw(data) {
978
- const view = Buffer.from(data);
979
- fs.writeSync(1, view);
980
- }
906
+ const imports = {};
981
907
 
982
- function createRunnerContext({ wasmPath, module, helperPath }) {
983
- return {
984
- wasmPath,
985
- helperPath,
986
- module,
987
- argv: process.argv.slice(2),
988
- env: process.env,
989
- readFrame(size) {
990
- return readExact(Number(size ?? 0));
991
- },
992
- writeFrame(data) {
993
- writeRaw(data);
994
- return true;
995
- },
996
- };
997
- }
998
-
999
- function createAsTestImports(ctx) {
1000
- const originalWrite = process.stdout.write.bind(process.stdout);
1001
- process.stdout.write = (chunk, ...args) => {
1002
- if (chunk instanceof ArrayBuffer) {
1003
- return ctx.writeFrame(chunk);
1004
- }
1005
- return originalWrite(chunk, ...args);
1006
- };
1007
- process.stdin.read = (size) => ctx.readFrame(size);
1008
- return {};
1009
- }
1010
-
1011
- function mergeImports(...groups) {
1012
- const out = {};
1013
- for (const group of groups) {
1014
- if (!group || typeof group != "object") continue;
1015
- for (const moduleName of Object.keys(group)) {
1016
- out[moduleName] = Object.assign(out[moduleName] || {}, group[moduleName]);
1017
- }
1018
- }
1019
- return out;
1020
- }
1021
-
1022
- async function loadRunnerHooks() {
1023
- if (!fs.existsSync(HOOKS_PATH)) {
1024
- return {
1025
- createUserImports() {
1026
- return {};
1027
- },
1028
- async runModule(_exports, _ctx) {},
1029
- };
1030
- }
1031
- const mod = await import(pathToFileURL(HOOKS_PATH).href + "?t=" + Date.now());
1032
- return {
1033
- createUserImports:
1034
- typeof mod.createUserImports == "function"
1035
- ? mod.createUserImports
1036
- : () => ({}),
1037
- runModule:
1038
- typeof mod.runModule == "function" ? mod.runModule : async () => {},
1039
- };
1040
- }
1041
-
1042
- async function instantiateModule(ctx, hooks) {
1043
- const helper = await import(pathToFileURL(ctx.helperPath).href);
1044
- if (typeof helper.instantiate !== "function") {
1045
- throw new Error("bindings helper missing instantiate export");
1046
- }
1047
- const imports = mergeImports(
1048
- createAsTestImports(ctx),
1049
- await hooks.createUserImports(ctx),
1050
- );
1051
- return helper.instantiate(ctx.module, imports);
1052
- }
1053
-
1054
- const wasmPathArg = process.argv[2];
1055
- if (!wasmPathArg) {
1056
- process.stderr.write("usage: node ./.as-test/runners/default.bindings.js <file.wasm>\\n");
1057
- process.exit(1);
1058
- }
1059
-
1060
- const wasmPath = path.resolve(process.cwd(), wasmPathArg);
1061
- const jsPath = wasmPath.replace(/\\.wasm$/, ".js");
1062
-
1063
- try {
1064
- const binary = fs.readFileSync(wasmPath);
1065
- const module = new WebAssembly.Module(binary);
1066
- const ctx = createRunnerContext({ wasmPath, module, helperPath: jsPath });
1067
- const hooks = await loadRunnerHooks();
1068
- const exports = await instantiateModule(ctx, hooks);
1069
- await hooks.runModule(exports, ctx);
1070
- } catch (error) {
1071
- process.stderr.write("failed to run bindings module: " + String(error) + "\\n");
1072
- process.exit(1);
1073
- }
908
+ instantiate(imports)
909
+ .then((instance) => {
910
+ instance.exports.start?.();
911
+ // Add extra startup logic here when needed.
912
+ })
913
+ .catch((error) => {
914
+ throw new Error("Failed to run bindings module: " + String(error));
915
+ });
1074
916
  `;
1075
917
  }
1076
918
  if (target == "web") {
@@ -1078,24 +920,6 @@ try {
1078
920
  }
1079
921
  return null;
1080
922
  }
1081
- function getDefaultBindingsRunnerHooksSource() {
1082
- return `export function createUserImports(_ctx) {
1083
- return {
1084
- // env: {
1085
- // now_ms: () => Date.now(),
1086
- // },
1087
- };
1088
- }
1089
-
1090
- export async function runModule(_exports, _ctx) {
1091
- // The generated bindings helper already calls exports._start().
1092
- // Add extra startup calls here when your module exposes them.
1093
- //
1094
- // Example:
1095
- // _exports.run?.();
1096
- }
1097
- `;
1098
- }
1099
923
  function resolveArtifactFileName(file, target, modeName, duplicateSpecBasenames = new Set()) {
1100
924
  const base = path
1101
925
  .basename(file)
@@ -1141,14 +965,55 @@ function resolveDisambiguator(file, duplicateSpecBasenames) {
1141
965
  .replace(/[^A-Za-z0-9._-]/g, "_")
1142
966
  .replace(/^_+|_+$/g, "");
1143
967
  }
1144
- function resolveBindingsHelperPath(wasmPath) {
1145
- const bindingsPath = wasmPath.replace(/\.wasm$/, ".bindings.js");
1146
- if (existsSync(bindingsPath))
1147
- return bindingsPath;
1148
- const legacyRunPath = wasmPath.replace(/\.wasm$/, ".run.js");
1149
- if (existsSync(legacyRunPath))
1150
- return legacyRunPath;
1151
- return bindingsPath;
968
+ function resolveRuntimeTargetEnv(target, wasmPath) {
969
+ if (target == "bindings") {
970
+ return resolveBindingsRuntimeEnv(wasmPath);
971
+ }
972
+ if (target == "web") {
973
+ return resolveWebRuntimeEnv(wasmPath);
974
+ }
975
+ if (target == "wasi") {
976
+ return {
977
+ AS_TEST_RUNTIME_TARGET: "wasi",
978
+ AS_TEST_WASM_PATH: wasmPath,
979
+ };
980
+ }
981
+ return {};
982
+ }
983
+ function resolveBindingsRuntimeEnv(wasmPath) {
984
+ const helperPath = wasmPath.replace(/\.wasm$/, ".js");
985
+ const kind = detectBindingsKind(wasmPath, helperPath);
986
+ const env = {
987
+ AS_TEST_RUNTIME_TARGET: "bindings",
988
+ AS_TEST_WASM_PATH: wasmPath,
989
+ AS_TEST_BINDINGS_KIND: kind,
990
+ };
991
+ if (kind != "none") {
992
+ env.AS_TEST_HELPER_PATH = helperPath;
993
+ }
994
+ return env;
995
+ }
996
+ function resolveWebRuntimeEnv(wasmPath) {
997
+ const env = resolveBindingsRuntimeEnv(wasmPath);
998
+ env.AS_TEST_RUNTIME_TARGET = "web";
999
+ return env;
1000
+ }
1001
+ function detectBindingsKind(wasmPath, helperPath) {
1002
+ if (!existsSync(wasmPath)) {
1003
+ throw new Error(`bindings artifact not found: ${wasmPath}`);
1004
+ }
1005
+ if (!existsSync(helperPath)) {
1006
+ return "none";
1007
+ }
1008
+ const source = readFileSync(helperPath, "utf8");
1009
+ if (/\bexport\s+(async\s+)?function\s+instantiate\b/.test(source)) {
1010
+ return "raw";
1011
+ }
1012
+ if (/\bexport\s+const\b/.test(source) &&
1013
+ /new URL\([^)]*\.wasm["']?,\s*import\.meta\.url\)/.test(source)) {
1014
+ return "esm";
1015
+ }
1016
+ throw new Error(`could not detect bindings kind for ${helperPath}; expected raw or esm helper output`);
1152
1017
  }
1153
1018
  function extractRuntimeScriptPath(runtimeRun) {
1154
1019
  const tokens = runtimeRun
@@ -1930,6 +1795,285 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1930
1795
  }
1931
1796
  return report;
1932
1797
  }
1798
+ async function runWebSessionProcess(session, specFile, crashDir, modeName, snapshots, snapshotEnabled, createSnapshots, overwriteSnapshots, reporter, tapMode = false, env = process.env) {
1799
+ const input = new PassThrough();
1800
+ const output = new PassThrough();
1801
+ let report = null;
1802
+ let parseError = null;
1803
+ let stderrBuffer = "";
1804
+ let stdoutBuffer = "";
1805
+ let sawChannelClose = false;
1806
+ const runtimeEvents = {
1807
+ sawFileStart: false,
1808
+ sawFileEnd: false,
1809
+ fileName: path.basename(specFile),
1810
+ fileVerdict: "none",
1811
+ fileTime: "",
1812
+ suiteStarts: 0,
1813
+ suiteEnds: 0,
1814
+ assertionFails: 0,
1815
+ warnings: 0,
1816
+ logs: 0,
1817
+ };
1818
+ const reportStream = {
1819
+ dataFrames: 0,
1820
+ dataBytes: 0,
1821
+ sawChunkStart: false,
1822
+ sawChunkEnd: false,
1823
+ chunkCountExpected: 0,
1824
+ chunkBytesExpected: 0,
1825
+ chunkTotalBytesExpected: 0,
1826
+ chunkFramesReceived: 0,
1827
+ chunkBytesReceived: 0,
1828
+ chunks: [],
1829
+ };
1830
+ class TestChannel extends Channel {
1831
+ onPassthrough(data) {
1832
+ stdoutBuffer += data.toString("utf8");
1833
+ if (tapMode) {
1834
+ process.stderr.write(data);
1835
+ }
1836
+ else {
1837
+ process.stdout.write(data);
1838
+ }
1839
+ }
1840
+ onCall(msg) {
1841
+ const event = msg;
1842
+ const kind = String(event.kind ?? "");
1843
+ if (kind === "event:assert-fail") {
1844
+ runtimeEvents.assertionFails++;
1845
+ reporter.onAssertionFail?.({
1846
+ key: String(event.key ?? ""),
1847
+ instr: String(event.instr ?? ""),
1848
+ left: String(event.left ?? ""),
1849
+ right: String(event.right ?? ""),
1850
+ message: String(event.message ?? ""),
1851
+ });
1852
+ return;
1853
+ }
1854
+ if (kind === "event:file-start") {
1855
+ runtimeEvents.sawFileStart = true;
1856
+ runtimeEvents.fileName = String(event.file ?? runtimeEvents.fileName);
1857
+ reporter.onFileStart?.({
1858
+ file: String(event.file ?? "unknown"),
1859
+ depth: 0,
1860
+ suiteKind: "file",
1861
+ description: String(event.file ?? "unknown"),
1862
+ });
1863
+ return;
1864
+ }
1865
+ if (kind === "event:file-end") {
1866
+ runtimeEvents.sawFileEnd = true;
1867
+ runtimeEvents.fileName = String(event.file ?? runtimeEvents.fileName);
1868
+ runtimeEvents.fileVerdict = String(event.verdict ?? "none");
1869
+ runtimeEvents.fileTime = String(event.time ?? "");
1870
+ reporter.onFileEnd?.({
1871
+ file: String(event.file ?? "unknown"),
1872
+ depth: 0,
1873
+ suiteKind: "file",
1874
+ description: String(event.file ?? "unknown"),
1875
+ verdict: String(event.verdict ?? "none"),
1876
+ time: String(event.time ?? ""),
1877
+ });
1878
+ return;
1879
+ }
1880
+ if (kind === "event:suite-start") {
1881
+ runtimeEvents.suiteStarts++;
1882
+ reporter.onSuiteStart?.({
1883
+ file: String(event.file ?? "unknown"),
1884
+ depth: Number(event.depth ?? 0),
1885
+ suiteKind: String(event.suiteKind ?? ""),
1886
+ description: String(event.description ?? ""),
1887
+ });
1888
+ return;
1889
+ }
1890
+ if (kind === "event:suite-end") {
1891
+ runtimeEvents.suiteEnds++;
1892
+ reporter.onSuiteEnd?.({
1893
+ file: String(event.file ?? "unknown"),
1894
+ depth: Number(event.depth ?? 0),
1895
+ suiteKind: String(event.suiteKind ?? ""),
1896
+ description: String(event.description ?? ""),
1897
+ verdict: String(event.verdict ?? "none"),
1898
+ });
1899
+ return;
1900
+ }
1901
+ if (kind === "event:warn") {
1902
+ runtimeEvents.warnings++;
1903
+ reporter.onWarning?.({
1904
+ message: String(event.message ?? ""),
1905
+ });
1906
+ return;
1907
+ }
1908
+ if (kind === "event:log") {
1909
+ runtimeEvents.logs++;
1910
+ reporter.onLog?.({
1911
+ file: String(event.file ?? "unknown"),
1912
+ depth: Number(event.depth ?? 0),
1913
+ text: String(event.text ?? ""),
1914
+ });
1915
+ return;
1916
+ }
1917
+ if (kind === "snapshot:assert") {
1918
+ const key = String(event.key ?? "");
1919
+ const actual = String(event.actual ?? "");
1920
+ const result = snapshots.assert(key, actual, snapshotEnabled, createSnapshots, overwriteSnapshots);
1921
+ if (result.warnMissing) {
1922
+ reporter.onSnapshotMissing?.({ key });
1923
+ }
1924
+ this.send(MessageType.CALL, Buffer.from(`${result.ok ? "1" : "0"}\n${result.expected}`, "utf8"));
1925
+ return;
1926
+ }
1927
+ if (kind === "report:start") {
1928
+ reportStream.sawChunkStart = true;
1929
+ reportStream.sawChunkEnd = false;
1930
+ reportStream.chunkCountExpected = Number(event.chunkCount ?? 0);
1931
+ reportStream.chunkBytesExpected = Number(event.chunkBytes ?? 0);
1932
+ reportStream.chunkTotalBytesExpected = Number(event.totalBytes ?? 0);
1933
+ reportStream.chunkFramesReceived = 0;
1934
+ reportStream.chunkBytesReceived = 0;
1935
+ reportStream.chunks = [];
1936
+ return;
1937
+ }
1938
+ if (kind === "report:end") {
1939
+ reportStream.sawChunkEnd = true;
1940
+ return;
1941
+ }
1942
+ this.sendJSON(MessageType.CALL, { ok: true, expected: "" });
1943
+ }
1944
+ onDataMessage(data) {
1945
+ reportStream.dataFrames++;
1946
+ reportStream.dataBytes += data.length;
1947
+ if (reportStream.sawChunkStart && !reportStream.sawChunkEnd) {
1948
+ reportStream.chunkFramesReceived++;
1949
+ reportStream.chunkBytesReceived += data.length;
1950
+ reportStream.chunks.push(data.toString("utf8"));
1951
+ return;
1952
+ }
1953
+ try {
1954
+ report = JSON.parse(data.toString("utf8"));
1955
+ parseError = null;
1956
+ }
1957
+ catch (error) {
1958
+ parseError = String(error);
1959
+ }
1960
+ }
1961
+ onClose() {
1962
+ sawChannelClose = true;
1963
+ }
1964
+ }
1965
+ const channel = new TestChannel(input, output);
1966
+ output.on("data", (chunk) => {
1967
+ session.sendReply(Buffer.from(chunk));
1968
+ });
1969
+ let code = 0;
1970
+ try {
1971
+ await session.runJob(Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] == "string")), path.basename(specFile), (frame) => {
1972
+ input.write(frame);
1973
+ });
1974
+ }
1975
+ catch (error) {
1976
+ code = 1;
1977
+ await session.close(error instanceof Error ? error : new Error(String(error)));
1978
+ stderrBuffer +=
1979
+ (error instanceof Error ? error.stack ?? error.message : String(error)) +
1980
+ "\n";
1981
+ }
1982
+ finally {
1983
+ input.end();
1984
+ output.end();
1985
+ }
1986
+ if (reportStream.sawChunkStart) {
1987
+ if (!reportStream.sawChunkEnd) {
1988
+ parseError =
1989
+ parseError ??
1990
+ "missing report:end marker for chunked report payload";
1991
+ }
1992
+ else {
1993
+ const chunkedPayload = reportStream.chunks.join("");
1994
+ try {
1995
+ report = JSON.parse(chunkedPayload);
1996
+ parseError = null;
1997
+ }
1998
+ catch (error) {
1999
+ parseError = `could not parse chunked report payload: ${String(error)}`;
2000
+ }
2001
+ if (reportStream.chunkCountExpected > 0 &&
2002
+ reportStream.chunkFramesReceived !== reportStream.chunkCountExpected) {
2003
+ parseError =
2004
+ parseError ??
2005
+ `chunk count mismatch: expected ${reportStream.chunkCountExpected}, received ${reportStream.chunkFramesReceived}`;
2006
+ }
2007
+ if (reportStream.chunkTotalBytesExpected > 0 &&
2008
+ reportStream.chunkBytesReceived !== reportStream.chunkTotalBytesExpected) {
2009
+ parseError =
2010
+ parseError ??
2011
+ `chunk size mismatch: expected ${reportStream.chunkTotalBytesExpected} bytes, received ${reportStream.chunkBytesReceived}`;
2012
+ }
2013
+ }
2014
+ }
2015
+ if (parseError) {
2016
+ const errorText = `could not parse report payload: ${parseError}`;
2017
+ const diagnostics = buildRuntimeReportDiagnostics(code, sawChannelClose, reportStream, runtimeEvents);
2018
+ const fullError = `${errorText}\n${diagnostics}`;
2019
+ persistCrashRecord(crashDir, {
2020
+ kind: "test",
2021
+ file: specFile,
2022
+ mode: modeName ?? "default",
2023
+ error: fullError,
2024
+ stdout: stdoutBuffer,
2025
+ stderr: stderrBuffer,
2026
+ });
2027
+ return createRuntimeFailureReport(specFile, modeName, "runtime returned an invalid report payload", fullError, stdoutBuffer, stderrBuffer);
2028
+ }
2029
+ if (!report) {
2030
+ const synthesized = synthesizeReportFromRuntimeEvents(specFile, runtimeEvents);
2031
+ if (synthesized) {
2032
+ reporter.onWarning?.({
2033
+ message: "runtime report payload missing; reconstructed result from streamed lifecycle events",
2034
+ });
2035
+ if (code !== 0 || hasMeaningfulRuntimeOutput(stderrBuffer)) {
2036
+ const errorParts = [];
2037
+ if (code !== 0) {
2038
+ errorParts.push(`child process exited with code ${code}`);
2039
+ }
2040
+ const stderrText = normalizeRuntimeOutput(stderrBuffer);
2041
+ if (stderrText.length) {
2042
+ errorParts.push(stderrText);
2043
+ }
2044
+ const diagnostics = buildRuntimeReportDiagnostics(code, sawChannelClose, reportStream, runtimeEvents);
2045
+ reporter.onWarning?.({
2046
+ message: `${errorParts.join("; ")}\n${diagnostics}`,
2047
+ });
2048
+ }
2049
+ return synthesized;
2050
+ }
2051
+ const diagnostics = buildRuntimeReportDiagnostics(code, sawChannelClose, reportStream, runtimeEvents);
2052
+ const fullError = `missing report payload from test runtime\n${diagnostics}`;
2053
+ persistCrashRecord(crashDir, {
2054
+ kind: "test",
2055
+ file: specFile,
2056
+ mode: modeName ?? "default",
2057
+ error: fullError,
2058
+ stdout: stdoutBuffer,
2059
+ stderr: stderrBuffer,
2060
+ });
2061
+ return createRuntimeFailureReport(specFile, modeName, "missing report payload from test runtime", fullError, stdoutBuffer, stderrBuffer);
2062
+ }
2063
+ if (code != 0 || hasMeaningfulRuntimeOutput(stderrBuffer)) {
2064
+ const diagnostics = buildRuntimeReportDiagnostics(code, sawChannelClose, reportStream, runtimeEvents);
2065
+ reporter.onWarning?.({
2066
+ message: [
2067
+ code !== 0 ? `child process exited with code ${code}` : "",
2068
+ normalizeRuntimeOutput(stderrBuffer),
2069
+ diagnostics,
2070
+ ]
2071
+ .filter(Boolean)
2072
+ .join("\n"),
2073
+ });
2074
+ }
2075
+ return report;
2076
+ }
1933
2077
  function synthesizeReportFromRuntimeEvents(specFile, runtimeEvents) {
1934
2078
  if (!runtimeEvents.sawFileEnd &&
1935
2079
  runtimeEvents.suiteStarts <= 0 &&