Skip to content

BRIKEV/twd-relay

Repository files navigation

twd-relay

WebSocket relay for TWD — lets AI agents and external tools trigger and observe in-browser test runs.

Your app runs tests in the browser with twd-js. twd-relay adds a relay server and a browser client so that a client (script, CI, or AI agent) can send a “run” command over WebSocket; the relay forwards it to the browser, and test events are streamed back. No Vite or specific framework required: the relay can run standalone and work with any app that loads the browser client.


Architecture

  • Relay server — WebSocket server that accepts one browser connection and many client connections. Clients send commands (run, status); the relay forwards them to the browser. The browser runs tests and streams events back; the relay broadcasts those to all clients. A lock prevents concurrent runs.

  • Browser client (twd-relay/browser) — Runs in your app. Connects to the relay, listens for commands, uses twd-js/runner to execute tests, and streams results back. Logs connection state in the console (e.g. [twd-relay] Connected to relay).

  • Vite plugin (twd-relay/vite) — Optional. Attaches the relay to your Vite dev server so the WebSocket is on the same origin. Also available: a standalone CLI that runs the relay on its own HTTP server (default port 9876).

Protocol (summary)

  1. Browser connects → sends { type: 'hello', role: 'browser' }
  2. Client connects → sends { type: 'hello', role: 'client' }
  3. Client sends { type: 'run', scope: 'all' } (optionally with testNames to filter) → relay forwards to browser
  4. Browser runs tests and streams events → relay broadcasts to clients
  5. Browser sends { type: 'heartbeat' } every 3s during a run (relay consumes these, never forwarded to clients)
  6. run:complete clears the run lock (and the send-run script exits)

Heartbeat & frozen-tab recovery

During an active test run the browser sends a heartbeat every 3 seconds. The relay tracks the last heartbeat time and checks every 10 seconds. If no heartbeat arrives for 120 seconds during an active run, the relay considers the run dead (browser tab frozen by the OS), resets the run lock, and broadcasts to all clients:

{ "type": "run:abandoned", "reason": "heartbeat_timeout" }

The CLI prints a clear message — Run abandoned — browser tab appears frozen. Refresh the browser tab and retry. — and exits with code 1. This is especially useful for AI agent workflows, where the agent gets an actionable signal instead of a silent 180s timeout followed by a cryptic RUN_IN_PROGRESS error.


Installation

npm install twd-relay

Peer dependency: twd-js (>=1.4.0). Your app must use twd-js for tests; the browser client imports twd-js/runner at runtime.


Quick start (standalone relay)

Works with any framework. Run the relay on one port and your app on another.

1. Start the relay (from this repo, or use the CLI in your project):

npm run relay
# or: npx twd-relay
# Listens on ws://localhost:9876/__twd/ws (use --port to change)

2. In your app, connect the browser client and call connect():

import { createBrowserClient } from 'twd-relay/browser';

const client = createBrowserClient({
  url: 'ws://localhost:9876/__twd/ws',
});
client.connect();

Once connected, the browser client sets a colored favicon and prefixes document.title so you can spot the active TWD tab at a glance:

Favicon Title prefix State
Blue [TWD] Connected, idle
Orange [TWD ...] Tests running
Green [TWD ✓] Last run passed
Red [TWD ✗] Last run had failures

On disconnect or eviction (another tab taking over), the original favicon and title are restored.

Aborting throttled runs

Chrome aggressively throttles timers in backgrounded tabs, which can stretch a 1-second test run to 30+ seconds. To avoid AI/CI hangs, the browser client monitors per-test wall-clock time. If any single test runs longer than 10 seconds (configurable), the browser emits run:aborted, the CLI prints a clear error with recovery guidance, and the run ends with exit code 1.

Override the threshold with --max-test-duration <ms> on twd-relay run, or pass maxTestDurationMs to createBrowserClient. Set it to 0 to disable detection entirely:

twd-relay run --max-test-duration 20000   # raise to 20s for heavy multistep tests
twd-relay run --max-test-duration 0       # disable detection

The default of 10 s is chosen to sit above the Testing Library default findBy* timeout (3 s). A legitimately failing test with one or two missed selectors still completes under the threshold, while throttled runs — where tests typically cluster in the 10–30 s range — trip the abort reliably.

Recovery when an abort fires: foreground the TWD tab (identified by the [TWD …] title prefix set by the favicon indicator) and retry. For unattended runs (CI, agents), prefer twd-cli: it drives a headless browser where the tab is always focused and throttling doesn't apply.

3. Open your app in a browser — the page connects to the relay as “browser”.

4. Trigger a run — something must connect as a client and send run:

  • From this repo: npm run send-run (or node scripts/send-run.js [--port 9876]). The script exits when it receives run:complete.
  • From another project (if ws is available, e.g. via twd-relay): use the one-liner below.

One-liner to trigger a run

Run from a directory where ws is installed (e.g. project with twd-relay):

node -e 'const Ws=require("ws");const w=new Ws("ws://localhost:9876/__twd/ws");let s=false;w.on("open",()=>w.send(JSON.stringify({type:"hello",role:"client"})));w.on("message",d=>{const m=JSON.parse(d);console.log(m.type,m);if(m.type==="connected"&&m.browser&&!s){s=true;w.send(JSON.stringify({type:"run",scope:"all"}));}if(m.type==="run:complete"){w.close();}});w.on("close",()=>process.exit(0));'

Change the URL if your relay uses another port or path.


Vite plugin (optional)

If you use Vite, you can attach the relay to the dev server so the WebSocket is on the same host/port:

// vite.config.ts
import { twdRemote } from 'twd-relay/vite';

export default defineConfig({
  plugins: [react(), twdRemote()],
});

Then in your app you can omit the URL; the client defaults to ws(s)://<current host>/__twd/ws.


Scripts (this repo)

Script Description
npm run build Build relay, browser, vite entry points + CLI
npm run relay Build and start the standalone relay (port 9876)
npm run send-run Connect as client and send run; exits on run:complete
npm run dev Start relay only (assumes already built)
npm run test Run tests (watch)
npm run test:ci Run tests with coverage

Exports

Export Use
twd-relay (main) Relay server: createTwdRelay(httpServer, options)
twd-relay/browser Browser client: createBrowserClient(options)
twd-relay/vite Vite plugin: twdRemote(options)

CLI: twd-relay (or npx twd-relay) — two subcommands:

  • twd-relay serve (default) — start the standalone relay
  • twd-relay run — connect to a relay and trigger a test run

CLI run command

Connect to an existing relay, trigger tests, stream output, and exit with 0 (all pass) or 1 (failures).

# Run all tests (connects to Vite dev server on port 5173 by default)
twd-relay run

# Run on a different port
twd-relay run --port 9876

# Run specific tests by name (substring match, case-insensitive)
twd-relay run --test "should show error"

# Multiple filters — runs tests matching any of them
twd-relay run --test "login" --test "signup"
Flag Description Default
--port <port> Relay port 5173
--host <host> Relay host localhost
--path <path> WebSocket path /__twd/ws
--timeout <ms> Timeout 180000
--test <name> Filter tests by name substring (repeatable)

When --test is used and no tests match, the CLI prints the available test names so you can correct the filter.


License

MIT · BRIKEV

About

WebSocket relay for TWD — lets AI agents and external tools trigger and observe in-browser test runs.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors