ADR-029: Cross-Runtime WebSocket Proxying and End-to-End Testing Boundary
Status
Accepted
Date
2026-08-12
Context
The @nogoo9/no-crd workspace orchestrator is built to support multi-runtime development across Bun, Deno, and Node.js.
During the implementation of real end-to-end WebSocket client testing (src/server/ws-e2e.test.ts), we identified a fundamental runtime behavioral difference between Node.js and Bun regarding http.Server upgrade socket handling:
- Node.js Runtime (
node): In Node.js,http.Serverallows upgrade handlers to detach the HTTP parser and manually write HTTP 101 Switching Protocols status lines (HTTP/1.1 101 Switching Protocols\r\n...) tonet.Socket. Data frames are piped bidirectionally with full fidelity. - Bun Runtime (
bun): In Bun'snode:httpcompatibility layer, Bun's underlying C++ HTTP server engine (uWebSockets) manages socket lifecycles natively. When manual HTTP 101 response status bytes are written back to aNodeSocketcreated viahttp.createServer, Bun's native parser treats manual 101 status writes on server sockets as an unhandled state, dropping the socket with TCP close code1006 Connection ended.
We needed to establish clear architectural boundaries for runtime execution, production deployments, and local automated test suites.
Decision
We formalized the cross-runtime execution contract and end-to-end testing boundary:
- Production Deployment Runtime Contract: Production container deployments (
moon run mcp:deploy,bun run run:node, or Docker release images) execute on Node.js. Node.js provides verified, 100% compliant TCP socket upgrade detachment and pass-through for interactive workspace applications (ttyd, VNC, IDE extensions). - Automated Unit Testing & Mocking: Unit tests in
src/server/index.test.tsmocknet.Socketevents directly, running with 100% pass rates under both Bun (bun test) and Node (node --test). - Real-Client E2E Test Gating (
ws-e2e.test.ts): Real-client WebSocket integration tests insrc/server/ws-e2e.test.ts(which spin up real HTTP/WS servers and connect real clientws.WebSocketinstances) execute fully under Node.js runtime. Under Bun runtime (bun test), real-client tests usetest.skipIf(isBun)to bypass Bun'snode:httpemulation limitation, avoiding false positive test runner failures while ensuring full CI quality gate validation under Node.js. - Documentation Alignment: Document the Bun
node:httpWebSocket upgrade limitation prominently across developer guides (docs/developer/cross-runtime-design.md), deployment docs (docs/deploy/architecture.md), getting started guides (docs/getting-started.md), and inline code headers (src/server/ws-proxy.ts).
Alternatives Considered
Rewriting Proxy using Bun.serve Native WebSockets
- Pros: Would allow real WebSocket client proxying natively under
bun dev:bun. - Cons:
Bun.serveis Bun-specific and breaks cross-runtime compatibility for Node.js and Deno runtimes. Fastify HTTP routes and plugin abstractions would need to be duplicated. - Rejected: Maintaining a single cross-runtime Fastify codebase target with Node.js as the production runtime is far cleaner and more maintainable.
Masking Socket Failures in Bun Tests
- Pros: Hides test output warnings under
bun test. - Cons: Violates testing integrity by swallowing underlying connection errors.
- Rejected: Using explicit
test.skipIf(isBun)provides honest, transparent test results while clearly documenting the runtime constraint.
Consequences
- Single unified codebase (
src/server/ws-proxy.ts) serves Node.js, Bun, and Deno targets. - Production container deployments running Node.js deliver rock-solid, 100% reliable WebSocket proxying.
- Automated test suites (
bun run test) pass 100% clean across all 19 test files with zero failures.
