Cross-implementation conformance tests for polymorph:websocket, on the shared
polymorph:test
infrastructure: one suite component runs the same corpus against every
implementation, and the aggregate joins the per-target result streams into
the committed matrix. The suite is the behavioral gate for the package — a
change to the WIT surface is co-dependent across every implementation by
construction, because this suite holds them to one behavior.
just conformance-ct # build, lock-check, run every leg, aggregate, matrix-check
just conformance-ct::run-wasmtime # one leg (aggregate separately)
just conformance-ct::lock-update # regenerate the lockfile after suite changes
just conformance-ct::matrix-update # accept an intentionally changed matrix
The matrix (driver-ct/matrix.md) is committed — the cross-target
review surface, diffed by matrix-check; CI also uploads the generated
copy as an artifact. Failure details appear in the per-target
driver-ct/results/<target>.jsonl streams and in the matrix's Failures
section.
| Piece | Role |
|---|---|
guest-ct/ |
The suite component: #[case]s importing polymorph:websocket/connections, owning every assertion. One wasm binary, run unchanged against every target. The committed tests.lock is its inventory (drift fails lock-check and the runner's own cross-check). |
server/ |
The suite-owned echo/reference server (conformance-echod): echo plus the fault modes the close-semantics rows need. Wire contract in server/PROTOCOL.md; server/echod.mjs holds the Node-side spawn helpers every JS leg shares. |
driver-ct/ |
The legs and the aggregate. ct-driver (Rust) embeds the wasmtime host (and, with --composed, runs the wac-composed in-guest provider under WASI p2+p3 instead); polyengine/ runs the same suite runtime-linked — no transpile step, no generated tree, no engine flag — under stock Deno (run.ts) and inside headless Chromium (run-browser.mjs plus the bundled browser/worker-entry.ts worker), thin glue (SUT import wiring, config) over the upstream runner core (@polyengine/ct-runner and @jsr/polymorph__test's page driver), which owns the case loop, verdict mapping, and tag-inventory drift check. targets.toml declares targets, features, and expected-fail entries; component-test aggregate validates and renders the matrix. The composed leg executes a different artifact by construction (the suite with the provider plugged in), so it binds its results envelope to the uncomposed suite's bytes (--suite-artifact, the runner's bind_suite_artifact attestation) and the aggregate's cross-target artifact agreement covers all four legs. |
wit/ |
The suite's world (sut-imports): only the surface under test — the export surface comes from the component-test SDK. The polymorph:websocket package arrives through the deps/polymorph-websocket symlink, never a copy. |
Each leg emits the component-test results wire format (JSONL: envelope,
one event per case, terminator) — see the component-test README for the
schema. The JS legs declare "scheduling":"none" in the envelope: they
execute everything, and the aggregate applies feature applicability from
the lockfile + manifest, so no scheduling logic lives in JavaScript.
Adding a target = a new leg that emits a result stream plus a
[targets.<id>] table in driver-ct/targets.toml; the suite and the
aggregate do not change.
Every leg configures the implementation under test with the same bounds
(declared in driver-ct/src/main.rs, mirrored by the polyengine runners): an
inbound-buffer bound small enough that the overflow rows trigger with a
bounded flood (it rides the WS_CONFORMANCE_MAX_INBOUND_BUFFER_BYTES
store environment, so the guest floods against exactly the configured
value), and connect/close bounds short enough that the /stall and
/ignore-close probes resolve well inside the per-case wall bound. That
bound (60s, the runner's --case-timeout; mirrored by the polyengine
runners' CASE_TIMEOUT_MS)
runs a single attempt, no retries: a nondeterministic failure is a
real signal and must surface, not be masked by a second attempt. Message
counts and sizes are the guest's own (params in guest-ct/src/body.rs):
every target runs the identical workload by construction.
Browser-specific scheduling: Chromium serializes in-flight WebSocket
handshakes per endpoint. The browser leg runs cases sequentially (one
worker), so nothing
is ever concurrent with the /stall hold; if a concurrent scheduler is
ever introduced, it needs an equivalent of the old harness's
handshake-blocking workaround.
guest-ct/src/body.rs: add the case body (aResult<(), String>dispatch arm, and aparamsentry if count-parameterized).guest-ct/src/lib.rs: add the#[case]delegator in the right category module.- If the test needs new server behavior, add the mode to
server/and document it inserver/PROTOCOL.md. just conformance-ct::lock-update— commit the lockfile diff (it is the review surface for corpus changes).just conformance-ct— all targets must pass (or carry atargets.tomldeclaration with a reason), thenmatrix-updateif row counts changed.
- Never assert implementation-identical behavior where the WIT records latitude; assert the contract.
- A target that cannot serve a capability gets a gated feature: a
[features.<x>]entry indriver-ct/targets.toml, tags on the affected cases, a!xdecline probe, andmissing-featureson the target — never a weakened test. - A known failure gets an
[[targets.<t>.expected-fail]]declaration with a reason and a tracking issue — never a deleted test. A passing expected-fail fails the aggregate, forcing the declaration's cleanup. - The committed lockfile is the corpus inventory; regenerate it with
lock-updateand review the diff. Drift failslock-checkand the runner's cross-check. - Never copy the root WIT: the suite consumes it through the
wit/deps/polymorph-websocketsymlink. - Conformance work must not change production host behavior except where a test deliberately drives a fix.
The suite and driver consume component-test as git-sourced cargo
dependencies, rev-pinned in the root Cargo.toml's
[workspace.dependencies] (both entries move together; Cargo.lock
records the resolution). The component-test CLI used by the lockfile
and aggregate recipes is cargo-installed into target/ct-tools at the
rev read back out of Cargo.lock (conformance-ct::_ct-tools), so the
libraries and the CLI cannot drift apart. Registry dependencies replace
the git pins when component-test publishes. The polyengine-browser driver
consumes the JS runner core from JSR: @jsr/polymorph__test in
driver-ct/polyengine/package.json, set to the release version matching
the cargo entries (both sides of a bump name one polymorph-test
release — the root Cargo.toml comment is the bump checklist).