@@ -50,6 +50,7 @@ function createMockClient(opts: MockClientOptions = {}) {
5050 const mockUserContext = {
5151 fetchQualifiedSegments : vi . fn ( ) . mockResolvedValue ( fetchQualifiedSegmentsResult ) ,
5252 getUserId : vi . fn ( ) . mockReturnValue ( 'test-user' ) ,
53+ qualifiedSegments : null as string [ ] | null ,
5354 } as unknown as OptimizelyUserContext ;
5455
5556 const onReadyDeferred = createDeferred ( ) ;
@@ -338,6 +339,181 @@ describe('UserContextManager', () => {
338339 } ) ;
339340 } ) ;
340341
342+ // ============================================================
343+ // Scenario 3: Pre-set Qualified Segments
344+ // ============================================================
345+ describe ( 'pre-set qualified segments' , ( ) => {
346+ describe ( 'qualifiedSegments + skipSegments=true' , ( ) => {
347+ it ( 'should set ctx.qualifiedSegments, fire onUserContextReady once, no background fetch' , async ( ) => {
348+ const { client, mockUserContext } = createMockClient ( {
349+ hasOdpManager : true ,
350+ hasVuidManager : false ,
351+ } ) ;
352+ const config = createManagerConfig ( client , { skipSegments : true } ) ;
353+ const manager = new UserContextManager ( config ) ;
354+
355+ manager . createUserContext ( { id : 'user-1' } , [ 'seg-a' , 'seg-b' ] ) ;
356+
357+ expect ( client . createUserContext ) . toHaveBeenCalledWith ( 'user-1' , undefined ) ;
358+ expect ( mockUserContext . qualifiedSegments ) . toEqual ( [ 'seg-a' , 'seg-b' ] ) ;
359+ expect ( config . onUserContextReady ) . toHaveBeenCalledTimes ( 1 ) ;
360+ expect ( config . onUserContextReady ) . toHaveBeenCalledWith ( mockUserContext ) ;
361+ expect ( mockUserContext . fetchQualifiedSegments ) . not . toHaveBeenCalled ( ) ;
362+ expect ( client . onReady ) . not . toHaveBeenCalled ( ) ;
363+
364+ manager . dispose ( ) ;
365+ } ) ;
366+ } ) ;
367+
368+ describe ( 'qualifiedSegments + skipSegments=false + ODP + segments match' , ( ) => {
369+ it ( 'should callback with pre-set segments immediately than skip second callback when background fetch returns matching segments' , async ( ) => {
370+ const { client, mockUserContext, onReadyDeferred } = createMockClient ( {
371+ hasOdpManager : true ,
372+ hasVuidManager : false ,
373+ isOdpIntegrated : true ,
374+ } ) ;
375+ // fetchQualifiedSegments will keep the same segments (simulate match)
376+ ( mockUserContext . fetchQualifiedSegments as ReturnType < typeof vi . fn > ) . mockImplementation ( async ( ) => {
377+ mockUserContext . qualifiedSegments = [ 'seg-a' , 'seg-b' ] ;
378+ return true ;
379+ } ) ;
380+ const config = createManagerConfig ( client , { skipSegments : false } ) ;
381+ const manager = new UserContextManager ( config ) ;
382+
383+ manager . createUserContext ( { id : 'user-1' } , [ 'seg-a' , 'seg-b' ] ) ;
384+
385+ // Immediate callback with pre-set segments
386+ expect ( mockUserContext . qualifiedSegments ) . toEqual ( [ 'seg-a' , 'seg-b' ] ) ;
387+ expect ( config . onUserContextReady ) . toHaveBeenCalledTimes ( 1 ) ;
388+
389+ // Background fetch waiting on onReady
390+ expect ( client . onReady ) . toHaveBeenCalled ( ) ;
391+
392+ onReadyDeferred . resolve ( undefined ) ;
393+ await flushPromises ( ) ;
394+
395+ // Background fetch returned matching segments — no second callback
396+ expect ( mockUserContext . fetchQualifiedSegments ) . toHaveBeenCalled ( ) ;
397+ expect ( config . onUserContextReady ) . toHaveBeenCalledTimes ( 1 ) ;
398+
399+ manager . dispose ( ) ;
400+ } ) ;
401+ } ) ;
402+
403+ describe ( 'qualifiedSegments + skipSegments=false + ODP + segments differ' , ( ) => {
404+ it ( 'should callback with pre-set segments immediately then callback again when background fetch returns different segments' , async ( ) => {
405+ const { client, mockUserContext, onReadyDeferred } = createMockClient ( {
406+ hasOdpManager : true ,
407+ hasVuidManager : false ,
408+ isOdpIntegrated : true ,
409+ } ) ;
410+ // fetchQualifiedSegments returns different segments
411+ ( mockUserContext . fetchQualifiedSegments as ReturnType < typeof vi . fn > ) . mockImplementation ( async ( ) => {
412+ mockUserContext . qualifiedSegments = [ 'seg-a' , 'seg-c' ] ;
413+ return true ;
414+ } ) ;
415+ const config = createManagerConfig ( client , { skipSegments : false } ) ;
416+ const manager = new UserContextManager ( config ) ;
417+
418+ manager . createUserContext ( { id : 'user-1' } , [ 'seg-a' , 'seg-b' ] ) ;
419+
420+ // Immediate callback with pre-set segments
421+ expect ( config . onUserContextReady ) . toHaveBeenCalledTimes ( 1 ) ;
422+
423+ onReadyDeferred . resolve ( undefined ) ;
424+ await flushPromises ( ) ;
425+
426+ // Background fetch returned different segments — second callback fires
427+ expect ( mockUserContext . fetchQualifiedSegments ) . toHaveBeenCalled ( ) ;
428+ expect ( config . onUserContextReady ) . toHaveBeenCalledTimes ( 2 ) ;
429+
430+ manager . dispose ( ) ;
431+ } ) ;
432+ } ) ;
433+
434+ describe ( 'qualifiedSegments + skipSegments=false + ODP not integrated' , ( ) => {
435+ it ( 'should callback with pre-set segments immediately and skip background fetch when ODP is not integrated' , async ( ) => {
436+ const { client, mockUserContext, onReadyDeferred } = createMockClient ( {
437+ hasOdpManager : true ,
438+ hasVuidManager : false ,
439+ isOdpIntegrated : false ,
440+ } ) ;
441+ const config = createManagerConfig ( client , { skipSegments : false } ) ;
442+ const manager = new UserContextManager ( config ) ;
443+
444+ manager . createUserContext ( { id : 'user-1' } , [ 'seg-a' ] ) ;
445+
446+ // Immediate callback with pre-set segments
447+ expect ( mockUserContext . qualifiedSegments ) . toEqual ( [ 'seg-a' ] ) ;
448+ expect ( config . onUserContextReady ) . toHaveBeenCalledTimes ( 1 ) ;
449+
450+ onReadyDeferred . resolve ( undefined ) ;
451+ await flushPromises ( ) ;
452+
453+ // ODP not integrated — no background fetch, no second callback
454+ expect ( client . isOdpIntegrated ) . toHaveBeenCalled ( ) ;
455+ expect ( mockUserContext . fetchQualifiedSegments ) . not . toHaveBeenCalled ( ) ;
456+ expect ( config . onUserContextReady ) . toHaveBeenCalledTimes ( 1 ) ;
457+
458+ manager . dispose ( ) ;
459+ } ) ;
460+ } ) ;
461+
462+ describe ( 'qualifiedSegments + skipSegments=false + no ODP manager' , ( ) => {
463+ it ( 'should callback with pre-set segments immediately and skip background fetch without ODP manager' , async ( ) => {
464+ const { client, mockUserContext } = createMockClient ( {
465+ hasOdpManager : false ,
466+ hasVuidManager : false ,
467+ } ) ;
468+ const config = createManagerConfig ( client , { skipSegments : false } ) ;
469+ const manager = new UserContextManager ( config ) ;
470+
471+ manager . createUserContext ( { id : 'user-1' } , [ 'seg-a' , 'seg-b' ] ) ;
472+ await flushPromises ( ) ;
473+
474+ // Immediate callback with pre-set segments only — no ODP manager, no background fetch
475+ expect ( mockUserContext . qualifiedSegments ) . toEqual ( [ 'seg-a' , 'seg-b' ] ) ;
476+ expect ( config . onUserContextReady ) . toHaveBeenCalledTimes ( 1 ) ;
477+ expect ( client . onReady ) . not . toHaveBeenCalled ( ) ;
478+ expect ( mockUserContext . fetchQualifiedSegments ) . not . toHaveBeenCalled ( ) ;
479+
480+ manager . dispose ( ) ;
481+ } ) ;
482+ } ) ;
483+
484+ describe ( 'qualifiedSegments=[] empty array' , ( ) => {
485+ it ( 'should treat empty array as explicit zero segments, callback immediately, then callback again after background fetch' , async ( ) => {
486+ const { client, mockUserContext, onReadyDeferred } = createMockClient ( {
487+ hasOdpManager : true ,
488+ hasVuidManager : false ,
489+ isOdpIntegrated : true ,
490+ } ) ;
491+ // fetchQualifiedSegments returns segments (differ from empty)
492+ ( mockUserContext . fetchQualifiedSegments as ReturnType < typeof vi . fn > ) . mockImplementation ( async ( ) => {
493+ mockUserContext . qualifiedSegments = [ 'seg-x' ] ;
494+ return true ;
495+ } ) ;
496+ const config = createManagerConfig ( client , { skipSegments : false } ) ;
497+ const manager = new UserContextManager ( config ) ;
498+
499+ manager . createUserContext ( { id : 'user-1' } , [ ] ) ; // empty array is truthy in JS
500+
501+ // Immediate callback with empty segments
502+ expect ( mockUserContext . qualifiedSegments ) . toEqual ( [ ] ) ;
503+ expect ( config . onUserContextReady ) . toHaveBeenCalledTimes ( 1 ) ;
504+
505+ onReadyDeferred . resolve ( undefined ) ;
506+ await flushPromises ( ) ;
507+
508+ // Background fetch returned different segments — second callback fires
509+ expect ( mockUserContext . fetchQualifiedSegments ) . toHaveBeenCalled ( ) ;
510+ expect ( config . onUserContextReady ) . toHaveBeenCalledTimes ( 2 ) ;
511+
512+ manager . dispose ( ) ;
513+ } ) ;
514+ } ) ;
515+ } ) ;
516+
341517 // ============================================================
342518 // Race conditions
343519 // ============================================================
@@ -411,6 +587,54 @@ describe('UserContextManager', () => {
411587
412588 manager . dispose ( ) ;
413589 } ) ;
590+
591+ it ( 'should suppress background fetch callback of stale request when user changes after pre-set segments callback' , async ( ) => {
592+ const segmentDeferred = createDeferred < boolean > ( ) ;
593+ const { client, mockUserContext, onReadyDeferred } = createMockClient ( {
594+ hasOdpManager : true ,
595+ hasVuidManager : false ,
596+ isOdpIntegrated : true ,
597+ } ) ;
598+ ( mockUserContext . fetchQualifiedSegments as ReturnType < typeof vi . fn > ) . mockReturnValue ( segmentDeferred . promise ) ;
599+
600+ const config = createManagerConfig ( client , { skipSegments : false } ) ;
601+ const manager = new UserContextManager ( config ) ;
602+
603+ // First call with qualifiedSegments — pre-set segments callback fires immediately
604+ manager . createUserContext ( { id : 'user-1' } , [ 'seg-a' ] ) ;
605+
606+ // Pre-set segments callback of first request fired
607+ expect ( config . onUserContextReady ) . toHaveBeenCalledTimes ( 1 ) ;
608+
609+ // Resolve onReady so background fetch starts
610+ onReadyDeferred . resolve ( undefined ) ;
611+ await flushPromises ( ) ;
612+
613+ expect ( mockUserContext . fetchQualifiedSegments ) . toHaveBeenCalled ( ) ;
614+
615+ // New user call invalidates the first request
616+ const newCtx = {
617+ getUserId : vi . fn ( ) . mockReturnValue ( 'user-2' ) ,
618+ qualifiedSegments : null as string [ ] | null ,
619+ fetchQualifiedSegments : vi . fn ( ) . mockResolvedValue ( true ) ,
620+ } as unknown as OptimizelyUserContext ;
621+ ( client . createUserContext as ReturnType < typeof vi . fn > ) . mockReturnValue ( newCtx ) ;
622+ manager . createUserContext ( { id : 'user-2' } ) ;
623+ await flushPromises ( ) ;
624+
625+ expect ( config . onUserContextReady ) . toHaveBeenCalledTimes ( 2 ) ;
626+
627+ // First request's background fetch completes — callback should be suppressed (stale)
628+ ( mockUserContext as unknown as { qualifiedSegments : string [ ] } ) . qualifiedSegments = [ 'seg-a' , 'seg-new' ] ;
629+ segmentDeferred . resolve ( true ) ;
630+
631+ await flushPromises ( ) ;
632+
633+ // Still only 2 calls — background fetch callback of stale request was suppressed
634+ expect ( config . onUserContextReady ) . toHaveBeenCalledTimes ( 2 ) ;
635+
636+ manager . dispose ( ) ;
637+ } ) ;
414638 } ) ;
415639
416640 // ============================================================
0 commit comments