diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 492d76b..af3dbd0 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -69,7 +69,7 @@ jobs: js/viewer/README.md js/viewer/generated js/viewer/suite \ js/viewer/polyengine \ _site/js/viewer/ - cp js/runner-polyengine/browser-worker.mjs js/runner-polyengine/engine.mjs \ + cp js/runner-polyengine/browser-worker.mjs js/runner-polyengine/worker-main.mjs \ _site/js/runner-polyengine/ cp components/fixture-suite/tests.lock _site/components/fixture-suite/ cp examples/aggregate/targets.toml _site/examples/aggregate/ diff --git a/js/deno.json b/js/deno.json index 7b4c024..23ecca7 100644 --- a/js/deno.json +++ b/js/deno.json @@ -1,6 +1,6 @@ { "name": "@polymorph/test", - "version": "0.2.1", + "version": "0.3.0", "license": "Apache-2.0", "//": "The polymorph:test stack's JS runner core on JSR — the one consumable JS surface (the repo-root npm-git facade is retired; this export map is the only one). Plain JS with relative and node: imports only; the browser-safe pieces (harness, context, imports, the workers) load in browsers unchanged, and the node: importers (node-runner, browser-driver) are Node/Deno-side drivers. The publish include-list IS the package: everything else under js/ is repo tooling (the polyengine runner leg, selftests, the viewer app).", "exports": { @@ -12,7 +12,6 @@ "./node-runner": "./node-runner.mjs", "./browser-driver": "./browser-driver.mjs", "./polyengine-worker": "./runner-polyengine/browser-worker.mjs", - "./polyengine-engine": "./runner-polyengine/engine.mjs", "./polyengine-worker-main": "./runner-polyengine/worker-main.mjs" }, "publish": { @@ -26,8 +25,7 @@ "node-runner.mjs", "browser-driver.mjs", "runner-polyengine/worker-main.mjs", - "runner-polyengine/browser-worker.mjs", - "runner-polyengine/engine.mjs" + "runner-polyengine/browser-worker.mjs" ] } } diff --git a/js/runner-polyengine/README.md b/js/runner-polyengine/README.md index 17b176a..370d63b 100644 --- a/js/runner-polyengine/README.md +++ b/js/runner-polyengine/README.md @@ -33,16 +33,16 @@ cases out as `not-applicable`. ## Browser leg `browser-worker.mjs` is the runtime-linked sibling of -`js/viewer/browser-worker.mjs` — same reply protocol, same shared -`harness.mjs` case loop (striping, freshCases, timeouts, mark -scheduling), no transpiled artifacts: the run message carries +`js/viewer/browser-worker.mjs` — same reply protocol, the same +`runSuite` case loop as the Deno leg (striping, freshCases, timeouts, +tag scheduling), no transpiled artifacts: the run message carries `{ bundleUrl, translatorUrl, suiteUrl, env?, missing?, only?, shard?, caseTimeoutMs? }` and the worker loads `polyengine-embedder.mjs`, built by `just polyengine-assets` from the pinned JSR graph (one platform-neutral ES module: embedder API + Translator + runner glue + wasi shims). It drops into `page-runner.mjs`'s `runSuitesInPage` via its `workerUrl` parameter — -page runner and browser driver unchanged. `engine.mjs` is the shared -glue; `selftest.mjs` drives the same engine path under plain `node` +page runner and browser driver unchanged. `worker-main.mjs` holds the +message loop; `selftest.mjs` drives the same `runSuite` path under plain `node` (NO `--experimental-wasm-jspi` — the callback ABI needs no engine flag), asserting the documented sample/fixture verdicts, trap containment, tag scheduling, and striping partition equality; it runs diff --git a/js/runner-polyengine/bench-mint.mjs b/js/runner-polyengine/bench-mint.mjs index 4891036..61a12df 100644 --- a/js/runner-polyengine/bench-mint.mjs +++ b/js/runner-polyengine/bench-mint.mjs @@ -66,6 +66,19 @@ console.error( const artifacts = { plan, componentBytes: suiteBytes, adapters }; const TESTS = "polymorph:test/tests@0.1.0"; +// The host-side `test-context` provider, verbatim from ct-runner's internal +// context.ts (un-exported since polyengine 0.6.0; runSuite wires its own, +// but this bench drives `instantiate` directly and must supply one). +class Context { + constructor(onDiagnostic) { + this.onDiagnostic = onDiagnostic; + } + // deno-lint-ignore require-await + async diagnostic(msg) { + this.onDiagnostic(msg); + } +} + function median(xs) { const v = [...xs].sort((a, b) => a - b); return v[Math.floor(v.length / 2)]; @@ -83,7 +96,7 @@ console.log( for (const n of caseCounts) { const imports = { ...polyengine.wasi({ cli: { env: { BENCH_CASES: String(n) } } }), - ...polyengine.testContextImportRecord(), + "polymorph:test/test-context@0.1.0": { Context }, }; const warmup = Math.min(3, instances); const samples = { instantiate: [], all1: [], all2: [], name0: [], run0: [] }; @@ -112,7 +125,7 @@ for (const n of caseCounts) { throw new Error(`unexpected case name '${name}'`); } - const ctx = new polyengine.Context(() => {}); + const ctx = new Context(() => {}); t = performance.now(); await cases1[0].run(ctx); // resolves = pass; throws = fail/trap const run0 = performance.now() - t; diff --git a/js/runner-polyengine/deno.json b/js/runner-polyengine/deno.json index 970ee1b..f3c53c5 100644 --- a/js/runner-polyengine/deno.json +++ b/js/runner-polyengine/deno.json @@ -30,14 +30,14 @@ // deno.lock, re-run `deno install --entrypoint runner.ts // browser-bundle-entry.ts` in this directory, and commit the diff (see // README.md "Pinning"). `@polyengine/protocol` is on its own independent - // version line (currently 0.2.2) and is bumped separately. + // version line (currently 0.3.1) and is bumped separately. "imports": { - "@polyengine/ct-runner": "jsr:@polyengine/ct-runner@0.5.0", - "@polyengine/protocol": "jsr:@polyengine/protocol@0.2.2", - "@polyengine/runtime/embedder": "jsr:@polyengine/runtime@0.5.0/embedder", - "@polyengine/runtime/shim": "jsr:@polyengine/runtime@0.5.0/shim", - "@polyengine/translator": "jsr:@polyengine/translator@0.5.0", - "@polyengine/wasi": "jsr:@polyengine/wasi@0.5.0" + "@polyengine/ct-runner": "jsr:@polyengine/ct-runner@0.6.3", + "@polyengine/protocol": "jsr:@polyengine/protocol@0.3.1", + "@polyengine/runtime/embedder": "jsr:@polyengine/runtime@0.6.3/embedder", + "@polyengine/runtime/shim": "jsr:@polyengine/runtime@0.6.3/shim", + "@polyengine/translator": "jsr:@polyengine/translator@0.6.3", + "@polyengine/wasi": "jsr:@polyengine/wasi@0.6.3" }, // Deno's supply-chain minimum-dependency-age gate (24h default) applies // even to exact prerelease pins; polyengine publishes per commit, so the diff --git a/js/runner-polyengine/deno.lock b/js/runner-polyengine/deno.lock index 4ab6829..137733b 100644 --- a/js/runner-polyengine/deno.lock +++ b/js/runner-polyengine/deno.lock @@ -1,51 +1,51 @@ { "version": "5", "specifiers": { - "jsr:@polyengine/ct-runner@0.5.0": "0.5.0", - "jsr:@polyengine/protocol@0.2.2": "0.2.2", - "jsr:@polyengine/protocol@~0.2.2": "0.2.2", - "jsr:@polyengine/runtime@0.5": "0.5.0", - "jsr:@polyengine/runtime@0.5.0": "0.5.0", - "jsr:@polyengine/translator@0.5.0": "0.5.0", - "jsr:@polyengine/wasi@0.5.0": "0.5.0" + "jsr:@polyengine/ct-runner@0.6.3": "0.6.3", + "jsr:@polyengine/protocol@0.3.1": "0.3.1", + "jsr:@polyengine/protocol@~0.3.1": "0.3.1", + "jsr:@polyengine/runtime@0.6.3": "0.6.3", + "jsr:@polyengine/runtime@~0.6.3": "0.6.3", + "jsr:@polyengine/translator@0.6.3": "0.6.3", + "jsr:@polyengine/wasi@0.6.3": "0.6.3" }, "jsr": { - "@polyengine/ct-runner@0.5.0": { - "integrity": "be3219d9ed283a10386fc76590149419888544980155e84f72b988f1b248a2e5", + "@polyengine/ct-runner@0.6.3": { + "integrity": "61821fbc7d8de1aab0a82a6ab3adfa89cc63b87c1a4573516e45a407d501027c", "dependencies": [ - "jsr:@polyengine/protocol@~0.2.2", - "jsr:@polyengine/runtime@0.5" + "jsr:@polyengine/protocol@~0.3.1", + "jsr:@polyengine/runtime@~0.6.3" ] }, - "@polyengine/protocol@0.2.2": { - "integrity": "ff470d7183924119ae181cc513093318f1587f7ad5c4bc02db7344e90c9ff52b" + "@polyengine/protocol@0.3.1": { + "integrity": "3cba0ded857c5cc911fcf9c0782c7566c79af89ddc8e0ec4e48ece60c807a518" }, - "@polyengine/runtime@0.5.0": { - "integrity": "36ea9b74dd38c0f5fb941695cfa3223190fda54bc18f7328600ece082dbbb794", + "@polyengine/runtime@0.6.3": { + "integrity": "8f1ffc1c547793bc4cb3c1dec6a62c8cf0eb5cdb135b70e6df1fd042c6ec58f0", "dependencies": [ - "jsr:@polyengine/protocol@~0.2.2" + "jsr:@polyengine/protocol@~0.3.1" ] }, - "@polyengine/translator@0.5.0": { - "integrity": "2f9770f468bd0c649732e6107d69715dea715c71564fe5b0ba0997b5d70e81c3", + "@polyengine/translator@0.6.3": { + "integrity": "ccd0bb64ce3081d70b13e224c77e423faf7acf9953f62d4e0d2bab97c626c932", "dependencies": [ - "jsr:@polyengine/runtime@0.5" + "jsr:@polyengine/runtime@~0.6.3" ] }, - "@polyengine/wasi@0.5.0": { - "integrity": "61275e66895084807393bf93876b74be9cc1ca37641d56a07cf8c7b9a78ce091", + "@polyengine/wasi@0.6.3": { + "integrity": "5c738fe95101f22e469346618e79ce261dd636a5d44e89fdcdd40c33d8c92e17", "dependencies": [ - "jsr:@polyengine/protocol@~0.2.2" + "jsr:@polyengine/protocol@~0.3.1" ] } }, "workspace": { "dependencies": [ - "jsr:@polyengine/ct-runner@0.5.0", - "jsr:@polyengine/protocol@0.2.2", - "jsr:@polyengine/runtime@0.5.0", - "jsr:@polyengine/translator@0.5.0", - "jsr:@polyengine/wasi@0.5.0" + "jsr:@polyengine/ct-runner@0.6.3", + "jsr:@polyengine/protocol@0.3.1", + "jsr:@polyengine/runtime@0.6.3", + "jsr:@polyengine/translator@0.6.3", + "jsr:@polyengine/wasi@0.6.3" ] } } diff --git a/js/runner-polyengine/engine.mjs b/js/runner-polyengine/engine.mjs deleted file mode 100644 index 4809db8..0000000 --- a/js/runner-polyengine/engine.mjs +++ /dev/null @@ -1,65 +0,0 @@ -// Shared engine glue for the polyengine legs (browser shard worker + Node -// selftest): load the repo-built polyengine embedder bundle (`just -// polyengine-assets`, from the pinned JSR graph), translate the suite -// component, and hand back exactly what harness.mjs `runCases` needs. -// polyengine is a runtime linker — the suite arrives as the COMPONENT wasm -// (no transpiled module, no core files, no imports module; WASI comes -// from the bundle's wasi(), test-context from its ct-runner glue). -// -// The Context handed to runCases MUST be the bundle's own Context class: -// `testContextImportRecord()` registers that exact class as the -// `polymorph:test/test-context` host resource, and polyengine lowers borrows -// of it by class identity. - -/** - * @param {object} input - * @param {object|string} input.bundle The polyengine embedder module, or a URL - * string to import it from (workers pass the URL; Node imports first). - * @param {Uint8Array} input.translatorBytes polyengine-translator-shim.wasm. - * @param {Uint8Array} input.suiteBytes The suite COMPONENT wasm. - * @param {[string, string][]} [input.env] wasi:cli environment pairs. - * @param {object} [input.hostImports] SUT host-import record fragments - * (interface id -> implementation), merged over the engine's wasi + - * test-context imports. MUST be built against the same embedder module - * instance as `bundle` (one runtime copy for handle interop; see - * worker-main.mjs — `@polyengine/protocol` vocabulary like - * ComponentException is cross-copy safe by construction). - * @returns {Promise<{newTests: () => Promise, Context, tagsOf}>} - */ -export async function loadSuite( - { bundle, translatorBytes, suiteBytes, env = [], hostImports = {} }, -) { - const polyengine = typeof bundle === "string" ? await import(bundle) : bundle; - const translator = await polyengine.Translator.create(translatorBytes); - const { plan, adapters } = translator.translate(suiteBytes); - const artifacts = { plan, componentBytes: suiteBytes, adapters }; - - const imports = { - ...polyengine.wasi({ cli: { env: Object.fromEntries(env) } }), - ...polyengine.testContextImportRecord(), - ...hostImports, - }; - - const newTests = async () => { - const inst = await polyengine.instantiate(artifacts, imports); - const tests = inst.exports["polymorph:test/tests@0.1.0"] ?? inst.exports["tests"]; - if (tests === undefined) { - throw new Error( - `suite exports no tests interface: ${Object.keys(inst.exports)}`, - ); - } - return tests; - }; - - // The suite's own L0 inventory (polyengine reads it from the component, - // nested core modules included — the same records harness.mjs's - // inventoryLookup reads from transpiled cores). A suite without records - // yields an always-undefined lookup, and runCases throws inventory - // drift on the first case — same posture as the jco worker. - const inventory = polyengine.loadTagsInventory(suiteBytes); - const tagsOf = inventory === null - ? () => undefined - : (name) => polyengine.tagsOf(inventory, name); - - return { newTests, Context: polyengine.Context, tagsOf }; -} diff --git a/js/runner-polyengine/selftest.mjs b/js/runner-polyengine/selftest.mjs index e2bbf9a..2b1c551 100644 --- a/js/runner-polyengine/selftest.mjs +++ b/js/runner-polyengine/selftest.mjs @@ -1,9 +1,10 @@ -// The polyengine engines' drift gate (verify-polyengine's selftest leg) — the -// runtime-linked sibling of js/viewer/selftest.mjs, running the SAME -// harness.mjs case loop over polyengine-instantiated suites. Plain `node`, -// NO --experimental-wasm-jspi: polyengine's callback-ABI path needs no -// engine flag, which is the browser-leg premise this gate pins on every -// PR (the real-browser proof lives in polyengine's own post-merge lanes). +// The polyengine engine's drift gate (verify-polyengine's selftest leg) — the +// runtime-linked sibling of js/viewer/selftest.mjs, driving the SAME +// ct-runner `runSuite` case loop the browser worker (./worker-main.mjs) and +// the Deno leg (./runner.ts) use. Plain `node`, NO +// --experimental-wasm-jspi: polyengine's callback-ABI path needs no engine +// flag, which is the browser-leg premise this gate pins on every PR (the +// real-browser proof lives in polyengine's own post-merge lanes). // // node js/runner-polyengine/selftest.mjs \ // @@ -11,8 +12,7 @@ import { strict as assert } from "node:assert"; import { readFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; -import { mergeCounts, runCases } from "../viewer/harness.mjs"; -import { loadSuite } from "./engine.mjs"; +import { mergeCounts } from "../viewer/harness.mjs"; const [bundlePath, translatorPath, samplePath, fixturePath] = process.argv.slice(2); if (!fixturePath) { @@ -23,38 +23,46 @@ if (!fixturePath) { process.exit(2); } -const bundle = await import(pathToFileURL(bundlePath).href); -const translatorBytes = new Uint8Array(readFileSync(translatorPath)); +const polyengine = await import(pathToFileURL(bundlePath).href); +const translator = await polyengine.Translator.create( + new Uint8Array(readFileSync(translatorPath)), +); -async function suiteOf(path, env) { - return await loadSuite({ - bundle, - translatorBytes, - suiteBytes: new Uint8Array(readFileSync(path)), - env, - }); +function artifactsOf(path) { + const suiteBytes = new Uint8Array(readFileSync(path)); + const { plan, adapters } = translator.translate(suiteBytes); + return { plan, componentBytes: suiteBytes, adapters }; } -async function run(engine, { missing = [], only, shard } = {}) { +async function run(artifacts, { missing, only, shard } = {}) { const events = []; - const counts = await runCases({ - cases: await (await engine.newTests()).all(), - Context: engine.Context, - tagsOf: engine.tagsOf, + const counts = await polyengine.runSuite(artifacts, { + imports: polyengine.wasi({ cli: { env: {} } }), + target: "polyengine/node-selftest", + suiteName: "selftest", missing, only, shard, - emit: (event, index) => events.push({ index, event }), - freshCases: async () => (await engine.newTests()).all(), + emit: (line, index) => { + // Envelope and terminator carry no case index — the worker drops them + // the same way (./worker-main.mjs). + if (index !== undefined) events.push({ index, event: JSON.parse(line) }); + }, }); events.sort((a, b) => a.index - b.index); - return { counts, events: events.map((e) => e.event) }; + // ./worker-main.mjs's counts derivation, verbatim: `RunCounts.total` is + // this stripe's pre-`only` census size, and the parent page-runner needs + // `selected` for its empty-selection check. + const selected = counts.passed + counts.failed + counts.skipped + counts.na; + return { + counts: { ...counts, selected, deselected: counts.total - selected }, + events: events.map((e) => e.event), + }; } // --- sample: the documented verdicts, no flags anywhere ----------------------- { - const engine = await suiteOf(samplePath); - const { counts, events } = await run(engine); + const { counts, events } = await run(artifactsOf(samplePath)); assert.deepEqual(counts, { passed: 1, failed: 1, @@ -73,8 +81,8 @@ async function run(engine, { missing = [], only, shard } = {}) { // --- fixture: trap containment + tag scheduling through the polyengine engine ----- { - const engine = await suiteOf(fixturePath); - const { counts, events } = await run(engine, { missing: ["hsm"] }); + const fixture = artifactsOf(fixturePath); + const { counts, events } = await run(fixture, { missing: ["hsm"] }); assert.deepEqual(counts, { passed: 6, failed: 1, @@ -94,45 +102,41 @@ async function run(engine, { missing = [], only, shard } = {}) { case: "fixture/hsm/attest", status: "not-applicable", detail: "hsm", + "diagnostics-complete": true, }); assert.equal(byCase["fixture/hsm/declined"].status, "pass"); console.log("selftest: fixture trap + tag scheduling ok"); - // Selection (#89): the unselected census is reported deselected — - // full coverage, never executed, capability winning over selection - // (hsm/attest stays not-applicable outside the filter, exactly the - // reference runner's precedence). The trap case is outside the + // Selection (#89): `runSuite` skips non-matching cases with no emit, so + // the deselected census is a count, not rows (docs/runner-policy.md's + // "selection is not capability" still holds — capability wins over + // selection for whatever the filter admits). The trap case is outside the // selection: nothing fails. - const sub = await run(engine, { missing: ["hsm"], only: "gen" }); + const sub = await run(fixture, { missing: ["hsm"], only: "gen" }); assert.deepEqual(sub.counts, { passed: 2, failed: 0, skipped: 0, - na: 1, - deselected: 5, + na: 0, + deselected: 6, selected: 2, total: 8, }); const subByCase = Object.fromEntries(sub.events.map((e) => [e.case, e])); - assert.deepEqual(subByCase["fixture/trap/boom"], { - case: "fixture/trap/boom", - status: "deselected", - detail: "only gen", - }); - assert.equal(subByCase["fixture/hsm/attest"].status, "not-applicable"); + assert.equal(subByCase["fixture/trap/boom"], undefined, "deselected: no row"); assert.equal(subByCase["fixture/gen/tc1"].status, "pass"); - assert.equal(sub.events.length, 8, "full census reported"); - // A selection matching nothing is a run error, not a vacuous green. - await assert.rejects( - () => run(engine, { only: "zzz" }), - /empty selection is a run error/, - ); - console.log("selftest: only -> deselected census ok"); + assert.equal(sub.events.length, 2, "only the selected cases are reported"); + // A selection matching nothing is a vacuous run here; the PARENT + // (js/viewer/page-runner.mjs) turns `selected === 0` into the run error. + const none = await run(fixture, { missing: ["hsm"], only: "zzz" }); + assert.equal(none.counts.selected, 0); + assert.equal(none.events.length, 0); + console.log("selftest: only -> selection counts ok"); - // Striping partition equality (harness semantics over the polyengine engine): - // two shards merge to the full counts, disjoint cases, full union. - const s0 = await run(engine, { missing: ["hsm"], shard: { index: 0, count: 2 } }); - const s1 = await run(engine, { missing: ["hsm"], shard: { index: 1, count: 2 } }); + // Striping partition equality (runSuite semantics over the polyengine + // engine): two shards merge to the full counts, disjoint cases, full union. + const s0 = await run(fixture, { missing: ["hsm"], shard: { index: 0, count: 2 } }); + const s1 = await run(fixture, { missing: ["hsm"], shard: { index: 1, count: 2 } }); assert.deepEqual(mergeCounts([s0.counts, s1.counts]), counts); const names = (r) => r.events.map((e) => e.case); const union = new Set([...names(s0), ...names(s1)]); @@ -141,4 +145,4 @@ async function run(engine, { missing = [], only, shard } = {}) { console.log("selftest: striping partition equality ok"); } -console.log("selftest: polyengine engines ok"); +console.log("selftest: polyengine engine ok"); diff --git a/js/runner-polyengine/worker-main.mjs b/js/runner-polyengine/worker-main.mjs index c6b04cd..2523ae9 100644 --- a/js/runner-polyengine/worker-main.mjs +++ b/js/runner-polyengine/worker-main.mjs @@ -1,5 +1,11 @@ // The polyengine shard worker's message loop, shared by the stock worker -// (./browser-worker.mjs) and downstream repos' bundled workers. +// (./browser-worker.mjs) and downstream repos' bundled workers. The case +// loop is ct-runner's `runSuite` — the same engine path the Deno leg +// (./runner.ts) drives — which mirrors js/viewer/harness.mjs semantics +// (striping, freshCases, timeouts, tag scheduling) and provides +// test-context itself. Per-shard rows are relayed as they are emitted; +// the envelope and terminator are dropped (the parent, page-runner.mjs, +// writes the single merged pair). // // A downstream conformance suite usually imports a SUT host module // (`polymorph:websocket/connections`, `polymorph:webcrypto/*`, …) that @@ -32,9 +38,6 @@ // { kind: "counts", counts } on completion, // { kind: "error", error } on harness breakage. -import { runCases } from "../viewer/harness.mjs"; -import { loadSuite } from "./engine.mjs"; - async function fetchBytes(url) { const res = await fetch(url); if (!res.ok) throw new Error(`fetching ${url}: ${res.status}`); @@ -52,7 +55,8 @@ async function fetchBytes(url) { * @param {(input: { polyengine: object, env: [string, string][] }) * => object | Promise} [options.suiteImports] Builds the * SUT host-import record for one suite instance; merged over the - * engine's own wasi + test-context imports. Called once per run + * engine's own wasi imports (`runSuite` wires test-context itself). + * Called once per run * message (instances share module-level host state exactly as the * repos' Deno legs do). */ @@ -68,7 +72,7 @@ export function workerMain({ polyengine, suiteImports } = {}) { translatorUrl, suiteUrl, env = [], - missing = [], + missing, only, shard, caseTimeoutMs, @@ -84,30 +88,47 @@ export function workerMain({ polyengine, suiteImports } = {}) { const hostImports = suiteImports ? await suiteImports({ polyengine: resolved, env }) : undefined; - const { newTests, Context, tagsOf } = await loadSuite({ - bundle: resolved, - translatorBytes, - suiteBytes, - env, - hostImports, - }); - const counts = await runCases({ - cases: await (await newTests()).all(), - Context, - tagsOf, - missing, + const translator = await resolved.Translator.create(translatorBytes); + const { plan, adapters } = translator.translate(suiteBytes); + const artifacts = { plan, componentBytes: suiteBytes, adapters }; + const imports = { + ...resolved.wasi({ cli: { env: Object.fromEntries(env) } }), + // No test-context here: `runSuite` wires its own provider and + // errors on a caller collision (ct-runner/src/run-suite.ts). + ...hostImports, + }; + + const counts = await resolved.runSuite(artifacts, { + imports, + // The envelope this run message produces is discarded (the parent + // page-runner writes the single merged envelope for all shards), + // so target/suite name are placeholders, not run identity. + target: "polyengine/worker", + suiteName: "shard", only, + missing, shard, caseTimeoutMs, - emit: (event, index) => self.postMessage({ kind: "event", index, event }), - // `freshCases: false` reuses the census instance for the whole - // shard — the documented trade for corpora whose per-case fresh - // instances outrun the renderer's wasm-memory reservations (a - // trapped case then poisons the rest of the shard, loudly). - ...(freshCases ? { freshCases: async () => (await newTests()).all() } : {}), + // `freshCases: false` reuses one instance for the whole shard — + // the documented trade for corpora whose per-case fresh instances + // outrun the renderer's wasm-memory reservations (a trapped case + // then poisons the rest of the shard, loudly). + freshCases, + emit: (line, caseIndex) => { + // Envelope and `{"segment-end":true}` terminator carry no case + // index; the parent writes its own, so drop them here. + if (caseIndex === undefined) return; + self.postMessage({ kind: "event", index: caseIndex, event: JSON.parse(line) }); + }, + }); + // `RunCounts.total` is this stripe's full case count, before `only` + // filtering; the parent's empty-selection check reads `selected`. + const selected = counts.passed + counts.failed + counts.skipped + counts.na; + self.postMessage({ + kind: "counts", + counts: { ...counts, selected, deselected: counts.total - selected }, }); - self.postMessage({ kind: "counts", counts }); } catch (err) { self.postMessage({ kind: "error", error: String(err?.stack ?? err) }); } diff --git a/js/viewer/selftest.mjs b/js/viewer/selftest.mjs index 1c0ae5c..7864fdd 100644 --- a/js/viewer/selftest.mjs +++ b/js/viewer/selftest.mjs @@ -9,7 +9,7 @@ // freshCases + caseTimeoutMs over synthetic cases. // // The suite-execution half of the old selftest lives in -// js/runner-polyengine/selftest.mjs now — same harness.mjs loop, same +// js/runner-polyengine/selftest.mjs now — ct-runner's runSuite loop, same // suites, polyengine engine (verify-polyengine's last leg). Plain `node`: // nothing here needs --experimental-wasm-jspi. // diff --git a/justfile b/justfile index b169d69..2d021b3 100644 --- a/justfile +++ b/justfile @@ -234,8 +234,8 @@ polyengine-assets: polyengine-pin-gate # inventory (polyengine#25): the fixture leg runs --missing hsm exactly like # Paths 1/4 and schedules the hsm case out as not-applicable; the sample # legs reuse Path 2/3's human golden and Path 2/4's fold golden. The -# selftest leg drives the BROWSER worker's engine path (js/runner-polyengine/ -# engine.mjs + the shared harness.mjs case loop) over the pinned embedder +# selftest leg drives the BROWSER worker's engine path (ct-runner's +# runSuite, as js/runner-polyengine/worker-main.mjs drives it) over the pinned embedder # bundle under plain node — no --experimental-wasm-jspi: the callback ABI # needs no engine flag, which is the browser-leg premise. verify-polyengine: build polyengine-assets