Skip to content

feat: create shared contract test utilities package (SDK-1866)#1151

Draft
joker23 wants to merge 20 commits intomainfrom
devin/SDK-1866-1772645970
Draft

feat: create shared contract test utilities package (SDK-1866)#1151
joker23 wants to merge 20 commits intomainfrom
devin/SDK-1866-1772645970

Conversation

@joker23
Copy link
Contributor

@joker23 joker23 commented Mar 4, 2026

Requirements

  • I have added test coverage for new or changed functionality
  • I have followed the repository's pull request submission guidelines
  • I have validated my changes against all supported platform versions

Related issues

SDK-1866

Describe the solution you've provided

Creates @launchdarkly/js-contract-test-utils under packages/tooling/contract-test-utils/ to consolidate duplicated contract test code across all five JavaScript SDK contract test implementations.

New shared package contents:

  • types/CommandParams.ts & types/ConfigParams.ts — extracted from browser, imports updated to use minimal compat types
  • types/compat.ts — minimal LDContext, LDLogger, LDEvaluationReason types compatible with both client and server SDKs
  • types/ContractTestConfig.ts — typed configuration interface for the CLI config file
  • logging/makeLogger.ts — extracted from browser
  • client-side/TestHook.ts — moved from browser (fetch-based), imports from @launchdarkly/js-client-sdk-common for cross-SDK compatibility
  • client-side/TestHarnessWebSocket.ts — extracted from browser, parameterized with capabilities list, client entity factory, and optional connection change callback
  • server-side/TestHook.ts — moved from server-node (got-based)
  • server-side/ClientPool.ts — new generic ClientPool<T> replacing the duplicated Record<string, Entity> + counter pattern
  • adapter/startAdapter.ts — extracted REST↔WebSocket bridge from browser and react-native adapters into a shared startAdapter() function
  • bin/sdk-testharness-server.ts — CLI executable for running contract test tooling commands
  • bin/loadConfig.ts — config file loader supporting contract-test.config.{json,js,mjs,cjs,ts,mts}
  • README.md — developer guide covering CLI usage, config file format, subpath exports, and package architecture

CLI executable (sdk-testharness-server / sts):

The shared package now registers a CLI executable via the bin field in package.json, with two names:

  • sdk-testharness-server (full name)
  • sts (alias)

Currently supports one command:

  • sdk-testharness-server adapter — starts the REST↔WebSocket adapter server

Configuration is loaded from a contract-test.config.{json,js,mjs,cjs,ts,mts} file in the current working directory (similar to how Jest reads jest.config.*). If no config file is found, defaults are used. Example config:

{
  "adapter": {
    "wsPort": 8001,
    "httpPort": 8000
  }
}

Subpath export strategy:

  • "." — universal types, logging, ClientPool (source .ts for bundlers)
  • "./client" — client-side TestHook + TestHarnessWebSocket (source .ts for bundlers)
  • "./server" — server-side TestHook (compiled dist/ for Node.js runtime)
  • "./adapter" — REST-to-WebSocket adapter + ContractTestConfig type (compiled dist/ for Node.js runtime)

Migrations:

  • Browser: Removed local CommandParams.ts, ConfigParams.ts, makeLogger.ts, TestHook.ts. TestHarnessWebSocket now extends the shared base class, passing capabilities and newSdkClientEntity via constructor. ClientEntity.ts imports from @launchdarkly/js-contract-test-utils/client. Adapter package reduced to a minimal package.json that delegates to the sdk-testharness-server adapter CLI — no local source code or tsconfig needed.
  • Server-node: Removed local TestHook.ts, imports ServerSideTestHook from shared package.
  • Shopify-oxygen: ClientPool now wraps the shared generic ClientPool<LDClient> instead of managing its own Record + counter. Uses noExternal in tsup config to bundle the shared package inline.
  • Electron: Removed local CommandParams.ts, ConfigParams.ts, makeLogger.ts, TestHook.ts. ClientEntity.ts and ClientFactory.ts import types and utilities from shared package.
  • React Native: Removed local CommandParams.ts, ConfigParams.ts, makeLogger.ts, TestHook.ts, TestHarnessWebSocket.ts. App.tsx uses shared TestHarnessWebSocket directly with capabilities list and connection change callback. Metro config updated to support subpath exports and TypeScript .js extension convention. Adapter package reduced to a minimal package.json that delegates to the sdk-testharness-server adapter CLI — no local source code or tsconfig needed.

