i365dev / free4chat-local-e2e
free4chat local full-stack E2E loop — single wrangler instance, Turnstile bypass switches, curl/MCP validation sequences, split-brain DO and daemon env traps. Use when validating room features (messaging, attachments, agent wakeups) against a local Worker+DO+KV stack without deploying.
free4chat local full-stack E2E loop
Use when Worker routes / DO logic / MCP tools changed and the full chain
(browser or curl → Worker → DO → Agent Runtime) must be proven locally.
Start the single-instance local stack
cd app
cp ../app/.dev.vars .dev.vars # needed for real SFU credentials; optional for pure room logic
NEXT_PUBLIC_TURNSTILE_DISABLED=1 npm run cf-build # client-side Turnstile off (see below)
npx wrangler dev --local --port 3000 \
--var TURNSTILE_DISABLED:true \
--var AGENT_MEDIA_ENABLED:trueThree hard rules (each one cost a debugging cycle):
Port must be 3000. The origin allow-list (
src/common/origin.ts)
only containshttp://localhost:3000. A different port yieldsforbidden_originfor browsers AND for curl-based attachment uploads.Exactly one wrangler instance. Two wranglers = two miniflare memory
spaces = split-brain DO state: messages written through instance A are
invisible to an agent polling instance B (symptom: "@tag gets no reply").
Before restarting:pkill -f "wrangler dev"and verifylsof -iTCP:<port> -sTCP:LISTENis empty.The daemon process freezes env at first spawn. After changing
FREE4CHAT_MCP_URL/FREE4CHAT_STT_PROVIDER/DOUBAO_API_KEY, kill thefree4chat-agent daemonprocess (Go resident daemon underagent/) and
join again; the old env keeps otherwise.
Runtime installation defaults
Install and join the Go Runtime without STT or TTS configured. Ordinary Room,
Task, and Harness tests do not need speech-provider credentials and must not
prompt for a password to enable them. Enable STT or TTS explicitly only when
the acceptance case exercises Live Transcript, Meeting Notes compatibility,
or Voice Reply; provision the corresponding credentials at that point and keep
them out of command output and test evidence.
Runtime provenance preflight
Before interpreting any Runtime, capability, media, or participant-transport
failure, run the provenance check with the same binary andFREE4CHAT_AGENT_DIR that the experiment will use:
free4chat-agent provenance --room <room-id>Proceed only when decision is valid, the CLI and daemon build identities
match, and the selected resident reports the expected opaque Runtime root andruntimeHostId. mismatch or ambiguous means the evidence is not yet
comparable: restart/reselect the host-owned daemon or correct the selected
Runtime root, then rerun the preflight. Do not compare direct Runtime evidence
with Room evidence until the preflight proves they belong to the intended
daemon/root. The command omits the raw root path, host seed, participant
handles, and Room selector from its output.
Turnstile switches (local bypass)
| Layer | Mechanism |
|---|---|
| Server | --var TURNSTILE_DISABLED:true — the explicit local/test bypass in sfu/server.ts; an empty/missing secret alone fails closed (503 turnstile_not_configured) |
| Client | build-time NEXT_PUBLIC_TURNSTILE_DISABLED=1 ⇒ useTurnstile loads no widget, requestToken resolves instantly |
| Browser widget kept | sitekey must be 1x00000000000000000000 (20 chars); variants with an AA suffix are invalid keys and fail with Turnstile 400020 |
Production builds set neither switch, and the production Worker must carry a real TURNSTILE_SECRET_KEY Worker secret.
Browser-free validation sequence (agent routes need no Origin/Turnstile)
Agent routes are listed in MISSING_ORIGIN_ALLOWED_ROUTES — omit the Origin
header entirely. Human routes (e.g. /api/room/attachments) requireOrigin: http://localhost:3000.
# 1. Agent joins (modern envelope + both headers are mandatory; legacy
# initialize is rejected with -32022 on the deployed dual-era stack)
curl -s -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" -H "Accept: application/json" \
-H "Mcp-Method: tools/call" -H "Mcp-Name: join_room" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
"name":"join_room","arguments":{"roomId":"r1","name":"Probe"},
"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientCapabilities":{}}}}'
# participantHandle = base64url(JSON{room,participantId,participantToken});
# participantToken is ONLY inside the handle, never a separate field.
# 2. Human participant (needed for human-only ops like attachment upload)
curl -s -X POST http://localhost:3000/api/sfu/session \
-H "Origin: http://localhost:3000" -H "Content-Type: application/json" \
-d '{"room":"r1","name":"LocalHuman","turnstileToken":"x"}'
# 3. Read events (addressed is computed per requesting participant: a message
# targeted at someone else reads as false here)
wait_for_events {participantHandle, cursor, timeoutSeconds} # returns immediately unless MCP_LONGPOLL_ENABLED=trueLifecycle traps
Human participants are reaped within seconds-to-minutes without a live
WebSocket (401 unauthorized) — create-and-act must happen in one short window.Agent lease is 90 s, renewed by waitforevents (immediate by default —
pass--var MCP_LONGPOLL_ENABLED:trueto exercise the legacy held long-poll);
killing a process without
leave leaves a ghost card until lease expiry.Room history can be replayed in full by any new member with cursor=0,
which makes assertions easy.
Automated browser Room E2E (the real Worker + real DO)
app/e2e/room/ is the repeatable control-plane regression: two REAL browser
pages join the same Room through the REAL OpenNext Worker, REALRoomSession Durable Object, REAL KV bindings, REAL WebSocket and REAL
roster/message broadcast. The only fake layers are external:
Cloudflare Realtime upstream → loopback fake HTTP server (
SFU_RTC_BASE_URL
env override;src/sfu/server.tskeeps the real production base when the
env var is absent);browser media bootstrap → Chromium's fake-device flags
(--use-fake-device-for-media-stream) plus a tinyRTCPeerConnection
stub inroom.spec.ts(the fake SFU's SDP is deliberately unparseable).
Origin handling: the harness binds a transparent TCP relay onhttp://localhost:3000 (the existing production allow-list origin) and
forwards everything untouched — src/common/origin.ts is NOT weakened.
cd app
yarn e2e:roomThe webServer command runs `NEXTPUBLICTURNSTILE_DISABLED=1 yarn cf-build &&
node e2e/room/run-local-worker.mjs(WranglercreateTestHarness()` + fake
Realtime + 3000 TCP relay). Run it at least twice to catch leaked ports or
stale state.
Hard invariant: SFU_RTC_BASE_URL is honored by EVERY known Cloudflare
Realtime outbound I/O — both the Worker SFU routes (realtimeRequest) and
the Durable Object media-cleanup effects (executeMediaCloseEffect) share
the same seam (src/common/realtimeUrl.ts). The fake fails closed (503) on
any unexpected outbound call AND records it; the spec's final audit assertsunexpected == [], so yarn e2e:room PASSES only if every Cloudflare
Realtime request made during the test was explicitly handled by the local
fake — nothing can silently escape to rtc.live.cloudflare.com, even on
best-effort cleanup paths that production intentionally swallows.
Which test for which change
| Change | Test |
|---|---|
| Homepage visual/headline (signal collapse, CTA, layout) | yarn e2e:homepage |
| Room layout / chat / participant cards / control-plane wiring | yarn e2e:room |
| Worker routes / DO logic / MCP tools | local curl/MCP flow below |
| Audio / screen share / Agent Voice / SFU negotiation | REAL deployed media dogfood only |
**Local Room E2E proves the collaboration/control plane. It does NOT prove
the Realtime SFU/media plane.** Do not claim media correctness from a
harness that fakesrtc.live.cloudflare.comor the peer connection.
For cross-layer Room App or Generated Task App lifecycle diagnosis, use
[`free4chat-lifecycle-diagnostics`](../free4chat-lifecycle-diagnostics/SKILL.md).
This skill owns causal Browser/Room/Runtime correlation; local E2E remains the
local Worker/DO/control-plane validation path.
The manual wrangler dev --local --port 3000 + curl/MCP/Agent Runtime
workflow below remains the interactive investigation path; the automated
harness and the manual workflow are complementary, not replacements.