@@ -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 > ( ) ;
@@ -429,5 +430,128 @@ describe('EppoClient Bandits E2E test', () => {
429
430
expect ( mockLogBanditAction . mock . calls [ 1 ] [ 0 ] . actionProbability ) . toBeCloseTo ( 0.256 ) ;
430
431
} ) ;
431
432
} ) ;
433
+
434
+ describe ( 'Assignment logging deduplication' , ( ) => {
435
+ let mockEvaluateFlag : jest . SpyInstance ;
436
+ let mockEvaluateBandit : jest . SpyInstance ;
437
+ let variationToReturn : string ;
438
+ let actionToReturn : string | null ;
439
+
440
+ // Convenience method for repeatedly making the exact same assignment call
441
+ function requestClientBanditAction ( ) : Omit < IAssignmentDetails < string > , 'evaluationDetails' > {
442
+ return client . getBanditAction ( flagKey , subjectKey , subjectAttributes , [ 'toyota' , 'honda' ] , 'control' ) ;
443
+ }
444
+
445
+ beforeAll ( ( ) => {
446
+ mockEvaluateFlag = jest
447
+ . spyOn ( Evaluator . prototype , 'evaluateFlag' )
448
+ . mockImplementation ( ( ) => {
449
+ return {
450
+ flagKey,
451
+ subjectKey,
452
+ subjectAttributes,
453
+ allocationKey : 'mock-allocation' ,
454
+ variation : { key : variationToReturn , value : variationToReturn } ,
455
+ extraLogging : { } ,
456
+ doLog : true ,
457
+ flagEvaluationDetails : {
458
+ flagEvaluationCode : 'MATCH' ,
459
+ flagEvaluationDescription : 'Mocked evaluation' ,
460
+ } ,
461
+ } as FlagEvaluation ;
462
+ } ) ;
463
+
464
+ mockEvaluateBandit = jest
465
+ . spyOn ( BanditEvaluator . prototype , 'evaluateBandit' )
466
+ . mockImplementation ( ( ) => {
467
+ return {
468
+ flagKey,
469
+ subjectKey,
470
+ subjectAttributes : { numericAttributes : { } , categoricalAttributes : { } } ,
471
+ actionKey : actionToReturn ,
472
+ actionAttributes : { numericAttributes : { } , categoricalAttributes : { } } ,
473
+ actionScore : 10 ,
474
+ actionWeight : 0.5 ,
475
+ gamma : 1.0 ,
476
+ optimalityGap : 5 ,
477
+ } as BanditEvaluation ;
478
+ } ) ;
479
+ } ) ;
480
+
481
+ beforeEach ( ( ) => {
482
+ client . useNonExpiringInMemoryAssignmentCache ( ) ;
483
+ } ) ;
484
+
485
+ afterEach ( ( ) => {
486
+ client . disableAssignmentCache ( ) ;
487
+ } ) ;
488
+
489
+ afterAll ( ( ) => {
490
+ mockEvaluateFlag . mockClear ( ) ;
491
+ mockEvaluateBandit . mockClear ( ) ;
492
+ } ) ;
493
+
494
+ it ( 'handles bandit actions appropriately' , async ( ) => {
495
+ // First assign to non-bandit variation
496
+ variationToReturn = 'non-bandit' ;
497
+ actionToReturn = null ;
498
+ const firstNonBanditAssignment = requestClientBanditAction ( ) ;
499
+
500
+ expect ( firstNonBanditAssignment . variation ) . toBe ( 'non-bandit' ) ;
501
+ expect ( firstNonBanditAssignment . action ) . toBeNull ( ) ;
502
+ expect ( mockLogAssignment ) . toHaveBeenCalledTimes ( 1 ) ;
503
+ expect ( mockLogBanditAction ) . not . toHaveBeenCalled ( ) ;
504
+
505
+ // Assign bandit action
506
+ variationToReturn = 'banner_bandit' ;
507
+ actionToReturn = 'toyota' ;
508
+ const firstBanditAssignment = requestClientBanditAction ( ) ;
509
+
510
+ expect ( firstBanditAssignment . variation ) . toBe ( 'banner_bandit' ) ;
511
+ expect ( firstBanditAssignment . action ) . toBe ( 'toyota' ) ;
512
+ expect ( mockLogAssignment ) . toHaveBeenCalledTimes ( 2 ) ;
513
+ expect ( mockLogBanditAction ) . toHaveBeenCalledTimes ( 1 ) ;
514
+
515
+ // Repeat bandit action assignment
516
+ variationToReturn = 'banner_bandit' ;
517
+ actionToReturn = 'toyota' ;
518
+ const secondBanditAssignment = requestClientBanditAction ( ) ;
519
+
520
+ expect ( secondBanditAssignment . variation ) . toBe ( 'banner_bandit' ) ;
521
+ expect ( secondBanditAssignment . action ) . toBe ( 'toyota' ) ;
522
+ expect ( mockLogAssignment ) . toHaveBeenCalledTimes ( 2 ) ;
523
+ expect ( mockLogBanditAction ) . toHaveBeenCalledTimes ( 1 ) ;
524
+
525
+ // New bandit action assignment
526
+ variationToReturn = 'banner_bandit' ;
527
+ actionToReturn = 'honda' ;
528
+ const thirdBanditAssignment = requestClientBanditAction ( ) ;
529
+
530
+ expect ( thirdBanditAssignment . variation ) . toBe ( 'banner_bandit' ) ;
531
+ expect ( thirdBanditAssignment . action ) . toBe ( 'honda' ) ;
532
+ expect ( mockLogAssignment ) . toHaveBeenCalledTimes ( 2 ) ;
533
+ expect ( mockLogBanditAction ) . toHaveBeenCalledTimes ( 2 ) ;
534
+
535
+ // Flip-flop to an earlier action assignment
536
+ variationToReturn = 'banner_bandit' ;
537
+ actionToReturn = 'toyota' ;
538
+ const fourthBanditAssignment = requestClientBanditAction ( ) ;
539
+
540
+ expect ( fourthBanditAssignment . variation ) . toBe ( 'banner_bandit' ) ;
541
+ expect ( thirdBanditAssignment . action ) . toBe ( 'toyota' ) ;
542
+ expect ( mockLogAssignment ) . toHaveBeenCalledTimes ( 2 ) ;
543
+ expect ( mockLogBanditAction ) . toHaveBeenCalledTimes ( 3 ) ;
544
+
545
+ // Flip-flop back to non-bandit assignment
546
+ variationToReturn = 'non-bandit' ;
547
+ actionToReturn = null ;
548
+ const secondNonBanditAssignment = requestClientBanditAction ( ) ;
549
+
550
+ expect ( secondNonBanditAssignment . variation ) . toBe ( 'non-bandit' ) ;
551
+ expect ( secondNonBanditAssignment . action ) . toBeNull ( ) ;
552
+ expect ( mockLogAssignment ) . toHaveBeenCalledTimes ( 3 ) ;
553
+ expect ( mockLogBanditAction ) . toHaveBeenCalledTimes ( 3 ) ;
554
+ } ) ;
555
+ } ) ;
432
556
} ) ;
433
557
} ) ;
0 commit comments