Documentation:

The shared package includes a comprehensive README.md covering:

  • CLI commands and usage (sdk-testharness-server / sts)
  • Config file format and supported extensions
  • Subpath exports table and import paths
  • Usage examples for adapter packages, client-side entities, and server-side entities
  • Build scripts reference
  • Package architecture overview

⚠️ Items for reviewer attention:

  1. Shopify Oxygen ClientPool ID format change: The original implementation returned IDs like "client-1", but the new generic ClientPool.nextId() returns just "1". This affects the Location header in responses (e.g., /clients/client-1/clients/1). Verify this doesn't break the test harness expectations.

  2. React Native Metro configuration: Added unstable_enablePackageExports flag to enable package.json exports field resolution, plus a custom resolveRequest that maps .js imports to .ts files. This handles TypeScript's ESM convention where import './index.js' resolves to ./index.ts. The unstable_ prefix suggests this Metro feature may change in future versions.

  3. TypeScript module resolution workaround: The shared package uses "moduleResolution": "node" with a paths mapping for got pointing to ../../../node_modules/got/dist/source. This is necessary because @launchdarkly/js-client-sdk-common doesn't work with node16 resolution, while got v14's exports map requires it. This workaround is fragile but functional.

  4. CLI bin resolution via yarn: The browser and react-native adapter packages no longer have their own TypeScript source — they just invoke sdk-testharness-server adapter in their start scripts, relying on yarn's automatic bin resolution. Verify this works correctly in the CI environment. The adapters were tested locally and successfully resolved the bin through yarn's PnP.

  5. Config file loading for TypeScript: The config loader uses dynamic import() for .ts/.mts files. This requires Node.js >= 22 with native TypeScript support or a loader like tsx. If the CI environment uses an older Node version without TypeScript support, .ts config files won't load (but .json, .js, .mjs, .cjs will work).

  6. No test coverage: This is a refactoring PR. Correctness relies on the existing contract test harness validating behavior. There are no unit tests for the shared utilities.

  7. Cross-SDK compatibility via js-client-sdk-common: The shared TestHook imports hook types from @launchdarkly/js-client-sdk-common instead of @launchdarkly/js-client-sdk to ensure compatibility across browser, electron, and react-native SDKs. This dependency provides the shared Hook interface all client SDKs implement.

Describe alternatives you've considered

  • Considered using native fetch for server-side TestHook instead of got, but kept got to minimize changes in this refactor.
  • Considered separate packages for client-side vs server-side utilities, but a single shared package is simpler for this scope.
  • Considered removing .js extensions from internal imports to avoid Metro resolution issues, but kept them to maintain Node.js ESM compatibility for server-side builds.
  • Considered having adapters continue compiling their own wrappers around startAdapter(), but simplified to direct CLI invocation for better ergonomics and less duplication.

Additional context

Link to Devin Session: https://app.devin.ai/sessions/83a6141bac50404e88e590a092884e19
Requested by: @joker23

