CellB

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

SourceWhereJudged against
celld’s own examples, byte-identicalconformance/celld-examples/celld
projects written here to probe APIs and edge cases more deeplyconformance/projects/celld
workerd’s Durable Object contract tests, copied unchangedconformance/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:

ScenarioWhat workerd does and celld refuses
wd-stub-storagestores ctx.exports.X({ props }) stubs and class handles in Durable Object storage
wd-worker-loader-stubspasses ctx.exports stubs and Durable Object stubs into a loaded Worker’s env, props and arguments
worker-details-rpc-targetreturns an RpcTarget from a Durable Object method as a stub
worker-details-pipelinecalls and property reads on a Durable Object stub call’s result before it settles (stub.target().inc(), stub.nested().inner.n)
isolation-api-pipelinethe 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

  1. conformance/scenarios.ts holds a request script per project: HTTP requests, waits and WebSocket sessions. Origin, host and named volatile fields are normalized; textMask masks values such as timestamps.
  2. conformance/record-celld.ts runs each project on celld dev and writes conformance/golden/<name>.json: the transcript and the user’s console lines. Its usage line is at the top of the file; it needs CELLD_BIN and ESBUILD_DIR (scripts/interop-env.sh prints both).
  3. The Node conformance suite loads each project with loadNodeProject and 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

  • knownGap marks 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.
  • workerdDiffers marks a project where workerd answers differently from celld. conformance/record-workerd.ts runs the scenarios that use only Durable Objects, vars and Worker Loaders on workerd through Miniflare 4 (the pinned miniflare devDependency), writes conformance/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 knownGap scenario turns red in Node with “Expect test to fail”. CellB now matches celld: remove the knownGap entry and record the change in gaps.md.
  • record-workerd.ts prints agrees with celld, but workerdDiffers is set (stale) and fails. Remove the workerdDiffers line.
  • A judge: "workerd" scenario fails after re-recording with record-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_VERSION in scripts/interop-env.sh and re-record all goldens together.
  • cron takes 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.wasm from 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.