Skip to content

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.Server allows upgrade handlers to detach the HTTP parser and manually write HTTP 101 Switching Protocols status lines (HTTP/1.1 101 Switching Protocols\r\n...) to net.Socket. Data frames are piped bidirectionally with full fidelity.
  • Bun Runtime (bun): In Bun's node:http compatibility layer, Bun's underlying C++ HTTP server engine (uWebSockets) manages socket lifecycles natively. When manual HTTP 101 response status bytes are written back to a NodeSocket created via http.createServer, Bun's native parser treats manual 101 status writes on server sockets as an unhandled state, dropping the socket with TCP close code 1006 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:

  1. 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).
  2. Automated Unit Testing & Mocking: Unit tests in src/server/index.test.ts mock net.Socket events directly, running with 100% pass rates under both Bun (bun test) and Node (node --test).
  3. Real-Client E2E Test Gating (ws-e2e.test.ts): Real-client WebSocket integration tests in src/server/ws-e2e.test.ts (which spin up real HTTP/WS servers and connect real client ws.WebSocket instances) execute fully under Node.js runtime. Under Bun runtime (bun test), real-client tests use test.skipIf(isBun) to bypass Bun's node:http emulation limitation, avoiding false positive test runner failures while ensuring full CI quality gate validation under Node.js.
  4. Documentation Alignment: Document the Bun node:http WebSocket 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.serve is 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.