@@ -9,8 +9,12 @@ import ApiEndpoints from './api-endpoints';
9
9
import ConfigurationRequestor from './configuration-requestor' ;
10
10
import { IConfigurationStore } from './configuration-store/configuration-store' ;
11
11
import { MemoryOnlyConfigurationStore } from './configuration-store/memory.store' ;
12
- import FetchHttpClient , { IHttpClient } from './http-client' ;
13
- import { BanditVariation , BanditParameters , Flag } from './interfaces' ;
12
+ import FetchHttpClient , {
13
+ IBanditParametersResponse ,
14
+ IHttpClient ,
15
+ IUniversalFlagConfigResponse ,
16
+ } from './http-client' ;
17
+ import { BanditParameters , BanditVariation , Flag } from './interfaces' ;
14
18
15
19
describe ( 'ConfigurationRequestor' , ( ) => {
16
20
let flagStore : IConfigurationStore < Flag > ;
@@ -111,105 +115,217 @@ describe('ConfigurationRequestor', () => {
111
115
describe ( 'Flags with bandits' , ( ) => {
112
116
let fetchSpy : jest . Mock ;
113
117
114
- beforeAll ( ( ) => {
118
+ function initiateFetchSpy (
119
+ responseMockGenerator : (
120
+ url : string ,
121
+ ) => IUniversalFlagConfigResponse | IBanditParametersResponse ,
122
+ ) {
115
123
fetchSpy = jest . fn ( ( url : string ) => {
116
- const responseFile = url . includes ( 'bandits' )
117
- ? MOCK_BANDIT_MODELS_RESPONSE_FILE
118
- : MOCK_FLAGS_WITH_BANDITS_RESPONSE_FILE ;
119
- const response = readMockUFCResponse ( responseFile ) ;
120
-
124
+ const response = responseMockGenerator ( url ) ;
121
125
return Promise . resolve ( {
122
126
ok : true ,
123
127
status : 200 ,
124
128
json : ( ) => Promise . resolve ( response ) ,
125
129
} ) ;
126
130
} ) as jest . Mock ;
127
131
global . fetch = fetchSpy ;
128
- } ) ;
132
+ }
129
133
130
- it ( 'Fetches and populates bandit parameters' , async ( ) => {
131
- await configurationRequestor . fetchAndStoreConfigurations ( ) ;
134
+ function responseMockGenerator ( url : string ) {
135
+ const responseFile = url . includes ( 'bandits' )
136
+ ? MOCK_BANDIT_MODELS_RESPONSE_FILE
137
+ : MOCK_FLAGS_WITH_BANDITS_RESPONSE_FILE ;
138
+ return readMockUFCResponse ( responseFile ) ;
139
+ }
132
140
133
- expect ( fetchSpy ) . toHaveBeenCalledTimes ( 2 ) ; // Once for UFC, another for bandits
134
-
135
- expect ( flagStore . getKeys ( ) . length ) . toBeGreaterThanOrEqual ( 2 ) ;
136
- expect ( flagStore . get ( 'banner_bandit_flag' ) ) . toBeDefined ( ) ;
137
- expect ( flagStore . get ( 'cold_start_bandit' ) ) . toBeDefined ( ) ;
138
-
139
- expect ( banditModelStore . getKeys ( ) . length ) . toBeGreaterThanOrEqual ( 2 ) ;
140
-
141
- const bannerBandit = banditModelStore . get ( 'banner_bandit' ) ;
142
- expect ( bannerBandit ?. banditKey ) . toBe ( 'banner_bandit' ) ;
143
- expect ( bannerBandit ?. modelName ) . toBe ( 'falcon' ) ;
144
- expect ( bannerBandit ?. modelVersion ) . toBe ( '123' ) ;
145
- const bannerModelData = bannerBandit ?. modelData ;
146
- expect ( bannerModelData ?. gamma ) . toBe ( 1 ) ;
147
- expect ( bannerModelData ?. defaultActionScore ) . toBe ( 0 ) ;
148
- expect ( bannerModelData ?. actionProbabilityFloor ) . toBe ( 0 ) ;
149
- const bannerCoefficients = bannerModelData ?. coefficients || { } ;
150
- expect ( Object . keys ( bannerCoefficients ) . length ) . toBe ( 2 ) ;
151
-
152
- // Deep dive for the nike action
153
- const nikeCoefficients = bannerCoefficients [ 'nike' ] ;
154
- expect ( nikeCoefficients . actionKey ) . toBe ( 'nike' ) ;
155
- expect ( nikeCoefficients . intercept ) . toBe ( 1 ) ;
156
- expect ( nikeCoefficients . actionNumericCoefficients ) . toHaveLength ( 1 ) ;
157
- const nikeBrandAffinityCoefficient = nikeCoefficients . actionNumericCoefficients [ 0 ] ;
158
- expect ( nikeBrandAffinityCoefficient . attributeKey ) . toBe ( 'brand_affinity' ) ;
159
- expect ( nikeBrandAffinityCoefficient . coefficient ) . toBe ( 1 ) ;
160
- expect ( nikeBrandAffinityCoefficient . missingValueCoefficient ) . toBe ( - 0.1 ) ;
161
- expect ( nikeCoefficients . actionCategoricalCoefficients ) . toHaveLength ( 2 ) ;
162
- const nikeLoyaltyTierCoefficient = nikeCoefficients . actionCategoricalCoefficients [ 0 ] ;
163
- expect ( nikeLoyaltyTierCoefficient . attributeKey ) . toBe ( 'loyalty_tier' ) ;
164
- expect ( nikeLoyaltyTierCoefficient . missingValueCoefficient ) . toBe ( 0 ) ;
165
- expect ( nikeLoyaltyTierCoefficient . valueCoefficients ) . toStrictEqual ( {
166
- gold : 4.5 ,
167
- silver : 3.2 ,
168
- bronze : 1.9 ,
141
+ describe ( 'Fetching bandits' , ( ) => {
142
+ beforeAll ( ( ) => {
143
+ initiateFetchSpy ( responseMockGenerator ) ;
169
144
} ) ;
170
- expect ( nikeCoefficients . subjectNumericCoefficients ) . toHaveLength ( 1 ) ;
171
- const nikeAccountAgeCoefficient = nikeCoefficients . subjectNumericCoefficients [ 0 ] ;
172
- expect ( nikeAccountAgeCoefficient . attributeKey ) . toBe ( 'account_age' ) ;
173
- expect ( nikeAccountAgeCoefficient . coefficient ) . toBe ( 0.3 ) ;
174
- expect ( nikeAccountAgeCoefficient . missingValueCoefficient ) . toBe ( 0 ) ;
175
- expect ( nikeCoefficients . subjectCategoricalCoefficients ) . toHaveLength ( 1 ) ;
176
- const nikeGenderIdentityCoefficient = nikeCoefficients . subjectCategoricalCoefficients [ 0 ] ;
177
- expect ( nikeGenderIdentityCoefficient . attributeKey ) . toBe ( 'gender_identity' ) ;
178
- expect ( nikeGenderIdentityCoefficient . missingValueCoefficient ) . toBe ( 2.3 ) ;
179
- expect ( nikeGenderIdentityCoefficient . valueCoefficients ) . toStrictEqual ( {
180
- female : 0.5 ,
181
- male : - 0.5 ,
145
+
146
+ it ( 'Fetches and populates bandit parameters' , async ( ) => {
147
+ await configurationRequestor . fetchAndStoreConfigurations ( ) ;
148
+
149
+ expect ( fetchSpy ) . toHaveBeenCalledTimes ( 2 ) ; // Once for UFC, another for bandits
150
+
151
+ expect ( flagStore . getKeys ( ) . length ) . toBeGreaterThanOrEqual ( 2 ) ;
152
+ expect ( flagStore . get ( 'banner_bandit_flag' ) ) . toBeDefined ( ) ;
153
+ expect ( flagStore . get ( 'cold_start_bandit' ) ) . toBeDefined ( ) ;
154
+
155
+ expect ( banditModelStore . getKeys ( ) . length ) . toBeGreaterThanOrEqual ( 2 ) ;
156
+
157
+ const bannerBandit = banditModelStore . get ( 'banner_bandit' ) ;
158
+ expect ( bannerBandit ?. banditKey ) . toBe ( 'banner_bandit' ) ;
159
+ expect ( bannerBandit ?. modelName ) . toBe ( 'falcon' ) ;
160
+ expect ( bannerBandit ?. modelVersion ) . toBe ( '123' ) ;
161
+ const bannerModelData = bannerBandit ?. modelData ;
162
+ expect ( bannerModelData ?. gamma ) . toBe ( 1 ) ;
163
+ expect ( bannerModelData ?. defaultActionScore ) . toBe ( 0 ) ;
164
+ expect ( bannerModelData ?. actionProbabilityFloor ) . toBe ( 0 ) ;
165
+ const bannerCoefficients = bannerModelData ?. coefficients || { } ;
166
+ expect ( Object . keys ( bannerCoefficients ) . length ) . toBe ( 2 ) ;
167
+
168
+ // Deep dive for the nike action
169
+ const nikeCoefficients = bannerCoefficients [ 'nike' ] ;
170
+ expect ( nikeCoefficients . actionKey ) . toBe ( 'nike' ) ;
171
+ expect ( nikeCoefficients . intercept ) . toBe ( 1 ) ;
172
+ expect ( nikeCoefficients . actionNumericCoefficients ) . toHaveLength ( 1 ) ;
173
+ const nikeBrandAffinityCoefficient = nikeCoefficients . actionNumericCoefficients [ 0 ] ;
174
+ expect ( nikeBrandAffinityCoefficient . attributeKey ) . toBe ( 'brand_affinity' ) ;
175
+ expect ( nikeBrandAffinityCoefficient . coefficient ) . toBe ( 1 ) ;
176
+ expect ( nikeBrandAffinityCoefficient . missingValueCoefficient ) . toBe ( - 0.1 ) ;
177
+ expect ( nikeCoefficients . actionCategoricalCoefficients ) . toHaveLength ( 2 ) ;
178
+ const nikeLoyaltyTierCoefficient = nikeCoefficients . actionCategoricalCoefficients [ 0 ] ;
179
+ expect ( nikeLoyaltyTierCoefficient . attributeKey ) . toBe ( 'loyalty_tier' ) ;
180
+ expect ( nikeLoyaltyTierCoefficient . missingValueCoefficient ) . toBe ( 0 ) ;
181
+ expect ( nikeLoyaltyTierCoefficient . valueCoefficients ) . toStrictEqual ( {
182
+ gold : 4.5 ,
183
+ silver : 3.2 ,
184
+ bronze : 1.9 ,
185
+ } ) ;
186
+ expect ( nikeCoefficients . subjectNumericCoefficients ) . toHaveLength ( 1 ) ;
187
+ const nikeAccountAgeCoefficient = nikeCoefficients . subjectNumericCoefficients [ 0 ] ;
188
+ expect ( nikeAccountAgeCoefficient . attributeKey ) . toBe ( 'account_age' ) ;
189
+ expect ( nikeAccountAgeCoefficient . coefficient ) . toBe ( 0.3 ) ;
190
+ expect ( nikeAccountAgeCoefficient . missingValueCoefficient ) . toBe ( 0 ) ;
191
+ expect ( nikeCoefficients . subjectCategoricalCoefficients ) . toHaveLength ( 1 ) ;
192
+ const nikeGenderIdentityCoefficient = nikeCoefficients . subjectCategoricalCoefficients [ 0 ] ;
193
+ expect ( nikeGenderIdentityCoefficient . attributeKey ) . toBe ( 'gender_identity' ) ;
194
+ expect ( nikeGenderIdentityCoefficient . missingValueCoefficient ) . toBe ( 2.3 ) ;
195
+ expect ( nikeGenderIdentityCoefficient . valueCoefficients ) . toStrictEqual ( {
196
+ female : 0.5 ,
197
+ male : - 0.5 ,
198
+ } ) ;
199
+
200
+ // Just spot check the adidas parameters
201
+ expect ( bannerCoefficients [ 'adidas' ] . subjectNumericCoefficients ) . toHaveLength ( 0 ) ;
202
+ expect (
203
+ bannerCoefficients [ 'adidas' ] . subjectCategoricalCoefficients [ 0 ] . valueCoefficients [
204
+ 'female'
205
+ ] ,
206
+ ) . toBe ( 0 ) ;
207
+
208
+ const coldStartBandit = banditModelStore . get ( 'cold_start_bandit' ) ;
209
+ expect ( coldStartBandit ?. banditKey ) . toBe ( 'cold_start_bandit' ) ;
210
+ expect ( coldStartBandit ?. modelName ) . toBe ( 'falcon' ) ;
211
+ expect ( coldStartBandit ?. modelVersion ) . toBe ( 'cold start' ) ;
212
+ const coldStartModelData = coldStartBandit ?. modelData ;
213
+ expect ( coldStartModelData ?. gamma ) . toBe ( 1 ) ;
214
+ expect ( coldStartModelData ?. defaultActionScore ) . toBe ( 0 ) ;
215
+ expect ( coldStartModelData ?. actionProbabilityFloor ) . toBe ( 0 ) ;
216
+ expect ( coldStartModelData ?. coefficients ) . toStrictEqual ( { } ) ;
182
217
} ) ;
183
218
184
- // Just spot check the adidas parameters
185
- expect ( bannerCoefficients [ 'adidas' ] . subjectNumericCoefficients ) . toHaveLength ( 0 ) ;
186
- expect (
187
- bannerCoefficients [ 'adidas' ] . subjectCategoricalCoefficients [ 0 ] . valueCoefficients [ 'female' ] ,
188
- ) . toBe ( 0 ) ;
189
-
190
- const coldStartBandit = banditModelStore . get ( 'cold_start_bandit' ) ;
191
- expect ( coldStartBandit ?. banditKey ) . toBe ( 'cold_start_bandit' ) ;
192
- expect ( coldStartBandit ?. modelName ) . toBe ( 'falcon' ) ;
193
- expect ( coldStartBandit ?. modelVersion ) . toBe ( 'cold start' ) ;
194
- const coldStartModelData = coldStartBandit ?. modelData ;
195
- expect ( coldStartModelData ?. gamma ) . toBe ( 1 ) ;
196
- expect ( coldStartModelData ?. defaultActionScore ) . toBe ( 0 ) ;
197
- expect ( coldStartModelData ?. actionProbabilityFloor ) . toBe ( 0 ) ;
198
- expect ( coldStartModelData ?. coefficients ) . toStrictEqual ( { } ) ;
199
- } ) ;
219
+ it ( 'Will not fetch bandit parameters if there is no store' , async ( ) => {
220
+ configurationRequestor = new ConfigurationRequestor ( httpClient , flagStore , null , null ) ;
221
+ await configurationRequestor . fetchAndStoreConfigurations ( ) ;
222
+ expect ( fetchSpy ) . toHaveBeenCalledTimes ( 1 ) ;
223
+ } ) ;
200
224
201
- it ( 'Will not fetch bandit parameters if there is no store' , async ( ) => {
202
- configurationRequestor = new ConfigurationRequestor ( httpClient , flagStore , null , null ) ;
203
- await configurationRequestor . fetchAndStoreConfigurations ( ) ;
204
- expect ( fetchSpy ) . toHaveBeenCalledTimes ( 1 ) ;
205
- } ) ;
225
+ it ( 'Should not fetch bandits if model version is un-changed' , async ( ) => {
226
+ await configurationRequestor . fetchAndStoreConfigurations ( ) ;
227
+ expect ( fetchSpy ) . toHaveBeenCalledTimes ( 2 ) ; // Once for UFC, another for bandits
206
228
207
- it ( 'Requests bandits only when model versions are different' , async ( ) => {
208
- await configurationRequestor . fetchAndStoreConfigurations ( ) ;
209
- expect ( fetchSpy ) . toHaveBeenCalledTimes ( 2 ) ; // Once for UFC, another for bandits
229
+ await configurationRequestor . fetchAndStoreConfigurations ( ) ;
230
+ expect ( fetchSpy ) . toHaveBeenCalledTimes ( 3 ) ; // Once just for UFC, bandits should be skipped
231
+ } ) ;
210
232
211
- await configurationRequestor . fetchAndStoreConfigurations ( ) ;
212
- expect ( fetchSpy ) . toHaveBeenCalledTimes ( 3 ) ; // Once just for UFC, bandits should be skipped
233
+ it ( 'Should fetch bandits if new bandit references model versions appeared' , async ( ) => {
234
+ let updateUFC = false ;
235
+ await configurationRequestor . fetchAndStoreConfigurations ( ) ;
236
+ await configurationRequestor . fetchAndStoreConfigurations ( ) ;
237
+ expect ( fetchSpy ) . toHaveBeenCalledTimes ( 3 ) ;
238
+
239
+ const customResponseMockGenerator = ( url : string ) => {
240
+ const responseFile = url . includes ( 'bandits' )
241
+ ? MOCK_BANDIT_MODELS_RESPONSE_FILE
242
+ : MOCK_FLAGS_WITH_BANDITS_RESPONSE_FILE ;
243
+
244
+ const response = readMockUFCResponse ( responseFile ) ;
245
+
246
+ if ( updateUFC === true ) {
247
+ // this if is needed to appease linter
248
+ if ( url . includes ( 'config' ) && 'banditReferences' in response ) {
249
+ response . banditReferences . warm_start = {
250
+ modelVersion : 'warm start' ,
251
+ flagVariations : [
252
+ {
253
+ key : 'warm_start_bandit' ,
254
+ flagKey : 'warm_start_bandit_flag' ,
255
+ variationKey : 'warm_start_bandit' ,
256
+ variationValue : 'warm_start_bandit' ,
257
+ } ,
258
+ ] ,
259
+ } ;
260
+ }
261
+
262
+ if ( url . includes ( 'bandits' ) && 'bandits' in response ) {
263
+ response . bandits . warm_start = {
264
+ banditKey : 'warm_start_bandit' ,
265
+ modelName : 'pigeon' ,
266
+ modelVersion : 'warm start' ,
267
+ modelData : {
268
+ gamma : 1.0 ,
269
+ defaultActionScore : 0.0 ,
270
+ actionProbabilityFloor : 0.0 ,
271
+ coefficients : { } ,
272
+ } ,
273
+ } ;
274
+ }
275
+ }
276
+ return response ;
277
+ } ;
278
+ updateUFC = true ;
279
+ initiateFetchSpy ( customResponseMockGenerator ) ;
280
+
281
+ await configurationRequestor . fetchAndStoreConfigurations ( ) ;
282
+ expect ( fetchSpy ) . toHaveBeenCalledTimes ( 2 ) ; // 2 because fetchSpy was re-initiated, 1UFC and 1bandits
283
+
284
+ // let's check if warm start was hydrated properly!
285
+ const warm_start_bandit = banditModelStore . get ( 'warm_start' ) ;
286
+ expect ( warm_start_bandit ) . toBeTruthy ( ) ;
287
+ expect ( warm_start_bandit ?. banditKey ) . toBe ( 'warm_start_bandit' ) ;
288
+ expect ( warm_start_bandit ?. modelVersion ) . toBe ( 'warm start' ) ;
289
+ expect ( warm_start_bandit ?. modelName ) . toBe ( 'pigeon' ) ;
290
+ expect ( warm_start_bandit ?. modelData . gamma ) . toBe ( 1 ) ;
291
+ expect ( warm_start_bandit ?. modelData . defaultActionScore ) . toBe ( 0 ) ;
292
+ expect ( warm_start_bandit ?. modelData . actionProbabilityFloor ) . toBe ( 0 ) ;
293
+ expect ( warm_start_bandit ?. modelData . coefficients ) . toStrictEqual ( { } ) ;
294
+ } ) ;
295
+
296
+ it ( 'Should not fetch bandits if bandit references model versions shrunk' , async ( ) => {
297
+ // Initial fetch
298
+ await configurationRequestor . fetchAndStoreConfigurations ( ) ;
299
+
300
+ // Let's mock UFC response so that cold_start is no longer retrieved
301
+ const customResponseMockGenerator = ( url : string ) => {
302
+ const responseFile = url . includes ( 'bandits' )
303
+ ? MOCK_BANDIT_MODELS_RESPONSE_FILE
304
+ : MOCK_FLAGS_WITH_BANDITS_RESPONSE_FILE ;
305
+
306
+ const response = readMockUFCResponse ( responseFile ) ;
307
+
308
+ if ( url . includes ( 'config' ) && 'banditReferences' in response ) {
309
+ delete response . banditReferences . cold_start_bandit ;
310
+ }
311
+ return response ;
312
+ } ;
313
+
314
+ initiateFetchSpy ( customResponseMockGenerator ) ;
315
+ await configurationRequestor . fetchAndStoreConfigurations ( ) ;
316
+ expect ( fetchSpy ) . toHaveBeenCalledTimes ( 1 ) ; // only once for UFC
317
+
318
+ // cold start should still be in memory
319
+ const warm_start_bandit = banditModelStore . get ( 'cold_start_bandit' ) ;
320
+ expect ( warm_start_bandit ) . toBeTruthy ( ) ;
321
+ expect ( warm_start_bandit ?. banditKey ) . toBe ( 'cold_start_bandit' ) ;
322
+ expect ( warm_start_bandit ?. modelVersion ) . toBe ( 'cold start' ) ;
323
+ expect ( warm_start_bandit ?. modelName ) . toBe ( 'falcon' ) ;
324
+ expect ( warm_start_bandit ?. modelData . gamma ) . toBe ( 1 ) ;
325
+ expect ( warm_start_bandit ?. modelData . defaultActionScore ) . toBe ( 0 ) ;
326
+ expect ( warm_start_bandit ?. modelData . actionProbabilityFloor ) . toBe ( 0 ) ;
327
+ expect ( warm_start_bandit ?. modelData . coefficients ) . toStrictEqual ( { } ) ;
328
+ } ) ;
213
329
} ) ;
214
330
} ) ;
215
331
} ) ;
0 commit comments