Use playground/kitchen-sink to verify a platform with a disposable app. The automated suite resets the app's database, KV, and storage, then exercises navigation, mutations, authentication, queues, and caching.
The reset must return a successful response with ok: true; otherwise the suite stops before acceptance checks. It deletes the heartbeat and revalidation keys with the rest of the fixture KV state.
Queue submission must also produce its consumer result within 60 seconds. A consumer timeout fails acceptance with queue/dispatch and D1 diagnostics.
Prepare with --runtime to add an isolated queue retry consumer, authenticated
version/log/AI routes, and typed WebSocket echo. Set SMOKE_RUNTIME=1 and supply
the app's existing secret privately through SMOKE_E2E_TOKEN to enable browser
test 14, which requires a fresh message round trip within 30 seconds. The
WebSocket handshake carries the secret as a header. These fixtures require a
configured nonempty secret and remain separate from the default hosted CI app.
Prepare the hosted fixture with oxnode platform/scripts/kitchen-sink-platform-fixture.ts, then deploy the isolated app directory it prints to the platform you want to test. The basic fixture omits Sandbox so it works on platforms without Containers access. For Sandbox acceptance, pass --sandbox when preparing the fixture and set SMOKE_SANDBOX=1 when running the tests. The platform must enable Sandbox and have Workers Paid, Containers access, and the required runtime-token permissions described in the Sandbox guide. Pass the deployed URL explicitly from the repository root:
vp exec playwright install chromium
SMOKE_URL=https://kitchen-sink.apps.example.com oxnode platform/scripts/kitchen-sink-smoke-test.tsUse an app whose data can be deleted. The script has no default destination. If Cloudflare Access protects the app, supply that application's CF_ACCESS_CLIENT_ID and CF_ACCESS_CLIENT_SECRET from your secret manager.
The hosted staging CI workflow initializes missing server values before deploying its existing kitchen-sink project at https://api.staging.void.cloud. This is CI fixture setup: it sets APP_NAME to Kitchen Sink, API_URL to https://api.example.com, and generates a random SECRET_KEY only when absent. It validates new values against env.ts, preserves configured secrets, and uses the workflow's existing staging credentials. It never reads a local .env or prints values. Smoke jobs are serialized; avoid editing these fixture secrets manually during setup because the secret API uses an upsert after listing names.
Setup failures report the operation and HTTP status when available, without printing API response bodies or secret values. If the API confirms a value was stored but updating the old worker failed, setup verifies the stored name and continues without retrying the write. The following deployment applies all stored values and must pass the smoke tests. Other errors, or a missing stored name, still fail setup.
The checklist below can also be run manually or with browser automation. Use unique values for created records, wait for each mutation to appear, and remove test records afterward. KV listings are eventually consistent; poll them instead of relying on a fixed delay. Record each result and report failures after completing the remaining cases.
Pass --email when preparing the fixture to add inbound handlers and /api/acceptance/email. Deploy it only to a disposable project on an email-enabled platform. Both GET and POST require x-void-e2e-token to match the app's SECRET_KEY; the fixture never copies local .env values. Set the required APP_NAME, API_URL, and random SECRET_KEY through the linked project's secret commands before deployment.
POST /api/acceptance/emailacceptssendEmail()options as JSON. Send a unique subject to a verified test mailbox and confirm both provider acceptance and actual mailbox delivery. Repeat the identical body andidempotencyKeyto confirm one delivery; change the body with the same key to confirmIDEMPOTENCY_CONFLICT. Check partial recipients, sender restrictions, and administrator-configured quotas separately.- Send inbound mail to
<slug>+support@<shared-mail-domain>and confirmGET /api/acceptance/email?subject=<encoded-subject>returns thesupportroute, original text/HTML, and attachment metadata. For a registered custom domain, usesupport@<domain>instead. The KV receipt expires after one hour; poll because KV reads can be eventually consistent. - Verify tag precedence with
<slug>+support+ticket-42@<shared-mail-domain>(support-ticket,params.ticket= ticket-42) and<slug>+support+vip@<shared-mail-domain>(support-vip). An unmatched recipient reachesdefault. - Subjects beginning with
REPLYsend a threaded reply. Verify its arrival andIn-Reply-To/References, and inspectvoid email logsfor the action outcome. - Subjects beginning with
FORWARDforward to the verified address in the original message'sx-void-e2e-forwardheader. Shared-domain native events support forwarding; customer-zone relayed mail recordsUNSUPPORTED_ACTIONwithout sending. Confirm that recorded distinction. - Subjects beginning with
REJECTreject withVoid email acceptance reject. Verify the sender's non-delivery outcome and the platform's inbound log.
Use void email usage, destinations, and logs to compare API outcomes with recorded provider outcomes. Test admin email policy/caps and custom-domain readiness, secret rotation, and removal through the operator UI or CLI. A provider acceptance result alone does not prove mailbox delivery.
--sandbox also adds POST /api/acceptance/sandbox, authenticated by x-void-e2e-token matching the app's SECRET_KEY. The endpoint runs through lazy startup; text, binary, Blob, streaming and relative file operations; metadata, rename, listing and removal; process cwd/env/stdout/stderr; piped stdin; PTY/resize/kill; cancellation; HTTP port access; disabled and enabled Internet access; isolation between IDs; and destroy/restart with ephemeral files. It uses unique IDs, limits idle time to 30 seconds, and stops its containers after the checks.
Set SMOKE_SANDBOX=1 and SMOKE_E2E_TOKEN to the fixture's secret when running the smoke script to include this test. Select a test user plan with enough runtime and at least two concurrent Sandbox instances, such as solo; the platform's free user tier has only one runtime second per month. The result includes the checks completed before any failure. The test allows up to three minutes for cold starts, and makes one permitted GET to Cloudflare's trace endpoint while testing Internet access.
-
Visit
/. -
Assert the page contains "Kitchen Sink" and "Auth" in its text.
-
Inspect the page's HTML source for hashed asset URLs (e.g.
.jsor.cssfiles with hash in the filename). -
Use the browser to GET one of those asset URLs with a unique query parameter and
credentials: 'omit'. For a public target, assertCache-Controlcontainsimmutablewithout a private/no-cache/no-store directive. For an Access-protected target, keep the service-token headers configured on the browser context and assertprivateandimmutablewithout public/no-cache/no-store directives: the authenticated staging request permits browser reuse while excluding shared caching, even when browser cookies are omitted. A unique URL bypasses the browser's stored response without sending cache-bypass directives to the origin. Inspect a freshGETresponse rather than a browser-cached response orHEAD.const el = document.querySelector('script[src*="/assets/"]') || document.querySelector('link[href*="/assets/"]'); const url = el.src || el.href; const response = await fetch(url + '?smoke=' + Date.now(), { credentials: 'omit' }); response.status + ' ' + response.headers.get('cache-control');
Expect
200 public, max-age=31536000, immutablefor a public target or200 private, max-age=31536000, immutablefor the Access-protected staging target. Cloudflare also documents that its Cache API is unavailable for Workers fronted by Access; this authenticated check does not assert an edge-cache hit.
- Visit
/. - Run JS to set a marker on window:
window.__SPA_MARKER = true. - Click the "DB" link (navigates to
/db). - Wait for the h1 to contain "DB".
- Assert the URL is now
/db. - Run JS to check
window.__SPA_MARKER— assert it is stilltrue(proves no full page reload). - Click the "KV" link (navigates to
/kv). - Wait for the h1 to contain "KV".
- Assert the URL is now
/kv.
- Visit
/. - Run JS to stamp the nav element:
document.querySelector("nav").__stamp = 1. - Click the "Env" link (navigates to
/env). - Wait for the h1 to contain "Env".
- Run JS to read
document.querySelector("nav").__stamp— assert it equals1(proves layout DOM was not recreated).
- Visit
/db. - Fill the title input (placeholder "Note title") with a unique value like
smoke-<timestamp>. - Fill the body input (placeholder "Note body") with
test body. - Click the "Add Note" button.
- Wait for the unique title to appear in the page text.
- Assert the page contains both the title and "test body".
- Find the table row containing the title and click its "Delete" button.
- Wait for the title to disappear from the page.
- Visit
/kv. - Fill the key input (placeholder "my-key") with a unique value like
smoke-kv-<timestamp>. - Fill the value input (placeholder "my-value") with
hello. - Click the "Set" button.
- Fill the
lookupinput with our key, confirm<key> = <value>appears on page. - Reload page to check if the key appears in the table. This will take a while as kv.list() is eventually consistent, not instant
- Find the table row containing the key and click its "Delete" button.
- Wait for the key to disappear from the page.
- Visit
/storage. - Upload a file named
smoke-test.txtwith contenthello worldvia the file input. - Click the "Upload" button.
- Wait for "smoke-test.txt" to appear in the page.
- Find the table row containing "smoke-test.txt" and click its "Delete" button.
- Wait for "smoke-test.txt" to disappear from the page.
- Visit
/env. - Assert the page text contains "DB" and "KV".
- Visit
/queues. - Fill the key input (placeholder "my-key") with a unique value like
smoke-q-<timestamp>. - Fill the value input (placeholder "my-value") with
queued. - Click the "Send to Queue" button.
- Poll for up to 60 seconds: reload
/queuesevery 2-3 seconds and check if the unique key appears in the "Processed Results" table. Results are written to D1 (strongly consistent), so they appear immediately once the consumer processes the message. - Assert the key was found within the timeout.
- Delete the test row from the table.
- Visit
/auth. - Assert the page shows "Not logged in".
- Click the "Register" tab button.
- In the "Email Register" section:
- Fill the email input with a unique email like
smoke-<timestamp>@example.com. - Fill the password input with
s3cureP@ss!. - Fill the name input (placeholder "Name") with
Smoke User. - Click the "Register" submit button.
- Fill the email input with a unique email like
- Wait for "Not logged in" to disappear — assert the page now shows the email address (user is logged in).
- Click the "GET /api/profile" button.
- Wait for a
<pre>element to appear containing"user". - Assert the
<pre>response contains the email address. - Click the "Logout" button.
- Wait for "Not logged in" to reappear.
- Click the "Login" tab button.
- In the "Email Login" section:
- Fill the email input with the same email used above.
- Fill the password input with
s3cureP@ss!. - Click the "Login" submit button.
- Wait for "Not logged in" to disappear — assert the user is logged in again.
- Visit
/cron. - Poll for up to 150 seconds for the heartbeat timestamp, allowing the next scheduled invocation and KV propagation.
- Assert it is a valid date recorded after the fixture reset, within the last 5 minutes, and no more than one second in the future. An old heartbeat cannot satisfy the check.
- If no fresh heartbeat arrives, fail the test. Check cron registration and dispatch logs, then verify the fixture's KV binding and
cron:last-heartbeatkey.
- Visit
/revalidate. - For a public target, poll for up to 15 seconds for an actual
x-isr-cache: HITresponse before continuing. Access-authenticated targets bypass the shared cache. - Note the valid date in the Rendered at field.
- Click Revalidate /revalidate and assert the POST response succeeds. The app displays a failed purge as an error and does not record a success timestamp.
- Wait for the button to return to its non-disabled state.
- Poll reloads for up to 15 seconds until Rendered at is a different valid date. A nonempty Last revalidation field alone does not establish that the cached page was purged.
Prepare and deploy the fixture with --sandbox, then run with SMOKE_SANDBOX=1.
- Visit
/sandboxand replace the analyzer with a function that returns a unique marker and the request count, and writes a marker to stderr. - Click Run in Sandbox and allow up to 90 seconds for the first container start.
- Assert the API returns
ok: true, exit code0, the unique marker and a positive request count. This exercises writing input and executable files, running Node, and reading the output file. - Assert stdout contains the result and stderr contains the marker, then confirm the result appears in the page.