@webex/contact-center 3.12.0-llmrefactor.3 → 3.12.0-llmrefactor.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -51,6 +51,7 @@ describe('TaskManager', () => {
51
51
  [TaskEvent.TASK_OFFERED]: TASK_EVENTS.TASK_OFFER_CONTACT,
52
52
  [TaskEvent.OFFER_CONSULT]: TASK_EVENTS.TASK_OFFER_CONSULT,
53
53
  [TaskEvent.HYDRATE]: TASK_EVENTS.TASK_HYDRATE,
54
+ [TaskEvent.CONTACT_OWNER_CHANGED]: TASK_EVENTS.TASK_HYDRATE,
54
55
  [TaskEvent.ASSIGN]: TASK_EVENTS.TASK_ASSIGNED,
55
56
  [TaskEvent.HOLD_SUCCESS]: TASK_EVENTS.TASK_HOLD,
56
57
  [TaskEvent.UNHOLD_SUCCESS]: TASK_EVENTS.TASK_RESUME,
@@ -2723,6 +2724,646 @@ describe('TaskManager', () => {
2723
2724
  });
2724
2725
  });
2725
2726
 
2727
+ describe('Contact owner propagation', () => {
2728
+ const currentAgentId = 'test-agent-id';
2729
+ const previousOwnerId = 'previous-owner-id';
2730
+ const promotedOwnerId = 'promoted-owner-id';
2731
+
2732
+ const createConferenceTaskData = (
2733
+ interactionId: string,
2734
+ owner: string,
2735
+ mainInteractionId = taskId
2736
+ ) => ({
2737
+ ...taskDataMock,
2738
+ agentId: currentAgentId,
2739
+ interactionId,
2740
+ owner,
2741
+ interaction: {
2742
+ mediaType: 'telephony',
2743
+ interactionId,
2744
+ mainInteractionId,
2745
+ owner,
2746
+ participants: {
2747
+ [currentAgentId]: {
2748
+ id: currentAgentId,
2749
+ pType: 'Agent',
2750
+ hasJoined: true,
2751
+ hasLeft: false,
2752
+ },
2753
+ [previousOwnerId]: {
2754
+ id: previousOwnerId,
2755
+ pType: 'Agent',
2756
+ hasJoined: true,
2757
+ hasLeft: false,
2758
+ },
2759
+ [promotedOwnerId]: {
2760
+ id: promotedOwnerId,
2761
+ pType: 'Agent',
2762
+ hasJoined: true,
2763
+ hasLeft: false,
2764
+ },
2765
+ },
2766
+ media: {
2767
+ [mainInteractionId]: {
2768
+ mediaResourceId: mainInteractionId,
2769
+ mediaType: 'telephony',
2770
+ mType: 'mainCall',
2771
+ participants: [currentAgentId, previousOwnerId, promotedOwnerId],
2772
+ },
2773
+ },
2774
+ },
2775
+ });
2776
+
2777
+ const installTask = (data) => {
2778
+ const task = createMockTask(data);
2779
+ taskManager.taskCollection = {[data.interactionId]: task};
2780
+ (taskManager as any).setupTaskListeners(task);
2781
+
2782
+ return task;
2783
+ };
2784
+
2785
+ const createPromotedAgentOwnerChangeData = (
2786
+ interactionId = 'promoted-child-interaction-id',
2787
+ mainInteractionId = taskId
2788
+ ) => {
2789
+ const data = createConferenceTaskData(interactionId, currentAgentId, mainInteractionId);
2790
+
2791
+ data.interaction.state = 'conference';
2792
+
2793
+ return {...data, type: CC_EVENTS.CONTACT_OWNER_CHANGED};
2794
+ };
2795
+
2796
+ const clearTasksAndFactoryHistory = () => {
2797
+ taskManager.taskCollection = {};
2798
+ (TaskFactory.createTask as jest.Mock).mockClear();
2799
+ };
2800
+
2801
+ it('emits one hydrate when ContactUpdated changes interaction.owner', () => {
2802
+ const task = installTask(createConferenceTaskData(taskId, previousOwnerId));
2803
+ const hydrateHandler = jest.fn();
2804
+ taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
2805
+
2806
+ webSocketManagerMock.emit(
2807
+ 'message',
2808
+ JSON.stringify({
2809
+ data: {
2810
+ ...createConferenceTaskData(taskId, promotedOwnerId),
2811
+ type: CC_EVENTS.CONTACT_UPDATED,
2812
+ },
2813
+ })
2814
+ );
2815
+
2816
+ expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
2817
+ expect(task.data.interaction.owner).toBe(promotedOwnerId);
2818
+ expect(hydrateHandler).toHaveBeenCalledTimes(1);
2819
+ expect(hydrateHandler).toHaveBeenCalledWith(task);
2820
+ });
2821
+
2822
+ it.each([
2823
+ ['the same owner', previousOwnerId],
2824
+ ['no owner', undefined],
2825
+ ['a blank owner', ' '],
2826
+ ])('keeps ContactUpdated data-only when it carries %s', (_description, owner) => {
2827
+ const task = installTask(createConferenceTaskData(taskId, previousOwnerId));
2828
+ const hydrateHandler = jest.fn();
2829
+ taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
2830
+ const interaction = createConferenceTaskData(taskId, previousOwnerId).interaction;
2831
+ if (owner === undefined) {
2832
+ delete interaction.owner;
2833
+ } else {
2834
+ interaction.owner = owner;
2835
+ }
2836
+
2837
+ webSocketManagerMock.emit(
2838
+ 'message',
2839
+ JSON.stringify({
2840
+ data: {
2841
+ ...createConferenceTaskData(taskId, previousOwnerId),
2842
+ type: CC_EVENTS.CONTACT_UPDATED,
2843
+ interaction,
2844
+ },
2845
+ })
2846
+ );
2847
+
2848
+ expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_UPDATED);
2849
+ expect(hydrateHandler).not.toHaveBeenCalled();
2850
+ });
2851
+
2852
+ it('correlates child-keyed ContactOwnerChanged and keeps the main task identity', () => {
2853
+ const childTaskId = 'surviving-child-id';
2854
+ const promotedChildId = 'promoted-child-id';
2855
+ const task = installTask(
2856
+ createConferenceTaskData(childTaskId, previousOwnerId, taskId)
2857
+ );
2858
+ const hydrateHandler = jest.fn();
2859
+ taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
2860
+
2861
+ const ownerChangeData = createConferenceTaskData(
2862
+ promotedChildId,
2863
+ promotedOwnerId,
2864
+ taskId
2865
+ );
2866
+ ownerChangeData.interaction.interactionId = taskId;
2867
+ webSocketManagerMock.emit(
2868
+ 'message',
2869
+ JSON.stringify({
2870
+ data: {...ownerChangeData, type: CC_EVENTS.CONTACT_OWNER_CHANGED},
2871
+ })
2872
+ );
2873
+
2874
+ expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
2875
+ expect(task.data.interactionId).toBe(taskId);
2876
+ expect(task.data.interaction.owner).toBe(promotedOwnerId);
2877
+ expect(taskManager.taskCollection[childTaskId]).toBeUndefined();
2878
+ expect(taskManager.taskCollection[promotedChildId]).toBeUndefined();
2879
+ expect(taskManager.taskCollection[taskId]).toBe(task);
2880
+ expect(Object.values(taskManager.taskCollection).filter((entry) => entry === task)).toHaveLength(
2881
+ 1
2882
+ );
2883
+ expect(hydrateHandler).toHaveBeenCalledTimes(1);
2884
+ });
2885
+
2886
+ it('uses the incoming promotion snapshot to correlate a stale child-keyed task', () => {
2887
+ const childTaskId = 'stale-child-interaction-id';
2888
+ const staleTaskData = createConferenceTaskData(childTaskId, previousOwnerId, taskId);
2889
+ staleTaskData.interaction.media = {
2890
+ [childTaskId]: {
2891
+ mediaResourceId: childTaskId,
2892
+ mediaType: 'telephony',
2893
+ mType: 'consult',
2894
+ participants: [currentAgentId, promotedOwnerId],
2895
+ },
2896
+ };
2897
+ const task = installTask(staleTaskData);
2898
+ const hydrateHandler = jest.fn();
2899
+ taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
2900
+
2901
+ webSocketManagerMock.emit(
2902
+ 'message',
2903
+ JSON.stringify({data: createPromotedAgentOwnerChangeData()})
2904
+ );
2905
+
2906
+ expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
2907
+ expect(task.data.interaction.owner).toBe(currentAgentId);
2908
+ expect(taskManager.taskCollection[childTaskId]).toBeUndefined();
2909
+ expect(taskManager.taskCollection[taskId]).toBe(task);
2910
+ expect(TaskFactory.createTask).not.toHaveBeenCalled();
2911
+ expect(hydrateHandler).toHaveBeenCalledTimes(1);
2912
+ });
2913
+
2914
+ it('reuses a task stored under the incoming main-call media key', () => {
2915
+ const mainMediaInteractionId = 'main-media-interaction-id';
2916
+ const promotedChildId = 'promoted-child-without-main-id';
2917
+ const task = installTask(
2918
+ createConferenceTaskData(
2919
+ mainMediaInteractionId,
2920
+ previousOwnerId,
2921
+ mainMediaInteractionId
2922
+ )
2923
+ );
2924
+ const hydrateHandler = jest.fn();
2925
+ taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
2926
+ const ownerChangeData = createPromotedAgentOwnerChangeData(
2927
+ promotedChildId,
2928
+ mainMediaInteractionId
2929
+ );
2930
+ delete ownerChangeData.interaction.mainInteractionId;
2931
+
2932
+ webSocketManagerMock.emit('message', JSON.stringify({data: ownerChangeData}));
2933
+
2934
+ expect(task.sendStateMachineEvent).toHaveBeenCalledTimes(1);
2935
+ expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
2936
+ expect(TaskFactory.createTask).not.toHaveBeenCalled();
2937
+ expect(taskManager.taskCollection[mainMediaInteractionId]).toBe(task);
2938
+ expect(taskManager.taskCollection[promotedChildId]).toBeUndefined();
2939
+ expect(Object.values(taskManager.taskCollection)).toEqual([task]);
2940
+ expect(task.listenerCount(TASK_EVENTS.TASK_HYDRATE)).toBe(1);
2941
+ expect(hydrateHandler).toHaveBeenCalledTimes(1);
2942
+ expect(hydrateHandler).toHaveBeenCalledWith(task);
2943
+ });
2944
+
2945
+ it('prefers the exact task for ContactOwnerChanged over related child tasks', () => {
2946
+ const exactTask = installTask(createConferenceTaskData(taskId, previousOwnerId));
2947
+ const childTaskId = 'related-child-id';
2948
+ const childTask = createMockTask(
2949
+ createConferenceTaskData(childTaskId, previousOwnerId, taskId)
2950
+ );
2951
+ taskManager.taskCollection[childTaskId] = childTask;
2952
+
2953
+ webSocketManagerMock.emit(
2954
+ 'message',
2955
+ JSON.stringify({
2956
+ data: {
2957
+ ...createConferenceTaskData(taskId, promotedOwnerId),
2958
+ type: CC_EVENTS.CONTACT_OWNER_CHANGED,
2959
+ },
2960
+ })
2961
+ );
2962
+
2963
+ expectLastStateMachineEvent(exactTask.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
2964
+ expect(childTask.sendStateMachineEvent).not.toHaveBeenCalled();
2965
+ });
2966
+
2967
+ it('ignores a related ContactOwnerChanged task whose current agent is consult-only', () => {
2968
+ const childTaskId = 'consult-only-child-id';
2969
+ const data = createConferenceTaskData(childTaskId, previousOwnerId, taskId);
2970
+ data.interaction.media[taskId].participants = [previousOwnerId, promotedOwnerId];
2971
+ data.interaction.media[childTaskId] = {
2972
+ mediaResourceId: childTaskId,
2973
+ mediaType: 'telephony',
2974
+ mType: 'consult',
2975
+ participants: [currentAgentId, promotedOwnerId],
2976
+ };
2977
+ const task = installTask(data);
2978
+
2979
+ webSocketManagerMock.emit(
2980
+ 'message',
2981
+ JSON.stringify({
2982
+ data: {
2983
+ ...createConferenceTaskData('unmatched-child-id', promotedOwnerId, taskId),
2984
+ type: CC_EVENTS.CONTACT_OWNER_CHANGED,
2985
+ },
2986
+ })
2987
+ );
2988
+
2989
+ expect(task.sendStateMachineEvent).not.toHaveBeenCalled();
2990
+ expect(taskManager.taskCollection[childTaskId]).toBe(task);
2991
+ expect(TaskFactory.createTask).not.toHaveBeenCalled();
2992
+ });
2993
+
2994
+ it.each([
2995
+ [
2996
+ 'the current agent is marked departed',
2997
+ (data) => {
2998
+ data.interaction.participants[currentAgentId].hasLeft = true;
2999
+ },
3000
+ ],
3001
+ [
3002
+ 'the current agent participant is missing',
3003
+ (data) => {
3004
+ delete data.interaction.participants[currentAgentId];
3005
+ },
3006
+ ],
3007
+ [
3008
+ 'the current agent is present only on a consult leg',
3009
+ (data) => {
3010
+ data.interaction.media[taskId].mType = 'consult';
3011
+ },
3012
+ ],
3013
+ ])('does not recover a promoted task when %s', (_description, mutatePayload) => {
3014
+ clearTasksAndFactoryHistory();
3015
+ const ownerChangeData = createPromotedAgentOwnerChangeData();
3016
+ mutatePayload(ownerChangeData);
3017
+
3018
+ webSocketManagerMock.emit('message', JSON.stringify({data: ownerChangeData}));
3019
+
3020
+ expect(TaskFactory.createTask).not.toHaveBeenCalled();
3021
+ expect(taskManager.getAllTasks()).toEqual({});
3022
+ });
3023
+
3024
+ it('ignores ambiguous active main-call matches for ContactOwnerChanged', () => {
3025
+ const firstTask = createMockTask(
3026
+ createConferenceTaskData('first-child-id', previousOwnerId, taskId)
3027
+ );
3028
+ const secondTask = createMockTask(
3029
+ createConferenceTaskData('second-child-id', previousOwnerId, taskId)
3030
+ );
3031
+ taskManager.taskCollection = {
3032
+ 'first-child-id': firstTask,
3033
+ 'second-child-id': secondTask,
3034
+ };
3035
+
3036
+ const ownerChangeData = createConferenceTaskData(
3037
+ 'unmatched-child-id',
3038
+ promotedOwnerId,
3039
+ taskId
3040
+ );
3041
+ ownerChangeData.interaction.interactionId = 'unmatched-child-id';
3042
+ webSocketManagerMock.emit(
3043
+ 'message',
3044
+ JSON.stringify({
3045
+ data: {...ownerChangeData, type: CC_EVENTS.CONTACT_OWNER_CHANGED},
3046
+ })
3047
+ );
3048
+
3049
+ expect(firstTask.sendStateMachineEvent).not.toHaveBeenCalled();
3050
+ expect(secondTask.sendStateMachineEvent).not.toHaveBeenCalled();
3051
+ });
3052
+
3053
+ it('does not recover a new task when multiple stale related tasks become eligible from the payload', () => {
3054
+ const createStaleRelatedTask = (interactionId: string) => {
3055
+ const data = createConferenceTaskData(interactionId, previousOwnerId, taskId);
3056
+ data.interaction.media[taskId].participants = [previousOwnerId, promotedOwnerId];
3057
+
3058
+ return createMockTask(data);
3059
+ };
3060
+ const firstTask = createStaleRelatedTask('first-stale-child-id');
3061
+ const secondTask = createStaleRelatedTask('second-stale-child-id');
3062
+ taskManager.taskCollection = {
3063
+ 'first-stale-child-id': firstTask,
3064
+ 'second-stale-child-id': secondTask,
3065
+ };
3066
+ (TaskFactory.createTask as jest.Mock).mockClear();
3067
+
3068
+ webSocketManagerMock.emit(
3069
+ 'message',
3070
+ JSON.stringify({data: createPromotedAgentOwnerChangeData()})
3071
+ );
3072
+
3073
+ expect(firstTask.sendStateMachineEvent).not.toHaveBeenCalled();
3074
+ expect(secondTask.sendStateMachineEvent).not.toHaveBeenCalled();
3075
+ expect(TaskFactory.createTask).not.toHaveBeenCalled();
3076
+ expect(Object.keys(taskManager.taskCollection)).toEqual([
3077
+ 'first-stale-child-id',
3078
+ 'second-stale-child-id',
3079
+ ]);
3080
+ });
3081
+
3082
+ it('ignores ContactOwnerChanged when different direct nested IDs match active tasks', () => {
3083
+ const firstTaskId = 'direct-main-id';
3084
+ const secondTaskId = 'direct-parent-id';
3085
+ const firstTask = createMockTask(
3086
+ createConferenceTaskData(firstTaskId, previousOwnerId, firstTaskId)
3087
+ );
3088
+ const secondTask = createMockTask(
3089
+ createConferenceTaskData(secondTaskId, previousOwnerId, secondTaskId)
3090
+ );
3091
+ taskManager.taskCollection = {
3092
+ [firstTaskId]: firstTask,
3093
+ [secondTaskId]: secondTask,
3094
+ };
3095
+
3096
+ const ownerChangeData = createConferenceTaskData(
3097
+ 'unmatched-child-id',
3098
+ promotedOwnerId,
3099
+ firstTaskId
3100
+ );
3101
+ ownerChangeData.interaction.interactionId = secondTaskId;
3102
+ webSocketManagerMock.emit(
3103
+ 'message',
3104
+ JSON.stringify({
3105
+ data: {...ownerChangeData, type: CC_EVENTS.CONTACT_OWNER_CHANGED},
3106
+ })
3107
+ );
3108
+
3109
+ expect(firstTask.sendStateMachineEvent).not.toHaveBeenCalled();
3110
+ expect(secondTask.sendStateMachineEvent).not.toHaveBeenCalled();
3111
+ });
3112
+
3113
+ it('ignores ContactOwnerChanged without an interaction correlation identifier', () => {
3114
+ const task = installTask(createConferenceTaskData(taskId, previousOwnerId));
3115
+
3116
+ webSocketManagerMock.emit(
3117
+ 'message',
3118
+ JSON.stringify({
3119
+ data: {
3120
+ type: CC_EVENTS.CONTACT_OWNER_CHANGED,
3121
+ interaction: {owner: promotedOwnerId},
3122
+ },
3123
+ })
3124
+ );
3125
+
3126
+ expect(task.sendStateMachineEvent).not.toHaveBeenCalled();
3127
+ });
3128
+
3129
+ it('recovers a missing promoted-agent task under the main interaction and emits one hydrate', () => {
3130
+ clearTasksAndFactoryHistory();
3131
+ const hydrateHandler = jest.fn();
3132
+ const incomingHandler = jest.fn();
3133
+ taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
3134
+ taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
3135
+
3136
+ webSocketManagerMock.emit(
3137
+ 'message',
3138
+ JSON.stringify({data: createPromotedAgentOwnerChangeData()})
3139
+ );
3140
+
3141
+ expect(TaskFactory.createTask).toHaveBeenCalledTimes(1);
3142
+ expect(TaskFactory.createTask).toHaveBeenCalledWith(
3143
+ contactMock,
3144
+ webCallingService,
3145
+ expect.objectContaining({
3146
+ interactionId: taskId,
3147
+ isAutoAnswering: false,
3148
+ isConsulted: false,
3149
+ isConferenceInProgress: true,
3150
+ wrapUpRequired: false,
3151
+ }),
3152
+ undefined,
3153
+ undefined,
3154
+ currentAgentId,
3155
+ undefined
3156
+ );
3157
+
3158
+ const recoveredTask = (TaskFactory.createTask as jest.Mock).mock.results[0].value;
3159
+ expect(recoveredTask.sendStateMachineEvent).toHaveBeenCalledTimes(2);
3160
+ expect(recoveredTask.sendStateMachineEvent).toHaveBeenNthCalledWith(
3161
+ 1,
3162
+ expect.objectContaining({
3163
+ type: TaskEvent.HYDRATE,
3164
+ agentId: currentAgentId,
3165
+ taskData: expect.objectContaining({interactionId: taskId}),
3166
+ })
3167
+ );
3168
+ expect(recoveredTask.sendStateMachineEvent).toHaveBeenNthCalledWith(
3169
+ 2,
3170
+ expect.objectContaining({
3171
+ type: TaskEvent.CONTACT_OWNER_CHANGED,
3172
+ taskData: expect.objectContaining({interactionId: taskId}),
3173
+ })
3174
+ );
3175
+ expect(taskManager.taskCollection[taskId]).toBe(recoveredTask);
3176
+ expect(taskManager.taskCollection['promoted-child-interaction-id']).toBeUndefined();
3177
+ expect(Object.values(taskManager.taskCollection)).toEqual([recoveredTask]);
3178
+ expect(recoveredTask.data.interaction.owner).toBe(currentAgentId);
3179
+ expect(hydrateHandler).toHaveBeenCalledTimes(1);
3180
+ expect(hydrateHandler).toHaveBeenCalledWith(recoveredTask);
3181
+ expect(incomingHandler).not.toHaveBeenCalled();
3182
+ });
3183
+
3184
+ it.each([
3185
+ [
3186
+ 'another agent owns the interaction',
3187
+ (data) => {
3188
+ data.interaction.owner = promotedOwnerId;
3189
+ data.owner = promotedOwnerId;
3190
+ },
3191
+ ],
3192
+ [
3193
+ 'the payload has no main-call media',
3194
+ (data) => {
3195
+ data.interaction.media = {};
3196
+ },
3197
+ ],
3198
+ [
3199
+ 'the interaction is explicitly terminated',
3200
+ (data) => {
3201
+ data.interaction.isTerminated = true;
3202
+ },
3203
+ ],
3204
+ ])('does not create a missing task when %s', (_description, mutatePayload) => {
3205
+ clearTasksAndFactoryHistory();
3206
+ const ownerChangeData = createPromotedAgentOwnerChangeData();
3207
+ mutatePayload(ownerChangeData);
3208
+
3209
+ webSocketManagerMock.emit('message', JSON.stringify({data: ownerChangeData}));
3210
+
3211
+ expect(TaskFactory.createTask).not.toHaveBeenCalled();
3212
+ expect(taskManager.getAllTasks()).toEqual({});
3213
+ });
3214
+
3215
+ it('does not create a missing task for an owner-changing ContactUpdated event', () => {
3216
+ clearTasksAndFactoryHistory();
3217
+ const contactUpdatedData = {
3218
+ ...createPromotedAgentOwnerChangeData(),
3219
+ type: CC_EVENTS.CONTACT_UPDATED,
3220
+ };
3221
+
3222
+ webSocketManagerMock.emit('message', JSON.stringify({data: contactUpdatedData}));
3223
+
3224
+ expect(TaskFactory.createTask).not.toHaveBeenCalled();
3225
+ expect(taskManager.getAllTasks()).toEqual({});
3226
+ });
3227
+
3228
+ it('reuses a recovered task for later AgentContact and ContactMerged events', () => {
3229
+ clearTasksAndFactoryHistory();
3230
+ const hydrateHandler = jest.fn();
3231
+ const mergedHandler = jest.fn();
3232
+ taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
3233
+ taskManager.on(TASK_EVENTS.TASK_MERGED, mergedHandler);
3234
+
3235
+ webSocketManagerMock.emit(
3236
+ 'message',
3237
+ JSON.stringify({data: createPromotedAgentOwnerChangeData()})
3238
+ );
3239
+
3240
+ const recoveredTask = taskManager.taskCollection[taskId];
3241
+ expect(recoveredTask).toBeDefined();
3242
+ expect(recoveredTask.listenerCount(TASK_EVENTS.TASK_HYDRATE)).toBe(1);
3243
+
3244
+ webSocketManagerMock.emit(
3245
+ 'message',
3246
+ JSON.stringify({
3247
+ data: {
3248
+ ...createConferenceTaskData(taskId, currentAgentId),
3249
+ type: CC_EVENTS.AGENT_CONTACT,
3250
+ },
3251
+ })
3252
+ );
3253
+ webSocketManagerMock.emit(
3254
+ 'message',
3255
+ JSON.stringify({
3256
+ data: {
3257
+ ...createConferenceTaskData(taskId, currentAgentId),
3258
+ type: CC_EVENTS.CONTACT_MERGED,
3259
+ childInteractionId: 'promoted-child-interaction-id',
3260
+ },
3261
+ })
3262
+ );
3263
+
3264
+ expect(TaskFactory.createTask).toHaveBeenCalledTimes(1);
3265
+ expect(taskManager.taskCollection[taskId]).toBe(recoveredTask);
3266
+ expect(taskManager.taskCollection['promoted-child-interaction-id']).toBeUndefined();
3267
+ expect(Object.values(taskManager.taskCollection)).toEqual([recoveredTask]);
3268
+ expect(recoveredTask.listenerCount(TASK_EVENTS.TASK_HYDRATE)).toBe(1);
3269
+ expect(hydrateHandler).toHaveBeenCalledTimes(2);
3270
+ expect(mergedHandler).toHaveBeenCalledTimes(1);
3271
+ expect(mergedHandler).toHaveBeenCalledWith(recoveredTask);
3272
+ });
3273
+
3274
+ it('does not let a late ParticipantLeftConference restore a departed owner', () => {
3275
+ const task = installTask(createConferenceTaskData(taskId, previousOwnerId));
3276
+
3277
+ webSocketManagerMock.emit(
3278
+ 'message',
3279
+ JSON.stringify({
3280
+ data: {
3281
+ ...createConferenceTaskData(taskId, promotedOwnerId),
3282
+ type: CC_EVENTS.CONTACT_UPDATED,
3283
+ },
3284
+ })
3285
+ );
3286
+
3287
+ const participantLeftData = createConferenceTaskData(taskId, previousOwnerId);
3288
+ participantLeftData.participantId = previousOwnerId;
3289
+ participantLeftData.interaction.participants[previousOwnerId].hasLeft = true;
3290
+ participantLeftData.interaction.media[taskId].participants = [
3291
+ currentAgentId,
3292
+ promotedOwnerId,
3293
+ ];
3294
+ webSocketManagerMock.emit(
3295
+ 'message',
3296
+ JSON.stringify({
3297
+ data: {...participantLeftData, type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE},
3298
+ })
3299
+ );
3300
+
3301
+ const participantLeaveEvent = expectLastStateMachineEvent(
3302
+ task.sendStateMachineEvent,
3303
+ TaskEvent.PARTICIPANT_LEAVE
3304
+ );
3305
+ expect(participantLeaveEvent.taskData.interaction.owner).toBe(promotedOwnerId);
3306
+ expect(task.data.interaction.owner).toBe(promotedOwnerId);
3307
+ });
3308
+
3309
+ it('does not infer that an incoming owner departed from a partial main-call roster', () => {
3310
+ const task = installTask(createConferenceTaskData(taskId, promotedOwnerId));
3311
+ const partialParticipantLeftData = createConferenceTaskData(taskId, previousOwnerId);
3312
+ partialParticipantLeftData.participantId = 'another-agent-id';
3313
+ partialParticipantLeftData.interaction.media[taskId].participants = [
3314
+ currentAgentId,
3315
+ promotedOwnerId,
3316
+ ];
3317
+
3318
+ webSocketManagerMock.emit(
3319
+ 'message',
3320
+ JSON.stringify({
3321
+ data: {
3322
+ ...partialParticipantLeftData,
3323
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
3324
+ },
3325
+ })
3326
+ );
3327
+
3328
+ const participantLeaveEvent = expectLastStateMachineEvent(
3329
+ task.sendStateMachineEvent,
3330
+ TaskEvent.PARTICIPANT_LEAVE
3331
+ );
3332
+ expect(participantLeaveEvent.taskData.interaction.owner).toBe(previousOwnerId);
3333
+ expect(task.data.interaction.owner).toBe(previousOwnerId);
3334
+ });
3335
+
3336
+ it('applies the promoted owner when ParticipantLeftConference arrives first', () => {
3337
+ const task = installTask(createConferenceTaskData(taskId, previousOwnerId));
3338
+ const participantLeftData = createConferenceTaskData(taskId, previousOwnerId);
3339
+ participantLeftData.participantId = previousOwnerId;
3340
+ participantLeftData.interaction.participants[previousOwnerId].hasLeft = true;
3341
+ participantLeftData.interaction.media[taskId].participants = [
3342
+ currentAgentId,
3343
+ promotedOwnerId,
3344
+ ];
3345
+
3346
+ webSocketManagerMock.emit(
3347
+ 'message',
3348
+ JSON.stringify({
3349
+ data: {...participantLeftData, type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE},
3350
+ })
3351
+ );
3352
+ webSocketManagerMock.emit(
3353
+ 'message',
3354
+ JSON.stringify({
3355
+ data: {
3356
+ ...createConferenceTaskData(taskId, promotedOwnerId),
3357
+ type: CC_EVENTS.CONTACT_UPDATED,
3358
+ },
3359
+ })
3360
+ );
3361
+
3362
+ expect(task.data.interaction.owner).toBe(promotedOwnerId);
3363
+ expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
3364
+ });
3365
+ });
3366
+
2726
3367
  describe('Conference event handling', () => {
2727
3368
  let task;
2728
3369