@@ -16,7 +16,12 @@ import { IConfigurationStore } from '../configuration-store/configuration-store'
1616import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store' ;
1717import { DEFAULT_POLL_INTERVAL_MS , MAX_EVENT_QUEUE_SIZE , POLL_JITTER_PCT } from '../constants' ;
1818import FetchHttpClient from '../http-client' ;
19- import { FormatEnum , PrecomputedFlag , VariationType } from '../interfaces' ;
19+ import {
20+ FormatEnum ,
21+ IObfuscatedPrecomputedBandit ,
22+ PrecomputedFlag ,
23+ VariationType ,
24+ } from '../interfaces' ;
2025import { decodeBase64 , encodeBase64 , getMD5Hash } from '../obfuscation' ;
2126import PrecomputedRequestor from '../precomputed-requestor' ;
2227
@@ -859,3 +864,174 @@ describe('EppoPrecomputedClient E2E test', () => {
859864 } ) ;
860865 } ) ;
861866} ) ;
867+
868+ describe ( 'Precomputed Bandit Store' , ( ) => {
869+ let precomputedFlagStore : IConfigurationStore < PrecomputedFlag > ;
870+ let precomputedBanditStore : IConfigurationStore < IObfuscatedPrecomputedBandit > ;
871+ let subject : Subject ;
872+ let mockLogger : IAssignmentLogger ;
873+
874+ beforeEach ( ( ) => {
875+ precomputedFlagStore = new MemoryOnlyConfigurationStore < PrecomputedFlag > ( ) ;
876+ precomputedBanditStore = new MemoryOnlyConfigurationStore < IObfuscatedPrecomputedBandit > ( ) ;
877+ subject = {
878+ subjectKey : 'test-subject' ,
879+ subjectAttributes : { attr1 : 'value1' } ,
880+ } ;
881+ mockLogger = td . object < IAssignmentLogger > ( ) ;
882+ } ) ;
883+
884+ it ( 'prints errors if initialized with a bandit store that is not initialized and without requestParameters' , ( ) => {
885+ const loggerErrorSpy = jest . spyOn ( logger , 'error' ) ;
886+ const loggerWarnSpy = jest . spyOn ( logger , 'warn' ) ;
887+
888+ const client = new EppoPrecomputedClient ( {
889+ precomputedFlagStore,
890+ precomputedBanditStore,
891+ subject,
892+ } ) ;
893+
894+ expect ( loggerErrorSpy ) . toHaveBeenCalledWith (
895+ '[Eppo SDK] EppoPrecomputedClient requires an initialized precomputedFlagStore if requestParameters are not provided' ,
896+ ) ;
897+ expect ( loggerErrorSpy ) . toHaveBeenCalledWith (
898+ '[Eppo SDK] EppoPrecomputedClient requires a precomputedFlagStore with a salt if requestParameters are not provided' ,
899+ ) ;
900+ expect ( loggerErrorSpy ) . toHaveBeenCalledWith (
901+ '[Eppo SDK] Passing banditOptions without requestParameters requires an initialized precomputedBanditStore' ,
902+ ) ;
903+ expect ( loggerWarnSpy ) . toHaveBeenCalledWith (
904+ '[Eppo SDK] EppoPrecomputedClient missing or empty salt for precomputedBanditStore' ,
905+ ) ;
906+
907+ loggerErrorSpy . mockRestore ( ) ;
908+ loggerWarnSpy . mockRestore ( ) ;
909+ } ) ;
910+
911+ it ( 'prints only salt-related errors if stores are initialized but missing salts' , async ( ) => {
912+ const loggerErrorSpy = jest . spyOn ( logger , 'error' ) ;
913+ const loggerWarnSpy = jest . spyOn ( logger , 'warn' ) ;
914+
915+ await precomputedFlagStore . setEntries ( {
916+ 'test-flag' : {
917+ flagKey : 'test-flag' ,
918+ variationType : VariationType . STRING ,
919+ variationKey : encodeBase64 ( 'control' ) ,
920+ variationValue : encodeBase64 ( 'test-value' ) ,
921+ allocationKey : encodeBase64 ( 'allocation-1' ) ,
922+ doLog : true ,
923+ extraLogging : { } ,
924+ } ,
925+ } ) ;
926+
927+ await precomputedBanditStore . setEntries ( {
928+ 'test-bandit' : {
929+ banditKey : encodeBase64 ( 'test-bandit' ) ,
930+ action : encodeBase64 ( 'action1' ) ,
931+ modelVersion : encodeBase64 ( 'v1' ) ,
932+ actionProbability : 0.5 ,
933+ optimalityGap : 0.1 ,
934+ actionNumericAttributes : {
935+ [ encodeBase64 ( 'attr1' ) ] : encodeBase64 ( '1.0' ) ,
936+ } ,
937+ actionCategoricalAttributes : {
938+ [ encodeBase64 ( 'attr2' ) ] : encodeBase64 ( 'value2' ) ,
939+ } ,
940+ } ,
941+ } ) ;
942+
943+ const client = new EppoPrecomputedClient ( {
944+ precomputedFlagStore,
945+ precomputedBanditStore,
946+ subject,
947+ } ) ;
948+
949+ expect ( loggerErrorSpy ) . toHaveBeenCalledWith (
950+ '[Eppo SDK] EppoPrecomputedClient requires a precomputedFlagStore with a salt if requestParameters are not provided' ,
951+ ) ;
952+ expect ( loggerWarnSpy ) . toHaveBeenCalledWith (
953+ '[Eppo SDK] EppoPrecomputedClient missing or empty salt for precomputedBanditStore' ,
954+ ) ;
955+
956+ loggerErrorSpy . mockRestore ( ) ;
957+ loggerWarnSpy . mockRestore ( ) ;
958+ } ) ;
959+
960+ it ( 'initializes correctly with both stores having salts' , async ( ) => {
961+ const loggerErrorSpy = jest . spyOn ( logger , 'error' ) ;
962+ const loggerWarnSpy = jest . spyOn ( logger , 'warn' ) ;
963+
964+ precomputedFlagStore . salt = 'flag-salt' ;
965+ precomputedBanditStore . salt = 'bandit-salt' ;
966+
967+ await precomputedFlagStore . setEntries ( {
968+ 'test-flag' : {
969+ flagKey : 'test-flag' ,
970+ variationType : VariationType . STRING ,
971+ variationKey : encodeBase64 ( 'control' ) ,
972+ variationValue : encodeBase64 ( 'test-value' ) ,
973+ allocationKey : encodeBase64 ( 'allocation-1' ) ,
974+ doLog : true ,
975+ extraLogging : { } ,
976+ } ,
977+ } ) ;
978+
979+ await precomputedBanditStore . setEntries ( {
980+ 'test-bandit' : {
981+ banditKey : encodeBase64 ( 'test-bandit' ) ,
982+ action : encodeBase64 ( 'action1' ) ,
983+ modelVersion : encodeBase64 ( 'v1' ) ,
984+ actionProbability : 0.5 ,
985+ optimalityGap : 0.1 ,
986+ actionNumericAttributes : {
987+ [ encodeBase64 ( 'attr1' ) ] : encodeBase64 ( '1.0' ) ,
988+ } ,
989+ actionCategoricalAttributes : {
990+ [ encodeBase64 ( 'attr2' ) ] : encodeBase64 ( 'value2' ) ,
991+ } ,
992+ } ,
993+ } ) ;
994+
995+ const client = new EppoPrecomputedClient ( {
996+ precomputedFlagStore,
997+ precomputedBanditStore,
998+ subject,
999+ } ) ;
1000+
1001+ expect ( loggerErrorSpy ) . not . toHaveBeenCalled ( ) ;
1002+ expect ( loggerWarnSpy ) . not . toHaveBeenCalled ( ) ;
1003+
1004+ loggerErrorSpy . mockRestore ( ) ;
1005+ loggerWarnSpy . mockRestore ( ) ;
1006+ } ) ;
1007+
1008+ it ( 'allows initialization without bandit store' , async ( ) => {
1009+ const loggerErrorSpy = jest . spyOn ( logger , 'error' ) ;
1010+ const loggerWarnSpy = jest . spyOn ( logger , 'warn' ) ;
1011+
1012+ precomputedFlagStore . salt = 'flag-salt' ;
1013+
1014+ await precomputedFlagStore . setEntries ( {
1015+ 'test-flag' : {
1016+ flagKey : 'test-flag' ,
1017+ variationType : VariationType . STRING ,
1018+ variationKey : encodeBase64 ( 'control' ) ,
1019+ variationValue : encodeBase64 ( 'test-value' ) ,
1020+ allocationKey : encodeBase64 ( 'allocation-1' ) ,
1021+ doLog : true ,
1022+ extraLogging : { } ,
1023+ } ,
1024+ } ) ;
1025+
1026+ const client = new EppoPrecomputedClient ( {
1027+ precomputedFlagStore,
1028+ subject,
1029+ } ) ;
1030+
1031+ expect ( loggerErrorSpy ) . not . toHaveBeenCalled ( ) ;
1032+ expect ( loggerWarnSpy ) . not . toHaveBeenCalled ( ) ;
1033+
1034+ loggerErrorSpy . mockRestore ( ) ;
1035+ loggerWarnSpy . mockRestore ( ) ;
1036+ } ) ;
1037+ } ) ;
0 commit comments