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.
- package/CHANGELOG.md +64 -0
- package/assembly/src/expectation.ts +2 -2
- package/assembly/src/stringify.ts +29 -4
- package/bin/commands/build-core.js +62 -6
- package/bin/commands/fuzz-core.js +22 -1
- package/bin/commands/init-core.js +371 -36
- package/bin/commands/run-core.js +130 -21
- package/bin/commands/web-session.js +50 -3
- package/bin/index.js +6 -2
- package/bin/wipc.js +46 -7
- package/lib/build/index.js +45 -15
- package/lib/build/web-runner/worker.js +27 -0
- package/lib/src/index.ts +66 -15
- package/package.json +18 -17
- package/transform/lib/equals.js +3 -3
- package/transform/lib/index.js +10 -1
- package/transform/lib/mock.js +63 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 2026-05-30 - v1.5.1
|
|
4
|
+
|
|
5
|
+
### An early-exiting runtime now fails instead of warning
|
|
6
|
+
|
|
7
|
+
- fix: when the runtime never delivers its final report payload, the CLI reconstructs a result from the streamed lifecycle events (`synthesizeReportFromRuntimeEvents`). Previously this always emitted a `WARN` and returned the reconstruction, only escalating to a `FAIL` if the child exited non-zero or wrote to stderr — so a spec that trapped/exited early with exit code `0` and no stderr came back as a passing reconstruction (the `runtime report payload missing; reconstructed result from streamed lifecycle events` warning storm). Both `runProcess` (WASI/bindings) and `runWebSessionProcess` (web) now treat `!runtimeEvents.sawFileEnd` — the runtime never emitted `event:file-end`, i.e. it exited before the file finished — as a failure: `appendRuntimeFailureReport` with a persisted crash record and a `test runtime exited before completing the test file` message, no misleading `WARN`. A run that _did_ reach `file-end` but simply failed to flush the final report frame is still the recoverable case (`WARN` + reconstructed result). This also closes a gap in the web path, which previously never escalated to a failure in the synthesized branch — it only warned, even on a non-zero exit.
|
|
8
|
+
|
|
9
|
+
### esm bindings now run
|
|
10
|
+
|
|
11
|
+
- fix: `instantiateEsmInstance` (`lib/src/index.ts`) now calls `patchNodeIo()` before importing the bindings helper. An esm helper auto-instantiates at import time and writes the WIPC report by calling the global `process.stdout.write(ArrayBuffer)` directly. `patchNodeIo()` — which teaches `process.stdout.write`/`process.stdin.read` to accept a raw `ArrayBuffer` and route it through `fs.writeSync` — was only wired into the raw path (via `withNodeIo`), never the esm path. So under esm bindings Node threw `ERR_INVALID_ARG_TYPE` ("chunk must be of type string or Buffer…, received an instance of ArrayBuffer") before any report was emitted, and the run crashed with `missing report payload from test runtime`. The patch is now in place by the time the helper instantiates.
|
|
12
|
+
|
|
13
|
+
### `--bindings` is respected instead of overridden
|
|
14
|
+
|
|
15
|
+
- feat: as-test no longer forces `--bindings raw` when you've already declared bindings yourself. `getDefaultBuildArgs` (`cli/commands/build-core.ts`) now takes a `bindingsAlreadyConfigured` flag and only appends `--bindings raw` when neither `buildOptions.args` nor a referenced asconfig declares `--bindings`. The other bindings flags (`AS_TEST_BINDINGS=1`, `--exportRuntime`, `--exportStart _start`) are still always injected. Two new detectors back this: `argsDeclareBindings(args)` (scans for `--bindings`/`--bindings=`) and `asconfigDeclaresBindings(configPath)` (reads `options.bindings`, follows `extends`), mirroring the existing try-as detection. Previously `--bindings esm` in `buildOptions.args` was combined with the forced `--bindings raw`, so `asc` emitted glue for **both** styles into one file; the runtime then mis-detected the kind and crashed. With this, `--bindings esm` produces esm-only glue and runs.
|
|
16
|
+
|
|
17
|
+
### Mocking works on every runtime
|
|
18
|
+
|
|
19
|
+
- change: `mock.spec.ts` is split into `mock.spec.ts` (mocking + `unmockFn` only) and `unmock.spec.ts` (the `unmockImport` cases). The split tracks the real esm/standalone-WASI boundary: the transform removes a `@external` import from the wasm when it is **only ever mocked**, but keeps it (for fall-back) when it is `unmockImport`'d anywhere. A pure-mock spec therefore imports nothing virtual and runs on **every** runtime — verified via `WebAssembly.Module.imports()`: the pure-mock wasi build imports only `wasi_snapshot_preview1`, while the unmock build imports `mock.foo`. `unmockFn` (function mocks) does not retain an import; only `unmockImport` does.
|
|
20
|
+
- feat: pure `mockImport` specs now run under **esm bindings** and the standalone WASI runtimes (`wasmtime`, `wasmer`, `wazero`) — `mock.spec.ts` is no longer excluded from those modes (it was in v1.5.0). Only `unmock.spec.ts`, which retains a real host binding the host can't supply under those runtimes, is excluded (`!**/unmock.spec.ts`).
|
|
21
|
+
- feat: two new modes in `as-test.config.json` — `node:bindings:raw` and `node:bindings:esm` (both `default: false`) — exercise each bindings style explicitly, and both are added to the `test:modes` matrix so `test:all` covers them.
|
|
22
|
+
|
|
23
|
+
### Watch mode exits with the last verdict
|
|
24
|
+
|
|
25
|
+
- feat: quitting `--watch` (ctrl+c, both the raw-mode `0x03` path and the `SIGINT` handler in `runWatchLoop`) now exits `1` when the most recent run left any spec failing **or a run is still in flight**, instead of always exiting `0`. The watch loop already tracks currently-failing `(spec, mode)` pairs in its sticky `failingSpecs` map and an `isRunning` flag, so the exit code is `isRunning || failingSpecs.size ? 1 : 0` — an interrupted run counts as a failure. This lets a red watch session fail CI and shell pipelines (`ast test --watch && deploy`) instead of masking the failure on quit.
|
|
26
|
+
|
|
27
|
+
### CI uses the main config
|
|
28
|
+
|
|
29
|
+
- chore: removed `as-test.ci.config.json`. `test:ci` now runs against the main `as-test.config.json` (`npm run test -- --mode node:bindings,node:wasi,wasmtime`), so CI uses the same modes, `features` (`try-as`), and per-mode spec exclusions as everything else — `try-as` no longer needs an explicit `--enable`, and the stale CI-only `wasmtime` exclusion that still ran `unmock.spec.ts` (and failed on the missing `mock::foo` host import) is gone.
|
|
30
|
+
|
|
31
|
+
## 2026-05-28 - v1.5.0
|
|
32
|
+
|
|
33
|
+
### `mockFn` and `mockImport` now work anywhere
|
|
34
|
+
|
|
35
|
+
- feat: `mockFn(target, callback)` and `unmockFn(target)` can be called from **anywhere** in the source — including inside a `test()` / `it()` callback — not just module top-level. The MockTransform now reads the visitor's enclosing-statement `ref` so it locates and removes the directive wherever it lives, defers list mutations to the end of `visitSource` so traversal isn't disturbed, and `unshift`s the generated mock fn to module scope so every rewritten call site can reach it. Previously, a nested `mockFn` silently no-op'd because the transform's top-level-statement scan never found the directive — calls remained un-rewritten and the test passed only because trivial assertions passed too.
|
|
36
|
+
- feat: `mockImport(path, callback)` + `unmockImport(path)` now retains the real import as a fallback (call this "route 2"). When a `mockImport`'d path is **never unmocked**, the `@external` import is removed and the wrapper always dispatches through `__mock_import` — same as before. When a path **is** unmocked somewhere, the transform keeps the real import (renamed `__as_test_real_<name>`, de-exported), and the wrapper becomes `if (__mock_import.has(path)) call_indirect(...) else __as_test_real_<name>(...)`. So a call after `unmockImport` falls back to the host binding instead of trapping on a missing `__mock_import` entry.
|
|
37
|
+
- fix: the runtime import-stub now covers any module the wasm declares (not just `env`), so a retained `mockImport` target with no host implementation (e.g. an artificial test import) gets a `() => 0` no-op stub before `WebAssembly.instantiate`. Without this, the wasm `LinkError`s on instantiation. `env` is no longer skipped — asc raw bindings put their `abort`/`trace`/`seed` defaults on the inner object and user env imports on the prototype, so the stub fills missing `env.*` user imports without clobbering asc's defaults.
|
|
38
|
+
- fix: in raw bindings the helper reads `imports.<module>` while building its import object, so the stub had to be applied **before** `helper.instantiate(module, imports)` is called — adding it inside the `WebAssembly.instantiate` wrapper alone was too late and surfaced as `instantiate@.../<spec>.js:...` failures (this was the chromium-headful + watch + full-suite crash where one bad job poisoned the persistent session).
|
|
39
|
+
|
|
40
|
+
### Class serializer rename — `__AS_TEST_TO_JSON()`
|
|
41
|
+
|
|
42
|
+
- change: the transform-injected class serializer is now emitted under an internal name `__AS_TEST_TO_JSON(): string` instead of `toJSON(): string`. Users can't accidentally call the generated method, and a hand-written `toJSON()` no longer collides with the generated one.
|
|
43
|
+
- feat: at runtime, `stringify<T>` for a managed class calls a user `toJSON()` only when its return type is a string (probed via a generic-parameter helper so the dead branch is pruned at compile time); a non-string `toJSON` cleanly falls back to the generated `__AS_TEST_TO_JSON()` structural serializer instead of being a compile error. The transform now generates `__AS_TEST_TO_JSON` for every eligible class so the fallback always exists.
|
|
44
|
+
|
|
45
|
+
### Chromium fix — fragmented WebSocket frames
|
|
46
|
+
|
|
47
|
+
- fix: chromium fragments outbound WebSocket binary messages at ~128KB. The two web server frame parsers (standalone runner in `lib/src/index.ts` and managed `PersistentWebSessionHost` in `cli/commands/web-session.ts`) read the opcode but **never checked the FIN bit or handled continuation frames (`0x0`)** — so chromium's continuation frame was silently dropped, the wipc byte stream desynced, and frame-header bytes bled into report payloads as `Bad control character in JSON`. Both parsers now reassemble fragments (buffer `0x1`/`0x2` with FIN=0, append `0x0` until FIN=1). firefox and webkit don't fragment at that size, which is why they always worked.
|
|
48
|
+
- fix: the CLI reassembled chunked report payloads by `data.toString("utf8")` per-chunk then joining. The producer (`assembly/util/wipc.ts`) chunks raw UTF-8 bytes, so a multibyte character straddling a chunk boundary was getting mis-decoded into replacement chars (or worse) on any transport, not just chromium. Chunks are now buffered as `Buffer`s and decoded UTF-8 once after `Buffer.concat`.
|
|
49
|
+
|
|
50
|
+
### Web mode — repro command + browser test infrastructure
|
|
51
|
+
|
|
52
|
+
- fix: when a managed web run (`ast test --mode firefox/chromium/webkit`) fails, the printed repro now points at the **managed CLI command** (`node ./bin/index.js run <spec> --mode <browser>`) instead of `node .as-test/runners/default.web.js <wasm>`. The managed path uses `PersistentWebSessionHost`'s panel UI; the standalone runner uses `lib/src/web-runner/*`'s terminal UI — completely different stacks. So the old repro never actually reproduced the failing run, and showed a different UI to boot. Top-level negations in `runCommandForLog` are now mode-aware via `webSession`.
|
|
53
|
+
- change: removed `tests/fixtures/fake-browser.mjs`. The integration suite's web tests now use real headless chromium (auto-detected from PATH or the Playwright cache), and `AS_TEST_SKIP_WEB=1` cleanly skips them in CI. CI workflows (`.github/workflows/as-test.yml`, `release.yml`) already set `AS_TEST_SKIP_WEB=1`. Without this, the shim's flow hung indefinitely in some local environments, eating ~hours of integration time.
|
|
54
|
+
- feat: a small "browser-closes-early" test now ships a `process.exit(0)` shebanged executable as `--browser` to verify the framework reports a clean disconnect error rather than hanging.
|
|
55
|
+
|
|
56
|
+
### Per-mode spec exclusion
|
|
57
|
+
|
|
58
|
+
- feat: mode-level `input` overrides can use `!`-prefixed patterns to exclude specific specs from that mode. The orchestrator's per-mode `run()` call now checks the **mode-only additions** (negations in the mode's `input` array that aren't in the top-level `input`) against the selector and returns an empty result for excluded files. Top-level negations remain ignored when a file is explicitly selected, so `test __tmp_foo` still works for paths the top-level config would otherwise filter.
|
|
59
|
+
- chore: `mock.spec` is excluded from `wasmtime`, `wasmer`, and `wazero` modes in `as-test.config.json`. Those CLI-only WASI runtimes can't accept arbitrary imports at instantiation, so the retained `env.*` `mockImport` target has no JS host to satisfy it.
|
|
60
|
+
|
|
61
|
+
### Scripts and CI standardization
|
|
62
|
+
|
|
63
|
+
- feat: new `build` script aliases `build:cli && build:lib && build:transform`. `test:all`, `release:check`, and `prepublishOnly` now compose with it instead of repeating the three sub-builds. `test:all` runs `build && test:modes && test:integration && test:examples`.
|
|
64
|
+
- chore: `test:modes` drops the duplicate `--parallel` (the base `test` script already has it), and `test:ci` composes with `npm run test --` instead of hard-coding the bin path.
|
|
65
|
+
- chore: `.github/workflows/as-test.yml` and `release.yml` now share identical test-job setup (checkout@v4, `actions/setup-node@v4`, `oven-sh/setup-bun@v1`, install Wasmtime, lint, typecheck, build, `test:ci`, `test:integration`). `as-test.yml` keeps the Test Summary action for PR feedback; the manual `curl`/`tar` Node bootstrap and the inline Bun installer are gone. `examples.yml` uses the `build` alias.
|
|
66
|
+
|
|
3
67
|
## 2026-05-27 - v1.4.1
|
|
4
68
|
|
|
5
69
|
### Dependency hygiene
|
|
@@ -20,8 +20,8 @@ function safeStringify<T>(value: T): string {
|
|
|
20
20
|
!isString<T>() &&
|
|
21
21
|
!isArray<T>()
|
|
22
22
|
) {
|
|
23
|
-
// @ts-expect-error:
|
|
24
|
-
if (!isDefined(value.toJSON)) {
|
|
23
|
+
// @ts-expect-error: warn only when nothing serializes it — no user `toJSON` and no transform-generated `__AS_TEST_TO_JSON` → renders as a `<TypeName>` placeholder.
|
|
24
|
+
if (!isDefined(value.toJSON) && !isDefined(value.__AS_TEST_TO_JSON)) {
|
|
25
25
|
sendWarning(
|
|
26
26
|
"Class " +
|
|
27
27
|
nameof<T>() +
|
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
// * Array / StaticArray → element-wise recursion
|
|
16
16
|
// * Set → array of values
|
|
17
17
|
// * Map → object with stringified keys
|
|
18
|
-
// * managed
|
|
19
|
-
// * managed
|
|
18
|
+
// * managed, user `toJSON()` returns string → call it
|
|
19
|
+
// * managed otherwise → `__AS_TEST_TO_JSON()`
|
|
20
|
+
// (transform-generated structural serializer; the fallback when a
|
|
21
|
+
// user `toJSON` returns a non-string, or when there's no `toJSON`)
|
|
22
|
+
// * managed with neither → "<TypeName>" placeholder
|
|
20
23
|
//
|
|
21
24
|
// The Date/ArrayBuffer/typed-array/StaticArray/Set/Map branches use
|
|
22
25
|
// `value instanceof X` guards. In a generic function AssemblyScript resolves
|
|
@@ -134,8 +137,16 @@ export function stringify<T>(value: T): string {
|
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
if (isManaged<T>()) {
|
|
137
|
-
//
|
|
138
|
-
|
|
140
|
+
// A user-supplied `toJSON` wins, but only when it returns a string.
|
|
141
|
+
// `preferToJSONString` decides that on the return type and otherwise
|
|
142
|
+
// falls back to the transform-generated structural serializer.
|
|
143
|
+
// @ts-ignore: optional user-supplied serializer
|
|
144
|
+
if (isDefined(value.toJSON)) {
|
|
145
|
+
// @ts-ignore: optional user-supplied serializer
|
|
146
|
+
return preferToJSONString(value, value.toJSON());
|
|
147
|
+
}
|
|
148
|
+
// @ts-ignore: transform-generated structural serializer
|
|
149
|
+
if (isDefined(value.__AS_TEST_TO_JSON)) return value.__AS_TEST_TO_JSON();
|
|
139
150
|
return escape("<" + nameof<T>() + ">");
|
|
140
151
|
}
|
|
141
152
|
|
|
@@ -144,6 +155,20 @@ export function stringify<T>(value: T): string {
|
|
|
144
155
|
return escape("<" + nameof<T>() + ">");
|
|
145
156
|
}
|
|
146
157
|
|
|
158
|
+
// Given a managed value and the result of calling its `toJSON()`, return
|
|
159
|
+
// that result when it's a string, otherwise the transform-generated
|
|
160
|
+
// `__AS_TEST_TO_JSON` structural serializer (or a `<TypeName>` placeholder
|
|
161
|
+
// if neither applies). `R` is the `toJSON` return type, so the
|
|
162
|
+
// `result`-returning branch is pruned — and never type-checked — for any
|
|
163
|
+
// non-string return type. That's what lets a `toJSON` returning, say,
|
|
164
|
+
// `i32` fall back here instead of being a hard compile error.
|
|
165
|
+
function preferToJSONString<T, R>(value: T, result: R): string {
|
|
166
|
+
if (isString<R>()) return result as string;
|
|
167
|
+
// @ts-ignore: transform-generated structural serializer
|
|
168
|
+
if (isDefined(value.__AS_TEST_TO_JSON)) return value.__AS_TEST_TO_JSON();
|
|
169
|
+
return escape("<" + nameof<T>() + ">");
|
|
170
|
+
}
|
|
171
|
+
|
|
147
172
|
// JSON string escape per RFC 8259, with explicit handling for UTF-16
|
|
148
173
|
// surrogates (matches json-as's serializeString behaviour).
|
|
149
174
|
//
|
|
@@ -68,9 +68,13 @@ export async function build(
|
|
|
68
68
|
const sourceInputPatterns =
|
|
69
69
|
overrides.kind === "fuzz" ? config.fuzz.input : config.input;
|
|
70
70
|
const inputPatterns = resolveInputPatterns(sourceInputPatterns, selectors);
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
const includePatterns = inputPatterns.filter((p) => !p.startsWith("!"));
|
|
72
|
+
const ignorePatterns = inputPatterns
|
|
73
|
+
.filter((p) => p.startsWith("!"))
|
|
74
|
+
.map((p) => p.slice(1));
|
|
75
|
+
const inputFiles = (
|
|
76
|
+
await glob(includePatterns, { ignore: ignorePatterns })
|
|
77
|
+
).sort((a, b) => a.localeCompare(b));
|
|
74
78
|
await assertNoArtifactCollisions(sourceInputPatterns);
|
|
75
79
|
warnOnUnknownModeReferences(inputFiles, loadedConfig.modes ?? {});
|
|
76
80
|
const coverageEnabled = resolveCoverageEnabled(
|
|
@@ -434,10 +438,13 @@ function getBuildCommand(
|
|
|
434
438
|
}
|
|
435
439
|
const tryAsAlreadyConfigured =
|
|
436
440
|
argsDeclareTryAs(userArgs) || asconfigDeclaresTryAs(config.config);
|
|
441
|
+
const bindingsAlreadyConfigured =
|
|
442
|
+
argsDeclareBindings(userArgs) || asconfigDeclaresBindings(config.config);
|
|
437
443
|
const defaultArgs = getDefaultBuildArgs(
|
|
438
444
|
config,
|
|
439
445
|
featureToggles,
|
|
440
446
|
tryAsAlreadyConfigured,
|
|
447
|
+
bindingsAlreadyConfigured,
|
|
441
448
|
);
|
|
442
449
|
const ascInvocation = resolveAscInvocation(pkgRunner);
|
|
443
450
|
// as-test's own transform goes first so CoverageTransform sees the
|
|
@@ -765,6 +772,7 @@ function getDefaultBuildArgs(
|
|
|
765
772
|
config,
|
|
766
773
|
featureToggles,
|
|
767
774
|
tryAsAlreadyConfigured = false,
|
|
775
|
+
bindingsAlreadyConfigured = false,
|
|
768
776
|
) {
|
|
769
777
|
const buildArgs = [];
|
|
770
778
|
const effectiveFeatures = resolveEffectiveFeatures(config, featureToggles);
|
|
@@ -791,7 +799,6 @@ function getDefaultBuildArgs(
|
|
|
791
799
|
if (INTERNAL_FEATURE_NAMES.has(feature)) continue;
|
|
792
800
|
buildArgs.push("--enable", feature);
|
|
793
801
|
}
|
|
794
|
-
// Should also strip any bindings-enabling from asconfig
|
|
795
802
|
if (
|
|
796
803
|
config.buildOptions.target == "bindings" ||
|
|
797
804
|
config.buildOptions.target == "web"
|
|
@@ -799,12 +806,18 @@ function getDefaultBuildArgs(
|
|
|
799
806
|
buildArgs.push(
|
|
800
807
|
"--use",
|
|
801
808
|
"AS_TEST_BINDINGS=1",
|
|
802
|
-
"--bindings",
|
|
803
|
-
"raw",
|
|
804
809
|
"--exportRuntime",
|
|
805
810
|
"--exportStart",
|
|
806
811
|
"_start",
|
|
807
812
|
);
|
|
813
|
+
// `raw` bindings are the default the runtime host knows how to drive.
|
|
814
|
+
// If the user already declared `--bindings` (via buildOptions.args or
|
|
815
|
+
// an asconfig), respect their choice — the runtime supports both `raw`
|
|
816
|
+
// and `esm` — and don't force `raw` on top of it (asc would otherwise
|
|
817
|
+
// emit glue for both styles, confusing kind detection).
|
|
818
|
+
if (!bindingsAlreadyConfigured) {
|
|
819
|
+
buildArgs.push("--bindings", "raw");
|
|
820
|
+
}
|
|
808
821
|
} else if (config.buildOptions.target == "wasi") {
|
|
809
822
|
const wasiShim = resolveWasiShim();
|
|
810
823
|
if (!wasiShim) {
|
|
@@ -875,6 +888,49 @@ function argsDeclareTryAs(args) {
|
|
|
875
888
|
}
|
|
876
889
|
return false;
|
|
877
890
|
}
|
|
891
|
+
function argsDeclareBindings(args) {
|
|
892
|
+
for (const arg of args) {
|
|
893
|
+
if (arg === "--bindings" || arg.startsWith("--bindings=")) return true;
|
|
894
|
+
}
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
function asconfigDeclaresBindings(configPath, seen = new Set()) {
|
|
898
|
+
if (!configPath || configPath === "none") return false;
|
|
899
|
+
const resolved = path.isAbsolute(configPath)
|
|
900
|
+
? configPath
|
|
901
|
+
: path.resolve(process.cwd(), configPath);
|
|
902
|
+
if (seen.has(resolved)) return false;
|
|
903
|
+
seen.add(resolved);
|
|
904
|
+
if (!existsSync(resolved)) return false;
|
|
905
|
+
let parsed;
|
|
906
|
+
try {
|
|
907
|
+
parsed = JSON.parse(readFileSync(resolved, "utf8"));
|
|
908
|
+
} catch {
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
if (!parsed || typeof parsed !== "object") return false;
|
|
912
|
+
const obj = parsed;
|
|
913
|
+
const options = obj.options;
|
|
914
|
+
if (options && typeof options === "object") {
|
|
915
|
+
const bindings = options.bindings;
|
|
916
|
+
if (typeof bindings === "string" && bindings.length) return true;
|
|
917
|
+
if (Array.isArray(bindings) && bindings.length) return true;
|
|
918
|
+
}
|
|
919
|
+
const extendsField = obj.extends;
|
|
920
|
+
const extendsList = Array.isArray(extendsField)
|
|
921
|
+
? extendsField
|
|
922
|
+
: typeof extendsField === "string"
|
|
923
|
+
? [extendsField]
|
|
924
|
+
: [];
|
|
925
|
+
for (const ext of extendsList) {
|
|
926
|
+
if (typeof ext !== "string") continue;
|
|
927
|
+
const extPath = path.isAbsolute(ext)
|
|
928
|
+
? ext
|
|
929
|
+
: path.resolve(path.dirname(resolved), ext);
|
|
930
|
+
if (asconfigDeclaresBindings(extPath, seen)) return true;
|
|
931
|
+
}
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
878
934
|
function asconfigDeclaresTryAs(configPath, seen = new Set()) {
|
|
879
935
|
if (!configPath || configPath === "none") return false;
|
|
880
936
|
const resolved = path.isAbsolute(configPath)
|
|
@@ -13,6 +13,10 @@ import { persistCrashRecord } from "../crash-store.js";
|
|
|
13
13
|
const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
|
|
14
14
|
const MAGIC = Buffer.from("WIPC");
|
|
15
15
|
const HEADER_SIZE = 9;
|
|
16
|
+
// See cli/wipc.ts: the magic can occur by chance in passthrough output, so a
|
|
17
|
+
// declared length above this bound means the match is coincidental.
|
|
18
|
+
const MAX_FRAME_SIZE = 16 * 1024 * 1024;
|
|
19
|
+
const KNOWN_FRAME_TYPES = new Set([0x00, 0x01, 0x02, 0x03]);
|
|
16
20
|
const MAX_DEFAULT_SEED = 0x7fffffff;
|
|
17
21
|
export async function fuzz(
|
|
18
22
|
configPath = DEFAULT_CONFIG_PATH,
|
|
@@ -135,7 +139,14 @@ async function runFuzzTarget(
|
|
|
135
139
|
};
|
|
136
140
|
const captured = captureFrames((type, payload, respond) => {
|
|
137
141
|
if (type == 0x02) {
|
|
138
|
-
|
|
142
|
+
// A coincidental magic + CALL match whose payload is not JSON must not
|
|
143
|
+
// crash the run (it would be misreported as a fuzz crash); drop it.
|
|
144
|
+
let event;
|
|
145
|
+
try {
|
|
146
|
+
event = JSON.parse(payload.toString("utf8"));
|
|
147
|
+
} catch {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
139
150
|
const kind = String(event.kind ?? "");
|
|
140
151
|
if (kind == "fuzz:config") {
|
|
141
152
|
const resolved = config;
|
|
@@ -419,6 +430,16 @@ function captureFrames(onFrame) {
|
|
|
419
430
|
if (buffer.length < HEADER_SIZE) return true;
|
|
420
431
|
const type = buffer.readUInt8(4);
|
|
421
432
|
const length = buffer.readUInt32LE(5);
|
|
433
|
+
// A coincidental magic match in passthrough output: an unknown type or
|
|
434
|
+
// an implausible length means these 4 bytes are data, not a frame.
|
|
435
|
+
// Emit them and resync past the magic so we don't stall or misparse.
|
|
436
|
+
if (!KNOWN_FRAME_TYPES.has(type) || length > MAX_FRAME_SIZE) {
|
|
437
|
+
const raw = buffer.subarray(0, MAGIC.length);
|
|
438
|
+
passthrough = Buffer.concat([passthrough, raw]);
|
|
439
|
+
originalWrite(raw);
|
|
440
|
+
buffer = buffer.subarray(MAGIC.length);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
422
443
|
const frameSize = HEADER_SIZE + length;
|
|
423
444
|
if (buffer.length < frameSize) return true;
|
|
424
445
|
const payload = buffer.subarray(HEADER_SIZE, frameSize);
|