From 3b021c251d9c3dbe75d8e650211d353c6e76860c Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Mon, 19 Jan 2026 20:17:48 -0500 Subject: [PATCH 1/3] Add offlineInit() for synchronous SDK initialization without network Adds the ability to initialize the SDK with pre-fetched configuration JSON, enabling: - Synchronous initialization without network requests - Use cases where configuration is bootstrapped from another source - Edge/serverless environments where polling isn't desired Also includes: - IOfflineClientConfig interface for offline initialization options - DEFAULT_ASSIGNMENT_CACHE_SIZE constant for consistent cache sizing - Deprecation annotations on event tracking (discontinued feature) Co-Authored-By: Claude Opus 4.5 --- src/i-client-config.ts | 57 ++++++++- src/index.spec.ts | 284 +++++++++++++++++++++++++++++++++++++++++ src/index.ts | 155 +++++++++++++++++++++- 3 files changed, 488 insertions(+), 8 deletions(-) diff --git a/src/i-client-config.ts b/src/i-client-config.ts index 359de91..3c84a9a 100644 --- a/src/i-client-config.ts +++ b/src/i-client-config.ts @@ -52,7 +52,10 @@ export interface IClientConfig { /** Amount of time in milliseconds to wait between API calls to refresh configuration data. Default of 30_000 (30s). */ pollingIntervalMs?: number; - /** Configuration settings for the event dispatcher */ + /** + * Configuration settings for the event dispatcher. + * @deprecated Eppo has discontinued eventing support. Event tracking will be handled by Datadog SDKs. + */ eventTracking?: { /** Maximum number of events to send per delivery request. Defaults to 1000 events. */ batchSize?: number; @@ -74,3 +77,55 @@ export interface IClientConfig { retryIntervalMs?: number; }; } + +/** + * Configuration used for offline initialization of the Eppo client. + * Offline initialization allows the SDK to be used without making any network requests. + * @public + */ +export interface IOfflineClientConfig { + /** + * The full flags configuration JSON string as returned by the Eppo API. + * This should be the complete response from the /flag-config/v1/config endpoint. + * + * Expected format: + * ```json + * { + * "createdAt": "2024-04-17T19:40:53.716Z", + * "format": "SERVER", + * "environment": { "name": "production" }, + * "flags": { ... } + * } + * ``` + */ + flagsConfiguration: string; + + /** + * Optional bandit models configuration JSON string as returned by the Eppo API. + * This should be the complete response from the bandit parameters endpoint. + * + * Expected format: + * ```json + * { + * "updatedAt": "2024-04-17T19:40:53.716Z", + * "bandits": { ... } + * } + * ``` + */ + banditsConfiguration?: string; + + /** + * Optional assignment logger for sending variation assignments to your data warehouse. + * Required for experiment analysis. + */ + assignmentLogger?: IAssignmentLogger; + + /** Optional bandit logger for sending bandit actions to your data warehouse */ + banditLogger?: IBanditLogger; + + /** + * Whether to throw an error if initialization fails. (default: true) + * If false, the client will be initialized with an empty configuration. + */ + throwOnFailedInitialization?: boolean; +} diff --git a/src/index.spec.ts b/src/index.spec.ts index a94c64d..83cd85e 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -40,6 +40,7 @@ import { IAssignmentLogger, init, NO_OP_EVENT_DISPATCHER, + offlineInit, } from '.'; import SpyInstance = jest.SpyInstance; @@ -913,3 +914,286 @@ describe('EppoClient E2E test', () => { }); }); }); + +describe('offlineInit', () => { + const flagKey = 'mock-experiment'; + + // Configuration for a single flag within the UFC. + const mockUfcFlagConfig: Flag = { + key: flagKey, + enabled: true, + variationType: VariationType.STRING, + variations: { + control: { + key: 'control', + value: 'control', + }, + 'variant-1': { + key: 'variant-1', + value: 'variant-1', + }, + 'variant-2': { + key: 'variant-2', + value: 'variant-2', + }, + }, + allocations: [ + { + key: 'traffic-split', + rules: [], + splits: [ + { + variationKey: 'control', + shards: [ + { + salt: 'some-salt', + ranges: [{ start: 0, end: 3400 }], + }, + ], + }, + { + variationKey: 'variant-1', + shards: [ + { + salt: 'some-salt', + ranges: [{ start: 3400, end: 6700 }], + }, + ], + }, + { + variationKey: 'variant-2', + shards: [ + { + salt: 'some-salt', + ranges: [{ start: 6700, end: 10000 }], + }, + ], + }, + ], + doLog: true, + }, + ], + totalShards: 10000, + }; + + // Helper to create a full configuration JSON string + const createFlagsConfigJson = ( + flags: Record, + options: { createdAt?: string; format?: string } = {}, + ): string => { + return JSON.stringify({ + createdAt: options.createdAt ?? '2024-04-17T19:40:53.716Z', + format: options.format ?? 'SERVER', + environment: { name: 'Test' }, + flags, + }); + }; + + describe('basic initialization', () => { + it('initializes with flag configurations and returns correct assignments', () => { + const client = offlineInit({ + flagsConfiguration: createFlagsConfigJson({ [flagKey]: mockUfcFlagConfig }), + }); + + // subject-10 should get variant-1 based on the hash + const assignment = client.getStringAssignment(flagKey, 'subject-10', {}, 'default-value'); + expect(assignment).toEqual('variant-1'); + }); + + it('returns default value when flag is not found', () => { + const client = offlineInit({ + flagsConfiguration: createFlagsConfigJson({ [flagKey]: mockUfcFlagConfig }), + }); + + const assignment = client.getStringAssignment( + 'non-existent-flag', + 'subject-10', + {}, + 'default-value', + ); + expect(assignment).toEqual('default-value'); + }); + + it('initializes with empty configuration', () => { + const client = offlineInit({ + flagsConfiguration: createFlagsConfigJson({}), + }); + + const assignment = client.getStringAssignment(flagKey, 'subject-10', {}, 'default-value'); + expect(assignment).toEqual('default-value'); + }); + + it('makes client available via getInstance()', () => { + offlineInit({ + flagsConfiguration: createFlagsConfigJson({ [flagKey]: mockUfcFlagConfig }), + }); + + const client = getInstance(); + const assignment = client.getStringAssignment(flagKey, 'subject-10', {}, 'default-value'); + expect(assignment).toEqual('variant-1'); + }); + }); + + describe('assignment logging', () => { + it('logs assignments when assignment logger is provided', () => { + const mockLogger = td.object(); + + const client = offlineInit({ + flagsConfiguration: createFlagsConfigJson({ [flagKey]: mockUfcFlagConfig }), + assignmentLogger: mockLogger, + }); + + client.getStringAssignment(flagKey, 'subject-10', { foo: 'bar' }, 'default-value'); + + expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1); + const loggedAssignment = td.explain(mockLogger.logAssignment).calls[0].args[0]; + expect(loggedAssignment.subject).toEqual('subject-10'); + expect(loggedAssignment.featureFlag).toEqual(flagKey); + expect(loggedAssignment.allocation).toEqual('traffic-split'); + }); + + it('does not throw when assignment logger throws', () => { + const mockLogger = td.object(); + td.when(mockLogger.logAssignment(td.matchers.anything())).thenThrow( + new Error('logging error'), + ); + + const client = offlineInit({ + flagsConfiguration: createFlagsConfigJson({ [flagKey]: mockUfcFlagConfig }), + assignmentLogger: mockLogger, + }); + + // Should not throw, even though logger throws + const assignment = client.getStringAssignment(flagKey, 'subject-10', {}, 'default-value'); + expect(assignment).toEqual('variant-1'); + }); + }); + + describe('configuration metadata', () => { + it('extracts createdAt from configuration as configPublishedAt', () => { + const createdAt = '2024-01-15T10:00:00.000Z'; + + const client = offlineInit({ + flagsConfiguration: createFlagsConfigJson({ [flagKey]: mockUfcFlagConfig }, { createdAt }), + }); + + const result = client.getStringAssignmentDetails(flagKey, 'subject-10', {}, 'default-value'); + expect(result.evaluationDetails.configPublishedAt).toBe(createdAt); + }); + }); + + describe('error handling', () => { + it('throws error by default when JSON parsing fails', () => { + expect(() => { + offlineInit({ + flagsConfiguration: 'invalid json', + }); + }).toThrow(); + }); + + it('does not throw when throwOnFailedInitialization is false', () => { + expect(() => { + offlineInit({ + flagsConfiguration: 'invalid json', + throwOnFailedInitialization: false, + }); + }).not.toThrow(); + }); + + it('does not throw with valid empty flags configuration', () => { + expect(() => { + offlineInit({ + flagsConfiguration: createFlagsConfigJson({}), + }); + }).not.toThrow(); + }); + }); + + describe('no network requests', () => { + it('does not have configurationRequestParameters (no polling)', () => { + const client = offlineInit({ + flagsConfiguration: createFlagsConfigJson({ [flagKey]: mockUfcFlagConfig }), + }); + + // Access the internal configurationRequestParameters - should be undefined for offline mode + const configurationRequestParameters = client['configurationRequestParameters']; + expect(configurationRequestParameters).toBeUndefined(); + }); + }); + + describe('bandit support', () => { + it('initializes with bandit references from configuration', () => { + const banditFlagKey = 'bandit-flag'; + const banditKey = 'test-bandit'; + + const banditFlagConfig: Flag = { + key: banditFlagKey, + enabled: true, + variationType: VariationType.STRING, + variations: { + bandit: { + key: 'bandit', + value: 'bandit', + }, + control: { + key: 'control', + value: 'control', + }, + }, + allocations: [ + { + key: 'bandit-allocation', + rules: [], + splits: [ + { + variationKey: 'bandit', + shards: [ + { + salt: 'salt', + ranges: [{ start: 0, end: 10000 }], + }, + ], + }, + ], + doLog: true, + }, + ], + totalShards: 10000, + }; + + // Create a configuration with bandit references + const flagsConfigJson = JSON.stringify({ + createdAt: '2024-04-17T19:40:53.716Z', + format: 'SERVER', + environment: { name: 'Test' }, + flags: { [banditFlagKey]: banditFlagConfig }, + banditReferences: { + [banditKey]: { + modelVersion: 'v1', + flagVariations: [ + { + key: 'bandit', + flagKey: banditFlagKey, + variationKey: 'bandit', + variationValue: 'bandit', + }, + ], + }, + }, + }); + + const client = offlineInit({ + flagsConfiguration: flagsConfigJson, + }); + + // Verify the client is initialized and can make assignments + const assignment = client.getStringAssignment( + banditFlagKey, + 'subject-1', + {}, + 'default-value', + ); + expect(assignment).toEqual('bandit'); + }); + }); +}); diff --git a/src/index.ts b/src/index.ts index ffbedae..0dc404f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import { Flag, FlagConfigurationRequestParameters, FlagKey, + FormatEnum, MemoryOnlyConfigurationStore, NamedEventQueue, applicationLogger, @@ -18,7 +19,7 @@ import { } from '@eppo/js-client-sdk-common'; import FileBackedNamedEventQueue from './events/file-backed-named-event-queue'; -import { IClientConfig } from './i-client-config'; +import { IClientConfig, IOfflineClientConfig } from './i-client-config'; import { sdkName, sdkVersion } from './sdk-data'; import { generateSalt } from './util'; import { isReadOnlyFs } from './util/index'; @@ -37,7 +38,7 @@ export { EppoAssignmentLogger, } from '@eppo/js-client-sdk-common'; -export { IClientConfig }; +export { IClientConfig, IOfflineClientConfig }; let clientInstance: EppoClient; @@ -49,6 +50,15 @@ let flagConfigurationStore: MemoryOnlyConfigurationStore; let banditVariationConfigurationStore: MemoryOnlyConfigurationStore; let banditModelConfigurationStore: MemoryOnlyConfigurationStore; +/** + * Default assignment cache size for server-side use cases. + * We estimate this will use no more than 10 MB of memory. + */ +const DEFAULT_ASSIGNMENT_CACHE_SIZE = 50_000; + +/** + * @deprecated Eppo has discontinued eventing support. Event tracking will be handled by Datadog SDKs. + */ export const NO_OP_EVENT_DISPATCHER: EventDispatcher = { // eslint-disable-next-line @typescript-eslint/no-empty-function attachContext: () => {}, @@ -111,11 +121,8 @@ export async function init(config: IClientConfig): Promise { clientInstance.setBanditLogger(config.banditLogger); } - // default to LRU cache with 50_000 entries. - // we estimate this will use no more than 10 MB of memory - // and should be appropriate for most server-side use cases. - clientInstance.useLRUInMemoryAssignmentCache(50_000); - clientInstance.useExpiringInMemoryBanditAssignmentCache(50_000); + clientInstance.useLRUInMemoryAssignmentCache(DEFAULT_ASSIGNMENT_CACHE_SIZE); + clientInstance.useExpiringInMemoryBanditAssignmentCache(DEFAULT_ASSIGNMENT_CACHE_SIZE); // Fetch configurations (which will also start regular polling per requestConfiguration) await clientInstance.fetchFlagConfigurations(); @@ -282,6 +289,140 @@ export function getBanditsConfiguration(): string | null { return JSON.stringify(configuration); } +/** + * Initializes the Eppo client in offline mode with a provided configuration. + * This method is synchronous and does not make any network requests. + * Use this when you want to initialize the SDK with a previously fetched configuration. + * @param config offline client configuration containing flag configurations as JSON strings + * @returns the initialized client instance + * @public + */ +export function offlineInit(config: IOfflineClientConfig): EppoClient { + const { + flagsConfiguration, + banditsConfiguration, + assignmentLogger, + banditLogger, + throwOnFailedInitialization = true, + } = config; + + try { + // Parse the flags configuration JSON + const flagsConfigResponse = JSON.parse(flagsConfiguration) as { + createdAt?: string; + format?: string; + environment?: { name: string }; + flags: Record; + banditReferences?: Record< + string, + { + modelVersion: string; + flagVariations: BanditVariation[]; + } + >; + }; + + // Create memory-only configuration stores + flagConfigurationStore = new MemoryOnlyConfigurationStore(); + banditVariationConfigurationStore = new MemoryOnlyConfigurationStore(); + banditModelConfigurationStore = new MemoryOnlyConfigurationStore(); + + // Set format from the configuration (default to SERVER) + const format = (flagsConfigResponse.format as FormatEnum) ?? FormatEnum.SERVER; + flagConfigurationStore.setFormat(format); + + // Load flag configurations into store + // Note: setEntries is async but MemoryOnlyConfigurationStore performs synchronous operations internally, + // so there's no race condition. We add .catch() for defensive error handling, matching JS client SDK pattern. + flagConfigurationStore + .setEntries(flagsConfigResponse.flags ?? {}) + .catch((err) => + applicationLogger.warn(`Error setting flags for memory-only configuration store: ${err}`), + ); + + // Set configuration timestamp if available + if (flagsConfigResponse.createdAt) { + flagConfigurationStore.setConfigPublishedAt(flagsConfigResponse.createdAt); + } + + // Set environment if available + if (flagsConfigResponse.environment) { + flagConfigurationStore.setEnvironment(flagsConfigResponse.environment); + } + + // Process bandit references from the flags configuration + // Index by flag key for quick lookup (instead of by bandit key) + if (flagsConfigResponse.banditReferences) { + const banditVariationsByFlagKey: Record = {}; + for (const banditReference of Object.values(flagsConfigResponse.banditReferences)) { + for (const flagVariation of banditReference.flagVariations) { + const { flagKey } = flagVariation; + if (!banditVariationsByFlagKey[flagKey]) { + banditVariationsByFlagKey[flagKey] = []; + } + banditVariationsByFlagKey[flagKey].push(flagVariation); + } + } + banditVariationConfigurationStore + .setEntries(banditVariationsByFlagKey) + .catch((err) => + applicationLogger.warn( + `Error setting bandit variations for memory-only configuration store: ${err}`, + ), + ); + } + + // Parse and load bandit models if provided + if (banditsConfiguration) { + const banditsConfigResponse = JSON.parse(banditsConfiguration) as { + updatedAt?: string; + bandits: Record; + }; + banditModelConfigurationStore + .setEntries(banditsConfigResponse.bandits ?? {}) + .catch((err) => + applicationLogger.warn( + `Error setting bandit models for memory-only configuration store: ${err}`, + ), + ); + } + + // Create client without request parameters (offline mode - no polling) + clientInstance = new EppoClient({ + flagConfigurationStore, + banditVariationConfigurationStore, + banditModelConfigurationStore, + // No configurationRequestParameters = offline mode, no network requests + }); + + // Set loggers if provided + if (assignmentLogger) { + clientInstance.setAssignmentLogger(assignmentLogger); + } + if (banditLogger) { + clientInstance.setBanditLogger(banditLogger); + } + + clientInstance.useLRUInMemoryAssignmentCache(DEFAULT_ASSIGNMENT_CACHE_SIZE); + clientInstance.useExpiringInMemoryBanditAssignmentCache(DEFAULT_ASSIGNMENT_CACHE_SIZE); + + return clientInstance; + } catch (error) { + if (throwOnFailedInitialization) { + throw error; + } + applicationLogger.warn( + `Eppo SDK offline initialization failed: ${error instanceof Error ? error.message : error}`, + ); + // Return the client instance even if initialization failed + // It will return default values for all assignments + return clientInstance; + } +} + +/** + * @deprecated Eppo has discontinued eventing support. Event tracking will be handled by Datadog SDKs. + */ function newEventDispatcher( sdkKey: string, config: IClientConfig['eventTracking'] = {}, From 35e63a57244f844a3d0f026bee187be00f785018 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Mon, 19 Jan 2026 22:59:05 -0500 Subject: [PATCH 2/3] Add getBanditAction verification to offlineInit bandit test Use realistic bandit model coefficients from bandit-models-v1.json and test subject "alice" from test-case-banner-bandit.json to verify that getBanditAction returns the expected variation and action after offline initialization. Co-Authored-By: Claude Opus 4.5 --- src/index.spec.ts | 130 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 110 insertions(+), 20 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 83cd85e..95db46c 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1122,34 +1122,36 @@ describe('offlineInit', () => { }); describe('bandit support', () => { - it('initializes with bandit references from configuration', () => { - const banditFlagKey = 'bandit-flag'; - const banditKey = 'test-bandit'; + it('initializes with bandit references and supports getBanditAction', () => { + // Use realistic names inspired by bandit-flags-v1.json and bandit-models-v1.json + const banditFlagKey = 'banner_bandit_flag'; + const banditKey = 'banner_bandit'; + // Flag configuration matching banner_bandit_flag structure const banditFlagConfig: Flag = { key: banditFlagKey, enabled: true, variationType: VariationType.STRING, variations: { - bandit: { - key: 'bandit', - value: 'bandit', - }, control: { key: 'control', value: 'control', }, + [banditKey]: { + key: banditKey, + value: banditKey, + }, }, allocations: [ { - key: 'bandit-allocation', + key: 'training', rules: [], splits: [ { - variationKey: 'bandit', + variationKey: banditKey, shards: [ { - salt: 'salt', + salt: 'traffic-split', ranges: [{ start: 0, end: 10000 }], }, ], @@ -1161,7 +1163,7 @@ describe('offlineInit', () => { totalShards: 10000, }; - // Create a configuration with bandit references + // Flags configuration with bandit references (matching bandit-flags-v1.json structure) const flagsConfigJson = JSON.stringify({ createdAt: '2024-04-17T19:40:53.716Z', format: 'SERVER', @@ -1169,31 +1171,119 @@ describe('offlineInit', () => { flags: { [banditFlagKey]: banditFlagConfig }, banditReferences: { [banditKey]: { - modelVersion: 'v1', + modelVersion: '123', flagVariations: [ { - key: 'bandit', + key: banditKey, flagKey: banditFlagKey, - variationKey: 'bandit', - variationValue: 'bandit', + allocationKey: 'training', + variationKey: banditKey, + variationValue: banditKey, }, ], }, }, }); + // Bandit model configuration (matching bandit-models-v1.json structure for banner_bandit) + const banditsConfigJson = JSON.stringify({ + bandits: { + [banditKey]: { + banditKey, + modelName: 'falcon', + modelVersion: '123', + updatedAt: '2023-09-13T04:52:06.462Z', + modelData: { + gamma: 1.0, + defaultActionScore: 0.0, + actionProbabilityFloor: 0.0, + coefficients: { + nike: { + actionKey: 'nike', + intercept: 1.0, + actionNumericCoefficients: [ + { + attributeKey: 'brand_affinity', + coefficient: 1.0, + missingValueCoefficient: -0.1, + }, + ], + actionCategoricalCoefficients: [ + { + attributeKey: 'loyalty_tier', + valueCoefficients: { gold: 4.5, silver: 3.2, bronze: 1.9 }, + missingValueCoefficient: 0.0, + }, + ], + subjectNumericCoefficients: [ + { attributeKey: 'account_age', coefficient: 0.3, missingValueCoefficient: 0.0 }, + ], + subjectCategoricalCoefficients: [ + { + attributeKey: 'gender_identity', + valueCoefficients: { female: 0.5, male: -0.5 }, + missingValueCoefficient: 2.3, + }, + ], + }, + adidas: { + actionKey: 'adidas', + intercept: 1.1, + actionNumericCoefficients: [ + { + attributeKey: 'brand_affinity', + coefficient: 2.0, + missingValueCoefficient: 1.2, + }, + ], + actionCategoricalCoefficients: [], + subjectNumericCoefficients: [], + subjectCategoricalCoefficients: [ + { + attributeKey: 'gender_identity', + valueCoefficients: { female: -1.0, male: 1.0 }, + missingValueCoefficient: 0.0, + }, + ], + }, + }, + }, + }, + }, + }); + const client = offlineInit({ flagsConfiguration: flagsConfigJson, + banditsConfiguration: banditsConfigJson, }); - // Verify the client is initialized and can make assignments - const assignment = client.getStringAssignment( + // Verify the client is initialized and can make flag assignments + const assignment = client.getStringAssignment(banditFlagKey, 'alice', {}, 'default-value'); + expect(assignment).toEqual(banditKey); + + // Verify bandit action selection using "alice" from test-case-banner-bandit.json + // alice with her attributes and actions should get nike + const banditResult = client.getBanditAction( banditFlagKey, - 'subject-1', - {}, + 'alice', + { + numericAttributes: { age: 25 }, + categoricalAttributes: { country: 'USA', gender_identity: 'female' }, + }, + { + nike: { + numericAttributes: { brand_affinity: 1.5 }, + categoricalAttributes: { loyalty_tier: 'silver' }, + }, + adidas: { + numericAttributes: { brand_affinity: -1.0 }, + categoricalAttributes: { loyalty_tier: 'bronze' }, + }, + }, 'default-value', ); - expect(assignment).toEqual('bandit'); + expect(banditResult.variation).toEqual(banditKey); + expect(banditResult.action).toEqual('nike'); }); }); }); From 41f68e206f2adf2fd2034c6783fe7475a98a5f99 Mon Sep 17 00:00:00 2001 From: Aaron Silverman Date: Mon, 19 Jan 2026 23:09:52 -0500 Subject: [PATCH 3/3] Refactor offlineInit tests for clarity - Rename "makes client available via getInstance()" to "can request assignment" - Move "no network requests" test into "basic initialization" section - Add assignment verification to "empty flags configuration" test Co-Authored-By: Claude Opus 4.5 --- src/index.spec.ts | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 95db46c..5646889 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1023,7 +1023,7 @@ describe('offlineInit', () => { expect(assignment).toEqual('default-value'); }); - it('makes client available via getInstance()', () => { + it('can request assignment', () => { offlineInit({ flagsConfiguration: createFlagsConfigJson({ [flagKey]: mockUfcFlagConfig }), }); @@ -1032,6 +1032,16 @@ describe('offlineInit', () => { const assignment = client.getStringAssignment(flagKey, 'subject-10', {}, 'default-value'); expect(assignment).toEqual('variant-1'); }); + + it('does not have configurationRequestParameters (no polling)', () => { + const client = offlineInit({ + flagsConfiguration: createFlagsConfigJson({ [flagKey]: mockUfcFlagConfig }), + }); + + // Access the internal configurationRequestParameters - should be undefined for offline mode + const configurationRequestParameters = client['configurationRequestParameters']; + expect(configurationRequestParameters).toBeUndefined(); + }); }); describe('assignment logging', () => { @@ -1101,23 +1111,12 @@ describe('offlineInit', () => { }); it('does not throw with valid empty flags configuration', () => { - expect(() => { - offlineInit({ - flagsConfiguration: createFlagsConfigJson({}), - }); - }).not.toThrow(); - }); - }); - - describe('no network requests', () => { - it('does not have configurationRequestParameters (no polling)', () => { const client = offlineInit({ - flagsConfiguration: createFlagsConfigJson({ [flagKey]: mockUfcFlagConfig }), + flagsConfiguration: createFlagsConfigJson({}), }); - // Access the internal configurationRequestParameters - should be undefined for offline mode - const configurationRequestParameters = client['configurationRequestParameters']; - expect(configurationRequestParameters).toBeUndefined(); + const assignment = client.getStringAssignment(flagKey, 'subject-1', {}, 'default-value'); + expect(assignment).toEqual('default-value'); }); });