feat: create shared contract test utilities package (SDK-1866)#1151
feat: create shared contract test utilities package (SDK-1866)#1151
Conversation
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>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
@launchdarkly/js-sdk-common size report |
|
@launchdarkly/js-client-sdk size report |
|
@launchdarkly/browser size report |
|
@launchdarkly/js-client-sdk-common size report |
- 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>
- 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>
… 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>
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>
Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Implemented the CLI executable and config file support as suggested:
CLI (sdk-testharness-server / sts):
- Registered via
binfield in the shared package'spackage.json(npm bin docs) sdk-testharness-server adaptercallsstartAdapter()with config from the config filestsis registered as an alias
Config file (contract-test.config.*):
- Supports
.json,.js,.mjs,.cjs,.ts,.mtsformats (similar to Jest'sjest.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>
Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
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>
|
The new package should have a README.md to guide developers on how to use it. |
Co-Authored-By: Steven Zhang <szhang@launchdarkly.com>
|
@joker23 Added a README.md to
|
Requirements
Related issues
SDK-1866
Describe the solution you've provided
Creates
@launchdarkly/js-contract-test-utilsunderpackages/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 typestypes/compat.ts— minimalLDContext,LDLogger,LDEvaluationReasontypes compatible with both client and server SDKstypes/ContractTestConfig.ts— typed configuration interface for the CLI config filelogging/makeLogger.ts— extracted from browserclient-side/TestHook.ts— moved from browser (fetch-based), imports from@launchdarkly/js-client-sdk-commonfor cross-SDK compatibilityclient-side/TestHarnessWebSocket.ts— extracted from browser, parameterized with capabilities list, client entity factory, and optional connection change callbackserver-side/TestHook.ts— moved from server-node (got-based)server-side/ClientPool.ts— new genericClientPool<T>replacing the duplicatedRecord<string, Entity>+ counter patternadapter/startAdapter.ts— extracted REST↔WebSocket bridge from browser and react-native adapters into a sharedstartAdapter()functionbin/sdk-testharness-server.ts— CLI executable for running contract test tooling commandsbin/loadConfig.ts— config file loader supportingcontract-test.config.{json,js,mjs,cjs,ts,mts}README.md— developer guide covering CLI usage, config file format, subpath exports, and package architectureCLI executable (
sdk-testharness-server/sts):The shared package now registers a CLI executable via the
binfield 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 serverConfiguration is loaded from a
contract-test.config.{json,js,mjs,cjs,ts,mts}file in the current working directory (similar to how Jest readsjest.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.tsfor bundlers)"./client"— client-side TestHook + TestHarnessWebSocket (source.tsfor bundlers)"./server"— server-side TestHook (compileddist/for Node.js runtime)"./adapter"— REST-to-WebSocket adapter +ContractTestConfigtype (compileddist/for Node.js runtime)Migrations:
CommandParams.ts,ConfigParams.ts,makeLogger.ts,TestHook.ts.TestHarnessWebSocketnow extends the shared base class, passing capabilities andnewSdkClientEntityvia constructor.ClientEntity.tsimports from@launchdarkly/js-contract-test-utils/client. Adapter package reduced to a minimalpackage.jsonthat delegates to thesdk-testharness-server adapterCLI — no local source code or tsconfig needed.TestHook.ts, importsServerSideTestHookfrom shared package.ClientPoolnow wraps the shared genericClientPool<LDClient>instead of managing its ownRecord+ counter. UsesnoExternalin tsup config to bundle the shared package inline.CommandParams.ts,ConfigParams.ts,makeLogger.ts,TestHook.ts.ClientEntity.tsandClientFactory.tsimport types and utilities from shared package.CommandParams.ts,ConfigParams.ts,makeLogger.ts,TestHook.ts,TestHarnessWebSocket.ts.App.tsxuses sharedTestHarnessWebSocketdirectly with capabilities list and connection change callback. Metro config updated to support subpath exports and TypeScript.jsextension convention. Adapter package reduced to a minimalpackage.jsonthat delegates to thesdk-testharness-server adapterCLI — no local source code or tsconfig needed.Documentation:
The shared package includes a comprehensive
README.mdcovering:sdk-testharness-server/sts)Shopify Oxygen ClientPool ID format change: The original implementation returned IDs like
"client-1", but the new genericClientPool.nextId()returns just"1". This affects theLocationheader in responses (e.g.,/clients/client-1→/clients/1). Verify this doesn't break the test harness expectations.React Native Metro configuration: Added
unstable_enablePackageExportsflag to enable package.jsonexportsfield resolution, plus a customresolveRequestthat maps.jsimports to.tsfiles. This handles TypeScript's ESM convention whereimport './index.js'resolves to./index.ts. Theunstable_prefix suggests this Metro feature may change in future versions.TypeScript module resolution workaround: The shared package uses
"moduleResolution": "node"with apathsmapping forgotpointing to../../../node_modules/got/dist/source. This is necessary because@launchdarkly/js-client-sdk-commondoesn't work withnode16resolution, whilegotv14'sexportsmap requires it. This workaround is fragile but functional.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 adapterin theirstartscripts, 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.Config file loading for TypeScript: The config loader uses dynamic
import()for.ts/.mtsfiles. This requires Node.js >= 22 with native TypeScript support or a loader liketsx. If the CI environment uses an older Node version without TypeScript support,.tsconfig files won't load (but.json,.js,.mjs,.cjswill work).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.
Cross-SDK compatibility via js-client-sdk-common: The shared
TestHookimports hook types from@launchdarkly/js-client-sdk-commoninstead of@launchdarkly/js-client-sdkto ensure compatibility across browser, electron, and react-native SDKs. This dependency provides the sharedHookinterface all client SDKs implement.Describe alternatives you've considered
fetchfor server-side TestHook instead ofgot, but keptgotto minimize changes in this refactor..jsextensions from internal imports to avoid Metro resolution issues, but kept them to maintain Node.js ESM compatibility for server-side builds.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 genericClientPool, and a reusableTestHarnessWebSocket) and introducing asdk-testharness-server/stsCLI 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 updatesmetro.config.jsto resolve packageexports/subpath imports.Written by Cursor Bugbot for commit 498810c. This will update automatically on new commits. Configure here.