Conformance: celld is the judge, workerd for stubs
CellB is not tested against expectations written here alone. Each Workers or Durable Objects behaviour that celld also has is run on the real celld, recorded, and replayed against CellB in Node and in Chromium; the two must answer the same. This page explains the corpus, the judges, and how a difference is either fixed or pinned.
Three sources of expectations
| Source | Where | Judged against |
|---|---|---|
| celld’s own examples, byte-identical | conformance/celld-examples/ | celld |
| projects written here to probe APIs and edge cases more deeply | conformance/projects/ | celld |
| workerd’s Durable Object contract tests, copied unchanged | conformance/workerd-tests/ (generated by scripts/vendor-workerd-tests.mjs) | celld |
celld publishes no tests of its own, so its examples, its documentation and workerd’s tests are the executable specification. ADR 0003 records why celld, not workerd, is the judge.
Where CellB follows workerd instead
celld refuses stubs, functions and RpcTargets across a stub or into a loaded Worker
(function () {} could not be cloned., RPC stubs cannot cross isolate boundaries yet.); workerd
passes them by reference, and so does CellB. A scenario with judge: "workerd" is compared with
conformance/golden-workerd/<name>.json instead of celld’s golden, in Node and in Chromium. Its
celld transcript is recorded too, so the difference stays on file, and it carries a
workerdDiffers line. The judged-by-workerd scenarios:
| Scenario | What workerd does and celld refuses |
|---|---|
wd-stub-storage | stores ctx.exports.X({ props }) stubs and class handles in Durable Object storage |
wd-worker-loader-stubs | passes ctx.exports stubs and Durable Object stubs into a loaded Worker’s env, props and arguments |
worker-details-rpc-target | returns an RpcTarget from a Durable Object method as a stub |
worker-details-pipeline | calls and property reads on a Durable Object stub call’s result before it settles (stub.target().inc(), stub.nested().inner.n) |
isolation-api-pipeline | the same on a loaded Worker’s entrypoint, and from loaded code on a host binding’s RpcTarget result |
Every other scenario is still judged by celld; a new judge: "workerd" needs a reason of this kind,
not a convenience.
How a project is compared
conformance/scenarios.tsholds a request script per project: HTTP requests, waits and WebSocket sessions. Origin, host and named volatile fields are normalized;textMaskmasks values such as timestamps.conformance/record-celld.tsruns each project oncelld devand writesconformance/golden/<name>.json: the transcript and the user’s console lines. Its usage line is at the top of the file; it needsCELLD_BINandESBUILD_DIR(scripts/interop-env.shprints both).- The Node conformance suite loads each project with
loadNodeProjectand requires the same transcript and logs. Chromium runs the same scenarios twice: from a Node-built bundle in a Dedicated Worker, and from sources compiled by the in-browser HTTP server.
Two alignments are applied everywhere: celld sends no Content-Type for a string body, and celld’s
HTTP server adds Content-Length.
The workerd tests run inside the Worker
Each vendored workerd test file sits next to a generated index.js. GET / calls every
test(ctrl, env, ctx) export in module-namespace order and answers
{ exportName: "ok" | "timeout" | "<Error>: <first line>" }, with a 5 s limit per export.
?skip=a,b leaves exports out, which is how one file is split into an agreeing scenario and a gap
scenario. A failed assertion is reported by its fields (AssertionError: <actual> <operator> <expected>), because Node’s assert, the browser polyfill and celld word the same failure
differently. The workerd test configuration (.wd-test) is translated by hand into
wrangler.jsonc; every class gets SQLite storage.
Known gaps and celld’s deviations are pinned, not hidden
knownGapmarks a recorded celld behaviour CellB does not reproduce yet. In Node the scenario is expected to fail (it.fails): if CellB starts to match, the test turns red and the marker must go. Chromium skips it. The current list is in gaps.md.workerdDiffersmarks a project where workerd answers differently from celld.conformance/record-workerd.tsruns the scenarios that use only Durable Objects,varsand Worker Loaders on workerd through Miniflare 4 (the pinnedminiflaredevDependency), writesconformance/golden-workerd/, and fails when a pinned difference no longer differs. Projects with other bindings are skipped: those are simulators, not workerd.
WebSockets are compared with the platform’s own
A shared case module encodes 20 WHATWG WebSocket behaviours with no Node- or DOM-only imports.
They run against CellB’s sockets (in-process, across worker-thread shards, through a ServiceWorker
router to shard workers) and against the platform’s native WebSocket talking to a real ws
server that speaks the same protocol as the test Room object. Chrome’s native WebSocket is the
reference. Node’s native WebSocket deviates on some cases depending on the Node version; the test
probes the running Node first and skips a case for that implementation only when the deviation is
observed.
Mutation harnesses check that the tests can fail
pnpm mutate (Node) and pnpm mutate:browser (Chromium) apply known defects one at a time, each
with the tests that must catch it, in a throw-away copy of the package. Every run starts with a
baseline of the unmutated copy and aborts if that fails, so a mutant is never “killed” by a broken
setup. MUTANTS=0,3 runs a subset.
What breaks
- A scenario fails with a bundler error at once (an unresolved
workerd:*import, or another module CellB does not provide). The project cannot load in CellB; nothing times out. - A
knownGapscenario turns red in Node with “Expect test to fail”. CellB now matches celld: remove theknownGapentry and record the change in gaps.md. record-workerd.tsprintsagrees with celld, but workerdDiffers is set (stale)and fails. Remove theworkerdDiffersline.- A
judge: "workerd"scenario fails after re-recording withrecord-workerd.ts. The workerd golden is the expectation there: a change in it is a change in the target, not noise. - Goldens recorded on another celld version disagree. Bump
CELLD_VERSIONinscripts/interop-env.shand re-record all goldens together. crontakes a real minute: it waits for a minute boundary, because the celld golden was recorded against real time.- A browser mutant is “killed” for the wrong reason if Vite cannot serve
sqlite3.wasmfrom the pnpm store; the baseline run catches it and aborts. - Without
CELLD_BIN, every celld test is skipped, not failed. A green run without it says nothing about celld compatibility.