@@ -8,10 +8,11 @@ import {
8
8
} from '../../test/testHelpers' ;
9
9
import ApiEndpoints from '../api-endpoints' ;
10
10
import { IAssignmentEvent , IAssignmentLogger } from '../assignment-logger' ;
11
- import { BanditEvaluator } from '../bandit-evaluator' ;
11
+ import { BanditEvaluation , BanditEvaluator } from '../bandit-evaluator' ;
12
12
import { IBanditEvent , IBanditLogger } from '../bandit-logger' ;
13
13
import ConfigurationRequestor from '../configuration-requestor' ;
14
14
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store' ;
15
+ import { Evaluator , FlagEvaluation } from '../evaluator' ;
15
16
import {
16
17
AllocationEvaluationCode ,
17
18
IFlagEvaluationDetails ,
@@ -20,7 +21,7 @@ import FetchHttpClient from '../http-client';
20
21
import { BanditVariation , BanditParameters , Flag } from '../interfaces' ;
21
22
import { Attributes , ContextAttributes } from '../types' ;
22
23
23
- import EppoClient from './eppo-client' ;
24
+ import EppoClient , { IAssignmentDetails } from './eppo-client' ;
24
25
25
26
describe ( 'EppoClient Bandits E2E test' , ( ) => {
26
27
const flagStore = new MemoryOnlyConfigurationStore < Flag > ( ) ;
@@ -204,6 +205,8 @@ describe('EppoClient Bandits E2E test', () => {
204
205
} ) ;
205
206
206
207
it ( 'Flushed queued logging events when a logger is set' , ( ) => {
208
+ client . useLRUInMemoryAssignmentCache ( 5 ) ;
209
+ client . useLRUInMemoryBanditAssignmentCache ( 5 ) ;
207
210
client . setAssignmentLogger ( null as unknown as IAssignmentLogger ) ;
208
211
client . setBanditLogger ( null as unknown as IBanditLogger ) ;
209
212
const banditAssignment = client . getBanditAction (
@@ -220,6 +223,20 @@ describe('EppoClient Bandits E2E test', () => {
220
223
expect ( mockLogAssignment ) . not . toHaveBeenCalled ( ) ;
221
224
expect ( mockLogBanditAction ) . not . toHaveBeenCalled ( ) ;
222
225
226
+ const repeatAssignment = client . getBanditAction (
227
+ flagKey ,
228
+ subjectKey ,
229
+ subjectAttributes ,
230
+ actions ,
231
+ 'control' ,
232
+ ) ;
233
+
234
+ expect ( repeatAssignment . variation ) . toBe ( 'banner_bandit' ) ;
235
+ expect ( repeatAssignment . action ) . toBe ( 'adidas' ) ;
236
+
237
+ expect ( mockLogAssignment ) . not . toHaveBeenCalled ( ) ;
238
+ expect ( mockLogBanditAction ) . not . toHaveBeenCalled ( ) ;
239
+
223
240
client . setAssignmentLogger ( { logAssignment : mockLogAssignment } ) ;
224
241
client . setBanditLogger ( { logBanditAction : mockLogBanditAction } ) ;
225
242
@@ -429,5 +446,137 @@ describe('EppoClient Bandits E2E test', () => {
429
446
expect ( mockLogBanditAction . mock . calls [ 1 ] [ 0 ] . actionProbability ) . toBeCloseTo ( 0.256 ) ;
430
447
} ) ;
431
448
} ) ;
449
+
450
+ describe ( 'Assignment logging deduplication' , ( ) => {
451
+ let mockEvaluateFlag : jest . SpyInstance ;
452
+ let mockEvaluateBandit : jest . SpyInstance ;
453
+ // The below two variables allow easily changing what the mock evaluation functions return throughout the test
454
+ let variationToReturn : string ;
455
+ let actionToReturn : string | null ;
456
+
457
+ // Convenience method for repeatedly making the exact same assignment call
458
+ function requestClientBanditAction ( ) : Omit < IAssignmentDetails < string > , 'evaluationDetails' > {
459
+ return client . getBanditAction (
460
+ flagKey ,
461
+ subjectKey ,
462
+ subjectAttributes ,
463
+ [ 'toyota' , 'honda' ] ,
464
+ 'control' ,
465
+ ) ;
466
+ }
467
+
468
+ beforeAll ( ( ) => {
469
+ mockEvaluateFlag = jest
470
+ . spyOn ( Evaluator . prototype , 'evaluateFlag' )
471
+ . mockImplementation ( ( ) => {
472
+ return {
473
+ flagKey,
474
+ subjectKey,
475
+ subjectAttributes,
476
+ allocationKey : 'mock-allocation' ,
477
+ variation : { key : variationToReturn , value : variationToReturn } ,
478
+ extraLogging : { } ,
479
+ doLog : true ,
480
+ flagEvaluationDetails : {
481
+ flagEvaluationCode : 'MATCH' ,
482
+ flagEvaluationDescription : 'Mocked evaluation' ,
483
+ } ,
484
+ } as FlagEvaluation ;
485
+ } ) ;
486
+
487
+ mockEvaluateBandit = jest
488
+ . spyOn ( BanditEvaluator . prototype , 'evaluateBandit' )
489
+ . mockImplementation ( ( ) => {
490
+ return {
491
+ flagKey,
492
+ subjectKey,
493
+ subjectAttributes : { numericAttributes : { } , categoricalAttributes : { } } ,
494
+ actionKey : actionToReturn ,
495
+ actionAttributes : { numericAttributes : { } , categoricalAttributes : { } } ,
496
+ actionScore : 10 ,
497
+ actionWeight : 0.5 ,
498
+ gamma : 1.0 ,
499
+ optimalityGap : 5 ,
500
+ } as BanditEvaluation ;
501
+ } ) ;
502
+ } ) ;
503
+
504
+ beforeEach ( ( ) => {
505
+ client . useNonExpiringInMemoryAssignmentCache ( ) ;
506
+ client . useNonExpiringInMemoryBanditAssignmentCache ( ) ;
507
+ } ) ;
508
+
509
+ afterEach ( ( ) => {
510
+ client . disableAssignmentCache ( ) ;
511
+ client . disableBanditAssignmentCache ( ) ;
512
+ } ) ;
513
+
514
+ afterAll ( ( ) => {
515
+ mockEvaluateFlag . mockClear ( ) ;
516
+ mockEvaluateBandit . mockClear ( ) ;
517
+ } ) ;
518
+
519
+ it ( 'handles bandit actions appropriately' , async ( ) => {
520
+ // First assign to non-bandit variation
521
+ variationToReturn = 'non-bandit' ;
522
+ actionToReturn = null ;
523
+ const firstNonBanditAssignment = requestClientBanditAction ( ) ;
524
+
525
+ expect ( firstNonBanditAssignment . variation ) . toBe ( 'non-bandit' ) ;
526
+ expect ( firstNonBanditAssignment . action ) . toBeNull ( ) ;
527
+ expect ( mockLogAssignment ) . toHaveBeenCalledTimes ( 1 ) ; // new variation assignment
528
+ expect ( mockLogBanditAction ) . not . toHaveBeenCalled ( ) ; // no bandit assignment
529
+
530
+ // Assign bandit action
531
+ variationToReturn = 'banner_bandit' ;
532
+ actionToReturn = 'toyota' ;
533
+ const firstBanditAssignment = requestClientBanditAction ( ) ;
534
+
535
+ expect ( firstBanditAssignment . variation ) . toBe ( 'banner_bandit' ) ;
536
+ expect ( firstBanditAssignment . action ) . toBe ( 'toyota' ) ;
537
+ expect ( mockLogAssignment ) . toHaveBeenCalledTimes ( 2 ) ; // new variation assignment
538
+ expect ( mockLogBanditAction ) . toHaveBeenCalledTimes ( 1 ) ; // new bandit assignment
539
+
540
+ // Repeat bandit action assignment
541
+ variationToReturn = 'banner_bandit' ;
542
+ actionToReturn = 'toyota' ;
543
+ const secondBanditAssignment = requestClientBanditAction ( ) ;
544
+
545
+ expect ( secondBanditAssignment . variation ) . toBe ( 'banner_bandit' ) ;
546
+ expect ( secondBanditAssignment . action ) . toBe ( 'toyota' ) ;
547
+ expect ( mockLogAssignment ) . toHaveBeenCalledTimes ( 2 ) ; // repeat variation assignment
548
+ expect ( mockLogBanditAction ) . toHaveBeenCalledTimes ( 1 ) ; // repeat bandit assignment
549
+
550
+ // New bandit action assignment
551
+ variationToReturn = 'banner_bandit' ;
552
+ actionToReturn = 'honda' ;
553
+ const thirdBanditAssignment = requestClientBanditAction ( ) ;
554
+
555
+ expect ( thirdBanditAssignment . variation ) . toBe ( 'banner_bandit' ) ;
556
+ expect ( thirdBanditAssignment . action ) . toBe ( 'honda' ) ;
557
+ expect ( mockLogAssignment ) . toHaveBeenCalledTimes ( 2 ) ; // repeat variation assignment
558
+ expect ( mockLogBanditAction ) . toHaveBeenCalledTimes ( 2 ) ; // new bandit assignment
559
+
560
+ // Flip-flop to an earlier action assignment
561
+ variationToReturn = 'banner_bandit' ;
562
+ actionToReturn = 'toyota' ;
563
+ const fourthBanditAssignment = requestClientBanditAction ( ) ;
564
+
565
+ expect ( fourthBanditAssignment . variation ) . toBe ( 'banner_bandit' ) ;
566
+ expect ( fourthBanditAssignment . action ) . toBe ( 'toyota' ) ;
567
+ expect ( mockLogAssignment ) . toHaveBeenCalledTimes ( 2 ) ; // repeat variation assignment
568
+ expect ( mockLogBanditAction ) . toHaveBeenCalledTimes ( 3 ) ; // "new" bandit assignment
569
+
570
+ // Flip-flop back to non-bandit assignment
571
+ variationToReturn = 'non-bandit' ;
572
+ actionToReturn = null ;
573
+ const secondNonBanditAssignment = requestClientBanditAction ( ) ;
574
+
575
+ expect ( secondNonBanditAssignment . variation ) . toBe ( 'non-bandit' ) ;
576
+ expect ( secondNonBanditAssignment . action ) . toBeNull ( ) ;
577
+ expect ( mockLogAssignment ) . toHaveBeenCalledTimes ( 3 ) ; // "new" variation assignment
578
+ expect ( mockLogBanditAction ) . toHaveBeenCalledTimes ( 3 ) ; // no bandit assignment
579
+ } ) ;
580
+ } ) ;
432
581
} ) ;
433
582
} ) ;
0 commit comments