@webex/contact-center 3.12.0-next.129 → 3.12.0-next.130
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/ai-docs/features/contact-owner-change-refactor.md +78 -0
- package/dist/services/task/TaskManager.js +39 -58
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/webex.js +1 -1
- package/package.json +1 -1
- package/src/services/task/TaskManager.ts +74 -87
- package/src/services/task/ai-docs/task-spec.md +5 -3
- package/test/unit/spec/services/task/TaskManager.ts +189 -3
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +33 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -778,6 +778,7 @@ export default class TaskManager extends EventEmitter {
|
|
|
778
778
|
const interactionId = message.data.interactionId;
|
|
779
779
|
const reservationInteractionId = message.data.reservationInteractionId;
|
|
780
780
|
let task = this.taskCollection[interactionId];
|
|
781
|
+
let ownerChangeResolvedByRelatedInteraction = false;
|
|
781
782
|
|
|
782
783
|
// When a campaign preview contact is accepted, the assigned event may arrive
|
|
783
784
|
// with a new interactionId while the task is stored under the original
|
|
@@ -792,7 +793,66 @@ export default class TaskManager extends EventEmitter {
|
|
|
792
793
|
}
|
|
793
794
|
|
|
794
795
|
if (!task && eventType === CC_EVENTS.CONTACT_OWNER_CHANGED) {
|
|
795
|
-
|
|
796
|
+
const incomingShowsCurrentAgentPromotion = Boolean(
|
|
797
|
+
this.agentId &&
|
|
798
|
+
message.data.interaction?.owner === this.agentId &&
|
|
799
|
+
TaskManager.isParticipantActiveOnMainInteraction(message.data, this.agentId)
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
task = this.findUniqueTaskByRelatedInteraction(
|
|
803
|
+
message.data,
|
|
804
|
+
(candidate) => {
|
|
805
|
+
const currentAgentId = this.agentId || candidate.data?.agentId;
|
|
806
|
+
|
|
807
|
+
return Boolean(
|
|
808
|
+
currentAgentId &&
|
|
809
|
+
(TaskManager.isParticipantActiveOnMainInteraction(candidate.data, currentAgentId) ||
|
|
810
|
+
incomingShowsCurrentAgentPromotion)
|
|
811
|
+
);
|
|
812
|
+
},
|
|
813
|
+
{includeCollectionKeys: true, includeMainCallMediaKeys: true}
|
|
814
|
+
);
|
|
815
|
+
ownerChangeResolvedByRelatedInteraction = Boolean(task);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
let ownerChangeMainInteractionId: string | undefined;
|
|
819
|
+
if (task && eventType === CC_EVENTS.CONTACT_OWNER_CHANGED) {
|
|
820
|
+
const taskMainInteractionId = task.data?.interaction?.mainInteractionId;
|
|
821
|
+
const payloadMainInteractionId = message.data.interaction?.mainInteractionId;
|
|
822
|
+
const hasConflictingMainInteractionIds = Boolean(
|
|
823
|
+
taskMainInteractionId &&
|
|
824
|
+
payloadMainInteractionId &&
|
|
825
|
+
taskMainInteractionId !== payloadMainInteractionId
|
|
826
|
+
);
|
|
827
|
+
// A promoted child task can retain stale embedded IDs. Prefer the
|
|
828
|
+
// authoritative incoming main-call identity before local snapshot IDs.
|
|
829
|
+
const stableMainInteractionId =
|
|
830
|
+
payloadMainInteractionId ||
|
|
831
|
+
TaskManager.getMainCallMediaId(message.data.interaction) ||
|
|
832
|
+
TaskManager.getMainCallMediaId(task.data?.interaction) ||
|
|
833
|
+
taskMainInteractionId ||
|
|
834
|
+
message.data.interaction?.interactionId ||
|
|
835
|
+
task.data?.interaction?.interactionId ||
|
|
836
|
+
task.data?.interactionId;
|
|
837
|
+
const taskAtStableKey = stableMainInteractionId
|
|
838
|
+
? this.taskCollection[stableMainInteractionId]
|
|
839
|
+
: undefined;
|
|
840
|
+
|
|
841
|
+
if (
|
|
842
|
+
!stableMainInteractionId ||
|
|
843
|
+
hasConflictingMainInteractionIds ||
|
|
844
|
+
(taskAtStableKey && taskAtStableKey !== task)
|
|
845
|
+
) {
|
|
846
|
+
LoggerProxy.warn('Unable to safely correlate owner change to the main interaction task', {
|
|
847
|
+
module: TASK_MANAGER_FILE,
|
|
848
|
+
method: 'prepareEventContext',
|
|
849
|
+
interactionId:
|
|
850
|
+
payloadMainInteractionId || taskMainInteractionId || message.data.interactionId,
|
|
851
|
+
});
|
|
852
|
+
task = undefined;
|
|
853
|
+
} else if (ownerChangeResolvedByRelatedInteraction) {
|
|
854
|
+
ownerChangeMainInteractionId = stableMainInteractionId;
|
|
855
|
+
}
|
|
796
856
|
}
|
|
797
857
|
|
|
798
858
|
if (!task && MAIN_INTERACTION_CORRELATED_EVENTS.has(eventType)) {
|
|
@@ -821,8 +881,11 @@ export default class TaskManager extends EventEmitter {
|
|
|
821
881
|
}
|
|
822
882
|
: message.data;
|
|
823
883
|
|
|
824
|
-
if (task &&
|
|
825
|
-
adjustedPayload =
|
|
884
|
+
if (task && ownerChangeMainInteractionId) {
|
|
885
|
+
adjustedPayload = {
|
|
886
|
+
...adjustedPayload,
|
|
887
|
+
interactionId: ownerChangeMainInteractionId,
|
|
888
|
+
};
|
|
826
889
|
}
|
|
827
890
|
|
|
828
891
|
if (task && eventType === CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE) {
|
|
@@ -860,53 +923,6 @@ export default class TaskManager extends EventEmitter {
|
|
|
860
923
|
};
|
|
861
924
|
}
|
|
862
925
|
|
|
863
|
-
/**
|
|
864
|
-
* ContactOwnerChanged can identify the promoted agent's child interaction while
|
|
865
|
-
* the surviving task is stored under the main interaction. Accept one related
|
|
866
|
-
* task when its existing snapshot or the authoritative incoming promotion proves
|
|
867
|
-
* current-agent main-call membership, while excluding non-promoted and ambiguous matches.
|
|
868
|
-
*/
|
|
869
|
-
private findTaskForContactOwnerChange(payload: WebSocketPayload): ITask | undefined {
|
|
870
|
-
const relatedCandidates = this.findRelatedTasks(payload, {
|
|
871
|
-
includeCollectionKeys: true,
|
|
872
|
-
includeMainCallMediaKeys: true,
|
|
873
|
-
});
|
|
874
|
-
const incomingShowsCurrentAgentPromotion =
|
|
875
|
-
this.isCurrentAgentPromotedOnIncomingMainCall(payload);
|
|
876
|
-
const eligibleCandidates = relatedCandidates.filter(
|
|
877
|
-
(candidate) =>
|
|
878
|
-
this.isCurrentAgentActiveOnMainInteraction(candidate.data) ||
|
|
879
|
-
incomingShowsCurrentAgentPromotion
|
|
880
|
-
);
|
|
881
|
-
|
|
882
|
-
if (eligibleCandidates.length === 1) {
|
|
883
|
-
return eligibleCandidates[0];
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
if (eligibleCandidates.length > 1) {
|
|
887
|
-
LoggerProxy.warn('Unable to correlate task event to a unique main interaction task', {
|
|
888
|
-
module: TASK_MANAGER_FILE,
|
|
889
|
-
method: 'findTaskForContactOwnerChange',
|
|
890
|
-
interactionId:
|
|
891
|
-
payload.interaction?.mainInteractionId ||
|
|
892
|
-
payload.interaction?.parentInteractionId ||
|
|
893
|
-
payload.interaction?.callProcessingDetails?.parentInteractionId ||
|
|
894
|
-
payload.interactionId,
|
|
895
|
-
});
|
|
896
|
-
|
|
897
|
-
return undefined;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
return undefined;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
private isCurrentAgentActiveOnMainInteraction(taskData: TaskData | undefined): boolean {
|
|
904
|
-
const currentAgentId = this.agentId || taskData?.agentId;
|
|
905
|
-
if (!currentAgentId) return false;
|
|
906
|
-
|
|
907
|
-
return TaskManager.isParticipantActiveOnMainInteraction(taskData, currentAgentId);
|
|
908
|
-
}
|
|
909
|
-
|
|
910
926
|
private static isParticipantActiveOnMainInteraction(
|
|
911
927
|
taskData: TaskData | WebSocketPayload | undefined,
|
|
912
928
|
participantId: string
|
|
@@ -921,14 +937,6 @@ export default class TaskManager extends EventEmitter {
|
|
|
921
937
|
);
|
|
922
938
|
}
|
|
923
939
|
|
|
924
|
-
private isCurrentAgentPromotedOnIncomingMainCall(payload: WebSocketPayload): boolean {
|
|
925
|
-
return Boolean(
|
|
926
|
-
this.agentId &&
|
|
927
|
-
payload.interaction?.owner === this.agentId &&
|
|
928
|
-
TaskManager.isParticipantActiveOnMainInteraction(payload, this.agentId)
|
|
929
|
-
);
|
|
930
|
-
}
|
|
931
|
-
|
|
932
940
|
private static getMainCallMediaId(
|
|
933
941
|
interaction: TaskData['interaction'] | undefined
|
|
934
942
|
): string | undefined {
|
|
@@ -960,31 +968,6 @@ export default class TaskManager extends EventEmitter {
|
|
|
960
968
|
);
|
|
961
969
|
}
|
|
962
970
|
|
|
963
|
-
/** Keep a related owner-change event from re-keying the surviving main task to a child ID. */
|
|
964
|
-
private static preserveMainTaskIdentity(
|
|
965
|
-
task: ITask,
|
|
966
|
-
payload: WebSocketPayload
|
|
967
|
-
): WebSocketPayload {
|
|
968
|
-
const currentMainInteractionId = task.data?.interaction?.mainInteractionId;
|
|
969
|
-
const payloadMainInteractionId = payload.interaction?.mainInteractionId;
|
|
970
|
-
const currentMainMediaId = TaskManager.getMainCallMediaId(task.data?.interaction);
|
|
971
|
-
const payloadMainMediaId = TaskManager.getMainCallMediaId(payload.interaction);
|
|
972
|
-
const stableInteractionId =
|
|
973
|
-
currentMainInteractionId ||
|
|
974
|
-
payloadMainInteractionId ||
|
|
975
|
-
currentMainMediaId ||
|
|
976
|
-
payloadMainMediaId ||
|
|
977
|
-
payload.interaction?.interactionId ||
|
|
978
|
-
task.data?.interaction?.interactionId ||
|
|
979
|
-
task.data?.interactionId;
|
|
980
|
-
|
|
981
|
-
if (!stableInteractionId || stableInteractionId === payload.interactionId) {
|
|
982
|
-
return payload;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
return {...payload, interactionId: stableInteractionId};
|
|
986
|
-
}
|
|
987
|
-
|
|
988
971
|
/**
|
|
989
972
|
* ParticipantLeftConference may arrive after an owner update while still carrying
|
|
990
973
|
* the departed owner. Keep the confirmed owner only when the new roster proves that
|
|
@@ -1024,9 +1007,10 @@ export default class TaskManager extends EventEmitter {
|
|
|
1024
1007
|
*/
|
|
1025
1008
|
private findUniqueTaskByRelatedInteraction(
|
|
1026
1009
|
payload: WebSocketPayload,
|
|
1027
|
-
candidateFilter: (task: ITask) => boolean = () => true
|
|
1010
|
+
candidateFilter: (task: ITask) => boolean = () => true,
|
|
1011
|
+
options: {includeCollectionKeys?: boolean; includeMainCallMediaKeys?: boolean} = {}
|
|
1028
1012
|
): ITask | undefined {
|
|
1029
|
-
const candidates = this.findRelatedTasks(payload).filter(candidateFilter);
|
|
1013
|
+
const candidates = this.findRelatedTasks(payload, options).filter(candidateFilter);
|
|
1030
1014
|
|
|
1031
1015
|
if (candidates.length === 1) {
|
|
1032
1016
|
return candidates[0];
|
|
@@ -1060,7 +1044,9 @@ export default class TaskManager extends EventEmitter {
|
|
|
1060
1044
|
payload.interaction?.parentInteractionId,
|
|
1061
1045
|
payload.interaction?.callProcessingDetails?.parentInteractionId,
|
|
1062
1046
|
...(includeMainCallMediaKeys ? [TaskManager.getMainCallMediaId(payload.interaction)] : []),
|
|
1063
|
-
].filter((
|
|
1047
|
+
].filter((relatedInteractionId): relatedInteractionId is string =>
|
|
1048
|
+
Boolean(relatedInteractionId)
|
|
1049
|
+
)
|
|
1064
1050
|
);
|
|
1065
1051
|
if (payloadInteractionIds.size === 0) return [];
|
|
1066
1052
|
|
|
@@ -1082,7 +1068,8 @@ export default class TaskManager extends EventEmitter {
|
|
|
1082
1068
|
];
|
|
1083
1069
|
|
|
1084
1070
|
return candidateInteractionIds.some(
|
|
1085
|
-
(
|
|
1071
|
+
(relatedInteractionId) =>
|
|
1072
|
+
Boolean(relatedInteractionId) && payloadInteractionIds.has(relatedInteractionId)
|
|
1086
1073
|
);
|
|
1087
1074
|
})
|
|
1088
1075
|
.map(([, candidate]) => candidate)
|
|
@@ -924,15 +924,17 @@ guards. The state-machine transition table lives in
|
|
|
924
924
|
Primary-Agent promotion remains backend-authoritative and follows the two-event
|
|
925
925
|
desktop contract. `ContactOwnerChanged` updates the promoted Agent; TaskManager
|
|
926
926
|
prefers an exact task, then one unique related task resolved through nested
|
|
927
|
-
main/parent identifiers or the `mainCall` media-map
|
|
928
|
-
candidate is eligible when its current
|
|
927
|
+
main/parent identifiers, the task collection key, or the `mainCall` media-map
|
|
928
|
+
identity. A related candidate is eligible when its current
|
|
929
929
|
snapshot contains the current Agent's participant entry with `hasLeft !== true`
|
|
930
930
|
and membership on the `mType: mainCall` leg. The incoming snapshot can provide
|
|
931
931
|
that evidence only when it also names the current Agent as `interaction.owner`;
|
|
932
932
|
this permits an authoritative promotion payload to repair a stale child-keyed
|
|
933
933
|
snapshot while still excluding consult-only tasks. The update keeps the
|
|
934
934
|
surviving main interaction identity even when the notification names a
|
|
935
|
-
promoted-Agent child interaction.
|
|
935
|
+
promoted-Agent child interaction. Conflicting main identifiers, ambiguous
|
|
936
|
+
matches, and a canonical key occupied by a distinct task are ignored rather
|
|
937
|
+
than replacing an existing task.
|
|
936
938
|
|
|
937
939
|
If no related task exists, TaskManager recovers only the promoted current Agent
|
|
938
940
|
from a non-terminal telephony `ContactOwnerChanged` payload that provides the same
|
|
@@ -2927,7 +2927,7 @@ describe('TaskManager', () => {
|
|
|
2927
2927
|
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
2928
2928
|
});
|
|
2929
2929
|
|
|
2930
|
-
it('reuses a task
|
|
2930
|
+
it('reuses a task identified by the incoming main-call media key', () => {
|
|
2931
2931
|
const mainMediaInteractionId = 'main-media-interaction-id';
|
|
2932
2932
|
const promotedChildId = 'promoted-child-without-main-id';
|
|
2933
2933
|
const task = installTask(
|
|
@@ -2958,6 +2958,41 @@ describe('TaskManager', () => {
|
|
|
2958
2958
|
expect(hydrateHandler).toHaveBeenCalledWith(task);
|
|
2959
2959
|
});
|
|
2960
2960
|
|
|
2961
|
+
it('reuses a stale task through its collection key and the incoming main-call media key', () => {
|
|
2962
|
+
const mainMediaInteractionId = 'collection-main-interaction-id';
|
|
2963
|
+
const staleChildId = 'stale-collection-child-id';
|
|
2964
|
+
const promotedChildId = 'promoted-collection-child-id';
|
|
2965
|
+
const staleTaskData = createConferenceTaskData(
|
|
2966
|
+
staleChildId,
|
|
2967
|
+
previousOwnerId,
|
|
2968
|
+
staleChildId
|
|
2969
|
+
);
|
|
2970
|
+
staleTaskData.interaction.media[staleChildId].mType = 'consult';
|
|
2971
|
+
const task = createMockTask(staleTaskData);
|
|
2972
|
+
taskManager.taskCollection = {[mainMediaInteractionId]: task};
|
|
2973
|
+
(taskManager as any).setupTaskListeners(task);
|
|
2974
|
+
const hydrateHandler = jest.fn();
|
|
2975
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
2976
|
+
const ownerChangeData = createPromotedAgentOwnerChangeData(
|
|
2977
|
+
promotedChildId,
|
|
2978
|
+
mainMediaInteractionId
|
|
2979
|
+
);
|
|
2980
|
+
delete ownerChangeData.interaction.mainInteractionId;
|
|
2981
|
+
|
|
2982
|
+
webSocketManagerMock.emit('message', JSON.stringify({data: ownerChangeData}));
|
|
2983
|
+
|
|
2984
|
+
expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
|
|
2985
|
+
expect(task.data.interactionId).toBe(mainMediaInteractionId);
|
|
2986
|
+
expect(task.data.interaction.owner).toBe(currentAgentId);
|
|
2987
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
2988
|
+
expect(taskManager.taskCollection[mainMediaInteractionId]).toBe(task);
|
|
2989
|
+
expect(taskManager.taskCollection[staleChildId]).toBeUndefined();
|
|
2990
|
+
expect(taskManager.taskCollection[promotedChildId]).toBeUndefined();
|
|
2991
|
+
expect(Object.values(taskManager.taskCollection)).toEqual([task]);
|
|
2992
|
+
expect(task.listenerCount(TASK_EVENTS.TASK_HYDRATE)).toBe(1);
|
|
2993
|
+
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
2994
|
+
});
|
|
2995
|
+
|
|
2961
2996
|
it('prefers the exact task for ContactOwnerChanged over related child tasks', () => {
|
|
2962
2997
|
const exactTask = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
2963
2998
|
const childTaskId = 'related-child-id';
|
|
@@ -2980,6 +3015,102 @@ describe('TaskManager', () => {
|
|
|
2980
3015
|
expect(childTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
2981
3016
|
});
|
|
2982
3017
|
|
|
3018
|
+
it('does not replace a distinct main task when an exact child task also exists', () => {
|
|
3019
|
+
const childTaskId = 'exact-child-id';
|
|
3020
|
+
const mainTask = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
3021
|
+
const childTask = createMockTask(
|
|
3022
|
+
createConferenceTaskData(childTaskId, previousOwnerId, taskId)
|
|
3023
|
+
);
|
|
3024
|
+
taskManager.taskCollection[childTaskId] = childTask;
|
|
3025
|
+
|
|
3026
|
+
webSocketManagerMock.emit(
|
|
3027
|
+
'message',
|
|
3028
|
+
JSON.stringify({
|
|
3029
|
+
data: {
|
|
3030
|
+
...createConferenceTaskData(childTaskId, promotedOwnerId, taskId),
|
|
3031
|
+
type: CC_EVENTS.CONTACT_OWNER_CHANGED,
|
|
3032
|
+
},
|
|
3033
|
+
})
|
|
3034
|
+
);
|
|
3035
|
+
|
|
3036
|
+
expect(childTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3037
|
+
expect(mainTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3038
|
+
expect(taskManager.taskCollection[taskId]).toBe(mainTask);
|
|
3039
|
+
expect(taskManager.taskCollection[childTaskId]).toBe(childTask);
|
|
3040
|
+
});
|
|
3041
|
+
|
|
3042
|
+
it('ignores a related owner change with conflicting explicit main interaction IDs', () => {
|
|
3043
|
+
const childTaskId = 'conflicting-main-child-id';
|
|
3044
|
+
const existingMainId = 'existing-main-id';
|
|
3045
|
+
const incomingMainId = 'incoming-main-id';
|
|
3046
|
+
const sharedParentId = 'shared-parent-id';
|
|
3047
|
+
const taskData = createConferenceTaskData(
|
|
3048
|
+
childTaskId,
|
|
3049
|
+
previousOwnerId,
|
|
3050
|
+
existingMainId
|
|
3051
|
+
);
|
|
3052
|
+
taskData.interaction.parentInteractionId = sharedParentId;
|
|
3053
|
+
const task = installTask(taskData);
|
|
3054
|
+
const ownerChangeData = createConferenceTaskData(
|
|
3055
|
+
'different-child-id',
|
|
3056
|
+
promotedOwnerId,
|
|
3057
|
+
incomingMainId
|
|
3058
|
+
);
|
|
3059
|
+
ownerChangeData.interaction.parentInteractionId = sharedParentId;
|
|
3060
|
+
|
|
3061
|
+
webSocketManagerMock.emit(
|
|
3062
|
+
'message',
|
|
3063
|
+
JSON.stringify({
|
|
3064
|
+
data: {...ownerChangeData, type: CC_EVENTS.CONTACT_OWNER_CHANGED},
|
|
3065
|
+
})
|
|
3066
|
+
);
|
|
3067
|
+
|
|
3068
|
+
expect(task.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3069
|
+
expect(taskManager.taskCollection[childTaskId]).toBe(task);
|
|
3070
|
+
expect(taskManager.taskCollection[existingMainId]).toBeUndefined();
|
|
3071
|
+
expect(taskManager.taskCollection[incomingMainId]).toBeUndefined();
|
|
3072
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3073
|
+
});
|
|
3074
|
+
|
|
3075
|
+
it('does not replace a distinct task already stored at the canonical main key', () => {
|
|
3076
|
+
const childTaskId = 'related-active-child-id';
|
|
3077
|
+
const canonicalMainId = 'occupied-canonical-main-id';
|
|
3078
|
+
const candidate = createMockTask(
|
|
3079
|
+
createConferenceTaskData(childTaskId, previousOwnerId, canonicalMainId)
|
|
3080
|
+
);
|
|
3081
|
+
const occupiedData = createConferenceTaskData(
|
|
3082
|
+
'occupied-task-child-id',
|
|
3083
|
+
previousOwnerId,
|
|
3084
|
+
canonicalMainId
|
|
3085
|
+
);
|
|
3086
|
+
occupiedData.interaction.media[canonicalMainId].mType = 'consult';
|
|
3087
|
+
const occupiedTask = createMockTask(occupiedData);
|
|
3088
|
+
taskManager.taskCollection = {
|
|
3089
|
+
[childTaskId]: candidate,
|
|
3090
|
+
[canonicalMainId]: occupiedTask,
|
|
3091
|
+
};
|
|
3092
|
+
|
|
3093
|
+
webSocketManagerMock.emit(
|
|
3094
|
+
'message',
|
|
3095
|
+
JSON.stringify({
|
|
3096
|
+
data: {
|
|
3097
|
+
...createConferenceTaskData(
|
|
3098
|
+
'incoming-related-child-id',
|
|
3099
|
+
promotedOwnerId,
|
|
3100
|
+
canonicalMainId
|
|
3101
|
+
),
|
|
3102
|
+
type: CC_EVENTS.CONTACT_OWNER_CHANGED,
|
|
3103
|
+
},
|
|
3104
|
+
})
|
|
3105
|
+
);
|
|
3106
|
+
|
|
3107
|
+
expect(candidate.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3108
|
+
expect(occupiedTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3109
|
+
expect(taskManager.taskCollection[childTaskId]).toBe(candidate);
|
|
3110
|
+
expect(taskManager.taskCollection[canonicalMainId]).toBe(occupiedTask);
|
|
3111
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3112
|
+
});
|
|
3113
|
+
|
|
2983
3114
|
it('ignores a related ContactOwnerChanged task whose current agent is consult-only', () => {
|
|
2984
3115
|
const childTaskId = 'consult-only-child-id';
|
|
2985
3116
|
const data = createConferenceTaskData(childTaskId, previousOwnerId, taskId);
|
|
@@ -3066,7 +3197,43 @@ describe('TaskManager', () => {
|
|
|
3066
3197
|
expect(secondTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3067
3198
|
});
|
|
3068
3199
|
|
|
3069
|
-
it('
|
|
3200
|
+
it('ignores ambiguous main-call media-key matches without creating a task', () => {
|
|
3201
|
+
const mainMediaInteractionId = 'ambiguous-main-media-id';
|
|
3202
|
+
const createMediaRelatedTask = (interactionId: string) => {
|
|
3203
|
+
const data = createConferenceTaskData(interactionId, previousOwnerId, interactionId);
|
|
3204
|
+
data.interaction.media = {
|
|
3205
|
+
[mainMediaInteractionId]: {
|
|
3206
|
+
mediaResourceId: mainMediaInteractionId,
|
|
3207
|
+
mediaType: 'telephony',
|
|
3208
|
+
mType: 'mainCall',
|
|
3209
|
+
participants: [currentAgentId, previousOwnerId, promotedOwnerId],
|
|
3210
|
+
},
|
|
3211
|
+
};
|
|
3212
|
+
|
|
3213
|
+
return createMockTask(data);
|
|
3214
|
+
};
|
|
3215
|
+
const firstTask = createMediaRelatedTask('first-media-child-id');
|
|
3216
|
+
const secondTask = createMediaRelatedTask('second-media-child-id');
|
|
3217
|
+
taskManager.taskCollection = {
|
|
3218
|
+
'first-media-child-id': firstTask,
|
|
3219
|
+
'second-media-child-id': secondTask,
|
|
3220
|
+
};
|
|
3221
|
+
(TaskFactory.createTask as jest.Mock).mockClear();
|
|
3222
|
+
const ownerChangeData = createPromotedAgentOwnerChangeData(
|
|
3223
|
+
'promoted-media-child-id',
|
|
3224
|
+
mainMediaInteractionId
|
|
3225
|
+
);
|
|
3226
|
+
delete ownerChangeData.interaction.mainInteractionId;
|
|
3227
|
+
|
|
3228
|
+
webSocketManagerMock.emit('message', JSON.stringify({data: ownerChangeData}));
|
|
3229
|
+
|
|
3230
|
+
expect(firstTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3231
|
+
expect(secondTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3232
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3233
|
+
expect(Object.values(taskManager.taskCollection)).toEqual([firstTask, secondTask]);
|
|
3234
|
+
});
|
|
3235
|
+
|
|
3236
|
+
it('does not recover a new task when multiple stale related tasks exist', () => {
|
|
3070
3237
|
const createStaleRelatedTask = (interactionId: string) => {
|
|
3071
3238
|
const data = createConferenceTaskData(interactionId, previousOwnerId, taskId);
|
|
3072
3239
|
data.interaction.media[taskId].participants = [previousOwnerId, promotedOwnerId];
|
|
@@ -3178,7 +3345,10 @@ describe('TaskManager', () => {
|
|
|
3178
3345
|
expect.objectContaining({
|
|
3179
3346
|
type: TaskEvent.HYDRATE,
|
|
3180
3347
|
agentId: currentAgentId,
|
|
3181
|
-
taskData: expect.objectContaining({
|
|
3348
|
+
taskData: expect.objectContaining({
|
|
3349
|
+
interactionId: taskId,
|
|
3350
|
+
interaction: expect.objectContaining({state: 'conference'}),
|
|
3351
|
+
}),
|
|
3182
3352
|
})
|
|
3183
3353
|
);
|
|
3184
3354
|
expect(recoveredTask.sendStateMachineEvent).toHaveBeenNthCalledWith(
|
|
@@ -3192,6 +3362,7 @@ describe('TaskManager', () => {
|
|
|
3192
3362
|
expect(taskManager.taskCollection['promoted-child-interaction-id']).toBeUndefined();
|
|
3193
3363
|
expect(Object.values(taskManager.taskCollection)).toEqual([recoveredTask]);
|
|
3194
3364
|
expect(recoveredTask.data.interaction.owner).toBe(currentAgentId);
|
|
3365
|
+
expect(recoveredTask.listenerCount(TASK_EVENTS.TASK_HYDRATE)).toBe(1);
|
|
3195
3366
|
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
3196
3367
|
expect(hydrateHandler).toHaveBeenCalledWith(recoveredTask);
|
|
3197
3368
|
expect(incomingHandler).not.toHaveBeenCalled();
|
|
@@ -3217,6 +3388,21 @@ describe('TaskManager', () => {
|
|
|
3217
3388
|
data.interaction.isTerminated = true;
|
|
3218
3389
|
},
|
|
3219
3390
|
],
|
|
3391
|
+
[
|
|
3392
|
+
'the interaction is not telephony',
|
|
3393
|
+
(data) => {
|
|
3394
|
+
data.interaction.mediaType = 'chat';
|
|
3395
|
+
},
|
|
3396
|
+
],
|
|
3397
|
+
[
|
|
3398
|
+
'the payload has no stable interaction identifier',
|
|
3399
|
+
(data) => {
|
|
3400
|
+
data.interactionId = undefined;
|
|
3401
|
+
data.interaction.interactionId = undefined;
|
|
3402
|
+
delete data.interaction.mainInteractionId;
|
|
3403
|
+
data.interaction.media = {};
|
|
3404
|
+
},
|
|
3405
|
+
],
|
|
3220
3406
|
])('does not create a missing task when %s', (_description, mutatePayload) => {
|
|
3221
3407
|
clearTasksAndFactoryHistory();
|
|
3222
3408
|
const ownerChangeData = createPromotedAgentOwnerChangeData();
|
|
@@ -539,6 +539,39 @@ describe('Task state machine', () => {
|
|
|
539
539
|
} as any,
|
|
540
540
|
});
|
|
541
541
|
|
|
542
|
+
it('hydrates a recovered active conference from IDLE to CONFERENCING', () => {
|
|
543
|
+
const service = startMachine();
|
|
544
|
+
const taskData = createTaskData({
|
|
545
|
+
agentId: 'agent-1',
|
|
546
|
+
interactionId: 'interaction-1',
|
|
547
|
+
mediaResourceId: 'interaction-1',
|
|
548
|
+
interaction: {
|
|
549
|
+
state: 'conference',
|
|
550
|
+
interactionId: 'interaction-1',
|
|
551
|
+
mainInteractionId: 'interaction-1',
|
|
552
|
+
owner: 'agent-1',
|
|
553
|
+
participants: {
|
|
554
|
+
'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
|
|
555
|
+
'agent-2': {id: 'agent-2', pType: 'Agent', hasLeft: false},
|
|
556
|
+
'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
|
|
557
|
+
},
|
|
558
|
+
media: {
|
|
559
|
+
'interaction-1': {
|
|
560
|
+
mediaResourceId: 'interaction-1',
|
|
561
|
+
mType: 'mainCall',
|
|
562
|
+
participants: ['agent-1', 'agent-2', 'customer-1'],
|
|
563
|
+
isHold: false,
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
} as any,
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
service.send({type: TaskEvent.HYDRATE, taskData});
|
|
570
|
+
|
|
571
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
|
|
572
|
+
expect(service.getSnapshot().context.taskData).toEqual(taskData);
|
|
573
|
+
});
|
|
574
|
+
|
|
542
575
|
it('boots from IDLE to CONSULTING on CONSULTING_ACTIVE for split-leg ordering', () => {
|
|
543
576
|
const service = startMachine();
|
|
544
577
|
const taskData = createTaskData({
|