@@ -9,8 +9,12 @@ import ApiEndpoints from './api-endpoints';
99import ConfigurationRequestor from './configuration-requestor' ;
1010import { IConfigurationStore } from './configuration-store/configuration-store' ;
1111import { 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' ;
1418
1519describe ( 'ConfigurationRequestor' , ( ) => {
1620 let flagStore : IConfigurationStore < Flag > ;
@@ -111,105 +115,217 @@ describe('ConfigurationRequestor', () => {
111115 describe ( 'Flags with bandits' , ( ) => {
112116 let fetchSpy : jest . Mock ;
113117
114- beforeAll ( ( ) => {
118+ function initiateFetchSpy (
119+ responseMockGenerator : (
120+ url : string ,
121+ ) => IUniversalFlagConfigResponse | IBanditParametersResponse ,
122+ ) {
115123 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 ) ;
121125 return Promise . resolve ( {
122126 ok : true ,
123127 status : 200 ,
124128 json : ( ) => Promise . resolve ( response ) ,
125129 } ) ;
126130 } ) as jest . Mock ;
127131 global . fetch = fetchSpy ;
128- } ) ;
132+ }
129133
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+ }
132140
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 ( 'v123' ) ;
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 ) ;
169144 } ) ;
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 ( 'v123' ) ;
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 ( { } ) ;
182217 } ) ;
183218
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+ } ) ;
200224
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
206228
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+ } ) ;
210232
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+ } ) ;
213329 } ) ;
214330 } ) ;
215331} ) ;
0 commit comments