Skip to content

Commit 31cde96

Browse files
minor tests improvements for bandits polling
1 parent dca096d commit 31cde96

File tree

1 file changed

+183
-177
lines changed

1 file changed

+183
-177
lines changed

src/configuration-requestor.spec.ts

Lines changed: 183 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ describe('ConfigurationRequestor', () => {
131131
global.fetch = fetchSpy;
132132
}
133133

134-
function responseMockGenerator(url: string) {
134+
function defaultResponseMockGenerator(url: string) {
135135
const responseFile = url.includes('bandits')
136136
? MOCK_BANDIT_MODELS_RESPONSE_FILE
137137
: MOCK_FLAGS_WITH_BANDITS_RESPONSE_FILE;
@@ -140,7 +140,7 @@ describe('ConfigurationRequestor', () => {
140140

141141
describe('Fetching bandits', () => {
142142
beforeAll(() => {
143-
initiateFetchSpy(responseMockGenerator);
143+
initiateFetchSpy(defaultResponseMockGenerator);
144144
});
145145

146146
it('Fetches and populates bandit parameters', async () => {
@@ -230,193 +230,199 @@ describe('ConfigurationRequestor', () => {
230230
expect(fetchSpy).toHaveBeenCalledTimes(3); // Once just for UFC, bandits should be skipped
231231
});
232232

233-
const warmStartBanditReference = {
234-
modelVersion: 'warm start',
235-
flagVariations: [
236-
{
237-
key: 'warm_start_bandit',
238-
flagKey: 'warm_start_bandit_flag',
239-
variationKey: 'warm_start_bandit',
240-
variationValue: 'warm_start_bandit',
241-
},
242-
],
243-
};
244-
245-
const warmStartBanditParameters = {
246-
banditKey: 'warm_start_bandit',
247-
modelName: 'pigeon',
248-
modelVersion: 'warm start',
249-
modelData: {
250-
gamma: 1.0,
251-
defaultActionScore: 0.0,
252-
actionProbabilityFloor: 0.0,
253-
coefficients: {},
254-
},
255-
};
256-
257-
const coldStartBanditParameters = {
258-
banditKey: 'cold_start_bandit',
259-
modelName: 'falcon',
260-
modelVersion: 'cold start',
261-
modelData: {
262-
gamma: 1.0,
263-
defaultActionScore: 0.0,
264-
actionProbabilityFloor: 0.0,
265-
coefficients: {},
266-
},
267-
};
268-
269-
function expectBanditToBeInModelStore(
270-
store: IConfigurationStore<BanditParameters>,
271-
banditKey: string,
272-
expectedBanditParameters: BanditParameters,
273-
) {
274-
const bandit = store.get(banditKey);
275-
expect(bandit).toBeTruthy();
276-
expect(bandit?.banditKey).toBe(expectedBanditParameters.banditKey);
277-
expect(bandit?.modelVersion).toBe(expectedBanditParameters.modelVersion);
278-
expect(bandit?.modelName).toBe(expectedBanditParameters.modelName);
279-
expect(bandit?.modelData.gamma).toBe(expectedBanditParameters.modelData.gamma);
280-
expect(bandit?.modelData.defaultActionScore).toBe(
281-
expectedBanditParameters.modelData.defaultActionScore,
282-
);
283-
expect(bandit?.modelData.actionProbabilityFloor).toBe(
284-
expectedBanditParameters.modelData.actionProbabilityFloor,
285-
);
286-
expect(bandit?.modelData.coefficients).toStrictEqual(
287-
expectedBanditParameters.modelData.coefficients,
288-
);
289-
}
290-
291-
function injectWarmStartBanditToResponseByUrl(
292-
url: string,
293-
response: IUniversalFlagConfigResponse | IBanditParametersResponse,
294-
) {
295-
if (url.includes('config') && 'banditReferences' in response) {
296-
response.banditReferences.warm_start_bandit = warmStartBanditReference;
297-
}
298-
299-
if (url.includes('bandits') && 'bandits' in response) {
300-
response.bandits.warm_start_bandit = warmStartBanditParameters;
301-
}
302-
}
303-
304-
it('Should fetch bandits if new bandit references model versions appeared', async () => {
305-
let updateUFC = false;
306-
await configurationRequestor.fetchAndStoreConfigurations();
307-
await configurationRequestor.fetchAndStoreConfigurations();
308-
expect(fetchSpy).toHaveBeenCalledTimes(3);
309-
310-
const customResponseMockGenerator = (url: string) => {
311-
const responseFile = url.includes('bandits')
312-
? MOCK_BANDIT_MODELS_RESPONSE_FILE
313-
: MOCK_FLAGS_WITH_BANDITS_RESPONSE_FILE;
314-
315-
const response = readMockUFCResponse(responseFile);
316-
317-
if (updateUFC === true) {
318-
injectWarmStartBanditToResponseByUrl(url, response);
319-
}
320-
return response;
233+
describe('Bandits polling', () => {
234+
const warmStartBanditReference = {
235+
modelVersion: 'warm start',
236+
flagVariations: [
237+
{
238+
key: 'warm_start_bandit',
239+
flagKey: 'warm_start_bandit_flag',
240+
variationKey: 'warm_start_bandit',
241+
variationValue: 'warm_start_bandit',
242+
},
243+
],
321244
};
322-
updateUFC = true;
323-
initiateFetchSpy(customResponseMockGenerator);
324245

325-
await configurationRequestor.fetchAndStoreConfigurations();
326-
expect(fetchSpy).toHaveBeenCalledTimes(2); // 2 because fetchSpy was re-initiated, 1UFC and 1bandits
327-
328-
// let's check if warm start was hydrated properly!
329-
expectBanditToBeInModelStore(
330-
banditModelStore,
331-
'warm_start_bandit',
332-
warmStartBanditParameters,
333-
);
334-
});
246+
const warmStartBanditParameters = {
247+
banditKey: 'warm_start_bandit',
248+
modelName: 'pigeon',
249+
modelVersion: 'warm start',
250+
modelData: {
251+
gamma: 1.0,
252+
defaultActionScore: 0.0,
253+
actionProbabilityFloor: 0.0,
254+
coefficients: {},
255+
},
256+
};
335257

336-
it('Should not fetch bandits if bandit references model versions shrunk', async () => {
337-
// Initial fetch
338-
await configurationRequestor.fetchAndStoreConfigurations();
258+
const coldStartBanditParameters = {
259+
banditKey: 'cold_start_bandit',
260+
modelName: 'falcon',
261+
modelVersion: 'cold start',
262+
modelData: {
263+
gamma: 1.0,
264+
defaultActionScore: 0.0,
265+
actionProbabilityFloor: 0.0,
266+
coefficients: {},
267+
},
268+
};
339269

340-
// Let's mock UFC response so that cold_start is no longer retrieved
341-
const customResponseMockGenerator = (url: string) => {
342-
const responseFile = url.includes('bandits')
343-
? MOCK_BANDIT_MODELS_RESPONSE_FILE
344-
: MOCK_FLAGS_WITH_BANDITS_RESPONSE_FILE;
270+
afterAll(() => {
271+
initiateFetchSpy(defaultResponseMockGenerator);
272+
});
345273

346-
const response = readMockUFCResponse(responseFile);
274+
function expectBanditToBeInModelStore(
275+
store: IConfigurationStore<BanditParameters>,
276+
banditKey: string,
277+
expectedBanditParameters: BanditParameters,
278+
) {
279+
const bandit = store.get(banditKey);
280+
expect(bandit).toBeTruthy();
281+
expect(bandit?.banditKey).toBe(expectedBanditParameters.banditKey);
282+
expect(bandit?.modelVersion).toBe(expectedBanditParameters.modelVersion);
283+
expect(bandit?.modelName).toBe(expectedBanditParameters.modelName);
284+
expect(bandit?.modelData.gamma).toBe(expectedBanditParameters.modelData.gamma);
285+
expect(bandit?.modelData.defaultActionScore).toBe(
286+
expectedBanditParameters.modelData.defaultActionScore,
287+
);
288+
expect(bandit?.modelData.actionProbabilityFloor).toBe(
289+
expectedBanditParameters.modelData.actionProbabilityFloor,
290+
);
291+
expect(bandit?.modelData.coefficients).toStrictEqual(
292+
expectedBanditParameters.modelData.coefficients,
293+
);
294+
}
347295

296+
function injectWarmStartBanditToResponseByUrl(
297+
url: string,
298+
response: IUniversalFlagConfigResponse | IBanditParametersResponse,
299+
) {
348300
if (url.includes('config') && 'banditReferences' in response) {
349-
delete response.banditReferences.cold_start_bandit;
301+
response.banditReferences.warm_start_bandit = warmStartBanditReference;
350302
}
351-
return response;
352-
};
353-
354-
initiateFetchSpy(customResponseMockGenerator);
355-
await configurationRequestor.fetchAndStoreConfigurations();
356-
expect(fetchSpy).toHaveBeenCalledTimes(1); // only once for UFC
357-
358-
// cold start should still be in memory
359-
expectBanditToBeInModelStore(
360-
banditModelStore,
361-
'cold_start_bandit',
362-
coldStartBanditParameters,
363-
);
364-
});
365303

366-
/**
367-
* 1. initial call - 1 fetch for ufc 1 for bandits
368-
* 2. 2nd call - 1 fetch for ufc only; bandits unchanged
369-
* 3. 3rd call - new bandit ref injected to UFC; 2 fetches, because new bandit appeared
370-
* 4. 4th call - we remove a bandit from ufc; 1 fetch because there is no need to update.
371-
* The bandit removed from UFC should still be in memory.
372-
**/
373-
it('should fetch bandits based on banditReference change in UFC', async () => {
374-
let injectWarmStart = false;
375-
let removeColdStartBandit = false;
376-
await configurationRequestor.fetchAndStoreConfigurations();
377-
expect(fetchSpy).toHaveBeenCalledTimes(2);
378-
379-
await configurationRequestor.fetchAndStoreConfigurations();
380-
expect(fetchSpy).toHaveBeenCalledTimes(3);
381-
382-
const customResponseMockGenerator = (url: string) => {
383-
const responseFile = url.includes('bandits')
384-
? MOCK_BANDIT_MODELS_RESPONSE_FILE
385-
: MOCK_FLAGS_WITH_BANDITS_RESPONSE_FILE;
386-
const response = readMockUFCResponse(responseFile);
387-
if (injectWarmStart === true) {
388-
injectWarmStartBanditToResponseByUrl(url, response);
389-
} else if (
390-
removeColdStartBandit === true &&
391-
'banditReferences' in response &&
392-
url.includes('config')
393-
) {
394-
delete response.banditReferences.cold_start_bandit;
304+
if (url.includes('bandits') && 'bandits' in response) {
305+
response.bandits.warm_start_bandit = warmStartBanditParameters;
395306
}
396-
return response;
397-
};
398-
injectWarmStart = true;
399-
initiateFetchSpy(customResponseMockGenerator);
307+
}
400308

401-
await configurationRequestor.fetchAndStoreConfigurations();
402-
expect(fetchSpy).toHaveBeenCalledTimes(2);
403-
expectBanditToBeInModelStore(
404-
banditModelStore,
405-
'warm_start_bandit',
406-
warmStartBanditParameters,
407-
);
408-
409-
injectWarmStart = false;
410-
removeColdStartBandit = true;
411-
initiateFetchSpy(customResponseMockGenerator);
412-
await configurationRequestor.fetchAndStoreConfigurations();
413-
expect(fetchSpy).toHaveBeenCalledTimes(1);
309+
it('Should fetch bandits if new bandit references model versions appeared', async () => {
310+
let updateUFC = false;
311+
await configurationRequestor.fetchAndStoreConfigurations();
312+
await configurationRequestor.fetchAndStoreConfigurations();
313+
expect(fetchSpy).toHaveBeenCalledTimes(3);
314+
315+
const customResponseMockGenerator = (url: string) => {
316+
const responseFile = url.includes('bandits')
317+
? MOCK_BANDIT_MODELS_RESPONSE_FILE
318+
: MOCK_FLAGS_WITH_BANDITS_RESPONSE_FILE;
319+
320+
const response = readMockUFCResponse(responseFile);
321+
322+
if (updateUFC === true) {
323+
injectWarmStartBanditToResponseByUrl(url, response);
324+
}
325+
return response;
326+
};
327+
updateUFC = true;
328+
initiateFetchSpy(customResponseMockGenerator);
329+
330+
await configurationRequestor.fetchAndStoreConfigurations();
331+
expect(fetchSpy).toHaveBeenCalledTimes(2); // 2 because fetchSpy was re-initiated, 1UFC and 1bandits
332+
333+
// let's check if warm start was hydrated properly!
334+
expectBanditToBeInModelStore(
335+
banditModelStore,
336+
'warm_start_bandit',
337+
warmStartBanditParameters,
338+
);
339+
});
340+
341+
it('Should not fetch bandits if bandit references model versions shrunk', async () => {
342+
// Initial fetch
343+
await configurationRequestor.fetchAndStoreConfigurations();
344+
345+
// Let's mock UFC response so that cold_start is no longer retrieved
346+
const customResponseMockGenerator = (url: string) => {
347+
const responseFile = url.includes('bandits')
348+
? MOCK_BANDIT_MODELS_RESPONSE_FILE
349+
: MOCK_FLAGS_WITH_BANDITS_RESPONSE_FILE;
350+
351+
const response = readMockUFCResponse(responseFile);
352+
353+
if (url.includes('config') && 'banditReferences' in response) {
354+
delete response.banditReferences.cold_start_bandit;
355+
}
356+
return response;
357+
};
358+
359+
initiateFetchSpy(customResponseMockGenerator);
360+
await configurationRequestor.fetchAndStoreConfigurations();
361+
expect(fetchSpy).toHaveBeenCalledTimes(1); // only once for UFC
362+
363+
// cold start should still be in memory
364+
expectBanditToBeInModelStore(
365+
banditModelStore,
366+
'cold_start_bandit',
367+
coldStartBanditParameters,
368+
);
369+
});
414370

415-
expectBanditToBeInModelStore(
416-
banditModelStore,
417-
'cold_start_bandit',
418-
coldStartBanditParameters,
419-
);
371+
/**
372+
* 1. initial call - 1 fetch for ufc 1 for bandits
373+
* 2. 2nd call - 1 fetch for ufc only; bandits unchanged
374+
* 3. 3rd call - new bandit ref injected to UFC; 2 fetches, because new bandit appeared
375+
* 4. 4th call - we remove a bandit from ufc; 1 fetch because there is no need to update.
376+
* The bandit removed from UFC should still be in memory.
377+
**/
378+
it('should fetch bandits based on banditReference change in UFC', async () => {
379+
let injectWarmStart = false;
380+
let removeColdStartBandit = false;
381+
await configurationRequestor.fetchAndStoreConfigurations();
382+
expect(fetchSpy).toHaveBeenCalledTimes(2);
383+
384+
await configurationRequestor.fetchAndStoreConfigurations();
385+
expect(fetchSpy).toHaveBeenCalledTimes(3);
386+
387+
const customResponseMockGenerator = (url: string) => {
388+
const responseFile = url.includes('bandits')
389+
? MOCK_BANDIT_MODELS_RESPONSE_FILE
390+
: MOCK_FLAGS_WITH_BANDITS_RESPONSE_FILE;
391+
const response = readMockUFCResponse(responseFile);
392+
if (injectWarmStart === true) {
393+
injectWarmStartBanditToResponseByUrl(url, response);
394+
} else if (
395+
removeColdStartBandit === true &&
396+
'banditReferences' in response &&
397+
url.includes('config')
398+
) {
399+
delete response.banditReferences.cold_start_bandit;
400+
}
401+
return response;
402+
};
403+
injectWarmStart = true;
404+
initiateFetchSpy(customResponseMockGenerator);
405+
406+
await configurationRequestor.fetchAndStoreConfigurations();
407+
expect(fetchSpy).toHaveBeenCalledTimes(2);
408+
expectBanditToBeInModelStore(
409+
banditModelStore,
410+
'warm_start_bandit',
411+
warmStartBanditParameters,
412+
);
413+
414+
injectWarmStart = false;
415+
removeColdStartBandit = true;
416+
initiateFetchSpy(customResponseMockGenerator);
417+
await configurationRequestor.fetchAndStoreConfigurations();
418+
expect(fetchSpy).toHaveBeenCalledTimes(1);
419+
420+
expectBanditToBeInModelStore(
421+
banditModelStore,
422+
'cold_start_bandit',
423+
coldStartBanditParameters,
424+
);
425+
});
420426
});
421427
});
422428
});

0 commit comments

Comments
 (0)