@@ -1324,3 +1324,163 @@ describe('tryCreatingProjectConfig', () => {
13241324 expect ( logger . error ) . not . toHaveBeenCalled ( ) ;
13251325 } ) ;
13261326} ) ;
1327+
1328+ describe ( 'Feature Rollout support' , ( ) => {
1329+ const makeDatafile = ( overrides : Record < string , any > = { } ) => {
1330+ const base : Record < string , any > = {
1331+ version : '4' ,
1332+ revision : '1' ,
1333+ projectId : 'rollout_test' ,
1334+ accountId : '12345' ,
1335+ sdkKey : 'test-key' ,
1336+ environmentKey : 'production' ,
1337+ events : [ ] ,
1338+ audiences : [ ] ,
1339+ typedAudiences : [ ] ,
1340+ attributes : [ ] ,
1341+ groups : [ ] ,
1342+ integrations : [ ] ,
1343+ holdouts : [ ] ,
1344+ experiments : [
1345+ {
1346+ id : 'exp_ab' ,
1347+ key : 'ab_experiment' ,
1348+ layerId : 'layer_ab' ,
1349+ status : 'Running' ,
1350+ variations : [ { id : 'var_ab_1' , key : 'variation_ab_1' , variables : [ ] } ] ,
1351+ trafficAllocation : [ { entityId : 'var_ab_1' , endOfRange : 10000 } ] ,
1352+ audienceIds : [ ] ,
1353+ audienceConditions : [ ] ,
1354+ forcedVariations : { } ,
1355+ } ,
1356+ {
1357+ id : 'exp_rollout' ,
1358+ key : 'rollout_experiment' ,
1359+ layerId : 'layer_rollout' ,
1360+ status : 'Running' ,
1361+ type : 'feature_rollout' ,
1362+ variations : [ { id : 'var_rollout_1' , key : 'variation_rollout_1' , variables : [ ] } ] ,
1363+ trafficAllocation : [ { entityId : 'var_rollout_1' , endOfRange : 5000 } ] ,
1364+ audienceIds : [ ] ,
1365+ audienceConditions : [ ] ,
1366+ forcedVariations : { } ,
1367+ } ,
1368+ ] ,
1369+ rollouts : [
1370+ {
1371+ id : 'rollout_1' ,
1372+ experiments : [
1373+ {
1374+ id : 'rollout_rule_1' ,
1375+ key : 'rollout_rule_1_key' ,
1376+ layerId : 'rollout_layer_1' ,
1377+ status : 'Running' ,
1378+ variations : [ { id : 'var_rr1' , key : 'variation_rr1' , variables : [ ] } ] ,
1379+ trafficAllocation : [ { entityId : 'var_rr1' , endOfRange : 10000 } ] ,
1380+ audienceIds : [ ] ,
1381+ audienceConditions : [ ] ,
1382+ forcedVariations : { } ,
1383+ } ,
1384+ {
1385+ id : 'rollout_everyone_else' ,
1386+ key : 'rollout_everyone_else_key' ,
1387+ layerId : 'rollout_layer_ee' ,
1388+ status : 'Running' ,
1389+ variations : [ { id : 'var_ee' , key : 'variation_everyone_else' , variables : [ ] } ] ,
1390+ trafficAllocation : [ { entityId : 'var_ee' , endOfRange : 10000 } ] ,
1391+ audienceIds : [ ] ,
1392+ audienceConditions : [ ] ,
1393+ forcedVariations : { } ,
1394+ } ,
1395+ ] ,
1396+ } ,
1397+ ] ,
1398+ featureFlags : [
1399+ {
1400+ id : 'feature_1' ,
1401+ key : 'feature_rollout_flag' ,
1402+ rolloutId : 'rollout_1' ,
1403+ experimentIds : [ 'exp_ab' , 'exp_rollout' ] ,
1404+ variables : [ ] ,
1405+ } ,
1406+ ] ,
1407+ ...overrides ,
1408+ } ;
1409+ return base ;
1410+ } ;
1411+
1412+ it ( 'should preserve type=undefined for experiments without type field (backward compatibility)' , ( ) => {
1413+ const datafile = makeDatafile ( ) ;
1414+ const config = projectConfig . createProjectConfig ( datafile as any ) ;
1415+ const abExperiment = config . experimentIdMap [ 'exp_ab' ] ;
1416+ expect ( abExperiment . type ) . toBeUndefined ( ) ;
1417+ } ) ;
1418+
1419+ it ( 'should inject everyone else variation into feature_rollout experiments' , ( ) => {
1420+ const datafile = makeDatafile ( ) ;
1421+ const config = projectConfig . createProjectConfig ( datafile as any ) ;
1422+ const rolloutExperiment = config . experimentIdMap [ 'exp_rollout' ] ;
1423+
1424+ // Should have 2 variations: original + injected everyone else
1425+ expect ( rolloutExperiment . variations ) . toHaveLength ( 2 ) ;
1426+ expect ( rolloutExperiment . variations [ 1 ] . id ) . toBe ( 'var_ee' ) ;
1427+ expect ( rolloutExperiment . variations [ 1 ] . key ) . toBe ( 'variation_everyone_else' ) ;
1428+
1429+ // Should have injected traffic allocation entry
1430+ const lastAllocation = rolloutExperiment . trafficAllocation [ rolloutExperiment . trafficAllocation . length - 1 ] ;
1431+ expect ( lastAllocation . entityId ) . toBe ( 'var_ee' ) ;
1432+ expect ( lastAllocation . endOfRange ) . toBe ( 10000 ) ;
1433+ } ) ;
1434+
1435+ it ( 'should update variation lookup maps with injected variation' , ( ) => {
1436+ const datafile = makeDatafile ( ) ;
1437+ const config = projectConfig . createProjectConfig ( datafile as any ) ;
1438+ const rolloutExperiment = config . experimentIdMap [ 'exp_rollout' ] ;
1439+
1440+ // variationKeyMap on the experiment should contain the injected variation
1441+ expect ( rolloutExperiment . variationKeyMap [ 'variation_everyone_else' ] ) . toBeDefined ( ) ;
1442+ expect ( rolloutExperiment . variationKeyMap [ 'variation_everyone_else' ] . id ) . toBe ( 'var_ee' ) ;
1443+
1444+ // Global variationIdMap should contain the injected variation
1445+ expect ( config . variationIdMap [ 'var_ee' ] ) . toBeDefined ( ) ;
1446+ expect ( config . variationIdMap [ 'var_ee' ] . key ) . toBe ( 'variation_everyone_else' ) ;
1447+ } ) ;
1448+
1449+ it ( 'should not modify non-rollout experiments (A/B, MAB, CMAB)' , ( ) => {
1450+ const datafile = makeDatafile ( ) ;
1451+ const config = projectConfig . createProjectConfig ( datafile as any ) ;
1452+ const abExperiment = config . experimentIdMap [ 'exp_ab' ] ;
1453+
1454+ // A/B experiment should still have only 1 variation
1455+ expect ( abExperiment . variations ) . toHaveLength ( 1 ) ;
1456+ expect ( abExperiment . variations [ 0 ] . id ) . toBe ( 'var_ab_1' ) ;
1457+ expect ( abExperiment . trafficAllocation ) . toHaveLength ( 1 ) ;
1458+ } ) ;
1459+
1460+ it ( 'should silently skip injection when feature has no rolloutId' , ( ) => {
1461+ const datafile = makeDatafile ( {
1462+ featureFlags : [
1463+ {
1464+ id : 'feature_no_rollout' ,
1465+ key : 'feature_no_rollout' ,
1466+ rolloutId : '' ,
1467+ experimentIds : [ 'exp_rollout' ] ,
1468+ variables : [ ] ,
1469+ } ,
1470+ ] ,
1471+ } ) ;
1472+ const config = projectConfig . createProjectConfig ( datafile as any ) ;
1473+ const rolloutExperiment = config . experimentIdMap [ 'exp_rollout' ] ;
1474+
1475+ // Should still have only 1 variation (no injection)
1476+ expect ( rolloutExperiment . variations ) . toHaveLength ( 1 ) ;
1477+ expect ( rolloutExperiment . variations [ 0 ] . id ) . toBe ( 'var_rollout_1' ) ;
1478+ } ) ;
1479+
1480+ it ( 'should correctly preserve experiment type field from datafile' , ( ) => {
1481+ const datafile = makeDatafile ( ) ;
1482+ const config = projectConfig . createProjectConfig ( datafile as any ) ;
1483+ const rolloutExperiment = config . experimentIdMap [ 'exp_rollout' ] ;
1484+ expect ( rolloutExperiment . type ) . toBe ( 'feature_rollout' ) ;
1485+ } ) ;
1486+ } ) ;
0 commit comments