This change consolidates all five JavaScript SDK contract test implementations into a single shared package, setting the foundation for maintainable contract test infrastructure across the monorepo. The final iteration adds a CLI executable (sdk-testharness-server / sts) with config file support (modeled after Jest's config pattern), further simplifying the adapter packages to just delegate to the shared CLI, and includes comprehensive documentation in the package README.


Note

Medium Risk
Medium risk because it refactors cross-SDK contract test harness infrastructure (adapter/CLI/WebSocket/hook plumbing) and changes Shopify Oxygen client ID formatting, which could break harness integration even though it's non-production code.

Overview
Adds a new internal workspace package, @launchdarkly/js-contract-test-utils, consolidating duplicated contract-test code (shared command/config types, logging, client/server test hooks, a generic ClientPool, and a reusable TestHarnessWebSocket) and introducing a sdk-testharness-server/sts CLI with configurable REST↔WebSocket adapter startup.

Migrates browser, React Native, Electron, server-node, and Shopify Oxygen contract test projects to depend on the shared package: browser/RN adapter packages become thin wrappers invoking the shared CLI, entities import shared types/hooks/WebSocket, server-node switches to shared server-side hook, Shopify Oxygen adopts the shared ClientPool (changing generated client IDs), and React Native updates metro.config.js to resolve package exports/subpath imports.

Written by Cursor Bugbot for commit 498810c. This will update automatically on new commits. Configure here.


Open with Devin

Create @launchdarkly/js-contract-test-utils package under packages/shared/contract-test-utils/ to eliminate code duplication across JavaScript SDK contract test implementations.

Changes:
- Extract CommandParams, ConfigParams, makeLogger into shared types
- Create client-side TestHook (fetch-based) and TestHarnessWebSocket
- Create server-side TestHook (got-based) and generic ClientPool<T>
- Migrate browser contract tests to use shared package
- Migrate server-node contract tests to use shared package
- Migrate shopify-oxygen contract tests to use shared package
- Register workspace in root package.json

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
@joker23 joker23 requested a review from a team as a code owner March 4, 2026 17:54
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@github-actions
Copy link
Contributor

github-actions bot commented Mar 4, 2026

@launchdarkly/js-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 25566 bytes
Compressed size limit: 26000
Uncompressed size: 125383 bytes

@github-actions
Copy link
Contributor

github-actions bot commented Mar 4, 2026

@launchdarkly/js-client-sdk size report
This is the brotli compressed size of the ESM build.
Compressed size: 24212 bytes
Compressed size limit: 25000
Uncompressed size: 83755 bytes

@github-actions
Copy link
Contributor

github-actions bot commented Mar 4, 2026

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

@github-actions
Copy link
Contributor

github-actions bot commented Mar 4, 2026

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

cursor[bot]

This comment was marked as resolved.

- Change package.json main/types/exports to point to src/index.ts instead of dist/
  so consumers can resolve the package without a separate build step
- Change imports from @launchdarkly/js-sdk-common to @launchdarkly/js-client-sdk
  which has proper exports for both bundler and node16 module resolution
- Remove @launchdarkly/js-sdk-common from dependencies (no longer needed directly)

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
devin-ai-integration[bot]

This comment was marked as resolved.

cursor[bot]

This comment was marked as resolved.

- Create separate entry points: /client and /server subpaths
- Move ClientPool to universal exports (zero external dependencies)
- Browser imports from /client (no node-server-sdk dependency)
- Server-node imports from /server (includes got + node-server-sdk)
- Shopify-oxygen imports ClientPool from base path
- Add typesVersions for moduleResolution: node compatibility

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
cursor[bot]

This comment was marked as resolved.

devin-ai-integration bot and others added 3 commits March 4, 2026 18:20
… compat types

- Move package from packages/shared/contract-test-utils/ to packages/tooling/contract-test-utils/
- Replace SDK-specific type imports (LDContext, LDLogger, LDEvaluationReason) with local compat types
- Remove duplicate ClientPool export from server.ts
- Update root workspace entry

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
- Update exports to point to compiled dist/ files for runtime compatibility
- Change tsconfig rootDir to 'src' for clean dist output
- Add shared package build step to all contract test build scripts
- Fixes ERR_MODULE_NOT_FOUND at runtime in server-node and shopify-oxygen

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
- Client/base exports point to .ts source (Vite/tsup handle TS natively)
- Server export points to compiled dist/ (Node.js runtime needs .js)
- Add tsconfig.server.json to compile only server-relevant files
- Server-node build uses build:server to avoid compiling client code
- Browser/shopify-oxygen don't need shared package pre-built (bundlers handle TS)

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
cursor[bot]

This comment was marked as resolved.

devin-ai-integration bot and others added 5 commits March 4, 2026 18:45
Add noExternal for @launchdarkly/js-contract-test-utils so tsup bundles
the shared package code instead of keeping it as an external import.
This is needed because the base export points to .ts source files which
Node.js cannot execute at runtime.

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
- Delete local CommandParams, ConfigParams, makeLogger, TestHook from electron contract tests
- Delete local CommandParams, ConfigParams, makeLogger, TestHook, TestHarnessWebSocket from react-native contract tests
- Update electron ClientEntity and ClientFactory to import from shared package
- Update react-native ClientEntity and App.tsx to import from shared package
- Change shared TestHook import from js-client-sdk to js-client-sdk-common for cross-SDK compatibility
- Add intentionalClose and onConnectionChange support to shared TestHarnessWebSocket
- Add SDKConfigProxyParams to shared ConfigParams for electron proxy support
- Add @launchdarkly/js-contract-test-utils dependency to both electron and react-native package.json

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
…ntract tests

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
…tion

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
cursor[bot]

This comment was marked as resolved.

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think it makes much sense to have this without the adaptor implementation (eg https://github.com/launchdarkly/js-core/tree/main/packages/sdk/browser/contract-tests/adapter) since the websocket contract is specific to that adaptor implementation. I think it would be good to have a shared adaptor implementation that is executable.

I am already trying to leverage the adaptor implementation from the browser in my react sdk branch (https://github.com/launchdarkly/js-core/tree/skz/react-sdk-next/packages/sdk/react/contract-tests).

Copy link
Contributor

Choose a reason for hiding this comment

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

Good call — the adapter was indeed duplicated identically between browser and react-native. I've now extracted it into the shared package as @launchdarkly/js-contract-test-utils/adapter (compiled to dist/ since it's Node.js code).

Both browser and react-native adapters are now one-liners:

import { startAdapter } from '@launchdarkly/js-contract-test-utils/adapter';
startAdapter();

The shared startAdapter() function accepts optional { wsPort, httpPort } config (defaults to 8001/8000). This should make it easy to reuse in the react SDK contract tests too.

See commit 5b4b3c1.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it would be more effective if we made this an executable and read off modifications from a configuration file (like what jest does for example). Reference https://docs.npmjs.com/cli/v11/configuring-npm/package-json#bin and we can call the runner sdk-testharness-server (sts as an alias) and the startAdapter function could be called with the command sdk-testharness-server adapter.

I think the configuration file can be called contract-test.config and we should support different formats (eg json, js, ts, etc)

Copy link
Contributor

Choose a reason for hiding this comment

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

Implemented the CLI executable and config file support as suggested:

CLI (sdk-testharness-server / sts):

  • Registered via bin field in the shared package's package.json (npm bin docs)
  • sdk-testharness-server adapter calls startAdapter() with config from the config file
  • sts is registered as an alias

Config file (contract-test.config.*):

  • Supports .json, .js, .mjs, .cjs, .ts, .mts formats (similar to Jest's jest.config.*)
  • Loaded from the current working directory via loadConfig()
  • If no config file is found, defaults are used

Example contract-test.config.json:

{
  "adapter": {
    "wsPort": 8001,
    "httpPort": 8000
  }
}

Adapter packages simplified:
Both browser and react-native adapter packages no longer need their own source code or tsconfig — they just delegate to the CLI:

{
  "scripts": {
    "build": "yarn workspace @launchdarkly/js-contract-test-utils build:adapter",
    "start": "yarn build && sdk-testharness-server adapter"
  }
}

Exported ContractTestConfig type so consumers can type their config files:

import type { ContractTestConfig } from '@launchdarkly/js-contract-test-utils/adapter';

All 43 CI checks pass.

…-native contract tests

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
cursor[bot]

This comment was marked as resolved.

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
cursor[bot]

This comment was marked as resolved.

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration bot and others added 4 commits March 5, 2026 17:34
Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
…time

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
…ile support

- Create sdk-testharness-server CLI with 'adapter' command
- Add contract-test.config.{json,js,mjs,cjs,ts,mts} config file support
- Register bin entries in shared package (sdk-testharness-server + sts alias)
- Simplify browser and react-native adapter packages to use CLI
- Remove adapter src/index.ts and tsconfig.json (no longer needed)
- Export ContractTestConfig type for typed configuration files

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
devin-ai-integration[bot]

This comment was marked as resolved.

@joker23
Copy link
Contributor Author

joker23 commented Mar 6, 2026

The new package should have a README.md to guide developers on how to use it.

Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
@devin-ai-integration
Copy link
Contributor

@joker23 Added a README.md to packages/tooling/contract-test-utils/ covering:

  • CLI usage (sdk-testharness-server / sts) with commands and configuration
  • Config file format and supported extensions (.json, .js, .mjs, .cjs, .ts, .mts)
  • Subpath exports table (., ./client, ./server, ./adapter)
  • Usage examples for adapter packages, client-side entities, and server-side entities
  • Build scripts
  • Package architecture overview

@joker23 joker23 marked this pull request as draft March 7, 2026 05:01
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.

1 participant