space-data-module-sdk 0.8.13 → 0.8.15

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.
@@ -0,0 +1,275 @@
1
+ /**
2
+ * `space-data-module conformance propagator --self-test` — must exit 0 BY
3
+ * failing (finding §5). A gate never observed to fail is indistinguishable
4
+ * from one that cannot fail, so the runner ships with a battery of mock
5
+ * propagators, each carrying ONE planted defect drawn from a real defect this
6
+ * program found in the wild, and requires the suite to catch every one of
7
+ * them — plus a conformant baseline it must NOT flag.
8
+ *
9
+ * The mocks implement the same driver interface the WASM ABI driver exposes,
10
+ * so the suite under test is byte-for-byte the suite that adjudicates real
11
+ * artifacts. No toolchain, no artifact, no network.
12
+ */
13
+
14
+ import { ORBPRO_STATE_VECTOR, ReferenceFrame, StateFlags, ErrorCode, REQUIRED_ABI_EXPORTS } from "./abiDriver.js";
15
+ import { computeVerdict, runPropagatorSuite } from "./propagatorSuite.js";
16
+ import { BANDS_DEFAULT, buildSelfTestCorpus } from "./selfTestCorpus.js";
17
+ import { propagateTwoBody } from "./twoBodyReference.js";
18
+
19
+ const PAGE = 65536;
20
+
21
+ /** Encode a state through the GENERATED layout, so byte-identity is honest. */
22
+ function packStateVector(state) {
23
+ const bytes = new Uint8Array(ORBPRO_STATE_VECTOR.size);
24
+ const view = new DataView(bytes.buffer);
25
+ const { offsets } = ORBPRO_STATE_VECTOR;
26
+ view.setFloat64(offsets.epoch, state.epoch, true);
27
+ for (const axis of [0, 1, 2]) {
28
+ view.setFloat64(offsets.position + axis * 8, state.position[axis], true);
29
+ view.setFloat64(offsets.velocity + axis * 8, state.velocity[axis], true);
30
+ }
31
+ view.setUint8(offsets.reference_frame, state.referenceFrame);
32
+ bytes[offsets.reference_frame + 1] = state.reserved?.[0] ?? 0;
33
+ bytes[offsets.reference_frame + 2] = state.reserved?.[1] ?? 0;
34
+ bytes[offsets.reference_frame + 3] = state.reserved?.[2] ?? 0;
35
+ view.setUint32(offsets.flags, state.flags, true);
36
+ return bytes;
37
+ }
38
+
39
+ function decodePacked(bytes) {
40
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
41
+ const { offsets } = ORBPRO_STATE_VECTOR;
42
+ return {
43
+ epoch: view.getFloat64(offsets.epoch, true),
44
+ position: [0, 1, 2].map((axis) => view.getFloat64(offsets.position + axis * 8, true)),
45
+ velocity: [0, 1, 2].map((axis) => view.getFloat64(offsets.velocity + axis * 8, true)),
46
+ referenceFrame: view.getUint8(offsets.reference_frame),
47
+ reserved: [1, 2, 3].map((i) => view.getUint8(offsets.reference_frame + i)),
48
+ flags: view.getUint32(offsets.flags, true),
49
+ };
50
+ }
51
+
52
+ /**
53
+ * A mock propagator with a simulated linear memory (never shrinks, grows in
54
+ * whole pages — WebAssembly semantics, so the leak test measures the same
55
+ * thing it measures on a real artifact).
56
+ */
57
+ class MockPropagatorDriver {
58
+ constructor(defect = null) {
59
+ this.defect = defect;
60
+ this.records = null;
61
+ // One page: tight enough that a per-cycle leak of a few KiB reaches a
62
+ // page boundary inside the self-test's measurement window.
63
+ this.capacityBytes = PAGE;
64
+ this.arenaBytes = 0;
65
+ this.bumpBytes = 0;
66
+ this.callSerial = 0;
67
+ }
68
+
69
+ exportNames() {
70
+ if (this.defect === "missing-exports") {
71
+ return REQUIRED_ABI_EXPORTS.filter((name) => name !== "plugin_destroy");
72
+ }
73
+ return [...REQUIRED_ABI_EXPORTS];
74
+ }
75
+
76
+ memoryBytes() {
77
+ return this.capacityBytes;
78
+ }
79
+
80
+ #reserve(byteLength) {
81
+ this.arenaBytes += byteLength;
82
+ const needed = this.arenaBytes + this.bumpBytes;
83
+ while (needed > this.capacityBytes) {
84
+ this.capacityBytes += PAGE;
85
+ }
86
+ }
87
+
88
+ alloc(byteLength) {
89
+ this.bumpBytes += byteLength;
90
+ const needed = this.arenaBytes + this.bumpBytes;
91
+ while (needed > this.capacityBytes) {
92
+ this.capacityBytes += PAGE;
93
+ }
94
+ return 8; // aligned, non-zero; the mock has no real address space
95
+ }
96
+
97
+ free() {
98
+ // bump allocator: freed on destroy, like the reference module's arena
99
+ }
100
+
101
+ #validate(record) {
102
+ if (!(record.eccentricity < 1) || !(record.meanMotionRevPerDay > 0)) {
103
+ return false;
104
+ }
105
+ return true;
106
+ }
107
+
108
+ initFromOmm(records) {
109
+ if (this.defect !== "confident-nonsense") {
110
+ for (const record of records) {
111
+ if (!this.#validate(record)) return ErrorCode.UNPHYSICAL;
112
+ }
113
+ }
114
+ this.records = [...records];
115
+ this.#reserve(records.length * 88);
116
+ return this.records.length;
117
+ }
118
+
119
+ ingestOne(record) {
120
+ if (this.defect !== "confident-nonsense" && !this.#validate(record)) {
121
+ return ErrorCode.UNPHYSICAL;
122
+ }
123
+ if (!this.records) this.records = [];
124
+ this.records.push(record);
125
+ this.#reserve(88);
126
+ if (this.defect === "count-fallback") {
127
+ // The defect the create-returns-handle primitive exists to kill: the
128
+ // module "returns success" instead of the handle it assigned.
129
+ return 0;
130
+ }
131
+ return this.records.length - 1;
132
+ }
133
+
134
+ #state(julianDate, record) {
135
+ const propagated = propagateTwoBody(record, julianDate);
136
+ let { position, velocity } = propagated;
137
+ if (this.defect === "units-km") {
138
+ position = position.map((value) => value / 1000);
139
+ velocity = velocity.map((value) => value / 1000);
140
+ }
141
+ if (this.defect === "nondeterministic") {
142
+ this.callSerial += 1;
143
+ position = position.map((value) => value + this.callSerial * 1e-9);
144
+ }
145
+ return {
146
+ epoch: julianDate,
147
+ position,
148
+ velocity,
149
+ referenceFrame:
150
+ this.defect === "frame-lies" ? ReferenceFrame.TEME : ReferenceFrame.ECEF,
151
+ reserved: [0, 0, 0],
152
+ flags: StateFlags.VALID,
153
+ };
154
+ }
155
+
156
+ propagate(julianDate, entityIndex) {
157
+ if (!this.records || this.records.length === 0) {
158
+ return { status: ErrorCode.NOT_INITIALIZED, state: null, bytes: new Uint8Array(64) };
159
+ }
160
+ if (!(entityIndex >= 0 && entityIndex < this.records.length)) {
161
+ return { status: ErrorCode.BAD_ENTITY_INDEX, state: null, bytes: new Uint8Array(64) };
162
+ }
163
+ const bytes = packStateVector(this.#state(julianDate, this.records[entityIndex]));
164
+ return { status: ErrorCode.OK, state: decodePacked(bytes), bytes };
165
+ }
166
+
167
+ propagateBatch(julianDate, count) {
168
+ if (!this.records || this.records.length === 0) {
169
+ return { status: ErrorCode.NOT_INITIALIZED, states: [] };
170
+ }
171
+ if (!(count >= 0 && count <= this.records.length)) {
172
+ return { status: ErrorCode.BAD_ENTITY_INDEX, states: [] };
173
+ }
174
+ const states = [];
175
+ for (let index = 0; index < count; index += 1) {
176
+ const state = this.#state(julianDate, this.records[index]);
177
+ if (this.defect === "batch-divergence") {
178
+ state.position = [state.position[0] + 1e-3, state.position[1], state.position[2]];
179
+ }
180
+ states.push(decodePacked(packStateVector(state)));
181
+ }
182
+ return { status: ErrorCode.OK, states };
183
+ }
184
+
185
+ entityCount() {
186
+ return this.records ? this.records.length : 0;
187
+ }
188
+
189
+ destroy() {
190
+ this.records = null;
191
+ this.bumpBytes = 0;
192
+ if (this.defect !== "leaky-destroy") {
193
+ this.arenaBytes = 0;
194
+ }
195
+ // capacityBytes deliberately never shrinks: wasm linear memory semantics.
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Every planted defect names the check that must catch it. Each is a real
201
+ * defect class this program found live (finding §4): the 1000x units error,
202
+ * the {} destroy, the silent acceptance of unphysical elements, the count-1
203
+ * identity race, an undeclared frame, a batch path that is not the same
204
+ * physics, nondeterminism.
205
+ */
206
+ export const PLANTED_DEFECTS = Object.freeze([
207
+ { defect: "units-km", mustFail: "tierB/anchors" },
208
+ { defect: "leaky-destroy", mustFail: "tier4/lifecycle-leak" },
209
+ { defect: "confident-nonsense", mustFail: "tierC/typed-refusals" },
210
+ { defect: "count-fallback", mustFail: "tierC/create-returns-handle" },
211
+ { defect: "frame-lies", mustFail: "tierC/frame-flags-reserved-declared" },
212
+ { defect: "batch-divergence", mustFail: "tierC/batch-single-agreement" },
213
+ { defect: "nondeterministic", mustFail: "tierC/determinism-byte-identity" },
214
+ { defect: "missing-exports", mustFail: "tier0/instantiation-and-exports" },
215
+ ]);
216
+
217
+ /**
218
+ * Run the self-test. Returns {ok, results}: ok only when the baseline mock is
219
+ * clean AND every planted defect is caught by the check that owns it.
220
+ */
221
+ export async function runPropagatorSelfTest(options = {}) {
222
+ const corpus = buildSelfTestCorpus();
223
+ // Small leak windows: the mocks' allocator is deterministic, so the steady
224
+ // state is reached immediately; what matters is that the LEAKY mock grows.
225
+ const leak = options.leak ?? { warmupCycles: 6, measureCycles: 40, entities: 128 };
226
+ const results = [];
227
+
228
+ const baselineChecks = await runPropagatorSuite(
229
+ async () => new MockPropagatorDriver(null),
230
+ { corpus, leak },
231
+ );
232
+ const baselineVerdict = computeVerdict(baselineChecks);
233
+ const baselineOk = baselineVerdict !== "FAIL";
234
+ results.push({
235
+ scenario: "baseline-conformant-mock",
236
+ expected: "no failures",
237
+ verdict: baselineVerdict,
238
+ ok: baselineOk,
239
+ failed: baselineChecks.filter((check) => check.status === "fail").map((c) => c.id),
240
+ });
241
+
242
+ for (const { defect, mustFail } of PLANTED_DEFECTS) {
243
+ const checks = await runPropagatorSuite(
244
+ async () => new MockPropagatorDriver(defect),
245
+ { corpus, leak },
246
+ );
247
+ const verdict = computeVerdict(checks);
248
+ const failedIds = checks.filter((check) => check.status === "fail").map((c) => c.id);
249
+ const caught = verdict === "FAIL" && failedIds.includes(mustFail);
250
+ results.push({
251
+ scenario: `planted:${defect}`,
252
+ expected: `FAIL at ${mustFail}`,
253
+ verdict,
254
+ ok: caught,
255
+ failed: failedIds,
256
+ });
257
+ }
258
+
259
+ return { ok: results.every((entry) => entry.ok), results, bands: BANDS_DEFAULT };
260
+ }
261
+
262
+ export function formatSelfTestReport(outcome) {
263
+ const lines = [];
264
+ lines.push(
265
+ `conformance self-test — ${outcome.ok ? "OK (the gate has been seen to fail)" : "BROKEN"}`,
266
+ );
267
+ for (const entry of outcome.results) {
268
+ const marker = entry.ok ? "ok " : "MISS";
269
+ lines.push(
270
+ ` [${marker}] ${entry.scenario}: expected ${entry.expected}, verdict ${entry.verdict}` +
271
+ (entry.failed.length > 0 ? ` (failed: ${entry.failed.join(", ")})` : ""),
272
+ );
273
+ }
274
+ return lines.join("\n");
275
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The self-test's corpus: generated at runtime from the independent two-body
3
+ * closed form, in exactly the vectors.json format the real kits use
4
+ * (space-data-network-modules propagator/keplerian-reference/vectors/ is the
5
+ * committed exemplar). Generating rather than committing keeps the self-test
6
+ * free of a data file that could drift from the generator — the corpus IS the
7
+ * generator here, which is admissible only because the self-test's job is to
8
+ * prove the SUITE can fail, not to conformance-test a real module.
9
+ */
10
+
11
+ import { ReferenceFrame, StateFlags } from "./abiDriver.js";
12
+ import { GENERIC_ELEMENTS } from "./propagatorSuite.js";
13
+ import { MU, propagateTwoBody } from "./twoBodyReference.js";
14
+
15
+ /** The exemplar corpus's tolerance bands, per quantity. */
16
+ export const BANDS_DEFAULT = Object.freeze({
17
+ position: Object.freeze({ abs: 1e-6, rel: 1e-9 }),
18
+ velocity: Object.freeze({ abs: 1e-9, rel: 1e-9 }),
19
+ time: Object.freeze({ abs: 1e-9, rel: 0 }),
20
+ });
21
+
22
+ export function buildSelfTestCorpus() {
23
+ const offsetsMinutes = [0, 45, 720];
24
+ const cases = [];
25
+ for (const elements of GENERIC_ELEMENTS) {
26
+ for (const minutes of offsetsMinutes) {
27
+ const julianDate = elements.epochJd + minutes / 1440;
28
+ const { position, velocity } = propagateTwoBody(elements, julianDate);
29
+ cases.push({
30
+ id: `self-test-${elements.noradCatId}@+${minutes}min`,
31
+ tier: "B",
32
+ operation: "plugin_propagate",
33
+ params: { elements, julianDate },
34
+ expect: {
35
+ "position.0": position[0],
36
+ "position.1": position[1],
37
+ "position.2": position[2],
38
+ "velocity.0": velocity[0],
39
+ "velocity.1": velocity[1],
40
+ "velocity.2": velocity[2],
41
+ epoch: julianDate,
42
+ reference_frame: ReferenceFrame.ECEF,
43
+ flags: StateFlags.VALID,
44
+ },
45
+ band: BANDS_DEFAULT,
46
+ });
47
+ }
48
+ }
49
+
50
+ return {
51
+ schemaVersion: 1,
52
+ conformance: {
53
+ model: "two-body point-mass, no drag, no J2, no third bodies",
54
+ mu: MU,
55
+ units: "SI throughout: metres, metres/second, Julian days, degrees on input",
56
+ outputFrame: `ECEF (${ReferenceFrame.ECEF})`,
57
+ },
58
+ tolerancePolicy: "fail <=> |observed - expected| > abs + rel * |expected|",
59
+ invariants: [
60
+ { id: "vis-viva-closure", tier: "C", applies: "every propagated state" },
61
+ { id: "period-closure", tier: "C", applies: "every element set" },
62
+ { id: "determinism", tier: "C", applies: "every case" },
63
+ { id: "frame-and-flags-declared", tier: "C", applies: "every propagated state" },
64
+ { id: "refusal-is-typed", tier: "C", applies: "every documented failure" },
65
+ ],
66
+ cases,
67
+ };
68
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Independent two-body closed form, for the conformance runner's SELF-TEST.
3
+ *
4
+ * This is adjudication scaffolding, not product physics: the finding's Tier B
5
+ * definition REQUIRES anchors computed independently of any module under test
6
+ * (graph/findings/official-harness-shapes.md §5), and the self-test needs a
7
+ * conformant baseline to prove that every planted defect is caught. Written
8
+ * from the textbook relations; it mirrors — by construction, not by copy-check
9
+ * — the reference corpus generator in space-data-network-modules
10
+ * propagator/keplerian-reference/vectors/index.mjs. Where the two agree, they
11
+ * agree because two-body motion has one answer.
12
+ */
13
+
14
+ /// WGS-72 gravitational parameter, m^3/s^2 — the model OMM mean elements are
15
+ /// fitted under.
16
+ export const MU = 398600.8e9;
17
+
18
+ /// Earth rotation rate, rad/s (IAU 1982).
19
+ export const EARTH_ROTATION_RATE = 7.292115146706979e-5;
20
+
21
+ export const SECONDS_PER_DAY = 86400.0;
22
+ export const J2000_JD = 2451545.0;
23
+
24
+ /** `fail <=> |observed - expected| > abs + rel * |expected|` */
25
+ export function withinBand(observed, expected, band) {
26
+ return Math.abs(observed - expected) <= band.abs + band.rel * Math.abs(expected);
27
+ }
28
+
29
+ /** Solve Kepler's equation independently of any module under test. */
30
+ export function solveKepler(meanAnomaly, eccentricity) {
31
+ let M = meanAnomaly % (2 * Math.PI);
32
+ if (M < 0) M += 2 * Math.PI;
33
+ let E = eccentricity < 0.8 ? M : Math.PI;
34
+ for (let i = 0; i < 200; i += 1) {
35
+ const f = E - eccentricity * Math.sin(E) - M;
36
+ const fp = 1 - eccentricity * Math.cos(E);
37
+ const d = f / fp;
38
+ E -= d;
39
+ if (Math.abs(d) < 1e-15) return E;
40
+ }
41
+ throw new Error("reference Kepler solve did not converge");
42
+ }
43
+
44
+ /** Greenwich Mean Sidereal Time, radians (IAU 1982). */
45
+ export function gmstRadians(julianDate) {
46
+ const tut1 = (julianDate - J2000_JD) / 36525.0;
47
+ const seconds =
48
+ 67310.54841 +
49
+ (876600.0 * 3600.0 + 8640184.812866) * tut1 +
50
+ 0.093104 * tut1 * tut1 -
51
+ 6.2e-6 * tut1 * tut1 * tut1;
52
+ let gmst = (seconds * ((2 * Math.PI) / SECONDS_PER_DAY)) % (2 * Math.PI);
53
+ if (gmst < 0) gmst += 2 * Math.PI;
54
+ return gmst;
55
+ }
56
+
57
+ /**
58
+ * Closed-form two-body propagation from mean elements to an ECEF state, in
59
+ * METERS (the ABI's normative unit — the km/meters contradiction was W0.1).
60
+ */
61
+ export function propagateTwoBody(elements, julianDate) {
62
+ const {
63
+ epochJd,
64
+ meanMotionRevPerDay,
65
+ eccentricity,
66
+ inclinationDeg,
67
+ raOfAscNodeDeg,
68
+ argOfPericenterDeg,
69
+ meanAnomalyDeg,
70
+ } = elements;
71
+
72
+ const deg = Math.PI / 180;
73
+ const n = (meanMotionRevPerDay * 2 * Math.PI) / SECONDS_PER_DAY;
74
+ const a = Math.cbrt(MU / (n * n));
75
+ const dt = (julianDate - epochJd) * SECONDS_PER_DAY;
76
+ const M = meanAnomalyDeg * deg + n * dt;
77
+ const E = solveKepler(M, eccentricity);
78
+
79
+ const cosE = Math.cos(E);
80
+ const sinE = Math.sin(E);
81
+ const beta = Math.sqrt(1 - eccentricity * eccentricity);
82
+ const xPqw = a * (cosE - eccentricity);
83
+ const yPqw = a * beta * sinE;
84
+ const eDot = n / (1 - eccentricity * cosE);
85
+ const vxPqw = -a * sinE * eDot;
86
+ const vyPqw = a * beta * cosE * eDot;
87
+
88
+ const O = raOfAscNodeDeg * deg;
89
+ const i = inclinationDeg * deg;
90
+ const w = argOfPericenterDeg * deg;
91
+ const cO = Math.cos(O);
92
+ const sO = Math.sin(O);
93
+ const ci = Math.cos(i);
94
+ const si = Math.sin(i);
95
+ const cw = Math.cos(w);
96
+ const sw = Math.sin(w);
97
+
98
+ const r11 = cO * cw - sO * sw * ci;
99
+ const r12 = -cO * sw - sO * cw * ci;
100
+ const r21 = sO * cw + cO * sw * ci;
101
+ const r22 = -sO * sw + cO * cw * ci;
102
+ const r31 = sw * si;
103
+ const r32 = cw * si;
104
+
105
+ const xI = r11 * xPqw + r12 * yPqw;
106
+ const yI = r21 * xPqw + r22 * yPqw;
107
+ const zI = r31 * xPqw + r32 * yPqw;
108
+ const vxI = r11 * vxPqw + r12 * vyPqw;
109
+ const vyI = r21 * vxPqw + r22 * vyPqw;
110
+ const vzI = r31 * vxPqw + r32 * vyPqw;
111
+
112
+ const theta = gmstRadians(julianDate);
113
+ const ct = Math.cos(theta);
114
+ const st = Math.sin(theta);
115
+
116
+ const x = ct * xI + st * yI;
117
+ const y = -st * xI + ct * yI;
118
+ const z = zI;
119
+ const vxRot = ct * vxI + st * vyI;
120
+ const vyRot = -st * vxI + ct * vyI;
121
+
122
+ return {
123
+ position: [x, y, z],
124
+ velocity: [
125
+ vxRot + EARTH_ROTATION_RATE * y,
126
+ vyRot - EARTH_ROTATION_RATE * x,
127
+ vzI,
128
+ ],
129
+ semiMajorAxis: a,
130
+ meanMotion: n,
131
+ };
132
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * The isomorphic module-test harness — ONE dist/isomorphic/module.wasm driven
3
+ * through the browser harness (SAB + Workers) and native WasmEdge with one
4
+ * API, so a module's behavior tests are written once and run per lane.
5
+ *
6
+ * MOVED INTO THE SDK by W1.4 of the official-harness-shapes program (it began
7
+ * life as space-data-network-modules/tests/lib/isomorphicHarness.mjs, which
8
+ * now re-exports this file). Living here makes it part of the SDK's testing
9
+ * contract: module repos stop copying it, and the conformance runner and the
10
+ * module suites drive artifacts through the same loader.
11
+ *
12
+ * Node-only (child_process, fs): import via `space-data-module-sdk/testing/isomorphic`.
13
+ */
14
+
15
+ import assert from "node:assert/strict";
16
+ import { spawnSync } from "node:child_process";
17
+ import fs from "node:fs";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ import { createBrowserModuleHarness } from "../host/browserModuleHarness.js";
21
+ import { loadModule } from "../host/isomorphicLoader.js";
22
+
23
+ export const STANDALONE_RUNTIME_KINDS = Object.freeze(["browser", "wasmedge"]);
24
+
25
+ function resolveWasmPath(wasmPath) {
26
+ if (wasmPath instanceof URL) {
27
+ return fileURLToPath(wasmPath);
28
+ }
29
+ return String(wasmPath);
30
+ }
31
+
32
+ export function isMissingWasmEdgeError(error) {
33
+ return /spawn wasmedge ENOENT|command not found|Failed to launch/i.test(
34
+ String(error),
35
+ );
36
+ }
37
+
38
+ export function isWasmEdgeAvailable() {
39
+ const result = spawnSync("wasmedge", ["--version"], { stdio: "ignore" });
40
+ return result.status === 0;
41
+ }
42
+
43
+ export async function createStandaloneHarness(runtimeKind, wasmPath, options = {}) {
44
+ const resolvedWasmPath = resolveWasmPath(wasmPath);
45
+ if (runtimeKind === "browser") {
46
+ return createBrowserModuleHarness({
47
+ wasmSource: fs.readFileSync(resolvedWasmPath),
48
+ surface: options.surface ?? "command",
49
+ host: options.host,
50
+ hostOptions: options.hostOptions,
51
+ args: options.args,
52
+ env: options.env,
53
+ logOutput: options.logOutput,
54
+ performance: options.performance,
55
+ wasmMemory: options.wasmMemory,
56
+ memory: options.memory,
57
+ sharedMemory: options.sharedMemory,
58
+ allowRawInvoke: options.allowRawInvoke,
59
+ initialMemoryBytes: options.initialMemoryBytes,
60
+ maximumMemoryBytes: options.maximumMemoryBytes,
61
+ });
62
+ }
63
+
64
+ if (runtimeKind === "wasmedge") {
65
+ return loadModule({
66
+ wasmSource: resolvedWasmPath,
67
+ runtimeKind: "wasmedge",
68
+ enableThreads: options.enableThreads ?? false,
69
+ wasmEdgeBinary: options.wasmEdgeBinary,
70
+ wasmEdgeRunnerBinary: options.wasmEdgeRunnerBinary,
71
+ args: options.args,
72
+ env: options.env,
73
+ cwd: options.cwd,
74
+ });
75
+ }
76
+
77
+ throw new Error(`Unsupported runtime kind: ${runtimeKind}`);
78
+ }
79
+
80
+ export async function createStandaloneHarnessOrSkip(
81
+ runtimeKind,
82
+ wasmPath,
83
+ t,
84
+ options = {},
85
+ ) {
86
+ if (runtimeKind === "wasmedge" && !isWasmEdgeAvailable()) {
87
+ t.skip("Install wasmedge to verify the server-path harness.");
88
+ return null;
89
+ }
90
+
91
+ try {
92
+ return await createStandaloneHarness(runtimeKind, wasmPath, options);
93
+ } catch (error) {
94
+ if (runtimeKind === "wasmedge" && isMissingWasmEdgeError(error)) {
95
+ t.skip("Install wasmedge to verify the server-path harness.");
96
+ return null;
97
+ }
98
+ throw error;
99
+ }
100
+ }
101
+
102
+ export function assertSuccessfulResponse(
103
+ response,
104
+ { outputPortId = "response" } = {},
105
+ ) {
106
+ assert.equal(response.statusCode, 0);
107
+ assert.ok(response.errorCode === "" || response.errorCode === null);
108
+ const frame = response.outputs.find((entry) => entry.portId === outputPortId);
109
+ assert.ok(frame, `missing ${outputPortId} output frame`);
110
+ return frame.payload;
111
+ }
112
+
113
+ function toPayloadBytes(payload) {
114
+ if (payload instanceof Uint8Array) {
115
+ return payload;
116
+ }
117
+ if (ArrayBuffer.isView(payload)) {
118
+ return new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength);
119
+ }
120
+ if (payload instanceof ArrayBuffer) {
121
+ return new Uint8Array(payload);
122
+ }
123
+ throw new TypeError("Binary module requests require Uint8Array-compatible payload bytes.");
124
+ }
125
+
126
+ function isBrowserDirectHarness(harness) {
127
+ return harness?.runtime?.kind === "browser" && harness?.runtime?.surface === "direct";
128
+ }
129
+
130
+ export async function invokeBinaryRequest(
131
+ harness,
132
+ payload,
133
+ {
134
+ methodId = "invoke",
135
+ inputPortId = "request",
136
+ inputTypeRef = null,
137
+ alignment = 8,
138
+ } = {},
139
+ ) {
140
+ const payloadBytes = toPayloadBytes(payload);
141
+
142
+ if (!isBrowserDirectHarness(harness)) {
143
+ const response = await harness.invoke({
144
+ methodId,
145
+ inputs: [
146
+ {
147
+ portId: inputPortId,
148
+ typeRef: inputTypeRef,
149
+ payload: payloadBytes,
150
+ },
151
+ ],
152
+ });
153
+ return response;
154
+ }
155
+
156
+ if (
157
+ typeof SharedArrayBuffer !== "function" ||
158
+ !(harness.memory?.buffer instanceof SharedArrayBuffer)
159
+ ) {
160
+ throw new Error(
161
+ "Browser direct binary requests require SharedArrayBuffer-backed module memory.",
162
+ );
163
+ }
164
+ const alloc = harness.instance?.exports?.plugin_alloc;
165
+ const free = harness.instance?.exports?.plugin_free;
166
+ if (typeof alloc !== "function" || typeof free !== "function") {
167
+ throw new Error(
168
+ "Browser direct binary requests require plugin_alloc and plugin_free exports.",
169
+ );
170
+ }
171
+
172
+ const payloadSize = payloadBytes.byteLength;
173
+ const payloadPtr = payloadSize > 0 ? alloc(payloadSize) : 0;
174
+ if (payloadSize > 0 && !payloadPtr) {
175
+ throw new Error("plugin_alloc returned null for binary request payload.");
176
+ }
177
+ if (payloadSize > 0 && payloadPtr % alignment !== 0) {
178
+ if (payloadPtr > 0) {
179
+ free(payloadPtr, payloadSize);
180
+ }
181
+ throw new Error(
182
+ `plugin_alloc returned a payload pointer (${payloadPtr}) that is not ${alignment}-byte aligned.`,
183
+ );
184
+ }
185
+
186
+ try {
187
+ if (payloadSize > 0) {
188
+ new Uint8Array(harness.memory.buffer, payloadPtr, payloadSize).set(payloadBytes);
189
+ }
190
+ const response = await harness.invoke({
191
+ methodId,
192
+ externalArena: new Uint8Array(harness.memory.buffer),
193
+ inputs: [
194
+ {
195
+ portId: inputPortId,
196
+ typeRef: inputTypeRef,
197
+ offset: payloadPtr,
198
+ size: payloadSize,
199
+ alignment,
200
+ },
201
+ ],
202
+ });
203
+ return response;
204
+ } finally {
205
+ if (payloadSize > 0) {
206
+ free(payloadPtr, payloadSize);
207
+ }
208
+ }
209
+ }
210
+
211
+ export async function invokeJsonRequest(
212
+ harness,
213
+ request,
214
+ {
215
+ methodId = "invoke",
216
+ inputPortId = "request",
217
+ outputPortId = "response",
218
+ inputTypeRef = null,
219
+ } = {},
220
+ ) {
221
+ const response = await harness.invoke({
222
+ methodId,
223
+ inputs: [
224
+ {
225
+ portId: inputPortId,
226
+ typeRef: inputTypeRef,
227
+ payload: Buffer.from(JSON.stringify(request), "utf8"),
228
+ },
229
+ ],
230
+ });
231
+ const payload = assertSuccessfulResponse(response, { outputPortId });
232
+ return JSON.parse(new TextDecoder().decode(payload));
233
+ }