From 6b8f8d33efd074032cd3bbb7aafe4637f7b583c4 Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Thu, 6 Aug 2026 17:09:51 -0400 Subject: [PATCH 1/2] Shadow location with a fixed stub in every WPT parity leg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wpt-env location stub was installed only when the global was absent, so the Chromium baseline leg evaluated test names against the real page origin — the run-browser static server's per-run ephemeral port — while the round-trip guest used the stub. The three Create-invalid-urls names that interpolate location then keyed differently in every run and never matched across legs: the recorded loss set went stale on the next run, the same three tests surfaced as phantom losses, and their stub-named round-trip passes tripped the unmeasured-pass hard failure. (Issue #26 attributed the embedded port to the echo server; echod's URL only ever reaches test bodies, never names.) Declare the stub as a module-scoped const instead: the generated group modules prepend wpt-env.js at module scope, so the lexical binding shadows window.location in a browser page and every leg evaluates identical names by construction. Record the now-stable Chromium ratchet (empty — the phantom losses dissolve; Chromium's two lenient constructor validations remain visible as non-gating exceeded notes), and drop the blocked-gate notes: the Chromium legs stay out of CI only for want of a Chrome 137+ binary on the runner. Verified: just wpt::parity green and byte-identical Node leg records; just wpt::parity-chromium green twice in a row across different ephemeral ports. Fixes #26 --- js/componentize/wpt/README.md | 8 +++--- js/componentize/wpt/justfile | 7 +++--- js/componentize/wpt/parity/losses-chromium.js | 11 ++++++++ js/componentize/wpt/parity/run-browser.mjs | 5 ---- js/componentize/wpt/wpt-env.js | 25 +++++++++++-------- 5 files changed, 32 insertions(+), 24 deletions(-) create mode 100644 js/componentize/wpt/parity/losses-chromium.js diff --git a/js/componentize/wpt/README.md b/js/componentize/wpt/README.md index ecfcbdc..89aecf4 100644 --- a/js/componentize/wpt/README.md +++ b/js/componentize/wpt/README.md @@ -31,10 +31,10 @@ unbundled. Each engine ratchets separately (`parity/losses-chromium.js`, re-recorded with `just wpt::update-losses-chromium`): a loss set is a fact about one engine's baseline. -The Chromium gate is currently blocked at the ratchet: some vendored test -names embed the echo server's per-run port, so the Chromium loss set does -not record stably (issue #26). Both legs complete; the gate joins CI when -the ratchet keys are stable. +The Chromium gate is not yet in CI: it needs a Chrome 137+ binary on the +runner. Test names are engine-independent by construction — `wpt-env.js` +shadows `location` with a fixed stub in every leg, so names never embed a +per-run origin. ## Vendoring policy diff --git a/js/componentize/wpt/justfile b/js/componentize/wpt/justfile index a0bea79..ffd0a9b 100644 --- a/js/componentize/wpt/justfile +++ b/js/componentize/wpt/justfile @@ -30,14 +30,13 @@ update-losses: _artifacts # baseline measures Chromium's own WebSocket, the round trip runs the # browser-profile transpile against websocket-jco in the environment it # targets. Ratchets separately in parity/losses-chromium.js — a loss set -# is a fact about one engine's baseline. Currently blocked at the ratchet: -# some test names embed the echo server's per-run port, so the loss set -# does not record stably (issue #26); not in CI until that resolves. +# is a fact about one engine's baseline. Not yet in CI: needs a Chrome +# 137+ binary on the runner. parity-chromium: _artifacts-web {{timeout_cmd}} node {{root}}/js/componentize/wpt/parity/run-browser.mjs # Re-record parity/losses-chromium.js from an actual Chromium run; same -# review bar as update-losses. Blocked with parity-chromium (issue #26). +# review bar as update-losses. update-losses-chromium: _artifacts-web {{timeout_cmd}} node {{root}}/js/componentize/wpt/parity/run-browser.mjs --update diff --git a/js/componentize/wpt/parity/losses-chromium.js b/js/componentize/wpt/parity/losses-chromium.js new file mode 100644 index 0000000..522b5b4 --- /dev/null +++ b/js/componentize/wpt/parity/losses-chromium.js @@ -0,0 +1,11 @@ +// The WPT parity ratchet: every test the platform baseline passes that the +// round trip (shim -> WIT -> jco -> websocket-jco) does not, keyed +// `group :: test name`. Generated by `just wpt::update-losses-chromium`; compare.mjs +// fails on any loss not listed here and on any listed loss no longer +// observed, so this file only changes deliberately and only shrinks as the +// shim serves more of the platform's behavior. Which kind each loss is — +// unserved or WIT-forced — is the shim header's deviations registry's to +// say (js/componentize/websocket.js). +export const KNOWN_LOSSES = [ + +]; diff --git a/js/componentize/wpt/parity/run-browser.mjs b/js/componentize/wpt/parity/run-browser.mjs index 60abce8..a1e4241 100644 --- a/js/componentize/wpt/parity/run-browser.mjs +++ b/js/componentize/wpt/parity/run-browser.mjs @@ -16,11 +16,6 @@ // // jco's async ABI needs JSPI; Chrome ships it enabled from 137 onward. // -// KNOWN BLOCKED (issue #26): both legs complete, but several vendored -// tests interpolate the echo server's per-run port into their names, so -// the recorded loss set does not key stably across runs. This runner -// stays out of CI until the ratchet keys are stable. -// // Usage: node run-browser.mjs [--update] import http from "node:http"; diff --git a/js/componentize/wpt/wpt-env.js b/js/componentize/wpt/wpt-env.js index 4b0a8fb..a1414c1 100644 --- a/js/componentize/wpt/wpt-env.js +++ b/js/componentize/wpt/wpt-env.js @@ -28,17 +28,20 @@ Object.defineProperty(globalThis, "SCHEME_DOMAIN_PORT", { }); // A minimal `location` for suites that build negative-case URLs from the -// page origin. The values are inert (nothing serves them); identical in -// both parity legs so the comparison stays fair. -if (globalThis.location === undefined) { - globalThis.location = { - protocol: "http:", - host: "web-platform.test:8001", - hostname: "web-platform.test", - origin: "http://web-platform.test:8001", - search: "", - }; -} +// page origin. The values are inert (nothing serves them) and are +// wptserve's canonical host:port, so interpolated test names match +// upstream WPT's. A module-scoped `const` deliberately shadows +// `window.location` when a leg runs in a browser page: suites that +// interpolate `location` into test names (Create-invalid-urls) must +// evaluate identical names in every leg, or the parity keys embed the +// page server's per-run ephemeral port and the legs' name sets diverge. +const location = { + protocol: "http:", + host: "web-platform.test:8001", + hostname: "web-platform.test", + origin: "http://web-platform.test:8001", + search: "", +}; function IsWebSocket() { if (!self.WebSocket) { From 233314b42f6c599a943ddfa0587d1f7b3b6779b0 Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Thu, 6 Aug 2026 19:38:44 -0400 Subject: [PATCH 2/2] Gate the Chromium WPT parity legs in CI on Playwright's pinned build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopt the webcrypto sibling's pattern: the parity browser legs always launch Playwright's own Chromium build, pinned by playwright-core's version in the parity lockfile, so losses-chromium.js measures one engine everywhere — local runs and CI alike — and a Chromium behavior shift arrives only with a deliberate playwright bump, never with a runner-image update. run-browser.mjs drops the system-Chrome discovery (findChrome stays with the conformance jco-browser leg, which prefers the runner's Chrome on purpose). The conformance job installs the pinned browser with a cache keyed on the parity lockfile (restore-keys let a bump download only what changed; --with-deps only on a cache miss), and conformance-checks gains the wpt::parity-chromium step. Verified: just wpt::parity-chromium green on the pinned build (Chrome for Testing 151 / chromium-1234) with the same empty loss set; just ci green end to end. Closes #35 --- .github/justfile | 1 + .github/workflows/ci.yml | 23 ++++++++++++++++++++++ js/componentize/wpt/README.md | 9 ++++++--- js/componentize/wpt/justfile | 6 ++++-- js/componentize/wpt/parity/run-browser.mjs | 13 +++++------- scripts/chrome.mjs | 7 ++++--- 6 files changed, 43 insertions(+), 16 deletions(-) diff --git a/.github/justfile b/.github/justfile index 092b842..023a96b 100644 --- a/.github/justfile +++ b/.github/justfile @@ -34,4 +34,5 @@ rust-checks: conformance-checks: @just gha::_step conformance-ct::all @just gha::_step wpt::parity + @just gha::_step wpt::parity-chromium @just gha::_step demo::test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55e0e3d..5f33f40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,6 +85,29 @@ jobs: with: path: ${{ steps.pnpm-store.outputs.path }} key: pnpm-store-v2-${{ runner.os }}-${{ hashFiles('conformance/driver-ct/jco/pnpm-lock.yaml', 'examples/jco-demo/pnpm-lock.yaml', 'js/componentize/wpt/parity/pnpm-lock.yaml') }} + # The WPT parity Chromium leg always runs Playwright's own build, + # pinned by playwright-core's version in the parity lockfile, so + # losses-chromium.js measures one engine everywhere. The cache key + # follows that lockfile; restore-keys lets a playwright bump start + # from the previous browsers and download only what changed. + - name: Cache Playwright browsers + id: playwright-cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('js/componentize/wpt/parity/pnpm-lock.yaml') }} + restore-keys: playwright-${{ runner.os }}- + - name: Install Playwright Chromium + # On a cache hit the install only verifies the browser is present; + # --with-deps (an apt run) is needed only when the browser was + # actually (re)downloaded. + run: | + cd js/componentize/wpt/parity + if [ "${{ steps.playwright-cache.outputs.cache-hit }}" = "true" ]; then + npx playwright-core install chromium + else + npx playwright-core install --with-deps chromium + fi - run: just gha::conformance-checks # The jco-browser adapter finds the runner's preinstalled Chrome via # its candidate paths; CHROME_PATH overrides if ever needed. diff --git a/js/componentize/wpt/README.md b/js/componentize/wpt/README.md index 89aecf4..03bc8e8 100644 --- a/js/componentize/wpt/README.md +++ b/js/componentize/wpt/README.md @@ -31,9 +31,12 @@ unbundled. Each engine ratchets separately (`parity/losses-chromium.js`, re-recorded with `just wpt::update-losses-chromium`): a loss set is a fact about one engine's baseline. -The Chromium gate is not yet in CI: it needs a Chrome 137+ binary on the -runner. Test names are engine-independent by construction — `wpt-env.js` -shadows `location` with a fixed stub in every leg, so names never embed a +The Chromium legs always run Playwright's own build, pinned by +playwright-core's version in the parity lockfile, so the loss set +measures one engine everywhere — local runs and CI alike; a Chromium +behavior shift arrives only with a deliberate playwright bump. Test +names are engine-independent by construction — `wpt-env.js` shadows +`location` with a fixed stub in every leg, so names never embed a per-run origin. ## Vendoring policy diff --git a/js/componentize/wpt/justfile b/js/componentize/wpt/justfile index ffd0a9b..0bb1c43 100644 --- a/js/componentize/wpt/justfile +++ b/js/componentize/wpt/justfile @@ -30,8 +30,10 @@ update-losses: _artifacts # baseline measures Chromium's own WebSocket, the round trip runs the # browser-profile transpile against websocket-jco in the environment it # targets. Ratchets separately in parity/losses-chromium.js — a loss set -# is a fact about one engine's baseline. Not yet in CI: needs a Chrome -# 137+ binary on the runner. +# is a fact about one engine's baseline. The browser is always +# Playwright's pinned Chromium build (install once with +# `npx playwright-core install --with-deps chromium` from parity/), so +# the loss set measures one engine everywhere. parity-chromium: _artifacts-web {{timeout_cmd}} node {{root}}/js/componentize/wpt/parity/run-browser.mjs diff --git a/js/componentize/wpt/parity/run-browser.mjs b/js/componentize/wpt/parity/run-browser.mjs index a1e4241..cef6623 100644 --- a/js/componentize/wpt/parity/run-browser.mjs +++ b/js/componentize/wpt/parity/run-browser.mjs @@ -14,7 +14,11 @@ // echo server directly: WebSocket is not subject to CORS, and a localhost // `http:` page may open `ws:` connections. // -// jco's async ABI needs JSPI; Chrome ships it enabled from 137 onward. +// The browser is always Playwright's own Chromium build, pinned by +// playwright-core's version (the parity lockfile), so losses-chromium.js +// measures one engine everywhere — local runs and CI alike. Install it +// once with `npx playwright-core install --with-deps chromium` (from this +// directory). jco's async ABI needs JSPI; Chromium ships it. // // Usage: node run-browser.mjs [--update] @@ -26,7 +30,6 @@ import { fileURLToPath } from "node:url"; import { chromium } from "playwright-core"; -import { findChrome } from "../../../../scripts/chrome.mjs"; import { spawnEchod } from "../../../../conformance/server/echod.mjs"; const HERE = dirname(fileURLToPath(import.meta.url)); @@ -99,18 +102,12 @@ async function main() { throw new Error(`missing browser-profile transpile in ${join(HERE, "generated-web")}; run "npm run transpile:web" first`); } - const executablePath = await findChrome(); - if (!executablePath) { - throw new Error("no Chrome/Chromium binary found; set CHROME_PATH to a Chrome 137+ executable"); - } - const echod = await spawnEchod(join(REPO_ROOT, "target", "debug", "conformance-echod")); const server = await startServer(); const base = `http://127.0.0.1:${server.address().port}`; process.stderr.write(`echo server at ${echod.base}; page served from ${base}\n`); const browser = await chromium.launch({ - executablePath, headless: true, args: ["--no-sandbox", "--disable-dev-shm-usage"], }); diff --git a/scripts/chrome.mjs b/scripts/chrome.mjs index 8800026..23cad2c 100644 --- a/scripts/chrome.mjs +++ b/scripts/chrome.mjs @@ -1,6 +1,7 @@ -// Chrome/Chromium discovery shared by every headless-browser runner (the -// conformance jco-browser leg and the WPT parity browser legs). -// Node-only. +// Chrome/Chromium discovery for the conformance jco-browser leg, which +// prefers a system Chrome. (The WPT parity browser legs do not use this: +// they always launch Playwright's own pinned build, so the recorded loss +// set measures one engine everywhere.) Node-only. import { access, readdir } from "node:fs/promises"; import { join } from "node:path";