Skip to content

test: Harden browser contract-test adapter against unanswered-command hangs#1399

Merged
kinyoklion merged 2 commits into
mainfrom
rlamb/harden-contract-test-adapter
May 29, 2026
Merged

test: Harden browser contract-test adapter against unanswered-command hangs#1399
kinyoklion merged 2 commits into
mainfrom
rlamb/harden-contract-test-adapter

Conversation

@kinyoklion
Copy link
Copy Markdown
Member

@kinyoklion kinyoklion commented May 29, 2026

Summary

The browser SDK contract-test adapter (startAdapter.ts) forwarded each command to the browser entity over WebSocket and waited forever for a reply. If the browser ever missed one reply, that REST request — and therefore the harness — hung indefinitely (even capability/status queries returned 000), with no recovery path. This permanently wedged the service, which is especially painful when reusing one service across many runs (e.g. flake hunting).

This PR makes the adapter resilient:

  • Timeout on send() (ADAPTER_REQUEST_TIMEOUT_MS, default 30s). A stalled command now fails that single request with HTTP 500 so the harness fails that one test and continues, instead of wedging the whole chain.
  • Reject in-flight commands on WebSocket close, so no REST request is left waiting on a dead socket.
  • Error handler on REST listen() so a silent bind failure (e.g. the port still being held → "WS up but REST never bound") is logged instead of disappearing.

Changes are confined to test tooling; no SDK runtime code is touched.

Verification

5 consecutive full suites against one reused adapter/browser stayed green — ec=0, fail=0, adapter responsive at ~1 ms throughout. The timeout path turns a previously-permanent hang into a single failed request the harness can recover from.

Notes

  • An earlier revision of this branch also had the entity destroy leftover clients on getCapabilities. That was removed: getCapabilities is not a safe "new run" signal because multiple harnesses can drive one entity concurrently, so it could wipe another run's active clients. The lingering-final-client cleanup needs a different, run-aware approach and is out of scope here.
  • packages/sdk/browser/example-fdv2/src/app.ts has unrelated local debugging tweaks and is intentionally not part of this branch.

Test coverage

This tooling package has no unit-test harness; validation was integration-based (above).


Note

Low Risk
Only internal contract-test adapter tooling is modified; no production SDK or auth/data paths are affected.

Overview
The contract-test adapter (startAdapter) no longer waits indefinitely for browser-entity WebSocket replies. send() now uses a configurable timeout (requestTimeoutMs / ADAPTER_REQUEST_TIMEOUT_MS, default 30s) and rejects with a clear error; waiters are scoped per WebSocket and include timers plus reject on socket close, so a dropped connection does not leave unrelated REST calls stuck.

Async REST routes are wrapped with a guard that maps failures (timeout, closed socket, etc.) to HTTP 500 instead of unhandled rejections that could hang the harness. The REST listen() server also gets an error handler so bind failures (e.g. EADDRINUSE) are logged.

Changes are limited to internal contract-test tooling; SDK runtime is unchanged.

Reviewed by Cursor Bugbot for commit d62dc65. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

@launchdarkly/js-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 26389 bytes
Compressed size limit: 29000
Uncompressed size: 129320 bytes

@github-actions
Copy link
Copy Markdown
Contributor

@launchdarkly/js-client-sdk size report
This is the brotli compressed size of the ESM build.
Compressed size: 31922 bytes
Compressed size limit: 34000
Uncompressed size: 113733 bytes

@github-actions
Copy link
Copy Markdown
Contributor

@launchdarkly/js-client-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 38516 bytes
Compressed size limit: 39000
Uncompressed size: 211129 bytes

@github-actions
Copy link
Copy Markdown
Contributor

@launchdarkly/browser size report
This is the brotli compressed size of the ESM build.
Compressed size: 179400 bytes
Compressed size limit: 200000
Uncompressed size: 830912 bytes

@kinyoklion kinyoklion changed the title Harden browser contract-test adapter and entity against client leaks/hangs chore: Harden browser contract-test adapter and entity against client leaks/hangs May 29, 2026
The adapter's send() waited forever for the browser entity to answer a
command over WebSocket. If the browser ever missed one reply, that REST
request -- and therefore the harness -- hung indefinitely (even status
queries returned 000), with no recovery, wedging the whole service.

- Add a timeout to send() (ADAPTER_REQUEST_TIMEOUT_MS, default 30s) so a
  stalled command fails that single request with HTTP 500 instead of
  hanging everything; the harness fails that one test and continues.
- Reject any in-flight commands when the entity's WebSocket closes, so
  no REST request is left waiting on a dead socket.
- Add an error handler on the REST listen() so a silent bind failure
  (e.g. port still held) is logged instead of disappearing.
@kinyoklion kinyoklion changed the title chore: Harden browser contract-test adapter and entity against client leaks/hangs Harden browser contract-test adapter against unanswered-command hangs May 29, 2026
@kinyoklion kinyoklion force-pushed the rlamb/harden-contract-test-adapter branch from ebc7c5d to 84ba13b Compare May 29, 2026 16:23
@kinyoklion kinyoklion changed the title Harden browser contract-test adapter against unanswered-command hangs test: Harden browser contract-test adapter against unanswered-command hangs May 29, 2026
@kinyoklion kinyoklion marked this pull request as ready for review May 29, 2026 21:01
@kinyoklion kinyoklion requested a review from a team as a code owner May 29, 2026 21:01
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 84ba13b. Configure here.

Comment thread packages/tooling/contract-test-utils/src/adapter/startAdapter.ts
Addresses review feedback: the ws.on('close') handler rejected every entry
in a waiters map that was shared across all connections, so a late close on
an old connection could reject waiters belonging to a newer (e.g.
reconnecting) connection -> spurious 500s on that connection's in-flight
REST requests.

Move the waiters map inside the connection handler so each connection has
its own. send(), the message handler, the timeout, and the close handler are
all already per-connection, and the REST server is rebuilt per connection
using that connection's send(); a per-connection map isolates them cleanly.
@kinyoklion kinyoklion merged commit b6a099f into main May 29, 2026
44 checks passed
@kinyoklion kinyoklion deleted the rlamb/harden-contract-test-adapter branch May 29, 2026 21:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants