@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.
- package/.sdd/manifest.json +1 -1
- package/ai-docs/CONTRACTS.md +1 -1
- package/dist/services/task/TaskManager.js +185 -9
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/webex.js +1 -1
- package/package.json +9 -9
- package/src/services/task/TaskManager.ts +298 -31
- package/src/services/task/ai-docs/task-spec.md +36 -1
- package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +11 -3
- package/test/unit/spec/services/task/TaskManager.ts +641 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -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
|
|