@webex/contact-center 3.12.0-next.72 → 3.12.0-next.74
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/AGENTS.md +438 -0
- package/ai-docs/README.md +131 -0
- package/ai-docs/RULES.md +455 -0
- package/ai-docs/patterns/event-driven-patterns.md +485 -0
- package/ai-docs/patterns/testing-patterns.md +480 -0
- package/ai-docs/patterns/typescript-patterns.md +365 -0
- package/ai-docs/templates/README.md +102 -0
- package/ai-docs/templates/documentation/create-agents-md.md +240 -0
- package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
- package/ai-docs/templates/existing-service/bug-fix.md +254 -0
- package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
- package/ai-docs/templates/new-method/00-master.md +80 -0
- package/ai-docs/templates/new-method/01-requirements.md +232 -0
- package/ai-docs/templates/new-method/02-implementation.md +295 -0
- package/ai-docs/templates/new-method/03-tests.md +201 -0
- package/ai-docs/templates/new-method/04-validation.md +141 -0
- package/ai-docs/templates/new-service/00-master.md +109 -0
- package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
- package/ai-docs/templates/new-service/02-code-generation.md +346 -0
- package/ai-docs/templates/new-service/03-integration.md +178 -0
- package/ai-docs/templates/new-service/04-test-generation.md +205 -0
- package/ai-docs/templates/new-service/05-validation.md +145 -0
- package/dist/cc.js +53 -38
- package/dist/cc.js.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +14 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.js +13 -5
- package/dist/index.js.map +1 -1
- package/dist/metrics/behavioral-events.js +26 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +9 -3
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/ApiAiAssistant.js +74 -3
- package/dist/services/ApiAiAssistant.js.map +1 -1
- package/dist/services/config/Util.js +2 -2
- package/dist/services/config/Util.js.map +1 -1
- package/dist/services/config/types.js +25 -9
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/core/Utils.js +74 -27
- package/dist/services/core/Utils.js.map +1 -1
- package/dist/services/core/websocket/WebSocketManager.js +2 -1
- package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
- package/dist/services/core/websocket/types.js.map +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/index.js.map +1 -1
- package/dist/services/task/Task.js +688 -0
- package/dist/services/task/Task.js.map +1 -0
- package/dist/services/task/TaskFactory.js +45 -0
- package/dist/services/task/TaskFactory.js.map +1 -0
- package/dist/services/task/TaskManager.js +725 -612
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/TaskUtils.js +162 -26
- package/dist/services/task/TaskUtils.js.map +1 -1
- package/dist/services/task/constants.js +6 -1
- package/dist/services/task/constants.js.map +1 -1
- package/dist/services/task/digital/Digital.js +77 -0
- package/dist/services/task/digital/Digital.js.map +1 -0
- package/dist/services/task/state-machine/TaskStateMachine.js +837 -0
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
- package/dist/services/task/state-machine/actions.js +543 -0
- package/dist/services/task/state-machine/actions.js.map +1 -0
- package/dist/services/task/state-machine/constants.js +161 -0
- package/dist/services/task/state-machine/constants.js.map +1 -0
- package/dist/services/task/state-machine/guards.js +340 -0
- package/dist/services/task/state-machine/guards.js.map +1 -0
- package/dist/services/task/state-machine/index.js +53 -0
- package/dist/services/task/state-machine/index.js.map +1 -0
- package/dist/services/task/state-machine/types.js +54 -0
- package/dist/services/task/state-machine/types.js.map +1 -0
- package/dist/services/task/state-machine/uiControlsComputer.js +553 -0
- package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
- package/dist/services/task/taskDataNormalizer.js +99 -0
- package/dist/services/task/taskDataNormalizer.js.map +1 -0
- package/dist/services/task/types.js +168 -16
- package/dist/services/task/types.js.map +1 -1
- package/dist/services/task/voice/Voice.js +1042 -0
- package/dist/services/task/voice/Voice.js.map +1 -0
- package/dist/services/task/voice/WebRTC.js +149 -0
- package/dist/services/task/voice/WebRTC.js.map +1 -0
- package/dist/types/cc.d.ts +19 -18
- package/dist/types/config.d.ts +6 -0
- package/dist/types/constants.d.ts +14 -1
- package/dist/types/index.d.ts +11 -5
- package/dist/types/metrics/constants.d.ts +7 -1
- package/dist/types/services/ApiAiAssistant.d.ts +11 -3
- package/dist/types/services/config/types.d.ts +113 -9
- package/dist/types/services/core/Utils.d.ts +23 -10
- package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
- package/dist/types/services/core/websocket/types.d.ts +1 -1
- package/dist/types/services/index.d.ts +1 -1
- package/dist/types/services/task/Task.d.ts +157 -0
- package/dist/types/services/task/TaskFactory.d.ts +12 -0
- package/dist/types/services/task/TaskUtils.d.ts +46 -2
- package/dist/types/services/task/constants.d.ts +5 -0
- package/dist/types/services/task/digital/Digital.d.ts +22 -0
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1144 -0
- package/dist/types/services/task/state-machine/actions.d.ts +10 -0
- package/dist/types/services/task/state-machine/constants.d.ts +107 -0
- package/dist/types/services/task/state-machine/guards.d.ts +90 -0
- package/dist/types/services/task/state-machine/index.d.ts +13 -0
- package/dist/types/services/task/state-machine/types.d.ts +267 -0
- package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
- package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
- package/dist/types/services/task/types.d.ts +551 -78
- package/dist/types/services/task/voice/Voice.d.ts +184 -0
- package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
- package/dist/types/types.d.ts +92 -0
- package/dist/types/webex.d.ts +1 -0
- package/dist/types.js +85 -0
- package/dist/types.js.map +1 -1
- package/dist/webex.js +14 -2
- package/dist/webex.js.map +1 -1
- package/package.json +15 -12
- package/src/cc.ts +63 -43
- package/src/config.ts +6 -0
- package/src/constants.ts +14 -1
- package/src/index.ts +14 -5
- package/src/metrics/ai-docs/AGENTS.md +348 -0
- package/src/metrics/ai-docs/ARCHITECTURE.md +336 -0
- package/src/metrics/behavioral-events.ts +28 -0
- package/src/metrics/constants.ts +9 -4
- package/src/services/ApiAiAssistant.ts +104 -3
- package/src/services/agent/ai-docs/AGENTS.md +238 -0
- package/src/services/agent/ai-docs/ARCHITECTURE.md +302 -0
- package/src/services/ai-docs/AGENTS.md +384 -0
- package/src/services/config/Util.ts +2 -2
- package/src/services/config/ai-docs/AGENTS.md +253 -0
- package/src/services/config/ai-docs/ARCHITECTURE.md +424 -0
- package/src/services/config/types.ts +116 -10
- package/src/services/core/Utils.ts +85 -34
- package/src/services/core/ai-docs/AGENTS.md +379 -0
- package/src/services/core/ai-docs/ARCHITECTURE.md +696 -0
- package/src/services/core/websocket/WebSocketManager.ts +2 -0
- package/src/services/core/websocket/types.ts +1 -1
- package/src/services/index.ts +1 -1
- package/src/services/task/Task.ts +837 -0
- package/src/services/task/TaskFactory.ts +55 -0
- package/src/services/task/TaskManager.ts +738 -697
- package/src/services/task/TaskUtils.ts +205 -25
- package/src/services/task/ai-docs/AGENTS.md +455 -0
- package/src/services/task/ai-docs/ARCHITECTURE.md +585 -0
- package/src/services/task/constants.ts +5 -0
- package/src/services/task/digital/Digital.ts +95 -0
- package/src/services/task/state-machine/TaskStateMachine.ts +1077 -0
- package/src/services/task/state-machine/actions.ts +685 -0
- package/src/services/task/state-machine/ai-docs/AGENTS.md +495 -0
- package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1135 -0
- package/src/services/task/state-machine/constants.ts +172 -0
- package/src/services/task/state-machine/guards.ts +406 -0
- package/src/services/task/state-machine/index.ts +28 -0
- package/src/services/task/state-machine/types.ts +241 -0
- package/src/services/task/state-machine/uiControlsComputer.ts +867 -0
- package/src/services/task/taskDataNormalizer.ts +137 -0
- package/src/services/task/types.ts +654 -85
- package/src/services/task/voice/Voice.ts +1267 -0
- package/src/services/task/voice/WebRTC.ts +187 -0
- package/src/types.ts +112 -1
- package/src/utils/AGENTS.md +276 -0
- package/src/webex.js +2 -0
- package/test/unit/spec/cc.ts +133 -3
- package/test/unit/spec/logger-proxy.ts +70 -0
- package/test/unit/spec/services/ApiAiAssistant.ts +122 -17
- package/test/unit/spec/services/WebCallingService.ts +7 -1
- package/test/unit/spec/services/config/index.ts +27 -27
- package/test/unit/spec/services/core/Utils.ts +335 -1
- package/test/unit/spec/services/core/websocket/WebSocketManager.ts +66 -40
- package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
- package/test/unit/spec/services/task/Task.ts +477 -0
- package/test/unit/spec/services/task/TaskFactory.ts +62 -0
- package/test/unit/spec/services/task/TaskManager.ts +821 -1936
- package/test/unit/spec/services/task/TaskUtils.ts +206 -0
- package/test/unit/spec/services/task/digital/Digital.ts +105 -0
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +1825 -0
- package/test/unit/spec/services/task/state-machine/guards.ts +479 -0
- package/test/unit/spec/services/task/state-machine/types.ts +18 -0
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +2020 -0
- package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
- package/test/unit/spec/services/task/voice/Voice.ts +631 -0
- package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
- package/dist/services/task/index.js +0 -1530
- package/dist/services/task/index.js.map +0 -1
- package/dist/types/services/task/index.d.ts +0 -650
- package/src/services/task/index.ts +0 -1806
- package/test/unit/spec/services/task/index.ts +0 -2205
|
@@ -5,16 +5,21 @@ import {CALL_EVENT_KEYS, CallingClientConfig, LINE_EVENTS} from '@webex/calling'
|
|
|
5
5
|
import {CC_AGENT_EVENTS, CC_EVENTS} from '../../../../../src/services/config/types';
|
|
6
6
|
import TaskManager from '../../../../../src/services/task/TaskManager';
|
|
7
7
|
import * as contact from '../../../../../src/services/task/contact';
|
|
8
|
-
import Task from '../../../../../src/services/task';
|
|
8
|
+
import Task from '../../../../../src/services/task/Task';
|
|
9
9
|
import {TASK_EVENTS} from '../../../../../src/services/task/types';
|
|
10
|
+
import {TaskEvent} from '../../../../../src/services/task/state-machine';
|
|
11
|
+
import WebRTC from '../../../../../src/services/task/voice/WebRTC';
|
|
12
|
+
import {Profile} from '../../../../../src/services/config/types';
|
|
10
13
|
import WebCallingService from '../../../../../src/services/WebCallingService';
|
|
11
14
|
import config from '../../../../../src/config';
|
|
12
15
|
import {CC_TASK_EVENTS} from '../../../../../src/services/config/types';
|
|
16
|
+
import TaskFactory from '../../../../../src/services/task/TaskFactory';
|
|
13
17
|
|
|
14
18
|
describe('TaskManager', () => {
|
|
15
19
|
let mockCall;
|
|
16
20
|
let mockApiAIAssistant;
|
|
17
21
|
let webSocketManagerMock;
|
|
22
|
+
let rtdWebSocketManagerMock;
|
|
18
23
|
let onSpy;
|
|
19
24
|
let offSpy;
|
|
20
25
|
let taskManager;
|
|
@@ -24,6 +29,108 @@ describe('TaskManager', () => {
|
|
|
24
29
|
let webex: WebexSDK;
|
|
25
30
|
const taskId = '0ae913a4-c857-4705-8d49-76dd3dde75e4';
|
|
26
31
|
|
|
32
|
+
const createMockTask = (data = taskDataMock) => {
|
|
33
|
+
const task = new EventEmitter() as any;
|
|
34
|
+
|
|
35
|
+
const updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
36
|
+
task.data = {...task.data, ...newData};
|
|
37
|
+
return task;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
Object.assign(task, {
|
|
41
|
+
data,
|
|
42
|
+
accept: jest.fn(),
|
|
43
|
+
decline: jest.fn(),
|
|
44
|
+
updateTaskData,
|
|
45
|
+
unregisterWebCallListeners: jest.fn(),
|
|
46
|
+
cancelAutoWrapupTimer: jest.fn(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const taskEventMap: Partial<Record<TaskEvent, string>> = {
|
|
50
|
+
[TaskEvent.TASK_INCOMING]: TASK_EVENTS.TASK_INCOMING,
|
|
51
|
+
[TaskEvent.TASK_OFFERED]: TASK_EVENTS.TASK_OFFER_CONTACT,
|
|
52
|
+
[TaskEvent.OFFER_CONSULT]: TASK_EVENTS.TASK_OFFER_CONSULT,
|
|
53
|
+
[TaskEvent.HYDRATE]: TASK_EVENTS.TASK_HYDRATE,
|
|
54
|
+
[TaskEvent.ASSIGN]: TASK_EVENTS.TASK_ASSIGNED,
|
|
55
|
+
[TaskEvent.HOLD_SUCCESS]: TASK_EVENTS.TASK_HOLD,
|
|
56
|
+
[TaskEvent.UNHOLD_SUCCESS]: TASK_EVENTS.TASK_RESUME,
|
|
57
|
+
[TaskEvent.CONSULT_CREATED]: TASK_EVENTS.TASK_CONSULT_CREATED,
|
|
58
|
+
[TaskEvent.CONSULTING_ACTIVE]: TASK_EVENTS.TASK_CONSULT_ACCEPTED,
|
|
59
|
+
[TaskEvent.CONSULT_END]: TASK_EVENTS.TASK_CONSULT_END,
|
|
60
|
+
[TaskEvent.CONSULT_FAILED]: CC_EVENTS.AGENT_CONSULT_FAILED,
|
|
61
|
+
[TaskEvent.CTQ_CANCEL]: TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED,
|
|
62
|
+
[TaskEvent.CTQ_CANCEL_FAILED]: TASK_EVENTS.TASK_CONSULT_QUEUE_FAILED,
|
|
63
|
+
[TaskEvent.END]: TASK_EVENTS.TASK_END,
|
|
64
|
+
[TaskEvent.CONTACT_ENDED]: TASK_EVENTS.TASK_END,
|
|
65
|
+
[TaskEvent.ASSIGN_FAILED]: TASK_EVENTS.TASK_REJECT,
|
|
66
|
+
[TaskEvent.INVITE_FAILED]: TASK_EVENTS.TASK_REJECT,
|
|
67
|
+
[TaskEvent.RONA]: TASK_EVENTS.TASK_REJECT,
|
|
68
|
+
[TaskEvent.OUTBOUND_FAILED]: TASK_EVENTS.TASK_OUTDIAL_FAILED,
|
|
69
|
+
[TaskEvent.RECORDING_STARTED]: TASK_EVENTS.TASK_RECORDING_STARTED,
|
|
70
|
+
[TaskEvent.PAUSE_RECORDING]: TASK_EVENTS.TASK_RECORDING_PAUSED,
|
|
71
|
+
[TaskEvent.RESUME_RECORDING]: TASK_EVENTS.TASK_RECORDING_RESUMED,
|
|
72
|
+
[TaskEvent.WRAPUP_COMPLETE]: TASK_EVENTS.TASK_WRAPPEDUP,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
task.sendStateMachineEvent = jest.fn().mockImplementation((event) => {
|
|
76
|
+
if (event.taskData) {
|
|
77
|
+
task.updateTaskData(event.taskData);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const mappedEvent = taskEventMap[event.type as TaskEvent];
|
|
81
|
+
if (mappedEvent) {
|
|
82
|
+
if (
|
|
83
|
+
[TaskEvent.ASSIGN_FAILED, TaskEvent.RONA, TaskEvent.INVITE_FAILED].includes(
|
|
84
|
+
event.type as TaskEvent
|
|
85
|
+
)
|
|
86
|
+
) {
|
|
87
|
+
task.emit(mappedEvent, event.reason ?? event.taskData?.reason);
|
|
88
|
+
} else if (event.type === TaskEvent.OUTBOUND_FAILED) {
|
|
89
|
+
task.emit(mappedEvent, event.reason);
|
|
90
|
+
} else {
|
|
91
|
+
task.emit(mappedEvent, task);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if ([TaskEvent.ASSIGN, TaskEvent.CONSULTING_ACTIVE].includes(event.type as TaskEvent)) {
|
|
96
|
+
task.emit(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, task);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Auto-answer is now handled at the Task layer (triggered by state machine actions)
|
|
100
|
+
if (
|
|
101
|
+
[TaskEvent.TASK_OFFERED, TaskEvent.OFFER_CONSULT].includes(event.type as TaskEvent) &&
|
|
102
|
+
(event.taskData?.isAutoAnswering === true || event.taskData?.isAutoAnswering === 'true')
|
|
103
|
+
) {
|
|
104
|
+
Promise.resolve(task.accept())
|
|
105
|
+
.then(() => {
|
|
106
|
+
task.emit(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
|
|
107
|
+
})
|
|
108
|
+
.catch(() => undefined);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Cleanup is now emitted by state machine actions (Task layer).
|
|
112
|
+
// Simulate the TASK_CLEANUP emission for unit tests using mock tasks.
|
|
113
|
+
const eventType = event.type as TaskEvent;
|
|
114
|
+
const shouldCleanup =
|
|
115
|
+
eventType === TaskEvent.CONTACT_ENDED ||
|
|
116
|
+
eventType === TaskEvent.END ||
|
|
117
|
+
eventType === TaskEvent.TASK_WRAPUP ||
|
|
118
|
+
eventType === TaskEvent.WRAPUP_COMPLETE ||
|
|
119
|
+
eventType === TaskEvent.ASSIGN_FAILED ||
|
|
120
|
+
eventType === TaskEvent.INVITE_FAILED ||
|
|
121
|
+
eventType === TaskEvent.RONA ||
|
|
122
|
+
eventType === TaskEvent.OUTBOUND_FAILED ||
|
|
123
|
+
(eventType === TaskEvent.CONSULT_END && task.data?.isConsulted === true);
|
|
124
|
+
|
|
125
|
+
if (shouldCleanup) {
|
|
126
|
+
const removeFromCollection = eventType !== TaskEvent.CONTACT_ENDED;
|
|
127
|
+
task.emit(TASK_EVENTS.TASK_CLEANUP, task, {removeFromCollection});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return task;
|
|
132
|
+
};
|
|
133
|
+
|
|
27
134
|
taskDataMock = {
|
|
28
135
|
type: CC_EVENTS.AGENT_CONTACT_RESERVED,
|
|
29
136
|
agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
|
|
@@ -43,18 +150,21 @@ describe('TaskManager', () => {
|
|
|
43
150
|
data: taskDataMock,
|
|
44
151
|
};
|
|
45
152
|
|
|
153
|
+
const expectLastStateMachineEvent = (
|
|
154
|
+
spy: jest.SpyInstance | jest.Mock,
|
|
155
|
+
expectedType: TaskEvent
|
|
156
|
+
) => {
|
|
157
|
+
expect(spy).toHaveBeenCalled();
|
|
158
|
+
const lastCall = spy.mock.calls[spy.mock.calls.length - 1] || [];
|
|
159
|
+
const event = lastCall[3]?.type ? lastCall[3] : lastCall[0];
|
|
160
|
+
expect(event?.type).toBe(expectedType);
|
|
161
|
+
return event;
|
|
162
|
+
};
|
|
163
|
+
|
|
46
164
|
beforeEach(() => {
|
|
47
165
|
contactMock = contact;
|
|
48
166
|
webSocketManagerMock = new EventEmitter();
|
|
49
|
-
|
|
50
|
-
sendEvent: jest.fn().mockResolvedValue({}),
|
|
51
|
-
setAIFeatureFlags: jest.fn(),
|
|
52
|
-
aiFeature: {
|
|
53
|
-
realtimeTranscripts: {
|
|
54
|
-
enable: true,
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
};
|
|
167
|
+
rtdWebSocketManagerMock = new EventEmitter();
|
|
58
168
|
|
|
59
169
|
webex = {
|
|
60
170
|
logger: {
|
|
@@ -84,25 +194,25 @@ describe('TaskManager', () => {
|
|
|
84
194
|
onSpy = jest.spyOn(webCallingService, 'on');
|
|
85
195
|
offSpy = jest.spyOn(webCallingService, 'off');
|
|
86
196
|
|
|
197
|
+
mockApiAIAssistant = {
|
|
198
|
+
sendEvent: jest.fn().mockResolvedValue({}),
|
|
199
|
+
};
|
|
200
|
+
|
|
87
201
|
taskManager = new TaskManager(
|
|
88
|
-
mockApiAIAssistant,
|
|
202
|
+
mockApiAIAssistant as any,
|
|
89
203
|
contactMock,
|
|
90
204
|
webCallingService,
|
|
91
|
-
webSocketManagerMock
|
|
205
|
+
webSocketManagerMock as any,
|
|
206
|
+
rtdWebSocketManagerMock as any
|
|
92
207
|
);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
accept: jest.fn(),
|
|
96
|
-
decline: jest.fn(),
|
|
97
|
-
updateTaskData: jest.fn().mockImplementation((updatedData) => {
|
|
98
|
-
taskMock.data = {...taskMock.data, ...updatedData};
|
|
99
|
-
return taskMock;
|
|
100
|
-
}),
|
|
101
|
-
data: taskDataMock,
|
|
102
|
-
};
|
|
103
|
-
taskManager.taskCollection[taskId] = taskMock;
|
|
104
|
-
taskManager.agentId = 'test-agent-id';
|
|
208
|
+
taskManager.taskCollection[taskId] = createMockTask(taskDataMock);
|
|
209
|
+
(taskManager as any).setupTaskListeners?.(taskManager.taskCollection[taskId]);
|
|
105
210
|
taskManager.call = mockCall;
|
|
211
|
+
taskManager.setAgentId('test-agent-id');
|
|
212
|
+
|
|
213
|
+
jest
|
|
214
|
+
.spyOn(TaskFactory, 'createTask')
|
|
215
|
+
.mockImplementation((contact, webCallingService, data, configFlags) => createMockTask(data));
|
|
106
216
|
});
|
|
107
217
|
|
|
108
218
|
afterEach(() => {
|
|
@@ -113,7 +223,8 @@ describe('TaskManager', () => {
|
|
|
113
223
|
it('should initialize TaskManager and register listeners', () => {
|
|
114
224
|
webSocketManagerMock.emit('message', JSON.stringify({data: taskDataMock}));
|
|
115
225
|
const incomingCallCb = onSpy.mock.calls[0][1];
|
|
116
|
-
const
|
|
226
|
+
const incomingHandler = jest.fn();
|
|
227
|
+
taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
|
|
117
228
|
|
|
118
229
|
expect(taskManager).toBeInstanceOf(TaskManager);
|
|
119
230
|
expect(webCallingService.listenerCount(LINE_EVENTS.INCOMING_CALL)).toBe(1);
|
|
@@ -122,10 +233,8 @@ describe('TaskManager', () => {
|
|
|
122
233
|
|
|
123
234
|
incomingCallCb(mockCall);
|
|
124
235
|
|
|
125
|
-
expect(
|
|
126
|
-
|
|
127
|
-
taskManager.getTask(taskId)
|
|
128
|
-
);
|
|
236
|
+
expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(taskId));
|
|
237
|
+
taskManager.off(TASK_EVENTS.TASK_INCOMING, incomingHandler);
|
|
129
238
|
});
|
|
130
239
|
|
|
131
240
|
it('should re-emit task related events', () => {
|
|
@@ -141,15 +250,32 @@ describe('TaskManager', () => {
|
|
|
141
250
|
|
|
142
251
|
webSocketManagerMock.emit('message', JSON.stringify(dummyPayload));
|
|
143
252
|
|
|
144
|
-
expect(taskEmitSpy).toHaveBeenCalledWith(
|
|
253
|
+
expect(taskEmitSpy).toHaveBeenCalledWith(
|
|
254
|
+
TASK_EVENTS.TASK_CONSULT_ACCEPTED,
|
|
255
|
+
taskManager.getTask(taskId)
|
|
256
|
+
);
|
|
145
257
|
});
|
|
146
258
|
|
|
147
259
|
it('should invoke sendEvent for configured start/stop backend events', () => {
|
|
260
|
+
taskManager.setConfigFlags({
|
|
261
|
+
isEndTaskEnabled: true,
|
|
262
|
+
isEndConsultEnabled: true,
|
|
263
|
+
webRtcEnabled: true,
|
|
264
|
+
autoWrapup: false,
|
|
265
|
+
aiFeature: {
|
|
266
|
+
id: 'ai-feature-1',
|
|
267
|
+
realtimeTranscripts: {
|
|
268
|
+
enable: true,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const interactionId = taskId;
|
|
148
274
|
const message = (type: CC_EVENTS) =>
|
|
149
275
|
JSON.stringify({
|
|
150
276
|
data: {
|
|
151
277
|
...taskDataMock,
|
|
152
|
-
|
|
278
|
+
interactionId,
|
|
153
279
|
type,
|
|
154
280
|
},
|
|
155
281
|
});
|
|
@@ -158,67 +284,114 @@ describe('TaskManager', () => {
|
|
|
158
284
|
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
|
|
159
285
|
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_CONFERENCED));
|
|
160
286
|
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_ENDED));
|
|
161
|
-
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_WRAPUP));
|
|
162
287
|
webSocketManagerMock.emit('message', message(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE));
|
|
288
|
+
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_WRAPUP));
|
|
163
289
|
|
|
164
290
|
expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledTimes(6);
|
|
165
291
|
expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledWith(
|
|
166
292
|
'test-agent-id',
|
|
167
|
-
|
|
293
|
+
interactionId,
|
|
168
294
|
'CUSTOM_EVENT',
|
|
169
295
|
'GET_TRANSCRIPTS',
|
|
170
296
|
'START'
|
|
171
297
|
);
|
|
172
298
|
expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledWith(
|
|
173
299
|
'test-agent-id',
|
|
174
|
-
|
|
300
|
+
interactionId,
|
|
175
301
|
'CUSTOM_EVENT',
|
|
176
302
|
'GET_TRANSCRIPTS',
|
|
177
303
|
'STOP'
|
|
178
304
|
);
|
|
179
305
|
});
|
|
180
306
|
|
|
181
|
-
it('should not invoke sendEvent
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
307
|
+
it('should not invoke sendEvent when realtime transcripts are disabled in aiFeature', () => {
|
|
308
|
+
taskManager.setConfigFlags({
|
|
309
|
+
isEndTaskEnabled: true,
|
|
310
|
+
isEndConsultEnabled: true,
|
|
311
|
+
webRtcEnabled: true,
|
|
312
|
+
autoWrapup: false,
|
|
313
|
+
aiFeature: {
|
|
314
|
+
id: 'ai-feature-1',
|
|
315
|
+
realtimeTranscripts: {
|
|
316
|
+
enable: false,
|
|
317
|
+
},
|
|
185
318
|
},
|
|
186
|
-
};
|
|
187
|
-
mockApiAIAssistant.setAIFeatureFlags(mockApiAIAssistant.aiFeature);
|
|
319
|
+
});
|
|
188
320
|
|
|
189
321
|
const message = (type: CC_EVENTS) =>
|
|
190
322
|
JSON.stringify({
|
|
191
323
|
data: {
|
|
192
324
|
...taskDataMock,
|
|
193
|
-
taskId,
|
|
325
|
+
interactionId: taskId,
|
|
194
326
|
type,
|
|
195
327
|
},
|
|
196
328
|
});
|
|
197
329
|
|
|
198
330
|
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONTACT_ASSIGNED));
|
|
199
331
|
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
|
|
200
|
-
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_CONFERENCED));
|
|
201
|
-
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_ENDED));
|
|
202
|
-
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_WRAPUP));
|
|
203
|
-
webSocketManagerMock.emit('message', message(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE));
|
|
204
332
|
|
|
205
333
|
expect(mockApiAIAssistant.sendEvent).not.toHaveBeenCalled();
|
|
206
334
|
});
|
|
207
335
|
|
|
208
|
-
it('should
|
|
336
|
+
it('should not invoke sendEvent when realtime transcripts config is missing', () => {
|
|
337
|
+
taskManager.setConfigFlags({
|
|
338
|
+
isEndTaskEnabled: true,
|
|
339
|
+
isEndConsultEnabled: true,
|
|
340
|
+
webRtcEnabled: true,
|
|
341
|
+
autoWrapup: false,
|
|
342
|
+
aiFeature: {
|
|
343
|
+
id: 'ai-feature-1',
|
|
344
|
+
suggestedResponses: {
|
|
345
|
+
enable: true,
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const message = (type: CC_EVENTS) =>
|
|
351
|
+
JSON.stringify({
|
|
352
|
+
data: {
|
|
353
|
+
...taskDataMock,
|
|
354
|
+
interactionId: taskId,
|
|
355
|
+
type,
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONTACT_ASSIGNED));
|
|
360
|
+
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
|
|
361
|
+
|
|
362
|
+
expect(mockApiAIAssistant.sendEvent).not.toHaveBeenCalled();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should emit REAL_TIME_TRANSCRIPTION from RTD websocket payload', () => {
|
|
209
366
|
const task = taskManager.getTask(taskId);
|
|
210
367
|
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
211
368
|
const realtimePayload = {
|
|
212
369
|
data: {
|
|
213
|
-
|
|
214
|
-
type: CC_EVENTS.REAL_TIME_TRANSCRIPTION,
|
|
370
|
+
agentId: 'test-agent-id',
|
|
215
371
|
data: {
|
|
216
|
-
content: '
|
|
372
|
+
content: 'Thank you. Okay.',
|
|
373
|
+
conversationId: taskId,
|
|
374
|
+
isFinal: true,
|
|
375
|
+
languageCode: 'en-US',
|
|
376
|
+
messageId: '1',
|
|
377
|
+
orgId: 'org-id',
|
|
378
|
+
publishTimestamp: 1773807297475,
|
|
379
|
+
role: 'AGENT',
|
|
380
|
+
trackingId: 'tracking-id',
|
|
381
|
+
utteranceId: 'utterance-id',
|
|
382
|
+
},
|
|
383
|
+
notifDetails: {
|
|
384
|
+
actionEvent: 'REAL_TIME_TRANSCRIPTION',
|
|
217
385
|
},
|
|
386
|
+
notifType: 'REAL_TIME_TRANSCRIPTION',
|
|
387
|
+
orgId: 'org-id',
|
|
218
388
|
},
|
|
389
|
+
orgId: 'org-id',
|
|
390
|
+
trackingId: 'notifs_tracking-id',
|
|
391
|
+
type: 'REAL_TIME_TRANSCRIPTION',
|
|
219
392
|
};
|
|
220
393
|
|
|
221
|
-
|
|
394
|
+
taskManager.handleRealtimeWebsocketEvent(JSON.stringify(realtimePayload));
|
|
222
395
|
|
|
223
396
|
expect(taskEmitSpy).toHaveBeenCalledWith(
|
|
224
397
|
CC_EVENTS.REAL_TIME_TRANSCRIPTION,
|
|
@@ -226,22 +399,37 @@ describe('TaskManager', () => {
|
|
|
226
399
|
);
|
|
227
400
|
});
|
|
228
401
|
|
|
229
|
-
it('should
|
|
230
|
-
const task = taskManager.getTask(taskId);
|
|
231
|
-
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
402
|
+
it('should ignore RTD transcript events when task is not found', () => {
|
|
232
403
|
const realtimePayload = {
|
|
233
404
|
data: {
|
|
234
|
-
notifType: CC_EVENTS.REAL_TIME_TRANSCRIPTION,
|
|
235
405
|
data: {
|
|
236
|
-
|
|
237
|
-
|
|
406
|
+
content: 'Thank you. Okay.',
|
|
407
|
+
conversationId: 'missing-task-id',
|
|
408
|
+
isFinal: true,
|
|
409
|
+
languageCode: 'en-US',
|
|
410
|
+
messageId: '1',
|
|
411
|
+
orgId: 'org-id',
|
|
412
|
+
publishTimestamp: 1773807297475,
|
|
413
|
+
role: 'AGENT',
|
|
414
|
+
trackingId: 'tracking-id',
|
|
415
|
+
utteranceId: 'utterance-id',
|
|
238
416
|
},
|
|
417
|
+
notifDetails: {
|
|
418
|
+
actionEvent: 'REAL_TIME_TRANSCRIPTION',
|
|
419
|
+
},
|
|
420
|
+
notifType: 'REAL_TIME_TRANSCRIPTION',
|
|
421
|
+
orgId: 'org-id',
|
|
239
422
|
},
|
|
423
|
+
orgId: 'org-id',
|
|
424
|
+
trackingId: 'notifs_tracking-id',
|
|
425
|
+
type: 'REAL_TIME_TRANSCRIPTION',
|
|
240
426
|
};
|
|
241
427
|
|
|
428
|
+
const existingTaskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
|
|
429
|
+
|
|
242
430
|
taskManager.handleRealtimeWebsocketEvent(JSON.stringify(realtimePayload));
|
|
243
431
|
|
|
244
|
-
expect(
|
|
432
|
+
expect(existingTaskEmitSpy).not.toHaveBeenCalled();
|
|
245
433
|
});
|
|
246
434
|
|
|
247
435
|
it('should not re-emit agent related events', () => {
|
|
@@ -259,7 +447,7 @@ describe('TaskManager', () => {
|
|
|
259
447
|
|
|
260
448
|
webSocketManagerMock.emit('message', JSON.stringify(dummyPayload));
|
|
261
449
|
|
|
262
|
-
expect(taskEmitSpy).not.
|
|
450
|
+
expect(taskEmitSpy).not.toHaveBeenCalled();
|
|
263
451
|
});
|
|
264
452
|
|
|
265
453
|
it('should handle WebSocket message for AGENT_CONTACT_RESERVED and emit task:incoming for browser case', () => {
|
|
@@ -280,14 +468,12 @@ describe('TaskManager', () => {
|
|
|
280
468
|
},
|
|
281
469
|
};
|
|
282
470
|
|
|
283
|
-
const
|
|
471
|
+
const incomingHandler = jest.fn();
|
|
472
|
+
taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
|
|
284
473
|
|
|
285
474
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
286
475
|
|
|
287
|
-
expect(
|
|
288
|
-
TASK_EVENTS.TASK_INCOMING,
|
|
289
|
-
taskManager.getTask(payload.data.interactionId)
|
|
290
|
-
);
|
|
476
|
+
expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(payload.data.interactionId));
|
|
291
477
|
expect(taskManager.getTask(payload.data.interactionId)).toBe(taskManager.getTask(taskId));
|
|
292
478
|
expect(taskManager.getAllTasks()).toHaveProperty(payload.data.interactionId);
|
|
293
479
|
|
|
@@ -312,6 +498,7 @@ describe('TaskManager', () => {
|
|
|
312
498
|
taskManager.getTask(payload.data.interactionId),
|
|
313
499
|
'emit'
|
|
314
500
|
);
|
|
501
|
+
const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
|
|
315
502
|
|
|
316
503
|
webSocketManagerMock.emit('message', JSON.stringify(assignedPayload));
|
|
317
504
|
|
|
@@ -319,7 +506,7 @@ describe('TaskManager', () => {
|
|
|
319
506
|
TASK_EVENTS.TASK_ASSIGNED,
|
|
320
507
|
taskManager.getTask(taskId)
|
|
321
508
|
);
|
|
322
|
-
expect(
|
|
509
|
+
expect(taskManagerEmitSpy).toHaveBeenCalledWith(
|
|
323
510
|
TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
|
|
324
511
|
taskManager.getTask(taskId)
|
|
325
512
|
);
|
|
@@ -344,16 +531,64 @@ describe('TaskManager', () => {
|
|
|
344
531
|
},
|
|
345
532
|
};
|
|
346
533
|
|
|
347
|
-
const
|
|
534
|
+
const incomingHandler = jest.fn();
|
|
535
|
+
taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
|
|
348
536
|
|
|
349
537
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
350
538
|
|
|
351
|
-
expect(
|
|
352
|
-
TASK_EVENTS.TASK_INCOMING,
|
|
353
|
-
taskManager.getTask(taskId)
|
|
354
|
-
);
|
|
539
|
+
expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(taskId));
|
|
355
540
|
expect(taskManager.getTask(payload.data.interactionId)).toBe(taskManager.getTask(taskId));
|
|
356
541
|
expect(taskManager.getAllTasks()).toHaveProperty(payload.data.interactionId);
|
|
542
|
+
taskManager.off(TASK_EVENTS.TASK_INCOMING, incomingHandler);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('should send mapped events through the state machine without duplicate updates', () => {
|
|
546
|
+
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
547
|
+
const task = taskManager.getTask(taskId);
|
|
548
|
+
const updateSpy = task.updateTaskData as jest.Mock;
|
|
549
|
+
updateSpy.mockClear();
|
|
550
|
+
const sendSpy = task.sendStateMachineEvent as jest.Mock;
|
|
551
|
+
sendSpy.mockClear();
|
|
552
|
+
const cleanupSpy = jest.spyOn(taskManager as any, 'handleTaskCleanup');
|
|
553
|
+
|
|
554
|
+
const assignFailedPayload = {
|
|
555
|
+
data: {
|
|
556
|
+
...initalPayload.data,
|
|
557
|
+
type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED,
|
|
558
|
+
reason: 'ASSIGN_FAILED',
|
|
559
|
+
},
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
webSocketManagerMock.emit('message', JSON.stringify(assignFailedPayload));
|
|
563
|
+
|
|
564
|
+
const stateMachineEvent = expectLastStateMachineEvent(sendSpy, TaskEvent.ASSIGN_FAILED);
|
|
565
|
+
expect(stateMachineEvent).toEqual({
|
|
566
|
+
type: TaskEvent.ASSIGN_FAILED,
|
|
567
|
+
reason: assignFailedPayload.data.reason,
|
|
568
|
+
});
|
|
569
|
+
expect(updateSpy).toHaveBeenCalledWith(assignFailedPayload.data);
|
|
570
|
+
expect(cleanupSpy).toHaveBeenCalledWith(task);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('should update task data directly when no state machine mapping exists', () => {
|
|
574
|
+
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
575
|
+
const task = taskManager.getTask(taskId);
|
|
576
|
+
const updateSpy = task.updateTaskData as jest.Mock;
|
|
577
|
+
updateSpy.mockClear();
|
|
578
|
+
const sendSpy = task.sendStateMachineEvent as jest.Mock;
|
|
579
|
+
sendSpy.mockClear();
|
|
580
|
+
|
|
581
|
+
const participantMovedPayload = {
|
|
582
|
+
data: {
|
|
583
|
+
...initalPayload.data,
|
|
584
|
+
type: CC_EVENTS.CONSULTED_PARTICIPANT_MOVING,
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
webSocketManagerMock.emit('message', JSON.stringify(participantMovedPayload));
|
|
589
|
+
|
|
590
|
+
expect(sendSpy).not.toHaveBeenCalled();
|
|
591
|
+
expect(updateSpy).toHaveBeenCalledWith(participantMovedPayload.data);
|
|
357
592
|
});
|
|
358
593
|
|
|
359
594
|
it('should return task by ID', () => {
|
|
@@ -438,12 +673,30 @@ describe('TaskManager', () => {
|
|
|
438
673
|
it('test call listeners being switched off on call end', () => {
|
|
439
674
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
440
675
|
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
);
|
|
676
|
+
const webrtcTask = new WebRTC(contactMock, webCallingService, taskDataMock, {
|
|
677
|
+
isEndTaskEnabled: true,
|
|
678
|
+
isEndConsultEnabled: true,
|
|
679
|
+
});
|
|
680
|
+
(taskManager as any).taskCollection[taskId] = webrtcTask;
|
|
681
|
+
// TaskManager must listen to task-level cleanup events emitted by the state machine.
|
|
682
|
+
// This is normally wired when TaskManager creates the task via TaskFactory.
|
|
683
|
+
(taskManager as any).setupTaskListeners(webrtcTask);
|
|
684
|
+
|
|
685
|
+
const task = taskManager.getTask(taskId)!;
|
|
686
|
+
// This test doesn't validate UI controls; avoid requiring full interaction.media
|
|
687
|
+
// shape for WebRTC UI controls computation.
|
|
688
|
+
jest.spyOn(task as any, 'updateUiControls').mockImplementation(() => undefined);
|
|
689
|
+
const originalEmit = task.emit;
|
|
690
|
+
jest.spyOn(task, 'emit').mockImplementation((event, arg) => {
|
|
691
|
+
if (event === CC_EVENTS.CONTACT_ENDED) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
return originalEmit.call(task, event, arg);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
const webCallListenerSpy = jest.spyOn(task, 'unregisterWebCallListeners');
|
|
446
698
|
const callOffSpy = jest.spyOn(mockCall, 'off');
|
|
699
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
447
700
|
const payload = {
|
|
448
701
|
data: {
|
|
449
702
|
type: CC_EVENTS.CONTACT_ENDED,
|
|
@@ -461,10 +714,25 @@ describe('TaskManager', () => {
|
|
|
461
714
|
},
|
|
462
715
|
};
|
|
463
716
|
|
|
717
|
+
// Ensure the state machine is hydrated into a connected state before CONTACT_ENDED
|
|
718
|
+
const hydratePayload = {
|
|
719
|
+
data: {
|
|
720
|
+
...payload.data,
|
|
721
|
+
type: CC_EVENTS.AGENT_CONTACT,
|
|
722
|
+
interaction: {state: 'connected', mediaType: 'telephony'},
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
taskManager.getTask(taskId).data = hydratePayload.data;
|
|
727
|
+
webSocketManagerMock.emit('message', JSON.stringify(hydratePayload));
|
|
728
|
+
|
|
464
729
|
taskManager.getTask(taskId).data = payload.data;
|
|
465
|
-
const task = taskManager.getTask(taskId);
|
|
466
730
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
467
|
-
|
|
731
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
732
|
+
sendStateMachineEventSpy,
|
|
733
|
+
TaskEvent.CONTACT_ENDED
|
|
734
|
+
);
|
|
735
|
+
expect(stateMachineEvent?.taskData.wrapUpRequired).toBe(false);
|
|
468
736
|
expect(webCallListenerSpy).toHaveBeenCalledWith();
|
|
469
737
|
expect(callOffSpy).toHaveBeenCalledWith(
|
|
470
738
|
CALL_EVENT_KEYS.REMOTE_MEDIA,
|
|
@@ -475,12 +743,13 @@ describe('TaskManager', () => {
|
|
|
475
743
|
expect(offSpy.mock.calls.length).toBe(2); // 1 for incoming call and 1 for remote media
|
|
476
744
|
expect(offSpy).toHaveBeenCalledWith(CALL_EVENT_KEYS.REMOTE_MEDIA, offSpy.mock.calls[0][1]);
|
|
477
745
|
expect(offSpy).toHaveBeenCalledWith(LINE_EVENTS.INCOMING_CALL, offSpy.mock.calls[1][1]);
|
|
746
|
+
sendStateMachineEventSpy.mockRestore();
|
|
478
747
|
});
|
|
479
748
|
|
|
480
749
|
it('should emit TASK_END event with wrapupRequired on regular call end', () => {
|
|
481
750
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
482
|
-
|
|
483
|
-
const
|
|
751
|
+
const task = taskManager.getTask(taskId);
|
|
752
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
484
753
|
const payload = {
|
|
485
754
|
data: {
|
|
486
755
|
type: CC_EVENTS.CONTACT_ENDED,
|
|
@@ -495,20 +764,25 @@ describe('TaskManager', () => {
|
|
|
495
764
|
destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
|
|
496
765
|
owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
|
|
497
766
|
queueMgr: 'aqm',
|
|
767
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
498
768
|
},
|
|
499
769
|
};
|
|
500
770
|
|
|
501
|
-
|
|
771
|
+
task.updateTaskData(payload.data);
|
|
502
772
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
503
|
-
|
|
504
|
-
|
|
773
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
774
|
+
sendStateMachineEventSpy,
|
|
775
|
+
TaskEvent.CONTACT_ENDED
|
|
776
|
+
);
|
|
777
|
+
expect(stateMachineEvent?.taskData.wrapUpRequired).toBe(true);
|
|
778
|
+
sendStateMachineEventSpy.mockRestore();
|
|
505
779
|
});
|
|
506
780
|
|
|
507
781
|
it('should emit TASK_REJECT event on AGENT_INVITE_FAILED event', () => {
|
|
508
782
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
509
783
|
|
|
510
|
-
const
|
|
511
|
-
const
|
|
784
|
+
const task = taskManager.getTask(taskId);
|
|
785
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
512
786
|
const payload = {
|
|
513
787
|
data: {
|
|
514
788
|
type: CC_EVENTS.AGENT_INVITE_FAILED,
|
|
@@ -527,35 +801,40 @@ describe('TaskManager', () => {
|
|
|
527
801
|
},
|
|
528
802
|
};
|
|
529
803
|
|
|
530
|
-
|
|
804
|
+
task.updateTaskData(payload.data);
|
|
531
805
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
expect(
|
|
806
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
807
|
+
sendStateMachineEventSpy,
|
|
808
|
+
TaskEvent.INVITE_FAILED
|
|
809
|
+
);
|
|
810
|
+
expect(stateMachineEvent?.reason).toBe(payload.data.reason);
|
|
811
|
+
sendStateMachineEventSpy.mockRestore();
|
|
537
812
|
});
|
|
538
813
|
|
|
539
|
-
it('should
|
|
814
|
+
it('should emit TASK_HYDRATE even if task is already present in taskManager', () => {
|
|
540
815
|
const payload = {
|
|
541
816
|
data: {
|
|
542
817
|
...initalPayload.data,
|
|
543
818
|
type: CC_EVENTS.AGENT_CONTACT,
|
|
544
819
|
},
|
|
545
820
|
};
|
|
546
|
-
const
|
|
821
|
+
const existingTask = taskManager.getTask(taskId);
|
|
822
|
+
const sendStateMachineEventSpy = jest.spyOn(existingTask, 'sendStateMachineEvent');
|
|
547
823
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
548
824
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
825
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
826
|
+
sendStateMachineEventSpy,
|
|
827
|
+
TaskEvent.HYDRATE
|
|
552
828
|
);
|
|
829
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
830
|
+
expect(existingTask).toBe(taskManager.getTask(taskId));
|
|
553
831
|
expect(taskManager.taskCollection[payload.data.interactionId]).toBe(
|
|
554
832
|
taskManager.getTask(taskId)
|
|
555
833
|
);
|
|
834
|
+
sendStateMachineEventSpy.mockRestore();
|
|
556
835
|
});
|
|
557
836
|
|
|
558
|
-
it('should emit
|
|
837
|
+
it('should emit TASK_HYDRATE event on AGENT_CONTACT when task is created from payload', () => {
|
|
559
838
|
taskManager.taskCollection = [];
|
|
560
839
|
const payload = {
|
|
561
840
|
data: {
|
|
@@ -565,13 +844,15 @@ describe('TaskManager', () => {
|
|
|
565
844
|
},
|
|
566
845
|
};
|
|
567
846
|
|
|
568
|
-
const taskEmitSpy = jest.spyOn(taskManager, 'emit');
|
|
569
847
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
570
848
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
849
|
+
const createdTask = taskManager.getTask(taskId);
|
|
850
|
+
const sendStateMachineEventSpy = createdTask.sendStateMachineEvent as jest.Mock;
|
|
851
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
852
|
+
sendStateMachineEventSpy,
|
|
853
|
+
TaskEvent.HYDRATE
|
|
574
854
|
);
|
|
855
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
575
856
|
expect(taskManager.taskCollection[payload.data.interactionId]).toBe(
|
|
576
857
|
taskManager.getTask(taskId)
|
|
577
858
|
);
|
|
@@ -586,10 +867,15 @@ describe('TaskManager', () => {
|
|
|
586
867
|
},
|
|
587
868
|
};
|
|
588
869
|
|
|
589
|
-
const taskEmitSpy = jest.spyOn(taskManager, 'emit');
|
|
590
870
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
591
871
|
|
|
592
|
-
|
|
872
|
+
const createdTask = taskManager.getTask(taskId);
|
|
873
|
+
const sendStateMachineEventSpy = createdTask.sendStateMachineEvent as jest.Mock;
|
|
874
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
875
|
+
sendStateMachineEventSpy,
|
|
876
|
+
TaskEvent.HYDRATE
|
|
877
|
+
);
|
|
878
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
593
879
|
expect(taskManager.taskCollection[payload.data.interactionId]).toBe(
|
|
594
880
|
taskManager.getTask(taskId)
|
|
595
881
|
);
|
|
@@ -662,7 +948,7 @@ describe('TaskManager', () => {
|
|
|
662
948
|
expect(createdTask.data.isConferenceInProgress).toBe(false);
|
|
663
949
|
});
|
|
664
950
|
|
|
665
|
-
it('should emit
|
|
951
|
+
it('should emit TASK_WRAPUP event on AGENT_WRAPUP event', () => {
|
|
666
952
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
667
953
|
|
|
668
954
|
const wrapupPayload = {
|
|
@@ -684,13 +970,15 @@ describe('TaskManager', () => {
|
|
|
684
970
|
};
|
|
685
971
|
|
|
686
972
|
const task = taskManager.getTask(taskId);
|
|
687
|
-
const updateTaskDataSpy = jest.
|
|
688
|
-
|
|
973
|
+
const updateTaskDataSpy = task.updateTaskData as jest.Mock;
|
|
974
|
+
updateTaskDataSpy.mockClear();
|
|
975
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
689
976
|
|
|
690
977
|
webSocketManagerMock.emit('message', JSON.stringify(wrapupPayload));
|
|
691
978
|
|
|
692
979
|
expect(updateTaskDataSpy).toHaveBeenCalledWith(wrapupPayload.data);
|
|
693
|
-
|
|
980
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TASK_WRAPUP);
|
|
981
|
+
sendStateMachineEventSpy.mockRestore();
|
|
694
982
|
});
|
|
695
983
|
|
|
696
984
|
it('should emit TASK_HOLD event on AGENT_CONTACT_HELD event', () => {
|
|
@@ -713,13 +1001,20 @@ describe('TaskManager', () => {
|
|
|
713
1001
|
},
|
|
714
1002
|
};
|
|
715
1003
|
|
|
716
|
-
const
|
|
717
|
-
const taskUpdateTaskDataSpy = jest.
|
|
1004
|
+
const task = taskManager.getTask(taskId);
|
|
1005
|
+
const taskUpdateTaskDataSpy = task.updateTaskData as jest.Mock;
|
|
1006
|
+
taskUpdateTaskDataSpy.mockClear();
|
|
1007
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
718
1008
|
|
|
719
1009
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
720
1010
|
|
|
721
1011
|
expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
|
|
722
|
-
|
|
1012
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1013
|
+
sendStateMachineEventSpy,
|
|
1014
|
+
TaskEvent.HOLD_SUCCESS
|
|
1015
|
+
);
|
|
1016
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
1017
|
+
sendStateMachineEventSpy.mockRestore();
|
|
723
1018
|
});
|
|
724
1019
|
|
|
725
1020
|
it('should emit TASK_RESUME event on AGENT_CONTACT_UNHELD event', () => {
|
|
@@ -742,11 +1037,18 @@ describe('TaskManager', () => {
|
|
|
742
1037
|
},
|
|
743
1038
|
};
|
|
744
1039
|
|
|
745
|
-
const
|
|
746
|
-
const taskUpdateTaskDataSpy = jest.
|
|
1040
|
+
const task = taskManager.getTask(taskId);
|
|
1041
|
+
const taskUpdateTaskDataSpy = task.updateTaskData as jest.Mock;
|
|
1042
|
+
taskUpdateTaskDataSpy.mockClear();
|
|
1043
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
747
1044
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
748
1045
|
expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
|
|
749
|
-
|
|
1046
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1047
|
+
sendStateMachineEventSpy,
|
|
1048
|
+
TaskEvent.UNHOLD_SUCCESS
|
|
1049
|
+
);
|
|
1050
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
1051
|
+
sendStateMachineEventSpy.mockRestore();
|
|
750
1052
|
});
|
|
751
1053
|
|
|
752
1054
|
it('handle AGENT_CONSULT_CREATED event', () => {
|
|
@@ -759,17 +1061,16 @@ describe('TaskManager', () => {
|
|
|
759
1061
|
|
|
760
1062
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
761
1063
|
const task = taskManager.getTask(taskId);
|
|
762
|
-
const
|
|
763
|
-
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
1064
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
764
1065
|
|
|
765
1066
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
766
1067
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
expect(
|
|
772
|
-
|
|
1068
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1069
|
+
sendStateMachineEventSpy,
|
|
1070
|
+
TaskEvent.CONSULT_CREATED
|
|
1071
|
+
);
|
|
1072
|
+
expect(stateMachineEvent?.taskData).toEqual({...payload.data, isConsulted: false});
|
|
1073
|
+
sendStateMachineEventSpy.mockRestore();
|
|
773
1074
|
});
|
|
774
1075
|
|
|
775
1076
|
it('handle AGENT_OFFER_CONTACT event', () => {
|
|
@@ -782,11 +1083,17 @@ describe('TaskManager', () => {
|
|
|
782
1083
|
|
|
783
1084
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
784
1085
|
|
|
785
|
-
const
|
|
1086
|
+
const task = taskManager.getTask(taskId);
|
|
1087
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
786
1088
|
|
|
787
1089
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
788
1090
|
|
|
789
|
-
|
|
1091
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1092
|
+
sendStateMachineEventSpy,
|
|
1093
|
+
TaskEvent.TASK_OFFERED
|
|
1094
|
+
);
|
|
1095
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
1096
|
+
sendStateMachineEventSpy.mockRestore();
|
|
790
1097
|
});
|
|
791
1098
|
|
|
792
1099
|
describe('Auto-Answer Functionality', () => {
|
|
@@ -796,8 +1103,8 @@ describe('TaskManager', () => {
|
|
|
796
1103
|
|
|
797
1104
|
const task = taskManager.getTask(taskId);
|
|
798
1105
|
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
799
|
-
const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
|
|
800
1106
|
const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
|
|
1107
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
801
1108
|
|
|
802
1109
|
// Step 2: Trigger AGENT_OFFER_CONTACT with auto-answer
|
|
803
1110
|
const autoAnswerPayload = {
|
|
@@ -821,9 +1128,14 @@ describe('TaskManager', () => {
|
|
|
821
1128
|
// Verify accept was called
|
|
822
1129
|
expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
|
|
823
1130
|
|
|
824
|
-
|
|
825
|
-
|
|
1131
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1132
|
+
sendStateMachineEventSpy,
|
|
1133
|
+
TaskEvent.TASK_OFFERED
|
|
1134
|
+
);
|
|
1135
|
+
expect(stateMachineEvent?.taskData).toEqual(autoAnswerPayload.data);
|
|
1136
|
+
// Verify task auto-answer event was emitted
|
|
826
1137
|
expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
|
|
1138
|
+
sendStateMachineEventSpy.mockRestore();
|
|
827
1139
|
});
|
|
828
1140
|
|
|
829
1141
|
it('should NOT emit TASK_AUTO_ANSWERED event when auto-answer fails', async () => {
|
|
@@ -869,6 +1181,7 @@ describe('TaskManager', () => {
|
|
|
869
1181
|
const task = taskManager.getTask(taskId);
|
|
870
1182
|
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
871
1183
|
const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
|
|
1184
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
872
1185
|
|
|
873
1186
|
// Step 2: Trigger AGENT_OFFER_CONSULT with auto-answer
|
|
874
1187
|
const consultAutoAnswerPayload = {
|
|
@@ -893,12 +1206,20 @@ describe('TaskManager', () => {
|
|
|
893
1206
|
// Verify accept was called
|
|
894
1207
|
expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
|
|
895
1208
|
|
|
896
|
-
|
|
897
|
-
|
|
1209
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1210
|
+
sendStateMachineEventSpy,
|
|
1211
|
+
TaskEvent.OFFER_CONSULT
|
|
1212
|
+
);
|
|
1213
|
+
expect(stateMachineEvent?.taskData).toEqual({
|
|
1214
|
+
...consultAutoAnswerPayload.data,
|
|
1215
|
+
isConsulted: true,
|
|
1216
|
+
});
|
|
1217
|
+
// Verify task auto-answer event was emitted
|
|
898
1218
|
expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
|
|
899
1219
|
|
|
900
1220
|
// Verify isConsulted flag is set correctly
|
|
901
1221
|
expect(task.data.isConsulted).toBe(true);
|
|
1222
|
+
sendStateMachineEventSpy.mockRestore();
|
|
902
1223
|
});
|
|
903
1224
|
|
|
904
1225
|
it('should NOT emit TASK_AUTO_ANSWERED when isAutoAnswering is false', async () => {
|
|
@@ -939,24 +1260,20 @@ describe('TaskManager', () => {
|
|
|
939
1260
|
});
|
|
940
1261
|
});
|
|
941
1262
|
|
|
942
|
-
it('should
|
|
1263
|
+
it('should remove OUTDIAL task from taskCollection on AGENT_OUTBOUND_FAILED when terminated', () => {
|
|
943
1264
|
const task = taskManager.getTask(taskId);
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
...task.data,
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
state: 'new',
|
|
953
|
-
isTerminated: true,
|
|
954
|
-
},
|
|
955
|
-
};
|
|
956
|
-
return task;
|
|
1265
|
+
Object.assign(task.data, {
|
|
1266
|
+
interaction: {
|
|
1267
|
+
...task.data.interaction,
|
|
1268
|
+
outboundType: 'OUTDIAL',
|
|
1269
|
+
state: 'new',
|
|
1270
|
+
isTerminated: true,
|
|
1271
|
+
},
|
|
1272
|
+
agentsPendingWrapUp: ['agent-123'],
|
|
957
1273
|
});
|
|
958
1274
|
task.unregisterWebCallListeners = jest.fn();
|
|
959
1275
|
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
1276
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
960
1277
|
|
|
961
1278
|
const payload = {
|
|
962
1279
|
data: {
|
|
@@ -969,6 +1286,7 @@ describe('TaskManager', () => {
|
|
|
969
1286
|
state: 'new',
|
|
970
1287
|
isTerminated: true,
|
|
971
1288
|
},
|
|
1289
|
+
agentsPendingWrapUp: ['agent-123'],
|
|
972
1290
|
interactionId: taskId,
|
|
973
1291
|
orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
|
|
974
1292
|
trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
|
|
@@ -983,23 +1301,61 @@ describe('TaskManager', () => {
|
|
|
983
1301
|
|
|
984
1302
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
985
1303
|
|
|
986
|
-
|
|
987
|
-
|
|
1304
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1305
|
+
|
|
1306
|
+
expect(taskManager.getTask(taskId)).toBeUndefined();
|
|
1307
|
+
expect(removeTaskSpy).toHaveBeenCalled();
|
|
1308
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1309
|
+
sendStateMachineEventSpy,
|
|
1310
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1311
|
+
);
|
|
1312
|
+
expect(stateMachineEvent?.reason).toBe('CUSTOMER_BUSY');
|
|
1313
|
+
sendStateMachineEventSpy.mockRestore();
|
|
988
1314
|
});
|
|
989
1315
|
|
|
990
1316
|
it('should emit TASK_OUTDIAL_FAILED event on AGENT_OUTBOUND_FAILED', () => {
|
|
991
1317
|
const task = taskManager.getTask(taskId);
|
|
992
|
-
|
|
993
|
-
const
|
|
1318
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1319
|
+
const payload = {
|
|
1320
|
+
data: {
|
|
1321
|
+
type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
|
|
1322
|
+
interactionId: taskId,
|
|
1323
|
+
reason: 'CUSTOMER_BUSY',
|
|
1324
|
+
},
|
|
1325
|
+
};
|
|
1326
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1327
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1328
|
+
sendStateMachineEventSpy,
|
|
1329
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1330
|
+
);
|
|
1331
|
+
expect(stateMachineEvent?.reason).toBe('CUSTOMER_BUSY');
|
|
1332
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
it('should pass taskData in OUTBOUND_FAILED event for shouldWrapUp guard evaluation', () => {
|
|
1336
|
+
const task = taskManager.getTask(taskId);
|
|
1337
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
994
1338
|
const payload = {
|
|
995
1339
|
data: {
|
|
996
1340
|
type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
|
|
997
1341
|
interactionId: taskId,
|
|
998
1342
|
reason: 'CUSTOMER_BUSY',
|
|
1343
|
+
agentsPendingWrapUp: ['agent-123'],
|
|
1344
|
+
interaction: {
|
|
1345
|
+
outboundType: 'OUTDIAL',
|
|
1346
|
+
isTerminated: true,
|
|
1347
|
+
},
|
|
999
1348
|
},
|
|
1000
1349
|
};
|
|
1001
1350
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1002
|
-
|
|
1351
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1352
|
+
sendStateMachineEventSpy,
|
|
1353
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1354
|
+
);
|
|
1355
|
+
expect(stateMachineEvent?.taskData).toBeDefined();
|
|
1356
|
+
expect(stateMachineEvent?.taskData?.agentsPendingWrapUp).toEqual(['agent-123']);
|
|
1357
|
+
expect(stateMachineEvent?.taskData?.interaction?.outboundType).toBe('OUTDIAL');
|
|
1358
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1003
1359
|
});
|
|
1004
1360
|
|
|
1005
1361
|
it('should handle AGENT_OUTBOUND_FAILED gracefully when task is undefined', () => {
|
|
@@ -1017,23 +1373,15 @@ describe('TaskManager', () => {
|
|
|
1017
1373
|
});
|
|
1018
1374
|
|
|
1019
1375
|
it('should NOT remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp exists', () => {
|
|
1020
|
-
const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
|
|
1021
|
-
taskManager.setAgentId(agentId);
|
|
1022
|
-
|
|
1023
1376
|
const task = taskManager.getTask(taskId);
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
...task.data,
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
mediaType: 'telephony',
|
|
1033
|
-
},
|
|
1034
|
-
agentsPendingWrapUp: [agentId],
|
|
1035
|
-
};
|
|
1036
|
-
return task;
|
|
1377
|
+
Object.assign(task.data, {
|
|
1378
|
+
interaction: {
|
|
1379
|
+
...task.data.interaction,
|
|
1380
|
+
outboundType: 'OUTDIAL',
|
|
1381
|
+
state: 'new',
|
|
1382
|
+
mediaType: 'telephony',
|
|
1383
|
+
},
|
|
1384
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
1037
1385
|
});
|
|
1038
1386
|
task.unregisterWebCallListeners = jest.fn();
|
|
1039
1387
|
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
@@ -1047,7 +1395,7 @@ describe('TaskManager', () => {
|
|
|
1047
1395
|
state: 'new',
|
|
1048
1396
|
mediaType: 'telephony',
|
|
1049
1397
|
},
|
|
1050
|
-
agentsPendingWrapUp: [
|
|
1398
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
1051
1399
|
},
|
|
1052
1400
|
};
|
|
1053
1401
|
|
|
@@ -1059,19 +1407,14 @@ describe('TaskManager', () => {
|
|
|
1059
1407
|
|
|
1060
1408
|
it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is empty', () => {
|
|
1061
1409
|
const task = taskManager.getTask(taskId);
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
...task.data,
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
mediaType: 'telephony',
|
|
1071
|
-
},
|
|
1072
|
-
agentsPendingWrapUp: [],
|
|
1073
|
-
};
|
|
1074
|
-
return task;
|
|
1410
|
+
Object.assign(task.data, {
|
|
1411
|
+
interaction: {
|
|
1412
|
+
...task.data.interaction,
|
|
1413
|
+
outboundType: 'OUTDIAL',
|
|
1414
|
+
state: 'new',
|
|
1415
|
+
mediaType: 'telephony',
|
|
1416
|
+
},
|
|
1417
|
+
agentsPendingWrapUp: [],
|
|
1075
1418
|
});
|
|
1076
1419
|
task.unregisterWebCallListeners = jest.fn();
|
|
1077
1420
|
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
@@ -1096,19 +1439,13 @@ describe('TaskManager', () => {
|
|
|
1096
1439
|
|
|
1097
1440
|
it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is undefined', () => {
|
|
1098
1441
|
const task = taskManager.getTask(taskId);
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
...task.data,
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
state: 'new',
|
|
1107
|
-
mediaType: 'telephony',
|
|
1108
|
-
},
|
|
1109
|
-
// agentsPendingWrapUp is undefined
|
|
1110
|
-
};
|
|
1111
|
-
return task;
|
|
1442
|
+
Object.assign(task.data, {
|
|
1443
|
+
interaction: {
|
|
1444
|
+
...task.data.interaction,
|
|
1445
|
+
outboundType: 'OUTDIAL',
|
|
1446
|
+
state: 'new',
|
|
1447
|
+
mediaType: 'telephony',
|
|
1448
|
+
},
|
|
1112
1449
|
});
|
|
1113
1450
|
task.unregisterWebCallListeners = jest.fn();
|
|
1114
1451
|
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
@@ -1147,333 +1484,29 @@ describe('TaskManager', () => {
|
|
|
1147
1484
|
}).not.toThrow();
|
|
1148
1485
|
});
|
|
1149
1486
|
|
|
1150
|
-
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1487
|
+
it('should remove OUTDIAL task from taskCollection on AGENT_CONTACT_ASSIGN_FAILED when NOT terminated (user-declined)', () => {
|
|
1488
|
+
const task = taskManager.getTask(taskId);
|
|
1489
|
+
Object.assign(task.data, {
|
|
1490
|
+
interaction: {
|
|
1491
|
+
...task.data.interaction,
|
|
1492
|
+
outboundType: 'OUTDIAL',
|
|
1493
|
+
state: 'new',
|
|
1494
|
+
isTerminated: false,
|
|
1495
|
+
},
|
|
1156
1496
|
});
|
|
1497
|
+
task.unregisterWebCallListeners = jest.fn();
|
|
1498
|
+
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
1157
1499
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const payload = {
|
|
1170
|
-
data: {
|
|
1171
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
1172
|
-
interactionId: taskId,
|
|
1173
|
-
interaction: {
|
|
1174
|
-
state: 'connected',
|
|
1175
|
-
mediaType: 'telephony',
|
|
1176
|
-
},
|
|
1177
|
-
agentsPendingWrapUp: [agentId, 'other-agent-id'],
|
|
1178
|
-
},
|
|
1179
|
-
};
|
|
1180
|
-
|
|
1181
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1182
|
-
|
|
1183
|
-
expect(task.updateTaskData).toHaveBeenCalledWith(
|
|
1184
|
-
expect.objectContaining({
|
|
1185
|
-
wrapUpRequired: true,
|
|
1186
|
-
})
|
|
1187
|
-
);
|
|
1188
|
-
});
|
|
1189
|
-
|
|
1190
|
-
it('should set wrapUpRequired to false when agent is not in agentsPendingWrapUp array', () => {
|
|
1191
|
-
const task = taskManager.getTask(taskId);
|
|
1192
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
1193
|
-
task.data = {
|
|
1194
|
-
...task.data,
|
|
1195
|
-
...newData,
|
|
1196
|
-
};
|
|
1197
|
-
return task;
|
|
1198
|
-
});
|
|
1199
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
1200
|
-
|
|
1201
|
-
const payload = {
|
|
1202
|
-
data: {
|
|
1203
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
1204
|
-
interactionId: taskId,
|
|
1205
|
-
interaction: {
|
|
1206
|
-
state: 'connected',
|
|
1207
|
-
mediaType: 'telephony',
|
|
1208
|
-
},
|
|
1209
|
-
agentsPendingWrapUp: ['other-agent-id', 'another-agent-id'],
|
|
1210
|
-
},
|
|
1211
|
-
};
|
|
1212
|
-
|
|
1213
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1214
|
-
|
|
1215
|
-
expect(task.updateTaskData).toHaveBeenCalledWith(
|
|
1216
|
-
expect.objectContaining({
|
|
1217
|
-
wrapUpRequired: false,
|
|
1218
|
-
})
|
|
1219
|
-
);
|
|
1220
|
-
});
|
|
1221
|
-
|
|
1222
|
-
it('should set wrapUpRequired to false when agentsPendingWrapUp is an empty array', () => {
|
|
1223
|
-
const task = taskManager.getTask(taskId);
|
|
1224
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
1225
|
-
task.data = {
|
|
1226
|
-
...task.data,
|
|
1227
|
-
...newData,
|
|
1228
|
-
};
|
|
1229
|
-
return task;
|
|
1230
|
-
});
|
|
1231
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
1232
|
-
|
|
1233
|
-
const payload = {
|
|
1234
|
-
data: {
|
|
1235
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
1236
|
-
interactionId: taskId,
|
|
1237
|
-
interaction: {
|
|
1238
|
-
state: 'connected',
|
|
1239
|
-
mediaType: 'telephony',
|
|
1240
|
-
},
|
|
1241
|
-
agentsPendingWrapUp: [],
|
|
1242
|
-
},
|
|
1243
|
-
};
|
|
1244
|
-
|
|
1245
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1246
|
-
|
|
1247
|
-
expect(task.updateTaskData).toHaveBeenCalledWith(
|
|
1248
|
-
expect.objectContaining({
|
|
1249
|
-
wrapUpRequired: false,
|
|
1250
|
-
})
|
|
1251
|
-
);
|
|
1252
|
-
});
|
|
1253
|
-
|
|
1254
|
-
it('should set wrapUpRequired to false when agentsPendingWrapUp is undefined', () => {
|
|
1255
|
-
const task = taskManager.getTask(taskId);
|
|
1256
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
1257
|
-
task.data = {
|
|
1258
|
-
...task.data,
|
|
1259
|
-
...newData,
|
|
1260
|
-
};
|
|
1261
|
-
return task;
|
|
1262
|
-
});
|
|
1263
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
1264
|
-
|
|
1265
|
-
const payload = {
|
|
1266
|
-
data: {
|
|
1267
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
1268
|
-
interactionId: taskId,
|
|
1269
|
-
interaction: {
|
|
1270
|
-
state: 'connected',
|
|
1271
|
-
mediaType: 'telephony',
|
|
1272
|
-
},
|
|
1273
|
-
// agentsPendingWrapUp is not defined
|
|
1274
|
-
},
|
|
1275
|
-
};
|
|
1276
|
-
|
|
1277
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1278
|
-
|
|
1279
|
-
expect(task.updateTaskData).toHaveBeenCalledWith(
|
|
1280
|
-
expect.objectContaining({
|
|
1281
|
-
wrapUpRequired: false,
|
|
1282
|
-
})
|
|
1283
|
-
);
|
|
1284
|
-
});
|
|
1285
|
-
|
|
1286
|
-
it('should set wrapUpRequired to false when agentsPendingWrapUp is null', () => {
|
|
1287
|
-
const task = taskManager.getTask(taskId);
|
|
1288
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
1289
|
-
task.data = {
|
|
1290
|
-
...task.data,
|
|
1291
|
-
...newData,
|
|
1292
|
-
};
|
|
1293
|
-
return task;
|
|
1294
|
-
});
|
|
1295
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
1296
|
-
|
|
1297
|
-
const payload = {
|
|
1298
|
-
data: {
|
|
1299
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
1300
|
-
interactionId: taskId,
|
|
1301
|
-
interaction: {
|
|
1302
|
-
state: 'connected',
|
|
1303
|
-
mediaType: 'telephony',
|
|
1304
|
-
},
|
|
1305
|
-
agentsPendingWrapUp: null,
|
|
1306
|
-
},
|
|
1307
|
-
};
|
|
1308
|
-
|
|
1309
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1310
|
-
|
|
1311
|
-
expect(task.updateTaskData).toHaveBeenCalledWith(
|
|
1312
|
-
expect.objectContaining({
|
|
1313
|
-
wrapUpRequired: false,
|
|
1314
|
-
})
|
|
1315
|
-
);
|
|
1316
|
-
});
|
|
1317
|
-
|
|
1318
|
-
it('should set wrapUpRequired to true when agent is the only one in agentsPendingWrapUp for non-campaign tasks', () => {
|
|
1319
|
-
const task = taskManager.getTask(taskId);
|
|
1320
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
1321
|
-
task.data = {
|
|
1322
|
-
...task.data,
|
|
1323
|
-
...newData,
|
|
1324
|
-
};
|
|
1325
|
-
return task;
|
|
1326
|
-
});
|
|
1327
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
1328
|
-
|
|
1329
|
-
const payload = {
|
|
1330
|
-
data: {
|
|
1331
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
1332
|
-
interactionId: taskId,
|
|
1333
|
-
interaction: {
|
|
1334
|
-
state: 'connected',
|
|
1335
|
-
mediaType: 'telephony',
|
|
1336
|
-
},
|
|
1337
|
-
agentsPendingWrapUp: [agentId],
|
|
1338
|
-
},
|
|
1339
|
-
};
|
|
1340
|
-
|
|
1341
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1342
|
-
|
|
1343
|
-
expect(task.updateTaskData).toHaveBeenCalledWith(
|
|
1344
|
-
expect.objectContaining({
|
|
1345
|
-
wrapUpRequired: true,
|
|
1346
|
-
})
|
|
1347
|
-
);
|
|
1348
|
-
});
|
|
1349
|
-
|
|
1350
|
-
it('should set wrapUpRequired to true for different interaction states when agent is in agentsPendingWrapUp for non-campaign tasks', () => {
|
|
1351
|
-
const task = taskManager.getTask(taskId);
|
|
1352
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
1353
|
-
task.data = {
|
|
1354
|
-
...task.data,
|
|
1355
|
-
...newData,
|
|
1356
|
-
interaction: {
|
|
1357
|
-
...task.data.interaction,
|
|
1358
|
-
...newData.interaction,
|
|
1359
|
-
},
|
|
1360
|
-
};
|
|
1361
|
-
return task;
|
|
1362
|
-
});
|
|
1363
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
1364
|
-
|
|
1365
|
-
// Test with 'connected' state
|
|
1366
|
-
const payloadConnected = {
|
|
1367
|
-
data: {
|
|
1368
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
1369
|
-
interactionId: taskId,
|
|
1370
|
-
interaction: {
|
|
1371
|
-
state: 'connected',
|
|
1372
|
-
mediaType: 'telephony',
|
|
1373
|
-
},
|
|
1374
|
-
agentsPendingWrapUp: [agentId],
|
|
1375
|
-
},
|
|
1376
|
-
};
|
|
1377
|
-
|
|
1378
|
-
webSocketManagerMock.emit('message', JSON.stringify(payloadConnected));
|
|
1379
|
-
|
|
1380
|
-
expect(task.updateTaskData).toHaveBeenNthCalledWith(
|
|
1381
|
-
1,
|
|
1382
|
-
expect.objectContaining({
|
|
1383
|
-
wrapUpRequired: true,
|
|
1384
|
-
})
|
|
1385
|
-
);
|
|
1386
|
-
|
|
1387
|
-
// Test with 'held' state to verify it still works regardless of state
|
|
1388
|
-
const payloadHeld = {
|
|
1389
|
-
data: {
|
|
1390
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
1391
|
-
interactionId: taskId,
|
|
1392
|
-
interaction: {
|
|
1393
|
-
state: 'held',
|
|
1394
|
-
mediaType: 'telephony',
|
|
1395
|
-
},
|
|
1396
|
-
agentsPendingWrapUp: [agentId],
|
|
1397
|
-
},
|
|
1398
|
-
};
|
|
1399
|
-
|
|
1400
|
-
webSocketManagerMock.emit('message', JSON.stringify(payloadHeld));
|
|
1401
|
-
|
|
1402
|
-
expect(task.updateTaskData).toHaveBeenNthCalledWith(
|
|
1403
|
-
2,
|
|
1404
|
-
expect.objectContaining({
|
|
1405
|
-
wrapUpRequired: true,
|
|
1406
|
-
})
|
|
1407
|
-
);
|
|
1408
|
-
});
|
|
1409
|
-
|
|
1410
|
-
it('should set wrapUpRequired to false for campaign preview tasks even when agent is in agentsPendingWrapUp', () => {
|
|
1411
|
-
const task = taskManager.getTask(taskId);
|
|
1412
|
-
// Set up task as a campaign preview task via outboundType
|
|
1413
|
-
task.data.interaction = {
|
|
1414
|
-
...task.data.interaction,
|
|
1415
|
-
outboundType: 'STANDARD_PREVIEW_CAMPAIGN',
|
|
1416
|
-
};
|
|
1417
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
1418
|
-
task.data = {
|
|
1419
|
-
...task.data,
|
|
1420
|
-
...newData,
|
|
1421
|
-
};
|
|
1422
|
-
return task;
|
|
1423
|
-
});
|
|
1424
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
1425
|
-
|
|
1426
|
-
const payload = {
|
|
1427
|
-
data: {
|
|
1428
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
1429
|
-
interactionId: taskId,
|
|
1430
|
-
interaction: {
|
|
1431
|
-
state: 'connected',
|
|
1432
|
-
mediaType: 'telephony',
|
|
1433
|
-
},
|
|
1434
|
-
agentsPendingWrapUp: [agentId],
|
|
1435
|
-
},
|
|
1436
|
-
};
|
|
1437
|
-
|
|
1438
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1439
|
-
|
|
1440
|
-
expect(task.updateTaskData).toHaveBeenCalledWith(
|
|
1441
|
-
expect.objectContaining({
|
|
1442
|
-
wrapUpRequired: false,
|
|
1443
|
-
})
|
|
1444
|
-
);
|
|
1445
|
-
});
|
|
1446
|
-
});
|
|
1447
|
-
|
|
1448
|
-
it('should remove OUTDIAL task from taskCollection on AGENT_CONTACT_ASSIGN_FAILED when NOT terminated (user-declined)', () => {
|
|
1449
|
-
const task = taskManager.getTask(taskId);
|
|
1450
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
1451
|
-
task.data = {
|
|
1452
|
-
...task.data,
|
|
1453
|
-
...newData,
|
|
1454
|
-
interaction: {
|
|
1455
|
-
...task.data.interaction,
|
|
1456
|
-
...newData.interaction,
|
|
1457
|
-
outboundType: 'OUTDIAL',
|
|
1458
|
-
state: 'new',
|
|
1459
|
-
isTerminated: false,
|
|
1460
|
-
},
|
|
1461
|
-
};
|
|
1462
|
-
return task;
|
|
1463
|
-
});
|
|
1464
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
1465
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
1466
|
-
|
|
1467
|
-
const payload = {
|
|
1468
|
-
data: {
|
|
1469
|
-
type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED,
|
|
1470
|
-
agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
|
|
1471
|
-
eventTime: 1733211616959,
|
|
1472
|
-
eventType: 'RoutingMessage',
|
|
1473
|
-
interaction: {
|
|
1474
|
-
outboundType: 'OUTDIAL',
|
|
1475
|
-
state: 'new',
|
|
1476
|
-
isTerminated: false,
|
|
1500
|
+
const payload = {
|
|
1501
|
+
data: {
|
|
1502
|
+
type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED,
|
|
1503
|
+
agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
|
|
1504
|
+
eventTime: 1733211616959,
|
|
1505
|
+
eventType: 'RoutingMessage',
|
|
1506
|
+
interaction: {
|
|
1507
|
+
outboundType: 'OUTDIAL',
|
|
1508
|
+
state: 'new',
|
|
1509
|
+
isTerminated: false,
|
|
1477
1510
|
},
|
|
1478
1511
|
interactionId: taskId,
|
|
1479
1512
|
orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
|
|
@@ -1503,20 +1536,16 @@ describe('TaskManager', () => {
|
|
|
1503
1536
|
|
|
1504
1537
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1505
1538
|
const task = taskManager.getTask(taskId);
|
|
1506
|
-
|
|
1507
|
-
task.data = {...newData, isConsulted: true};
|
|
1508
|
-
return task;
|
|
1509
|
-
});
|
|
1510
|
-
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
1539
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1511
1540
|
|
|
1512
1541
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1513
1542
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
expect(
|
|
1519
|
-
|
|
1543
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1544
|
+
sendStateMachineEventSpy,
|
|
1545
|
+
TaskEvent.OFFER_CONSULT
|
|
1546
|
+
);
|
|
1547
|
+
expect(stateMachineEvent?.taskData).toEqual({...payload.data, isConsulted: true});
|
|
1548
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1520
1549
|
});
|
|
1521
1550
|
|
|
1522
1551
|
it('should emit TASK_CONSULT_ACCEPTED event on AGENT_CONSULTING event', () => {
|
|
@@ -1535,24 +1564,18 @@ describe('TaskManager', () => {
|
|
|
1535
1564
|
};
|
|
1536
1565
|
|
|
1537
1566
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1538
|
-
taskManager.getTask(taskId).updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
1539
|
-
taskManager.getTask(taskId).data = {...newData, isConsulted: true};
|
|
1540
|
-
return taskManager.getTask(taskId);
|
|
1541
|
-
});
|
|
1542
1567
|
|
|
1543
|
-
const
|
|
1544
|
-
const
|
|
1568
|
+
const task = taskManager.getTask(taskId);
|
|
1569
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1570
|
+
|
|
1545
1571
|
webSocketManagerMock.emit('message', JSON.stringify(initialConsultingPayload));
|
|
1546
1572
|
webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
taskManager.getTask(taskId)
|
|
1551
|
-
);
|
|
1552
|
-
expect(taskManagerEmitSpy).toHaveBeenCalledWith(
|
|
1553
|
-
TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
|
|
1554
|
-
taskManager.getTask(taskId)
|
|
1573
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1574
|
+
sendStateMachineEventSpy,
|
|
1575
|
+
TaskEvent.CONSULTING_ACTIVE
|
|
1555
1576
|
);
|
|
1577
|
+
expect(stateMachineEvent?.taskData).toEqual(consultingPayload.data);
|
|
1578
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1556
1579
|
});
|
|
1557
1580
|
|
|
1558
1581
|
it('should emit TASK_CONSULT_ENDED event on AGENT_CONSULT_ENDED event', () => {
|
|
@@ -1564,14 +1587,16 @@ describe('TaskManager', () => {
|
|
|
1564
1587
|
};
|
|
1565
1588
|
|
|
1566
1589
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1567
|
-
|
|
1568
|
-
const
|
|
1590
|
+
taskManager.getTask(taskId).data.isConsulted = true;
|
|
1591
|
+
const task = taskManager.getTask(taskId);
|
|
1592
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1569
1593
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
taskManager.getTask(taskId)
|
|
1594
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1595
|
+
sendStateMachineEventSpy,
|
|
1596
|
+
TaskEvent.CONSULT_END
|
|
1574
1597
|
);
|
|
1598
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
1599
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1575
1600
|
});
|
|
1576
1601
|
|
|
1577
1602
|
it('should emit TASK_CONSULT_ENDED event and remove currentTask when on AGENT_CONSULT_ENDED event when requested for a consult', () => {
|
|
@@ -1584,19 +1609,14 @@ describe('TaskManager', () => {
|
|
|
1584
1609
|
|
|
1585
1610
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1586
1611
|
|
|
1587
|
-
taskManager.getTask(taskId).
|
|
1588
|
-
taskManager.getTask(taskId).data = {...newData, isConsulted: true};
|
|
1589
|
-
return taskManager.getTask(taskId);
|
|
1590
|
-
});
|
|
1612
|
+
taskManager.getTask(taskId).data.isConsulted = true;
|
|
1591
1613
|
const task = taskManager.getTask(taskId);
|
|
1592
1614
|
|
|
1593
|
-
const
|
|
1594
|
-
const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
|
|
1615
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1595
1616
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1596
|
-
|
|
1597
|
-
expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.AGENT_CONSULT_ENDED, payload.data);
|
|
1598
|
-
expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONSULT_END, task);
|
|
1617
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONSULT_END);
|
|
1599
1618
|
expect(taskManager.getTask(taskId)).toBeUndefined(); // Ensure task is removed from the task collection after the consult ends
|
|
1619
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1600
1620
|
});
|
|
1601
1621
|
|
|
1602
1622
|
it('should emit TASK_CANCELLED event on AGENT_CTQ_CANCELLED event', () => {
|
|
@@ -1608,14 +1628,15 @@ describe('TaskManager', () => {
|
|
|
1608
1628
|
};
|
|
1609
1629
|
|
|
1610
1630
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1611
|
-
const
|
|
1612
|
-
const
|
|
1631
|
+
const task = taskManager.getTask(taskId);
|
|
1632
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1613
1633
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
taskManager.getTask(taskId)
|
|
1634
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1635
|
+
sendStateMachineEventSpy,
|
|
1636
|
+
TaskEvent.CTQ_CANCEL
|
|
1618
1637
|
);
|
|
1638
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
1639
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1619
1640
|
});
|
|
1620
1641
|
|
|
1621
1642
|
it('should handle AGENT_CONSULT_FAILED event', () => {
|
|
@@ -1629,9 +1650,15 @@ describe('TaskManager', () => {
|
|
|
1629
1650
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1630
1651
|
|
|
1631
1652
|
// Always spy on the updated task object after CONTACT_RESERVED is emitted
|
|
1632
|
-
const
|
|
1653
|
+
const task = taskManager.getTask(taskId);
|
|
1654
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1633
1655
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1634
|
-
|
|
1656
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1657
|
+
sendStateMachineEventSpy,
|
|
1658
|
+
TaskEvent.CONSULT_FAILED
|
|
1659
|
+
);
|
|
1660
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
1661
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1635
1662
|
});
|
|
1636
1663
|
|
|
1637
1664
|
it('should emit TASK_CONSULT_QUEUE_FAILED on AGENT_CTQ_CANCEL_FAILED event', () => {
|
|
@@ -1643,14 +1670,15 @@ describe('TaskManager', () => {
|
|
|
1643
1670
|
};
|
|
1644
1671
|
|
|
1645
1672
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1646
|
-
const
|
|
1647
|
-
const
|
|
1673
|
+
const task = taskManager.getTask(taskId);
|
|
1674
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1648
1675
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
taskManager.getTask(taskId)
|
|
1676
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1677
|
+
sendStateMachineEventSpy,
|
|
1678
|
+
TaskEvent.CTQ_CANCEL_FAILED
|
|
1653
1679
|
);
|
|
1680
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
1681
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1654
1682
|
});
|
|
1655
1683
|
|
|
1656
1684
|
it('should emit TASK_REJECT event on AGENT_CONTACT_OFFER_RONA event', () => {
|
|
@@ -1693,15 +1721,14 @@ describe('TaskManager', () => {
|
|
|
1693
1721
|
};
|
|
1694
1722
|
|
|
1695
1723
|
taskManager.taskCollection[taskId] = taskManager.getTask(taskId);
|
|
1696
|
-
const
|
|
1697
|
-
const
|
|
1724
|
+
const task = taskManager.getTask(taskId);
|
|
1725
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1698
1726
|
|
|
1699
1727
|
webSocketManagerMock.emit('message', JSON.stringify(ronaPayload));
|
|
1700
1728
|
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
expect(metricsTrackSpy.mock.calls[0][0]).toBe('Agent RONA');
|
|
1729
|
+
const stateMachineEvent = expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.RONA);
|
|
1730
|
+
expect(stateMachineEvent?.reason).toBe(ronaPayload.data.reason);
|
|
1731
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1705
1732
|
});
|
|
1706
1733
|
|
|
1707
1734
|
it('should emit TASK_REJECT event on AGENT_CONTACT_ASSIGN_FAILED event', () => {
|
|
@@ -1745,20 +1772,16 @@ describe('TaskManager', () => {
|
|
|
1745
1772
|
|
|
1746
1773
|
taskManager.taskCollection[taskId] = taskManager.getTask(taskId);
|
|
1747
1774
|
const task = taskManager.getTask(taskId);
|
|
1748
|
-
const
|
|
1749
|
-
const taskUpdateDataSpy = jest.spyOn(task, 'updateTaskData');
|
|
1750
|
-
const metricsTrackSpy = jest.spyOn(taskManager.metricsManager, 'trackEvent');
|
|
1775
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1751
1776
|
|
|
1752
1777
|
webSocketManagerMock.emit('message', JSON.stringify(assignFailedPayload));
|
|
1753
1778
|
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
assignFailedPayload.data.reason
|
|
1779
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1780
|
+
sendStateMachineEventSpy,
|
|
1781
|
+
TaskEvent.ASSIGN_FAILED
|
|
1758
1782
|
);
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
expect(metricsTrackSpy.mock.calls[0][0]).toBe('Agent Contact Assign Failed');
|
|
1783
|
+
expect(stateMachineEvent?.reason).toBe(assignFailedPayload.data.reason);
|
|
1784
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1762
1785
|
});
|
|
1763
1786
|
|
|
1764
1787
|
it('should remove currentTask from taskCollection on AGENT_WRAPPEDUP event', () => {
|
|
@@ -1781,11 +1804,12 @@ describe('TaskManager', () => {
|
|
|
1781
1804
|
|
|
1782
1805
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1783
1806
|
const task = taskManager.getTask(taskId);
|
|
1784
|
-
const
|
|
1807
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1785
1808
|
|
|
1786
1809
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1787
1810
|
|
|
1788
|
-
|
|
1811
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.WRAPUP_COMPLETE);
|
|
1812
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1789
1813
|
expect(taskManager.getTask(taskId)).toBeUndefined();
|
|
1790
1814
|
});
|
|
1791
1815
|
|
|
@@ -1811,7 +1835,8 @@ describe('TaskManager', () => {
|
|
|
1811
1835
|
};
|
|
1812
1836
|
|
|
1813
1837
|
const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
|
|
1814
|
-
const taskUpdateTaskDataSpy =
|
|
1838
|
+
const taskUpdateTaskDataSpy = taskManager.getTask(taskId).updateTaskData as jest.Mock;
|
|
1839
|
+
taskUpdateTaskDataSpy.mockClear();
|
|
1815
1840
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1816
1841
|
expect(taskEmitSpy).not.toHaveBeenCalled();
|
|
1817
1842
|
expect(taskUpdateTaskDataSpy).not.toHaveBeenCalled();
|
|
@@ -1820,7 +1845,8 @@ describe('TaskManager', () => {
|
|
|
1820
1845
|
it('should emit TASK_CONSULTING event when agent is consulting', () => {
|
|
1821
1846
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1822
1847
|
taskManager.getTask(taskId).data.isConsulted = false;
|
|
1823
|
-
const
|
|
1848
|
+
const task = taskManager.getTask(taskId);
|
|
1849
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1824
1850
|
const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
|
|
1825
1851
|
const consultingPayload = {
|
|
1826
1852
|
data: {
|
|
@@ -1830,20 +1856,21 @@ describe('TaskManager', () => {
|
|
|
1830
1856
|
},
|
|
1831
1857
|
};
|
|
1832
1858
|
webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
|
|
1833
|
-
|
|
1834
|
-
expect(taskEmitSpy).toHaveBeenCalledWith(
|
|
1835
|
-
TASK_EVENTS.TASK_CONSULTING,
|
|
1836
|
-
taskManager.getTask(taskId)
|
|
1837
|
-
);
|
|
1859
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONSULTING_ACTIVE);
|
|
1838
1860
|
expect(taskManagerEmitSpy).toHaveBeenCalledWith(
|
|
1839
1861
|
TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
|
|
1840
1862
|
taskManager.getTask(taskId)
|
|
1841
1863
|
);
|
|
1864
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1842
1865
|
});
|
|
1843
1866
|
|
|
1844
|
-
it('should
|
|
1867
|
+
it('should update task data on AGENT_CONTACT_UNASSIGNED', () => {
|
|
1845
1868
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1846
|
-
const
|
|
1869
|
+
const task = taskManager.getTask(taskId);
|
|
1870
|
+
const sendStateMachineEventSpy = task.sendStateMachineEvent as jest.Mock;
|
|
1871
|
+
sendStateMachineEventSpy.mockClear();
|
|
1872
|
+
const updateTaskDataSpy = task.updateTaskData as jest.Mock;
|
|
1873
|
+
updateTaskDataSpy.mockClear();
|
|
1847
1874
|
const unassignedPayload = {
|
|
1848
1875
|
data: {
|
|
1849
1876
|
type: CC_EVENTS.AGENT_CONTACT_UNASSIGNED,
|
|
@@ -1861,11 +1888,76 @@ describe('TaskManager', () => {
|
|
|
1861
1888
|
},
|
|
1862
1889
|
};
|
|
1863
1890
|
webSocketManagerMock.emit('message', JSON.stringify(unassignedPayload));
|
|
1864
|
-
expect(
|
|
1865
|
-
|
|
1866
|
-
|
|
1891
|
+
expect(sendStateMachineEventSpy).not.toHaveBeenCalled();
|
|
1892
|
+
expect(updateTaskDataSpy).toHaveBeenCalledWith(unassignedPayload.data);
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
it('preserves consult fields from state context during consulting payload refresh', () => {
|
|
1896
|
+
const task = createMockTask({
|
|
1897
|
+
...taskDataMock,
|
|
1898
|
+
interaction: {state: 'consulting'} as any,
|
|
1899
|
+
interactionId: taskId,
|
|
1900
|
+
});
|
|
1901
|
+
task.stateMachineService = {
|
|
1902
|
+
getSnapshot: () => ({
|
|
1903
|
+
value: 'CONSULTING',
|
|
1904
|
+
context: {
|
|
1905
|
+
consultDestinationAgentId: 'agent-preserved',
|
|
1906
|
+
consultDestinationType: 'agent',
|
|
1907
|
+
},
|
|
1908
|
+
}),
|
|
1909
|
+
};
|
|
1910
|
+
|
|
1911
|
+
const incomingTaskData = {
|
|
1912
|
+
...taskDataMock,
|
|
1913
|
+
interaction: {state: 'consulting'} as any,
|
|
1914
|
+
interactionId: taskId,
|
|
1915
|
+
destAgentId: null,
|
|
1916
|
+
destinationType: null,
|
|
1917
|
+
};
|
|
1918
|
+
|
|
1919
|
+
(taskManager as any).updateTaskData(task, incomingTaskData);
|
|
1920
|
+
|
|
1921
|
+
expect(task.updateTaskData).toHaveBeenCalledWith(
|
|
1922
|
+
expect.objectContaining({
|
|
1923
|
+
destAgentId: 'agent-preserved',
|
|
1924
|
+
destinationType: 'agent',
|
|
1925
|
+
})
|
|
1926
|
+
);
|
|
1927
|
+
});
|
|
1928
|
+
|
|
1929
|
+
it('does not preserve stale consult fields once consult is no longer active', () => {
|
|
1930
|
+
const task = createMockTask({
|
|
1931
|
+
...taskDataMock,
|
|
1932
|
+
interaction: {state: 'connected'} as any,
|
|
1933
|
+
interactionId: taskId,
|
|
1934
|
+
});
|
|
1935
|
+
task.stateMachineService = {
|
|
1936
|
+
getSnapshot: () => ({
|
|
1937
|
+
value: 'CONNECTED',
|
|
1938
|
+
context: {
|
|
1939
|
+
consultDestinationAgentId: 'agent-stale',
|
|
1940
|
+
consultDestinationType: 'agent',
|
|
1941
|
+
},
|
|
1942
|
+
}),
|
|
1943
|
+
};
|
|
1944
|
+
|
|
1945
|
+
const incomingTaskData = {
|
|
1946
|
+
...taskDataMock,
|
|
1947
|
+
interaction: {state: 'connected'} as any,
|
|
1948
|
+
interactionId: taskId,
|
|
1949
|
+
destAgentId: null,
|
|
1950
|
+
destinationType: null,
|
|
1951
|
+
};
|
|
1952
|
+
|
|
1953
|
+
(taskManager as any).updateTaskData(task, incomingTaskData);
|
|
1954
|
+
|
|
1955
|
+
expect(task.updateTaskData).toHaveBeenCalledWith(
|
|
1956
|
+
expect.objectContaining({
|
|
1957
|
+
destAgentId: null,
|
|
1958
|
+
destinationType: null,
|
|
1959
|
+
})
|
|
1867
1960
|
);
|
|
1868
|
-
expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, taskManager.getTask(taskId));
|
|
1869
1961
|
});
|
|
1870
1962
|
|
|
1871
1963
|
it('should handle chat interaction and emit TASK_INCOMING immediately', () => {
|
|
@@ -1877,16 +1969,16 @@ describe('TaskManager', () => {
|
|
|
1877
1969
|
},
|
|
1878
1970
|
};
|
|
1879
1971
|
|
|
1880
|
-
const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
|
|
1881
|
-
|
|
1882
1972
|
// Simulate receiving a chat task
|
|
1883
1973
|
webSocketManagerMock.emit('message', JSON.stringify(chatPayload));
|
|
1884
1974
|
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1975
|
+
const chatTask = taskManager.getTask(chatPayload.data.interactionId);
|
|
1976
|
+
const sendStateMachineEventSpy = chatTask.sendStateMachineEvent as jest.Mock;
|
|
1977
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1978
|
+
sendStateMachineEventSpy,
|
|
1979
|
+
TaskEvent.TASK_INCOMING
|
|
1889
1980
|
);
|
|
1981
|
+
expect(stateMachineEvent?.taskData).toEqual(chatPayload.data);
|
|
1890
1982
|
expect(taskManager.getAllTasks()).toHaveProperty(chatPayload.data.interactionId);
|
|
1891
1983
|
});
|
|
1892
1984
|
|
|
@@ -1899,16 +1991,16 @@ describe('TaskManager', () => {
|
|
|
1899
1991
|
},
|
|
1900
1992
|
};
|
|
1901
1993
|
|
|
1902
|
-
const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
|
|
1903
|
-
|
|
1904
1994
|
// Simulate receiving an email task
|
|
1905
1995
|
webSocketManagerMock.emit('message', JSON.stringify(emailPayload));
|
|
1906
1996
|
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1997
|
+
const emailTask = taskManager.getTask(emailPayload.data.interactionId);
|
|
1998
|
+
const sendStateMachineEventSpy = emailTask.sendStateMachineEvent as jest.Mock;
|
|
1999
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
2000
|
+
sendStateMachineEventSpy,
|
|
2001
|
+
TaskEvent.TASK_INCOMING
|
|
1911
2002
|
);
|
|
2003
|
+
expect(stateMachineEvent?.taskData).toEqual(emailPayload.data);
|
|
1912
2004
|
expect(taskManager.getAllTasks()).toHaveProperty(emailPayload.data.interactionId);
|
|
1913
2005
|
});
|
|
1914
2006
|
|
|
@@ -1922,13 +2014,11 @@ describe('TaskManager', () => {
|
|
|
1922
2014
|
},
|
|
1923
2015
|
};
|
|
1924
2016
|
|
|
1925
|
-
const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
|
|
1926
2017
|
webSocketManagerMock.emit('message', JSON.stringify(chatReservedPayload));
|
|
2018
|
+
const task = taskManager.getTask(chatReservedPayload.data.interactionId);
|
|
2019
|
+
const sendStateMachineEventSpy = task.sendStateMachineEvent as jest.Mock;
|
|
1927
2020
|
|
|
1928
|
-
|
|
1929
|
-
TASK_EVENTS.TASK_INCOMING,
|
|
1930
|
-
taskManager.getTask(chatReservedPayload.data.interactionId)
|
|
1931
|
-
);
|
|
2021
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TASK_INCOMING);
|
|
1932
2022
|
|
|
1933
2023
|
// 2. Chat task is assigned
|
|
1934
2024
|
const chatAssignedPayload = {
|
|
@@ -1938,12 +2028,9 @@ describe('TaskManager', () => {
|
|
|
1938
2028
|
},
|
|
1939
2029
|
};
|
|
1940
2030
|
|
|
1941
|
-
const task = taskManager.getTask(chatReservedPayload.data.interactionId);
|
|
1942
|
-
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
1943
|
-
|
|
1944
2031
|
webSocketManagerMock.emit('message', JSON.stringify(chatAssignedPayload));
|
|
1945
2032
|
|
|
1946
|
-
|
|
2033
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.ASSIGN);
|
|
1947
2034
|
|
|
1948
2035
|
// 3. Chat task is ended with state 'new' to trigger cleanup
|
|
1949
2036
|
const chatEndedPayload = {
|
|
@@ -1951,12 +2038,15 @@ describe('TaskManager', () => {
|
|
|
1951
2038
|
...chatReservedPayload.data,
|
|
1952
2039
|
type: CC_EVENTS.CONTACT_ENDED,
|
|
1953
2040
|
interaction: {mediaType: 'chat', state: 'new'}, // Change to 'new' state
|
|
2041
|
+
wrapUpRequired: false,
|
|
1954
2042
|
},
|
|
1955
2043
|
};
|
|
1956
2044
|
|
|
2045
|
+
// Simulate state on the task to allow cleanup logic
|
|
2046
|
+
task.data.interaction.state = 'new';
|
|
1957
2047
|
webSocketManagerMock.emit('message', JSON.stringify(chatEndedPayload));
|
|
1958
2048
|
|
|
1959
|
-
|
|
2049
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONTACT_ENDED);
|
|
1960
2050
|
// Verify task is removed since it was in a 'new' state
|
|
1961
2051
|
expect(taskManager.getTask(chatReservedPayload.data.interactionId)).toBeUndefined();
|
|
1962
2052
|
});
|
|
@@ -2045,13 +2135,8 @@ describe('TaskManager', () => {
|
|
|
2045
2135
|
expect(taskManager.getAllTasks()).toHaveProperty(task2Payload.data.interactionId);
|
|
2046
2136
|
expect(taskManager.getAllTasks()).toHaveProperty(task3Payload.data.interactionId);
|
|
2047
2137
|
|
|
2048
|
-
// Create spies for all tasks
|
|
2049
|
-
const task1EmitSpy = jest.spyOn(taskManager.getTask(task1Payload.data.interactionId), 'emit');
|
|
2050
|
-
const task2EmitSpy = jest.spyOn(taskManager.getTask(task2Payload.data.interactionId), 'emit');
|
|
2051
|
-
const task3EmitSpy = jest.spyOn(taskManager.getTask(task3Payload.data.interactionId), 'emit');
|
|
2052
|
-
|
|
2053
|
-
// Store reference to task2 before it gets removed
|
|
2054
2138
|
const task2 = taskManager.getTask(task2Payload.data.interactionId);
|
|
2139
|
+
const task2SendStateMachineEventSpy = task2.sendStateMachineEvent as jest.Mock;
|
|
2055
2140
|
|
|
2056
2141
|
// End only the second task (chat task)
|
|
2057
2142
|
const chatEndedPayload = {
|
|
@@ -2059,15 +2144,18 @@ describe('TaskManager', () => {
|
|
|
2059
2144
|
...task2Payload.data,
|
|
2060
2145
|
type: CC_EVENTS.CONTACT_ENDED,
|
|
2061
2146
|
interaction: {mediaType: 'chat', state: 'new'}, // Using 'new' to trigger cleanup
|
|
2147
|
+
wrapUpRequired: false,
|
|
2062
2148
|
},
|
|
2063
2149
|
};
|
|
2064
2150
|
|
|
2151
|
+
task2.data.interaction.state = 'new';
|
|
2065
2152
|
webSocketManagerMock.emit('message', JSON.stringify(chatEndedPayload));
|
|
2066
2153
|
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2154
|
+
const firstEndEvent = expectLastStateMachineEvent(
|
|
2155
|
+
task2SendStateMachineEventSpy,
|
|
2156
|
+
TaskEvent.CONTACT_ENDED
|
|
2157
|
+
);
|
|
2158
|
+
expect(firstEndEvent?.taskData).toEqual(chatEndedPayload.data);
|
|
2071
2159
|
|
|
2072
2160
|
// Verify task2 was removed from collection (since state was 'new')
|
|
2073
2161
|
expect(taskManager.getTask(task2Payload.data.interactionId)).toBeUndefined();
|
|
@@ -2078,6 +2166,7 @@ describe('TaskManager', () => {
|
|
|
2078
2166
|
|
|
2079
2167
|
// Store reference to task3 before we end it
|
|
2080
2168
|
const task3 = taskManager.getTask(task3Payload.data.interactionId);
|
|
2169
|
+
const task3SendStateMachineEventSpy = task3.sendStateMachineEvent as jest.Mock;
|
|
2081
2170
|
|
|
2082
2171
|
// Now end task3 with a state that doesn't trigger cleanup
|
|
2083
2172
|
const emailEndedPayload = {
|
|
@@ -2085,31 +2174,32 @@ describe('TaskManager', () => {
|
|
|
2085
2174
|
...task3Payload.data,
|
|
2086
2175
|
type: CC_EVENTS.CONTACT_ENDED,
|
|
2087
2176
|
interaction: {mediaType: 'email', state: 'connected'}, // Using 'connected' to NOT trigger cleanup
|
|
2177
|
+
wrapUpRequired: true,
|
|
2178
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
2088
2179
|
},
|
|
2089
2180
|
};
|
|
2090
2181
|
|
|
2182
|
+
task3.data.interaction.state = 'connected';
|
|
2091
2183
|
webSocketManagerMock.emit('message', JSON.stringify(emailEndedPayload));
|
|
2092
2184
|
|
|
2093
|
-
|
|
2094
|
-
|
|
2185
|
+
const secondEndEvent = expectLastStateMachineEvent(
|
|
2186
|
+
task3SendStateMachineEventSpy,
|
|
2187
|
+
TaskEvent.CONTACT_ENDED
|
|
2188
|
+
);
|
|
2189
|
+
expect(secondEndEvent?.taskData).toEqual(emailEndedPayload.data);
|
|
2095
2190
|
|
|
2096
2191
|
// Verify task3 is still in collection (since state was 'connected')
|
|
2097
2192
|
expect(taskManager.getTask(task3Payload.data.interactionId)).toBeDefined();
|
|
2098
2193
|
|
|
2099
2194
|
// Verify task1 remains unaffected
|
|
2100
|
-
expect(task1EmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END);
|
|
2101
2195
|
expect(taskManager.getTask(task1Payload.data.interactionId)).toBeDefined();
|
|
2102
2196
|
});
|
|
2103
2197
|
|
|
2104
|
-
it('should emit
|
|
2198
|
+
it('should emit TRANSFER_SUCCESS event on AGENT_VTEAM_TRANSFERRED event', () => {
|
|
2105
2199
|
// First create a task by emitting the initial payload
|
|
2106
2200
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
2107
|
-
|
|
2108
|
-
// Get a reference to the task from taskCollection
|
|
2109
2201
|
const task = taskManager.getTask(taskId);
|
|
2110
|
-
|
|
2111
|
-
// Now spy on the task's emit method
|
|
2112
|
-
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
2202
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
2113
2203
|
|
|
2114
2204
|
const vteamTransferredPayload = {
|
|
2115
2205
|
data: {
|
|
@@ -2133,8 +2223,9 @@ describe('TaskManager', () => {
|
|
|
2133
2223
|
|
|
2134
2224
|
webSocketManagerMock.emit('message', JSON.stringify(vteamTransferredPayload));
|
|
2135
2225
|
|
|
2136
|
-
// Check that
|
|
2137
|
-
|
|
2226
|
+
// Check that the state machine received the END event
|
|
2227
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TRANSFER_SUCCESS);
|
|
2228
|
+
sendStateMachineEventSpy.mockRestore();
|
|
2138
2229
|
|
|
2139
2230
|
// The task should still exist in the collection based on current implementation
|
|
2140
2231
|
expect(taskManager.getTask(taskId)).toBeDefined();
|
|
@@ -2149,12 +2240,16 @@ describe('TaskManager', () => {
|
|
|
2149
2240
|
},
|
|
2150
2241
|
};
|
|
2151
2242
|
const task = taskManager.getTask(taskId);
|
|
2152
|
-
const
|
|
2153
|
-
task.data = {...(task.data || {}), ...(data || {})};
|
|
2154
|
-
return task;
|
|
2155
|
-
});
|
|
2243
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
2156
2244
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2157
|
-
expect(
|
|
2245
|
+
expect(sendStateMachineEventSpy).toHaveBeenCalled();
|
|
2246
|
+
const stateMachineEvent = sendStateMachineEventSpy.mock.calls.at(-1)?.[0];
|
|
2247
|
+
expect(stateMachineEvent?.type).toBe(TaskEvent.TASK_WRAPUP);
|
|
2248
|
+
expect(stateMachineEvent?.taskData).toEqual({
|
|
2249
|
+
...payload.data,
|
|
2250
|
+
wrapUpRequired: true,
|
|
2251
|
+
});
|
|
2252
|
+
sendStateMachineEventSpy.mockRestore();
|
|
2158
2253
|
});
|
|
2159
2254
|
|
|
2160
2255
|
it('should not attempt cleanup twice when AGENT_CONTACT_UNASSIGNED is followed by AGENT_WRAPUP', () => {
|
|
@@ -2230,1367 +2325,157 @@ describe('TaskManager', () => {
|
|
|
2230
2325
|
});
|
|
2231
2326
|
|
|
2232
2327
|
describe('should emit appropriate task events for recording events', () => {
|
|
2233
|
-
|
|
2328
|
+
const eventMap: Record<string, TaskEvent | null> = {
|
|
2329
|
+
STARTED: TaskEvent.RECORDING_STARTED,
|
|
2330
|
+
PAUSED: TaskEvent.PAUSE_RECORDING,
|
|
2331
|
+
PAUSE_FAILED: null,
|
|
2332
|
+
RESUMED: TaskEvent.RESUME_RECORDING,
|
|
2333
|
+
RESUME_FAILED: null,
|
|
2334
|
+
};
|
|
2335
|
+
|
|
2336
|
+
['STARTED', 'PAUSED', 'PAUSE_FAILED', 'RESUMED', 'RESUME_FAILED'].forEach((suffix) => {
|
|
2234
2337
|
const ccEvent = CC_EVENTS[`CONTACT_RECORDING_${suffix}`];
|
|
2235
|
-
const
|
|
2236
|
-
it(`should
|
|
2338
|
+
const expectedTaskEvent = eventMap[suffix];
|
|
2339
|
+
it(`should ${expectedTaskEvent ? 'send' : 'not send'} ${
|
|
2340
|
+
expectedTaskEvent ?? 'a'
|
|
2341
|
+
} state machine event on ${ccEvent} event`, () => {
|
|
2237
2342
|
const payload = {data: {...initalPayload.data, type: ccEvent}};
|
|
2238
2343
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
2239
2344
|
const task = taskManager.getTask(taskId);
|
|
2240
|
-
const
|
|
2345
|
+
const sendStateMachineEventSpy = task.sendStateMachineEvent as jest.Mock;
|
|
2346
|
+
sendStateMachineEventSpy.mockClear();
|
|
2241
2347
|
|
|
2242
2348
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2243
|
-
|
|
2349
|
+
if (expectedTaskEvent) {
|
|
2350
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
2351
|
+
sendStateMachineEventSpy,
|
|
2352
|
+
expectedTaskEvent
|
|
2353
|
+
);
|
|
2354
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
2355
|
+
} else {
|
|
2356
|
+
expect(sendStateMachineEventSpy).not.toHaveBeenCalled();
|
|
2357
|
+
}
|
|
2244
2358
|
});
|
|
2245
2359
|
});
|
|
2246
2360
|
});
|
|
2247
2361
|
|
|
2248
2362
|
describe('Conference event handling', () => {
|
|
2249
2363
|
let task;
|
|
2250
|
-
const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
|
|
2251
2364
|
|
|
2252
2365
|
beforeEach(() => {
|
|
2253
|
-
// Set the agentId on taskManager before tests run
|
|
2254
|
-
taskManager.setAgentId(agentId);
|
|
2255
|
-
|
|
2256
2366
|
task = {
|
|
2257
2367
|
data: {interactionId: taskId},
|
|
2258
2368
|
emit: jest.fn(),
|
|
2259
|
-
updateTaskData: jest.fn()
|
|
2260
|
-
|
|
2261
|
-
task.data = {...task.data, ...updatedData};
|
|
2262
|
-
return task;
|
|
2263
|
-
}),
|
|
2369
|
+
updateTaskData: jest.fn(),
|
|
2370
|
+
sendStateMachineEvent: jest.fn(),
|
|
2264
2371
|
};
|
|
2265
|
-
taskManager.taskCollection[taskId] = task;
|
|
2372
|
+
taskManager.taskCollection[taskId] = task as any;
|
|
2266
2373
|
});
|
|
2267
2374
|
|
|
2268
|
-
it('
|
|
2375
|
+
it('sends AGENT_CONSULT_CONFERENCED to state machine as CONFERENCE_START', () => {
|
|
2269
2376
|
const payload = {
|
|
2270
|
-
data: {
|
|
2271
|
-
type: CC_EVENTS.AGENT_CONSULT_CONFERENCED,
|
|
2272
|
-
interactionId: taskId,
|
|
2273
|
-
isConferencing: true,
|
|
2274
|
-
},
|
|
2377
|
+
data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCED, interactionId: taskId},
|
|
2275
2378
|
};
|
|
2276
|
-
|
|
2277
2379
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
expect(
|
|
2380
|
+
expect(task.sendStateMachineEvent).toHaveBeenCalled();
|
|
2381
|
+
const call = task.sendStateMachineEvent.mock.calls[0][0];
|
|
2382
|
+
expect(call.type).toBe(TaskEvent.CONFERENCE_START);
|
|
2281
2383
|
});
|
|
2282
2384
|
|
|
2283
|
-
it('
|
|
2385
|
+
it('sends PARTICIPANT_JOINED_CONFERENCE to state machine as CONFERENCE_START', () => {
|
|
2284
2386
|
const payload = {
|
|
2285
|
-
data: {
|
|
2286
|
-
type: CC_EVENTS.AGENT_CONSULT_CONFERENCING,
|
|
2287
|
-
interactionId: taskId,
|
|
2288
|
-
isConferencing: true,
|
|
2289
|
-
},
|
|
2387
|
+
data: {type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE, interactionId: taskId},
|
|
2290
2388
|
};
|
|
2291
|
-
|
|
2292
2389
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
expect(task.emit).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
|
|
2390
|
+
expect(task.sendStateMachineEvent).toHaveBeenCalled();
|
|
2391
|
+
const call = task.sendStateMachineEvent.mock.calls[0][0];
|
|
2392
|
+
expect(call.type).toBe(TaskEvent.CONFERENCE_START);
|
|
2297
2393
|
});
|
|
2298
2394
|
|
|
2299
|
-
it('
|
|
2395
|
+
it('sends AGENT_CONSULT_CONFERENCE_FAILED to state machine as CONFERENCE_FAILED', () => {
|
|
2300
2396
|
const payload = {
|
|
2301
|
-
data: {
|
|
2302
|
-
type: CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED,
|
|
2303
|
-
interactionId: taskId,
|
|
2304
|
-
reason: 'Network error',
|
|
2305
|
-
},
|
|
2397
|
+
data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED, interactionId: taskId},
|
|
2306
2398
|
};
|
|
2307
|
-
|
|
2308
2399
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2400
|
+
expect(task.sendStateMachineEvent).toHaveBeenCalled();
|
|
2401
|
+
const call = task.sendStateMachineEvent.mock.calls[0][0];
|
|
2402
|
+
expect(call.type).toBe(TaskEvent.CONFERENCE_FAILED);
|
|
2312
2403
|
});
|
|
2313
2404
|
|
|
2314
|
-
it('
|
|
2405
|
+
it('sends PARTICIPANT_LEFT_CONFERENCE to state machine as PARTICIPANT_LEAVE', () => {
|
|
2315
2406
|
const payload = {
|
|
2316
|
-
data: {
|
|
2317
|
-
type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE,
|
|
2318
|
-
interactionId: taskId,
|
|
2319
|
-
participantId: 'new-participant-123',
|
|
2320
|
-
participantType: 'agent',
|
|
2321
|
-
interaction: {
|
|
2322
|
-
participants: {
|
|
2323
|
-
[agentId]: {pType: 'Agent', hasLeft: false},
|
|
2324
|
-
'new-participant-123': {pType: 'Agent', hasLeft: false},
|
|
2325
|
-
},
|
|
2326
|
-
media: {
|
|
2327
|
-
[taskId]: {
|
|
2328
|
-
mType: 'mainCall',
|
|
2329
|
-
participants: [agentId, 'new-participant-123'],
|
|
2330
|
-
},
|
|
2331
|
-
},
|
|
2332
|
-
},
|
|
2333
|
-
},
|
|
2407
|
+
data: {type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, interactionId: taskId},
|
|
2334
2408
|
};
|
|
2335
|
-
|
|
2336
2409
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
expect(
|
|
2340
|
-
// No specific task event emission for participant joined - just data update
|
|
2410
|
+
expect(task.sendStateMachineEvent).toHaveBeenCalled();
|
|
2411
|
+
const call = task.sendStateMachineEvent.mock.calls[0][0];
|
|
2412
|
+
expect(call.type).toBe(TaskEvent.PARTICIPANT_LEAVE);
|
|
2341
2413
|
});
|
|
2342
2414
|
|
|
2343
|
-
it('
|
|
2415
|
+
it('handles AGENT_CONSULT_CONFERENCING event without errors', () => {
|
|
2344
2416
|
const payload = {
|
|
2345
|
-
data: {
|
|
2346
|
-
type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE,
|
|
2347
|
-
interactionId: taskId,
|
|
2348
|
-
participantId: 'new-agent-789',
|
|
2349
|
-
interaction: {
|
|
2350
|
-
participants: {
|
|
2351
|
-
[agentId]: {pType: 'Agent', hasLeft: false},
|
|
2352
|
-
'agent-2': {pType: 'Agent', hasLeft: false},
|
|
2353
|
-
'new-agent-789': {pType: 'Agent', hasLeft: false},
|
|
2354
|
-
'customer-1': {pType: 'Customer', hasLeft: false},
|
|
2355
|
-
},
|
|
2356
|
-
media: {
|
|
2357
|
-
[taskId]: {
|
|
2358
|
-
mType: 'mainCall',
|
|
2359
|
-
participants: [agentId, 'agent-2', 'new-agent-789', 'customer-1'],
|
|
2360
|
-
},
|
|
2361
|
-
},
|
|
2362
|
-
},
|
|
2363
|
-
},
|
|
2417
|
+
data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCING, interactionId: taskId},
|
|
2364
2418
|
};
|
|
2365
|
-
|
|
2366
|
-
const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
|
|
2367
|
-
|
|
2368
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2369
|
-
|
|
2370
|
-
// Verify updateTaskData was called exactly once
|
|
2371
|
-
expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);
|
|
2372
|
-
|
|
2373
|
-
// Verify it was called with isConferenceInProgress already calculated
|
|
2374
|
-
expect(updateTaskDataSpy).toHaveBeenCalledWith(
|
|
2375
|
-
expect.objectContaining({
|
|
2376
|
-
participantId: 'new-agent-789',
|
|
2377
|
-
isConferenceInProgress: true, // 3 active agents
|
|
2378
|
-
})
|
|
2379
|
-
);
|
|
2380
|
-
|
|
2381
|
-
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_JOINED, task);
|
|
2382
|
-
});
|
|
2383
|
-
|
|
2384
|
-
describe('PARTICIPANT_LEFT_CONFERENCE event handling', () => {
|
|
2385
|
-
it('should call updateTaskData only once for PARTICIPANT_LEFT_CONFERENCE with pre-calculated isConferenceInProgress', () => {
|
|
2386
|
-
const payload = {
|
|
2387
|
-
data: {
|
|
2388
|
-
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
2389
|
-
interactionId: taskId,
|
|
2390
|
-
interaction: {
|
|
2391
|
-
participants: {
|
|
2392
|
-
[agentId]: {pType: 'Agent', hasLeft: false},
|
|
2393
|
-
'agent-2': {pType: 'Agent', hasLeft: true}, // This agent left
|
|
2394
|
-
'customer-1': {pType: 'Customer', hasLeft: false},
|
|
2395
|
-
},
|
|
2396
|
-
media: {
|
|
2397
|
-
[taskId]: {
|
|
2398
|
-
mType: 'mainCall',
|
|
2399
|
-
participants: [agentId, 'customer-1'], // agent-2 removed from participants
|
|
2400
|
-
},
|
|
2401
|
-
},
|
|
2402
|
-
},
|
|
2403
|
-
},
|
|
2404
|
-
};
|
|
2405
|
-
|
|
2406
|
-
const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
|
|
2407
|
-
|
|
2408
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2409
|
-
|
|
2410
|
-
// Verify updateTaskData was called exactly once
|
|
2411
|
-
expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);
|
|
2412
|
-
|
|
2413
|
-
// Verify it was called with isConferenceInProgress already calculated
|
|
2414
|
-
expect(updateTaskDataSpy).toHaveBeenCalledWith(
|
|
2415
|
-
expect.objectContaining({
|
|
2416
|
-
isConferenceInProgress: false, // Only 1 active agent remains
|
|
2417
|
-
})
|
|
2418
|
-
);
|
|
2419
|
-
|
|
2420
|
-
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
|
|
2421
|
-
});
|
|
2422
|
-
|
|
2423
|
-
it('should emit TASK_PARTICIPANT_LEFT event when participant leaves conference', () => {
|
|
2424
|
-
const payload = {
|
|
2425
|
-
data: {
|
|
2426
|
-
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
2427
|
-
interactionId: taskId,
|
|
2428
|
-
interaction: {
|
|
2429
|
-
participants: {
|
|
2430
|
-
[agentId]: {
|
|
2431
|
-
hasLeft: false,
|
|
2432
|
-
},
|
|
2433
|
-
},
|
|
2434
|
-
},
|
|
2435
|
-
},
|
|
2436
|
-
};
|
|
2437
|
-
|
|
2438
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2439
|
-
|
|
2440
|
-
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
|
|
2441
|
-
});
|
|
2442
|
-
|
|
2443
|
-
it('should NOT remove task when agent is still in interaction', () => {
|
|
2444
|
-
const payload = {
|
|
2445
|
-
data: {
|
|
2446
|
-
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
2447
|
-
interactionId: taskId,
|
|
2448
|
-
interaction: {
|
|
2449
|
-
participants: {
|
|
2450
|
-
[agentId]: {
|
|
2451
|
-
hasLeft: false,
|
|
2452
|
-
},
|
|
2453
|
-
},
|
|
2454
|
-
},
|
|
2455
|
-
},
|
|
2456
|
-
};
|
|
2457
|
-
|
|
2458
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2459
|
-
|
|
2460
|
-
// Task should still exist in collection
|
|
2461
|
-
expect(taskManager.getTask(taskId)).toBeDefined();
|
|
2462
|
-
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
|
|
2463
|
-
});
|
|
2464
|
-
|
|
2465
|
-
it('should NOT remove task when agent left but is in main interaction', () => {
|
|
2466
|
-
const payload = {
|
|
2467
|
-
data: {
|
|
2468
|
-
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
2469
|
-
interactionId: taskId,
|
|
2470
|
-
interaction: {
|
|
2471
|
-
participants: {
|
|
2472
|
-
[agentId]: {
|
|
2473
|
-
hasLeft: true,
|
|
2474
|
-
},
|
|
2475
|
-
},
|
|
2476
|
-
media: {
|
|
2477
|
-
[taskId]: {
|
|
2478
|
-
mType: 'mainCall',
|
|
2479
|
-
participants: [agentId],
|
|
2480
|
-
},
|
|
2481
|
-
},
|
|
2482
|
-
},
|
|
2483
|
-
},
|
|
2484
|
-
};
|
|
2485
|
-
|
|
2486
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
2487
|
-
|
|
2488
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2489
|
-
|
|
2490
|
-
// Task should still exist - not removed
|
|
2491
|
-
expect(removeTaskSpy).not.toHaveBeenCalled();
|
|
2492
|
-
expect(taskManager.getTask(taskId)).toBeDefined();
|
|
2493
|
-
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
|
|
2494
|
-
});
|
|
2495
|
-
|
|
2496
|
-
it('should NOT remove task when agent left but is primary (owner)', () => {
|
|
2497
|
-
const payload = {
|
|
2498
|
-
data: {
|
|
2499
|
-
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
2500
|
-
interactionId: taskId,
|
|
2501
|
-
interaction: {
|
|
2502
|
-
participants: {
|
|
2503
|
-
[agentId]: {
|
|
2504
|
-
hasLeft: true,
|
|
2505
|
-
},
|
|
2506
|
-
},
|
|
2507
|
-
owner: agentId,
|
|
2508
|
-
media: {
|
|
2509
|
-
[taskId]: {
|
|
2510
|
-
mType: 'consultCall',
|
|
2511
|
-
participants: ['other-agent'],
|
|
2512
|
-
},
|
|
2513
|
-
},
|
|
2514
|
-
},
|
|
2515
|
-
},
|
|
2516
|
-
};
|
|
2517
|
-
|
|
2518
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
2519
|
-
|
|
2520
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2521
|
-
|
|
2522
|
-
// Task should still exist - not removed because agent is primary
|
|
2523
|
-
expect(removeTaskSpy).not.toHaveBeenCalled();
|
|
2524
|
-
expect(taskManager.getTask(taskId)).toBeDefined();
|
|
2525
|
-
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
|
|
2526
|
-
});
|
|
2527
|
-
|
|
2528
|
-
it('should remove task when agent left and is NOT in main interaction and is NOT primary', () => {
|
|
2529
|
-
const payload = {
|
|
2530
|
-
data: {
|
|
2531
|
-
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
2532
|
-
interactionId: taskId,
|
|
2533
|
-
interaction: {
|
|
2534
|
-
participants: {
|
|
2535
|
-
[agentId]: {
|
|
2536
|
-
hasLeft: true,
|
|
2537
|
-
},
|
|
2538
|
-
},
|
|
2539
|
-
owner: 'another-agent-id',
|
|
2540
|
-
media: {
|
|
2541
|
-
[taskId]: {
|
|
2542
|
-
mType: 'mainCall',
|
|
2543
|
-
participants: ['another-agent-id'],
|
|
2544
|
-
},
|
|
2545
|
-
},
|
|
2546
|
-
},
|
|
2547
|
-
},
|
|
2548
|
-
};
|
|
2549
|
-
|
|
2550
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
2551
|
-
|
|
2552
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2553
|
-
|
|
2554
|
-
// Task should be removed
|
|
2555
|
-
expect(removeTaskSpy).toHaveBeenCalled();
|
|
2556
|
-
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
|
|
2557
|
-
});
|
|
2558
|
-
|
|
2559
|
-
it('should remove task when agent is not in participants list', () => {
|
|
2560
|
-
const payload = {
|
|
2561
|
-
data: {
|
|
2562
|
-
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
2563
|
-
interactionId: taskId,
|
|
2564
|
-
interaction: {
|
|
2565
|
-
participants: {
|
|
2566
|
-
'other-agent-id': {
|
|
2567
|
-
hasLeft: false,
|
|
2568
|
-
},
|
|
2569
|
-
},
|
|
2570
|
-
owner: 'another-agent-id',
|
|
2571
|
-
},
|
|
2572
|
-
},
|
|
2573
|
-
};
|
|
2574
|
-
|
|
2575
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
2576
|
-
|
|
2577
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2578
|
-
|
|
2579
|
-
// Task should be removed because agent is not in participants
|
|
2580
|
-
expect(removeTaskSpy).toHaveBeenCalled();
|
|
2581
|
-
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
|
|
2582
|
-
});
|
|
2583
|
-
|
|
2584
|
-
it('should update isConferenceInProgress based on remaining active agents', () => {
|
|
2585
|
-
const payload = {
|
|
2586
|
-
data: {
|
|
2587
|
-
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
2588
|
-
interactionId: taskId,
|
|
2589
|
-
interaction: {
|
|
2590
|
-
participants: {
|
|
2591
|
-
[agentId]: {
|
|
2592
|
-
hasLeft: false,
|
|
2593
|
-
pType: 'Agent',
|
|
2594
|
-
},
|
|
2595
|
-
'agent-2': {
|
|
2596
|
-
hasLeft: false,
|
|
2597
|
-
pType: 'Agent',
|
|
2598
|
-
},
|
|
2599
|
-
'customer-1': {
|
|
2600
|
-
hasLeft: false,
|
|
2601
|
-
pType: 'Customer',
|
|
2602
|
-
},
|
|
2603
|
-
},
|
|
2604
|
-
media: {
|
|
2605
|
-
[taskId]: {
|
|
2606
|
-
mType: 'mainCall',
|
|
2607
|
-
participants: [agentId, 'agent-2', 'customer-1'],
|
|
2608
|
-
},
|
|
2609
|
-
},
|
|
2610
|
-
},
|
|
2611
|
-
},
|
|
2612
|
-
};
|
|
2613
|
-
|
|
2614
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2615
|
-
|
|
2616
|
-
// isConferenceInProgress should be true (2 active agents)
|
|
2617
|
-
expect(task.data.isConferenceInProgress).toBe(true);
|
|
2618
|
-
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
|
|
2619
|
-
});
|
|
2620
|
-
|
|
2621
|
-
it('should set isConferenceInProgress to false when only one agent remains', () => {
|
|
2622
|
-
const payload = {
|
|
2623
|
-
data: {
|
|
2624
|
-
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
2625
|
-
interactionId: taskId,
|
|
2626
|
-
interaction: {
|
|
2627
|
-
participants: {
|
|
2628
|
-
[agentId]: {
|
|
2629
|
-
hasLeft: false,
|
|
2630
|
-
pType: 'Agent',
|
|
2631
|
-
},
|
|
2632
|
-
'agent-2': {
|
|
2633
|
-
hasLeft: true,
|
|
2634
|
-
pType: 'Agent',
|
|
2635
|
-
},
|
|
2636
|
-
'customer-1': {
|
|
2637
|
-
hasLeft: false,
|
|
2638
|
-
pType: 'Customer',
|
|
2639
|
-
},
|
|
2640
|
-
},
|
|
2641
|
-
media: {
|
|
2642
|
-
[taskId]: {
|
|
2643
|
-
mType: 'mainCall',
|
|
2644
|
-
participants: [agentId, 'customer-1'],
|
|
2645
|
-
},
|
|
2646
|
-
},
|
|
2647
|
-
},
|
|
2648
|
-
},
|
|
2649
|
-
};
|
|
2650
|
-
|
|
2651
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2652
|
-
|
|
2653
|
-
// isConferenceInProgress should be false (only 1 active agent)
|
|
2654
|
-
expect(task.data.isConferenceInProgress).toBe(false);
|
|
2655
|
-
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
|
|
2656
|
-
});
|
|
2657
|
-
|
|
2658
|
-
it('should handle participant left when no participants data exists', () => {
|
|
2659
|
-
const payload = {
|
|
2660
|
-
data: {
|
|
2661
|
-
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
2662
|
-
interactionId: taskId,
|
|
2663
|
-
interaction: {},
|
|
2664
|
-
},
|
|
2665
|
-
};
|
|
2666
|
-
|
|
2667
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
2668
|
-
|
|
2419
|
+
expect(() => {
|
|
2669
2420
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2670
|
-
|
|
2671
|
-
// When no participants data exists, checkParticipantNotInInteraction returns true
|
|
2672
|
-
// Since agent won't be in main interaction either, task should be removed
|
|
2673
|
-
expect(removeTaskSpy).toHaveBeenCalled();
|
|
2674
|
-
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
|
|
2675
|
-
});
|
|
2421
|
+
}).not.toThrow();
|
|
2676
2422
|
});
|
|
2677
2423
|
|
|
2678
|
-
it('
|
|
2424
|
+
it('handles PARTICIPANT_LEFT_CONFERENCE_FAILED event without errors', () => {
|
|
2679
2425
|
const payload = {
|
|
2680
|
-
data: {
|
|
2681
|
-
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE_FAILED,
|
|
2682
|
-
interactionId: taskId,
|
|
2683
|
-
reason: 'Exit failed',
|
|
2684
|
-
},
|
|
2426
|
+
data: {type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE_FAILED, interactionId: taskId},
|
|
2685
2427
|
};
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
expect(task.data.reason).toBe('Exit failed');
|
|
2690
|
-
// No event emission expected for failure - handled by contact method promise rejection
|
|
2428
|
+
expect(() => {
|
|
2429
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2430
|
+
}).not.toThrow();
|
|
2691
2431
|
});
|
|
2692
2432
|
|
|
2693
|
-
it('
|
|
2433
|
+
it('only routes conference events to matching tasks', () => {
|
|
2694
2434
|
const otherTaskId = 'other-task-id';
|
|
2695
|
-
const otherTask = {
|
|
2435
|
+
const otherTask: any = {
|
|
2696
2436
|
data: {interactionId: otherTaskId},
|
|
2697
2437
|
emit: jest.fn(),
|
|
2438
|
+
updateTaskData: jest.fn(),
|
|
2439
|
+
sendStateMachineEvent: jest.fn(),
|
|
2698
2440
|
};
|
|
2699
2441
|
taskManager.taskCollection[otherTaskId] = otherTask;
|
|
2700
2442
|
|
|
2701
2443
|
const payload = {
|
|
2702
|
-
data: {
|
|
2703
|
-
type: CC_EVENTS.AGENT_CONSULT_CONFERENCED,
|
|
2704
|
-
interactionId: taskId,
|
|
2705
|
-
isConferencing: true,
|
|
2706
|
-
},
|
|
2444
|
+
data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCED, interactionId: taskId},
|
|
2707
2445
|
};
|
|
2708
|
-
|
|
2709
2446
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2710
2447
|
|
|
2711
|
-
|
|
2712
|
-
expect(
|
|
2713
|
-
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
|
|
2714
|
-
|
|
2715
|
-
// Other task should not be affected
|
|
2716
|
-
expect(otherTask.data.isConferencing).toBeUndefined();
|
|
2717
|
-
expect(otherTask.emit).not.toHaveBeenCalled();
|
|
2448
|
+
expect(task.sendStateMachineEvent).toHaveBeenCalled();
|
|
2449
|
+
expect(otherTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
2718
2450
|
});
|
|
2719
2451
|
});
|
|
2720
2452
|
|
|
2721
|
-
describe('
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
it('should remove OUTDIAL task on CONTACT_ENDED when current agent is NOT in agentsPendingWrapUp', () => {
|
|
2729
|
-
const task = taskManager.getTask(taskId);
|
|
2730
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
2731
|
-
task.data = {
|
|
2732
|
-
...task.data,
|
|
2733
|
-
...newData,
|
|
2734
|
-
interaction: {
|
|
2735
|
-
...task.data.interaction,
|
|
2736
|
-
outboundType: 'OUTDIAL',
|
|
2737
|
-
state: 'new',
|
|
2738
|
-
mediaType: 'telephony',
|
|
2739
|
-
},
|
|
2740
|
-
agentsPendingWrapUp: ['different-agent-123'], // Current agent not in the list
|
|
2741
|
-
};
|
|
2742
|
-
return task;
|
|
2743
|
-
});
|
|
2744
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
2745
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
2746
|
-
|
|
2747
|
-
const payload = {
|
|
2748
|
-
data: {
|
|
2749
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
2750
|
-
interactionId: taskId,
|
|
2751
|
-
interaction: {
|
|
2752
|
-
outboundType: 'OUTDIAL',
|
|
2753
|
-
state: 'new',
|
|
2754
|
-
mediaType: 'telephony',
|
|
2755
|
-
},
|
|
2756
|
-
agentsPendingWrapUp: ['different-agent-123'], // Current agent not in the list
|
|
2757
|
-
},
|
|
2758
|
-
};
|
|
2759
|
-
|
|
2760
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2761
|
-
|
|
2762
|
-
expect(removeTaskSpy).toHaveBeenCalled();
|
|
2763
|
-
expect(taskManager.getTask(taskId)).toBeUndefined();
|
|
2764
|
-
});
|
|
2765
|
-
|
|
2766
|
-
it('should NOT remove OUTDIAL task on CONTACT_ENDED when current agent IS in agentsPendingWrapUp', () => {
|
|
2767
|
-
const task = taskManager.getTask(taskId);
|
|
2768
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
2769
|
-
task.data = {
|
|
2770
|
-
...task.data,
|
|
2771
|
-
...newData,
|
|
2772
|
-
interaction: {
|
|
2773
|
-
...task.data.interaction,
|
|
2774
|
-
outboundType: 'OUTDIAL',
|
|
2775
|
-
state: 'new',
|
|
2776
|
-
mediaType: 'telephony',
|
|
2777
|
-
},
|
|
2778
|
-
agentsPendingWrapUp: [agentId, 'other-agent-456'], // Current agent IS in the list
|
|
2779
|
-
};
|
|
2780
|
-
return task;
|
|
2781
|
-
});
|
|
2782
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
2783
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
2784
|
-
|
|
2785
|
-
const payload = {
|
|
2786
|
-
data: {
|
|
2787
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
2788
|
-
interactionId: taskId,
|
|
2789
|
-
interaction: {
|
|
2790
|
-
outboundType: 'OUTDIAL',
|
|
2791
|
-
state: 'new',
|
|
2792
|
-
mediaType: 'telephony',
|
|
2793
|
-
},
|
|
2794
|
-
agentsPendingWrapUp: [agentId, 'other-agent-456'], // Current agent IS in the list
|
|
2795
|
-
},
|
|
2796
|
-
};
|
|
2797
|
-
|
|
2798
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2799
|
-
|
|
2800
|
-
expect(removeTaskSpy).not.toHaveBeenCalled();
|
|
2801
|
-
expect(taskManager.getTask(taskId)).toBeDefined();
|
|
2802
|
-
});
|
|
2803
|
-
|
|
2804
|
-
it('should remove OUTDIAL task when needsWrapUp is false and task is outdial', () => {
|
|
2805
|
-
const task = taskManager.getTask(taskId);
|
|
2806
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
2807
|
-
task.data = {
|
|
2808
|
-
...task.data,
|
|
2809
|
-
...newData,
|
|
2810
|
-
interaction: {
|
|
2811
|
-
...task.data.interaction,
|
|
2812
|
-
outboundType: 'OUTDIAL',
|
|
2813
|
-
state: 'WRAPUP', // Not 'new' state
|
|
2814
|
-
mediaType: 'telephony',
|
|
2815
|
-
},
|
|
2816
|
-
agentsPendingWrapUp: [], // No agents pending wrap-up
|
|
2817
|
-
};
|
|
2818
|
-
return task;
|
|
2819
|
-
});
|
|
2820
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
2821
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
2822
|
-
|
|
2823
|
-
const payload = {
|
|
2824
|
-
data: {
|
|
2825
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
2826
|
-
interactionId: taskId,
|
|
2827
|
-
interaction: {
|
|
2828
|
-
outboundType: 'OUTDIAL',
|
|
2829
|
-
state: 'WRAPUP', // Not 'new' state
|
|
2830
|
-
mediaType: 'telephony',
|
|
2831
|
-
},
|
|
2832
|
-
agentsPendingWrapUp: [], // No agents pending wrap-up
|
|
2833
|
-
},
|
|
2834
|
-
};
|
|
2835
|
-
|
|
2836
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2837
|
-
|
|
2838
|
-
expect(removeTaskSpy).toHaveBeenCalled();
|
|
2839
|
-
expect(taskManager.getTask(taskId)).toBeUndefined();
|
|
2840
|
-
});
|
|
2841
|
-
|
|
2842
|
-
it('should remove OUTDIAL task when needsWrapUp is false (agentsPendingWrapUp is undefined)', () => {
|
|
2843
|
-
const task = taskManager.getTask(taskId);
|
|
2844
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
2845
|
-
task.data = {
|
|
2846
|
-
...task.data,
|
|
2847
|
-
...newData,
|
|
2848
|
-
interaction: {
|
|
2849
|
-
...task.data.interaction,
|
|
2850
|
-
outboundType: 'OUTDIAL',
|
|
2851
|
-
state: 'WRAPUP',
|
|
2852
|
-
mediaType: 'telephony',
|
|
2853
|
-
},
|
|
2854
|
-
agentsPendingWrapUp: undefined, // No agentsPendingWrapUp field
|
|
2855
|
-
};
|
|
2856
|
-
return task;
|
|
2857
|
-
});
|
|
2858
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
2859
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
2860
|
-
|
|
2861
|
-
const payload = {
|
|
2862
|
-
data: {
|
|
2863
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
2864
|
-
interactionId: taskId,
|
|
2865
|
-
interaction: {
|
|
2866
|
-
outboundType: 'OUTDIAL',
|
|
2867
|
-
state: 'WRAPUP',
|
|
2868
|
-
mediaType: 'telephony',
|
|
2869
|
-
},
|
|
2870
|
-
// agentsPendingWrapUp not included
|
|
2871
|
-
},
|
|
2872
|
-
};
|
|
2873
|
-
|
|
2874
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2875
|
-
|
|
2876
|
-
expect(removeTaskSpy).toHaveBeenCalled();
|
|
2877
|
-
expect(taskManager.getTask(taskId)).toBeUndefined();
|
|
2878
|
-
});
|
|
2453
|
+
describe('state machine integration', () => {
|
|
2454
|
+
it('maps CC events to task state machine events using normalized payload', () => {
|
|
2455
|
+
const mapped = (TaskManager as any).mapEventToTaskStateMachineEvent(
|
|
2456
|
+
CC_EVENTS.AGENT_CONTACT_ASSIGNED,
|
|
2457
|
+
{...taskDataMock, type: CC_EVENTS.AGENT_CONTACT_ASSIGNED}
|
|
2458
|
+
);
|
|
2879
2459
|
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
task.data = {
|
|
2884
|
-
...task.data,
|
|
2885
|
-
...newData,
|
|
2886
|
-
interaction: {
|
|
2887
|
-
...task.data.interaction,
|
|
2888
|
-
outboundType: 'OUTDIAL',
|
|
2889
|
-
state: 'WRAPUP',
|
|
2890
|
-
mediaType: 'telephony',
|
|
2891
|
-
},
|
|
2892
|
-
agentsPendingWrapUp: [agentId], // Current agent needs wrap-up
|
|
2893
|
-
};
|
|
2894
|
-
return task;
|
|
2460
|
+
expect(mapped).toEqual({
|
|
2461
|
+
type: TaskEvent.ASSIGN,
|
|
2462
|
+
taskData: {...taskDataMock, type: CC_EVENTS.AGENT_CONTACT_ASSIGNED},
|
|
2895
2463
|
});
|
|
2896
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
2897
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
2898
|
-
|
|
2899
|
-
const payload = {
|
|
2900
|
-
data: {
|
|
2901
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
2902
|
-
interactionId: taskId,
|
|
2903
|
-
interaction: {
|
|
2904
|
-
outboundType: 'OUTDIAL',
|
|
2905
|
-
state: 'WRAPUP',
|
|
2906
|
-
mediaType: 'telephony',
|
|
2907
|
-
},
|
|
2908
|
-
agentsPendingWrapUp: [agentId], // Current agent needs wrap-up
|
|
2909
|
-
},
|
|
2910
|
-
};
|
|
2911
|
-
|
|
2912
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2913
|
-
|
|
2914
|
-
expect(removeTaskSpy).not.toHaveBeenCalled();
|
|
2915
|
-
expect(taskManager.getTask(taskId)).toBeDefined();
|
|
2916
2464
|
});
|
|
2917
2465
|
|
|
2918
|
-
it('
|
|
2466
|
+
it('sends mapped events to the task state machine service', () => {
|
|
2467
|
+
const payload = {...taskDataMock, type: CC_EVENTS.AGENT_CONTACT_ASSIGNED};
|
|
2919
2468
|
const task = taskManager.getTask(taskId);
|
|
2920
|
-
|
|
2921
|
-
task.data = {
|
|
2922
|
-
...task.data,
|
|
2923
|
-
...newData,
|
|
2924
|
-
interaction: {
|
|
2925
|
-
...task.data.interaction,
|
|
2926
|
-
outboundType: 'PREVIEW', // Not OUTDIAL
|
|
2927
|
-
state: 'new',
|
|
2928
|
-
mediaType: 'telephony',
|
|
2929
|
-
},
|
|
2930
|
-
agentsPendingWrapUp: [agentId],
|
|
2931
|
-
};
|
|
2932
|
-
return task;
|
|
2933
|
-
});
|
|
2934
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
2935
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
2936
|
-
|
|
2937
|
-
const payload = {
|
|
2938
|
-
data: {
|
|
2939
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
2940
|
-
interactionId: taskId,
|
|
2941
|
-
interaction: {
|
|
2942
|
-
outboundType: 'PREVIEW',
|
|
2943
|
-
state: 'new',
|
|
2944
|
-
mediaType: 'telephony',
|
|
2945
|
-
},
|
|
2946
|
-
agentsPendingWrapUp: [agentId],
|
|
2947
|
-
},
|
|
2948
|
-
};
|
|
2949
|
-
|
|
2950
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2469
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
2951
2470
|
|
|
2952
|
-
|
|
2953
|
-
expect(taskManager.getTask(taskId)).toBeUndefined();
|
|
2954
|
-
});
|
|
2471
|
+
webSocketManagerMock.emit('message', JSON.stringify({data: payload}));
|
|
2955
2472
|
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
task.data = {
|
|
2960
|
-
...task.data,
|
|
2961
|
-
...newData,
|
|
2962
|
-
interaction: {
|
|
2963
|
-
...task.data.interaction,
|
|
2964
|
-
outboundType: 'OUTDIAL',
|
|
2965
|
-
state: 'new',
|
|
2966
|
-
mediaType: 'telephony',
|
|
2967
|
-
},
|
|
2968
|
-
agentsPendingWrapUp: ['agent-1', 'agent-2', 'agent-3'], // Current agent not in the list
|
|
2969
|
-
};
|
|
2970
|
-
return task;
|
|
2473
|
+
expect(sendStateMachineEventSpy).toHaveBeenCalledWith({
|
|
2474
|
+
type: TaskEvent.ASSIGN,
|
|
2475
|
+
taskData: payload,
|
|
2971
2476
|
});
|
|
2972
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
2973
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
2974
|
-
|
|
2975
|
-
const payload = {
|
|
2976
|
-
data: {
|
|
2977
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
2978
|
-
interactionId: taskId,
|
|
2979
|
-
interaction: {
|
|
2980
|
-
outboundType: 'OUTDIAL',
|
|
2981
|
-
state: 'new',
|
|
2982
|
-
mediaType: 'telephony',
|
|
2983
|
-
},
|
|
2984
|
-
agentsPendingWrapUp: ['agent-1', 'agent-2', 'agent-3'],
|
|
2985
|
-
},
|
|
2986
|
-
};
|
|
2987
|
-
|
|
2988
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2989
|
-
|
|
2990
|
-
expect(removeTaskSpy).toHaveBeenCalled();
|
|
2991
|
-
expect(taskManager.getTask(taskId)).toBeUndefined();
|
|
2992
|
-
});
|
|
2993
|
-
|
|
2994
|
-
it('should handle agentsPendingWrapUp with multiple agents correctly - keep if current agent is in list', () => {
|
|
2995
|
-
const task = taskManager.getTask(taskId);
|
|
2996
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
2997
|
-
task.data = {
|
|
2998
|
-
...task.data,
|
|
2999
|
-
...newData,
|
|
3000
|
-
interaction: {
|
|
3001
|
-
...task.data.interaction,
|
|
3002
|
-
outboundType: 'OUTDIAL',
|
|
3003
|
-
state: 'new',
|
|
3004
|
-
mediaType: 'telephony',
|
|
3005
|
-
},
|
|
3006
|
-
agentsPendingWrapUp: ['agent-1', agentId, 'agent-3'], // Current agent IS in the list
|
|
3007
|
-
};
|
|
3008
|
-
return task;
|
|
3009
|
-
});
|
|
3010
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
3011
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
3012
|
-
|
|
3013
|
-
const payload = {
|
|
3014
|
-
data: {
|
|
3015
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
3016
|
-
interactionId: taskId,
|
|
3017
|
-
interaction: {
|
|
3018
|
-
outboundType: 'OUTDIAL',
|
|
3019
|
-
state: 'new',
|
|
3020
|
-
mediaType: 'telephony',
|
|
3021
|
-
},
|
|
3022
|
-
agentsPendingWrapUp: ['agent-1', agentId, 'agent-3'],
|
|
3023
|
-
},
|
|
3024
|
-
};
|
|
3025
|
-
|
|
3026
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
3027
|
-
|
|
3028
|
-
expect(removeTaskSpy).not.toHaveBeenCalled();
|
|
3029
|
-
expect(taskManager.getTask(taskId)).toBeDefined();
|
|
3030
|
-
});
|
|
3031
|
-
});
|
|
3032
|
-
|
|
3033
|
-
describe('CONTACT_MERGED event handling', () => {
|
|
3034
|
-
let task;
|
|
3035
|
-
let taskEmitSpy;
|
|
3036
|
-
let managerEmitSpy;
|
|
3037
|
-
|
|
3038
|
-
beforeEach(() => {
|
|
3039
|
-
// Create initial task
|
|
3040
|
-
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
3041
|
-
task = taskManager.getTask(taskId);
|
|
3042
|
-
taskEmitSpy = jest.spyOn(task, 'emit');
|
|
3043
|
-
managerEmitSpy = jest.spyOn(taskManager, 'emit');
|
|
3044
|
-
});
|
|
3045
|
-
|
|
3046
|
-
it('should update existing task data and emit TASK_MERGED event when CONTACT_MERGED is received', () => {
|
|
3047
|
-
const mergedPayload = {
|
|
3048
|
-
data: {
|
|
3049
|
-
type: CC_EVENTS.CONTACT_MERGED,
|
|
3050
|
-
interactionId: taskId,
|
|
3051
|
-
agentId: taskDataMock.agentId,
|
|
3052
|
-
interaction: {
|
|
3053
|
-
...taskDataMock.interaction,
|
|
3054
|
-
state: 'merged',
|
|
3055
|
-
customField: 'updated-value',
|
|
3056
|
-
},
|
|
3057
|
-
},
|
|
3058
|
-
};
|
|
3059
|
-
|
|
3060
|
-
webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
|
|
3061
|
-
|
|
3062
|
-
const updatedTask = taskManager.getTask(taskId);
|
|
3063
|
-
expect(updatedTask).toBeDefined();
|
|
3064
|
-
expect(updatedTask.data.interaction.customField).toBe('updated-value');
|
|
3065
|
-
expect(updatedTask.data.interaction.state).toBe('merged');
|
|
3066
|
-
expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, updatedTask);
|
|
3067
|
-
});
|
|
3068
|
-
|
|
3069
|
-
it('should create new task when CONTACT_MERGED is received for non-existing task', () => {
|
|
3070
|
-
const newMergedTaskId = 'new-merged-task-id';
|
|
3071
|
-
const mergedPayload = {
|
|
3072
|
-
data: {
|
|
3073
|
-
type: CC_EVENTS.CONTACT_MERGED,
|
|
3074
|
-
interactionId: newMergedTaskId,
|
|
3075
|
-
agentId: taskDataMock.agentId,
|
|
3076
|
-
interaction: {
|
|
3077
|
-
mediaType: 'telephony',
|
|
3078
|
-
state: 'merged',
|
|
3079
|
-
participants: {
|
|
3080
|
-
[taskDataMock.agentId]: {
|
|
3081
|
-
isWrapUp: false,
|
|
3082
|
-
hasJoined: true,
|
|
3083
|
-
},
|
|
3084
|
-
},
|
|
3085
|
-
},
|
|
3086
|
-
},
|
|
3087
|
-
};
|
|
3088
|
-
|
|
3089
|
-
// Verify task doesn't exist before
|
|
3090
|
-
expect(taskManager.getTask(newMergedTaskId)).toBeUndefined();
|
|
3091
|
-
|
|
3092
|
-
webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
|
|
3093
|
-
|
|
3094
|
-
// Verify task was created
|
|
3095
|
-
const newTask = taskManager.getTask(newMergedTaskId);
|
|
3096
|
-
expect(newTask).toBeDefined();
|
|
3097
|
-
expect(newTask.data.interactionId).toBe(newMergedTaskId);
|
|
3098
|
-
expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, newTask);
|
|
3099
|
-
});
|
|
3100
|
-
|
|
3101
|
-
it('should remove child task when childInteractionId is present in CONTACT_MERGED', () => {
|
|
3102
|
-
const childTaskId = 'child-task-id';
|
|
3103
|
-
const parentTaskId = 'parent-task-id';
|
|
3104
|
-
|
|
3105
|
-
// Create child task
|
|
3106
|
-
const childPayload = {
|
|
3107
|
-
data: {
|
|
3108
|
-
type: CC_EVENTS.AGENT_CONTACT_RESERVED,
|
|
3109
|
-
interactionId: childTaskId,
|
|
3110
|
-
agentId: taskDataMock.agentId,
|
|
3111
|
-
interaction: {mediaType: 'telephony'},
|
|
3112
|
-
},
|
|
3113
|
-
};
|
|
3114
|
-
webSocketManagerMock.emit('message', JSON.stringify(childPayload));
|
|
3115
|
-
|
|
3116
|
-
// Verify child task exists
|
|
3117
|
-
expect(taskManager.getTask(childTaskId)).toBeDefined();
|
|
3118
|
-
|
|
3119
|
-
// Create parent task
|
|
3120
|
-
const parentPayload = {
|
|
3121
|
-
data: {
|
|
3122
|
-
type: CC_EVENTS.AGENT_CONTACT_RESERVED,
|
|
3123
|
-
interactionId: parentTaskId,
|
|
3124
|
-
agentId: taskDataMock.agentId,
|
|
3125
|
-
interaction: {mediaType: 'telephony'},
|
|
3126
|
-
},
|
|
3127
|
-
};
|
|
3128
|
-
webSocketManagerMock.emit('message', JSON.stringify(parentPayload));
|
|
3129
|
-
|
|
3130
|
-
// Send CONTACT_MERGED with childInteractionId
|
|
3131
|
-
const mergedPayload = {
|
|
3132
|
-
data: {
|
|
3133
|
-
type: CC_EVENTS.CONTACT_MERGED,
|
|
3134
|
-
interactionId: parentTaskId,
|
|
3135
|
-
childInteractionId: childTaskId,
|
|
3136
|
-
agentId: taskDataMock.agentId,
|
|
3137
|
-
interaction: {
|
|
3138
|
-
mediaType: 'telephony',
|
|
3139
|
-
state: 'merged',
|
|
3140
|
-
},
|
|
3141
|
-
},
|
|
3142
|
-
};
|
|
3143
|
-
|
|
3144
|
-
webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
|
|
3145
|
-
|
|
3146
|
-
// Verify child task was removed
|
|
3147
|
-
expect(taskManager.getTask(childTaskId)).toBeUndefined();
|
|
3148
|
-
|
|
3149
|
-
// Verify parent task still exists
|
|
3150
|
-
expect(taskManager.getTask(parentTaskId)).toBeDefined();
|
|
3151
|
-
|
|
3152
|
-
// Verify TASK_MERGED event was emitted
|
|
3153
|
-
expect(managerEmitSpy).toHaveBeenCalledWith(
|
|
3154
|
-
TASK_EVENTS.TASK_MERGED,
|
|
3155
|
-
expect.objectContaining({
|
|
3156
|
-
data: expect.objectContaining({
|
|
3157
|
-
interactionId: parentTaskId,
|
|
3158
|
-
}),
|
|
3159
|
-
})
|
|
3160
|
-
);
|
|
3161
|
-
});
|
|
3162
|
-
|
|
3163
|
-
it('should handle CONTACT_MERGED with EP-DN participant correctly', () => {
|
|
3164
|
-
const epdnTaskId = 'epdn-merged-task';
|
|
3165
|
-
const mergedPayload = {
|
|
3166
|
-
data: {
|
|
3167
|
-
type: CC_EVENTS.CONTACT_MERGED,
|
|
3168
|
-
interactionId: epdnTaskId,
|
|
3169
|
-
agentId: taskDataMock.agentId,
|
|
3170
|
-
interaction: {
|
|
3171
|
-
mediaType: 'telephony',
|
|
3172
|
-
state: 'merged',
|
|
3173
|
-
participants: {
|
|
3174
|
-
[taskDataMock.agentId]: {
|
|
3175
|
-
type: 'Agent',
|
|
3176
|
-
isWrapUp: false,
|
|
3177
|
-
hasJoined: true,
|
|
3178
|
-
},
|
|
3179
|
-
'epdn-participant': {
|
|
3180
|
-
type: 'EpDn',
|
|
3181
|
-
epId: 'entry-point-123',
|
|
3182
|
-
isWrapUp: false,
|
|
3183
|
-
},
|
|
3184
|
-
},
|
|
3185
|
-
},
|
|
3186
|
-
},
|
|
3187
|
-
};
|
|
3188
|
-
|
|
3189
|
-
webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
|
|
3190
|
-
|
|
3191
|
-
const mergedTask = taskManager.getTask(epdnTaskId);
|
|
3192
|
-
expect(mergedTask).toBeDefined();
|
|
3193
|
-
expect(mergedTask.data.interaction.participants['epdn-participant']).toBeDefined();
|
|
3194
|
-
expect(mergedTask.data.interaction.participants['epdn-participant'].type).toBe('EpDn');
|
|
3195
|
-
expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, mergedTask);
|
|
3196
|
-
});
|
|
3197
|
-
|
|
3198
|
-
it('should not affect other tasks when CONTACT_MERGED is received', () => {
|
|
3199
|
-
const otherTaskId = 'other-unrelated-task';
|
|
3200
|
-
const otherPayload = {
|
|
3201
|
-
data: {
|
|
3202
|
-
type: CC_EVENTS.AGENT_CONTACT_RESERVED,
|
|
3203
|
-
interactionId: otherTaskId,
|
|
3204
|
-
agentId: taskDataMock.agentId,
|
|
3205
|
-
interaction: {mediaType: 'chat'},
|
|
3206
|
-
},
|
|
3207
|
-
};
|
|
3208
|
-
webSocketManagerMock.emit('message', JSON.stringify(otherPayload));
|
|
3209
|
-
|
|
3210
|
-
const otherTask = taskManager.getTask(otherTaskId);
|
|
3211
|
-
const otherTaskEmitSpy = jest.spyOn(otherTask, 'emit');
|
|
3212
|
-
|
|
3213
|
-
// Send CONTACT_MERGED for the original task
|
|
3214
|
-
const mergedPayload = {
|
|
3215
|
-
data: {
|
|
3216
|
-
type: CC_EVENTS.CONTACT_MERGED,
|
|
3217
|
-
interactionId: taskId,
|
|
3218
|
-
agentId: taskDataMock.agentId,
|
|
3219
|
-
interaction: {
|
|
3220
|
-
mediaType: 'telephony',
|
|
3221
|
-
state: 'merged',
|
|
3222
|
-
},
|
|
3223
|
-
},
|
|
3224
|
-
};
|
|
3225
|
-
|
|
3226
|
-
webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
|
|
3227
|
-
|
|
3228
|
-
// Verify other task was not affected
|
|
3229
|
-
expect(otherTaskEmitSpy).not.toHaveBeenCalled();
|
|
3230
|
-
expect(otherTask.data.interaction.mediaType).toBe('chat');
|
|
3231
|
-
|
|
3232
|
-
// Verify original task was updated
|
|
3233
|
-
expect(managerEmitSpy).toHaveBeenCalledWith(
|
|
3234
|
-
TASK_EVENTS.TASK_MERGED,
|
|
3235
|
-
expect.objectContaining({
|
|
3236
|
-
data: expect.objectContaining({
|
|
3237
|
-
interactionId: taskId,
|
|
3238
|
-
}),
|
|
3239
|
-
})
|
|
3240
|
-
);
|
|
3241
|
-
});
|
|
3242
|
-
});
|
|
3243
|
-
|
|
3244
|
-
describe('Campaign Preview Reservation', () => {
|
|
3245
|
-
it('should create a task and emit TASK_CAMPAIGN_PREVIEW_RESERVATION when AgentOfferCampaignReservation is received', () => {
|
|
3246
|
-
const campaignPayload = {
|
|
3247
|
-
data: {
|
|
3248
|
-
type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
|
|
3249
|
-
interactionId: 'campaign-interaction-123',
|
|
3250
|
-
agentId: taskDataMock.agentId,
|
|
3251
|
-
orgId: taskDataMock.orgId,
|
|
3252
|
-
trackingId: 'campaign-tracking-456',
|
|
3253
|
-
interaction: {
|
|
3254
|
-
mediaType: 'telephony',
|
|
3255
|
-
callProcessingDetails: {
|
|
3256
|
-
campaignId: 'campaign-789',
|
|
3257
|
-
},
|
|
3258
|
-
},
|
|
3259
|
-
},
|
|
3260
|
-
};
|
|
3261
|
-
|
|
3262
|
-
const managerEmitSpy = jest.spyOn(taskManager, 'emit');
|
|
3263
|
-
|
|
3264
|
-
webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
|
|
3265
|
-
|
|
3266
|
-
// Should emit with a task object (not raw data)
|
|
3267
|
-
expect(managerEmitSpy).toHaveBeenCalledWith(
|
|
3268
|
-
TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION,
|
|
3269
|
-
expect.objectContaining({
|
|
3270
|
-
data: expect.objectContaining({
|
|
3271
|
-
interactionId: 'campaign-interaction-123',
|
|
3272
|
-
type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
|
|
3273
|
-
wrapUpRequired: false,
|
|
3274
|
-
isAutoAnswering: false,
|
|
3275
|
-
}),
|
|
3276
|
-
})
|
|
3277
|
-
);
|
|
3278
|
-
|
|
3279
|
-
// Task should be in the collection so subsequent events (e.g. AGENT_CONTACT_ASSIGNED) can find it
|
|
3280
|
-
expect(taskManager['taskCollection']['campaign-interaction-123']).toBeDefined();
|
|
3281
|
-
});
|
|
3282
|
-
|
|
3283
|
-
it('should not emit TASK_INCOMING for campaign preview reservation when incoming WebRTC call arrives', () => {
|
|
3284
|
-
const campaignPayload = {
|
|
3285
|
-
data: {
|
|
3286
|
-
type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
|
|
3287
|
-
interactionId: 'campaign-interaction-123',
|
|
3288
|
-
agentId: taskDataMock.agentId,
|
|
3289
|
-
orgId: taskDataMock.orgId,
|
|
3290
|
-
trackingId: 'campaign-tracking-456',
|
|
3291
|
-
interaction: {
|
|
3292
|
-
mediaType: 'telephony',
|
|
3293
|
-
callProcessingDetails: {
|
|
3294
|
-
campaignId: 'campaign-789',
|
|
3295
|
-
},
|
|
3296
|
-
},
|
|
3297
|
-
},
|
|
3298
|
-
};
|
|
3299
|
-
|
|
3300
|
-
// Remove the default task so only the campaign preview task is in the collection
|
|
3301
|
-
delete taskManager['taskCollection'][taskId];
|
|
3302
|
-
|
|
3303
|
-
// Create campaign preview task via the reservation event
|
|
3304
|
-
webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
|
|
3305
|
-
|
|
3306
|
-
const managerEmitSpy = jest.spyOn(taskManager, 'emit');
|
|
3307
|
-
|
|
3308
|
-
// Simulate an incoming WebRTC call
|
|
3309
|
-
const incomingCallCb = onSpy.mock.calls[0][1];
|
|
3310
|
-
incomingCallCb(mockCall);
|
|
3311
|
-
|
|
3312
|
-
// TASK_INCOMING should NOT be emitted because the only telephony task is a campaign preview
|
|
3313
|
-
expect(managerEmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, expect.anything());
|
|
3314
|
-
});
|
|
3315
|
-
|
|
3316
|
-
it('should update existing task when AgentOfferCampaignReservation is received for known interactionId', () => {
|
|
3317
|
-
const campaignPayload = {
|
|
3318
|
-
data: {
|
|
3319
|
-
type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
|
|
3320
|
-
interactionId: 'campaign-interaction-123',
|
|
3321
|
-
agentId: taskDataMock.agentId,
|
|
3322
|
-
orgId: taskDataMock.orgId,
|
|
3323
|
-
trackingId: 'campaign-tracking-456',
|
|
3324
|
-
interaction: {
|
|
3325
|
-
mediaType: 'telephony',
|
|
3326
|
-
callProcessingDetails: {
|
|
3327
|
-
campaignId: 'campaign-789',
|
|
3328
|
-
},
|
|
3329
|
-
},
|
|
3330
|
-
},
|
|
3331
|
-
};
|
|
3332
|
-
|
|
3333
|
-
// Send the first reservation to create the task
|
|
3334
|
-
webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
|
|
3335
|
-
|
|
3336
|
-
const managerEmitSpy = jest.spyOn(taskManager, 'emit');
|
|
3337
|
-
|
|
3338
|
-
// Send a second reservation for the same interactionId
|
|
3339
|
-
webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
|
|
3340
|
-
|
|
3341
|
-
expect(managerEmitSpy).toHaveBeenCalledWith(
|
|
3342
|
-
TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION,
|
|
3343
|
-
expect.objectContaining({
|
|
3344
|
-
data: expect.objectContaining({
|
|
3345
|
-
interactionId: 'campaign-interaction-123',
|
|
3346
|
-
}),
|
|
3347
|
-
})
|
|
3348
|
-
);
|
|
3349
|
-
});
|
|
3350
|
-
|
|
3351
|
-
it('should emit TASK_CAMPAIGN_CONTACT_UPDATED and NOT remove task when CampaignContactUpdated is received', () => {
|
|
3352
|
-
const campaignInteractionId = 'campaign-interaction-123';
|
|
3353
|
-
|
|
3354
|
-
// First create a campaign preview task
|
|
3355
|
-
const reservationPayload = {
|
|
3356
|
-
data: {
|
|
3357
|
-
type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
|
|
3358
|
-
interactionId: campaignInteractionId,
|
|
3359
|
-
agentId: taskDataMock.agentId,
|
|
3360
|
-
orgId: taskDataMock.orgId,
|
|
3361
|
-
trackingId: 'campaign-tracking-456',
|
|
3362
|
-
interaction: {
|
|
3363
|
-
mediaType: 'telephony',
|
|
3364
|
-
callProcessingDetails: {
|
|
3365
|
-
campaignId: 'campaign-789',
|
|
3366
|
-
},
|
|
3367
|
-
},
|
|
3368
|
-
},
|
|
3369
|
-
};
|
|
3370
|
-
|
|
3371
|
-
webSocketManagerMock.emit('message', JSON.stringify(reservationPayload));
|
|
3372
|
-
|
|
3373
|
-
// Verify task exists in collection
|
|
3374
|
-
const task = taskManager['taskCollection'][campaignInteractionId];
|
|
3375
|
-
expect(task).toBeDefined();
|
|
3376
|
-
|
|
3377
|
-
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
3378
|
-
|
|
3379
|
-
// Now send CampaignContactUpdated
|
|
3380
|
-
const campaignContactUpdatedPayload = {
|
|
3381
|
-
data: {
|
|
3382
|
-
type: CC_EVENTS.CAMPAIGN_CONTACT_UPDATED,
|
|
3383
|
-
interactionId: campaignInteractionId,
|
|
3384
|
-
agentId: taskDataMock.agentId,
|
|
3385
|
-
orgId: taskDataMock.orgId,
|
|
3386
|
-
interaction: {
|
|
3387
|
-
mediaType: 'telephony',
|
|
3388
|
-
state: 'new',
|
|
3389
|
-
callProcessingDetails: {
|
|
3390
|
-
campaignId: 'campaign-789',
|
|
3391
|
-
},
|
|
3392
|
-
},
|
|
3393
|
-
},
|
|
3394
|
-
};
|
|
3395
|
-
|
|
3396
|
-
webSocketManagerMock.emit('message', JSON.stringify(campaignContactUpdatedPayload));
|
|
3397
|
-
|
|
3398
|
-
// Task should still exist in collection (not removed — non-terminal event)
|
|
3399
|
-
expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
|
|
3400
|
-
|
|
3401
|
-
// TASK_CAMPAIGN_CONTACT_UPDATED should have been emitted
|
|
3402
|
-
expect(taskEmitSpy).toHaveBeenCalledWith(
|
|
3403
|
-
TASK_EVENTS.TASK_CAMPAIGN_CONTACT_UPDATED,
|
|
3404
|
-
expect.objectContaining({
|
|
3405
|
-
data: expect.objectContaining({
|
|
3406
|
-
interactionId: campaignInteractionId,
|
|
3407
|
-
}),
|
|
3408
|
-
})
|
|
3409
|
-
);
|
|
3410
|
-
|
|
3411
|
-
// TASK_END should NOT have been emitted
|
|
3412
|
-
expect(taskEmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.anything());
|
|
3413
|
-
});
|
|
3414
|
-
|
|
3415
|
-
it('should emit TASK_CAMPAIGN_PREVIEW_ACCEPT_FAILED when CampaignPreviewAcceptFailed is received', () => {
|
|
3416
|
-
const campaignInteractionId = 'campaign-interaction-123';
|
|
3417
|
-
|
|
3418
|
-
// First create a campaign preview task
|
|
3419
|
-
const reservationPayload = {
|
|
3420
|
-
data: {
|
|
3421
|
-
type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
|
|
3422
|
-
interactionId: campaignInteractionId,
|
|
3423
|
-
agentId: taskDataMock.agentId,
|
|
3424
|
-
orgId: taskDataMock.orgId,
|
|
3425
|
-
trackingId: 'campaign-tracking-456',
|
|
3426
|
-
interaction: {
|
|
3427
|
-
mediaType: 'telephony',
|
|
3428
|
-
callProcessingDetails: {
|
|
3429
|
-
campaignId: 'campaign-789',
|
|
3430
|
-
},
|
|
3431
|
-
},
|
|
3432
|
-
},
|
|
3433
|
-
};
|
|
3434
|
-
|
|
3435
|
-
webSocketManagerMock.emit('message', JSON.stringify(reservationPayload));
|
|
3436
|
-
|
|
3437
|
-
const task = taskManager['taskCollection'][campaignInteractionId];
|
|
3438
|
-
expect(task).toBeDefined();
|
|
3439
|
-
|
|
3440
|
-
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
3441
|
-
|
|
3442
|
-
const failPayload = {
|
|
3443
|
-
data: {
|
|
3444
|
-
type: CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED,
|
|
3445
|
-
interactionId: campaignInteractionId,
|
|
3446
|
-
campaignId: 'campaign-789',
|
|
3447
|
-
reason: 'INTERNAL_ERROR',
|
|
3448
|
-
},
|
|
3449
|
-
};
|
|
3450
|
-
|
|
3451
|
-
webSocketManagerMock.emit('message', JSON.stringify(failPayload));
|
|
3452
|
-
|
|
3453
|
-
expect(taskEmitSpy).toHaveBeenCalledWith(
|
|
3454
|
-
TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_ACCEPT_FAILED,
|
|
3455
|
-
expect.objectContaining({
|
|
3456
|
-
data: expect.objectContaining({interactionId: campaignInteractionId}),
|
|
3457
|
-
})
|
|
3458
|
-
);
|
|
3459
|
-
// Task should still exist (failure is non-terminal)
|
|
3460
|
-
expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
|
|
3461
|
-
|
|
3462
|
-
// Failure payload (reason, campaignId) should be merged into task.data
|
|
3463
|
-
expect(task.data.reason).toBe('INTERNAL_ERROR');
|
|
3464
|
-
|
|
3465
|
-
// Original reservation data must be preserved
|
|
3466
|
-
expect(task.data.interaction).toBeDefined();
|
|
3467
|
-
expect(task.data.interaction.callProcessingDetails.campaignId).toBe('campaign-789');
|
|
3468
|
-
});
|
|
3469
|
-
|
|
3470
|
-
it('should emit TASK_CAMPAIGN_PREVIEW_SKIP_FAILED when CampaignPreviewSkipFailed is received', () => {
|
|
3471
|
-
const campaignInteractionId = 'campaign-interaction-123';
|
|
3472
|
-
|
|
3473
|
-
const reservationPayload = {
|
|
3474
|
-
data: {
|
|
3475
|
-
type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
|
|
3476
|
-
interactionId: campaignInteractionId,
|
|
3477
|
-
agentId: taskDataMock.agentId,
|
|
3478
|
-
orgId: taskDataMock.orgId,
|
|
3479
|
-
trackingId: 'campaign-tracking-456',
|
|
3480
|
-
interaction: {
|
|
3481
|
-
mediaType: 'telephony',
|
|
3482
|
-
callProcessingDetails: {
|
|
3483
|
-
campaignId: 'campaign-789',
|
|
3484
|
-
},
|
|
3485
|
-
},
|
|
3486
|
-
},
|
|
3487
|
-
};
|
|
3488
|
-
|
|
3489
|
-
webSocketManagerMock.emit('message', JSON.stringify(reservationPayload));
|
|
3490
|
-
|
|
3491
|
-
const task = taskManager['taskCollection'][campaignInteractionId];
|
|
3492
|
-
expect(task).toBeDefined();
|
|
3493
|
-
|
|
3494
|
-
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
3495
|
-
|
|
3496
|
-
const failPayload = {
|
|
3497
|
-
data: {
|
|
3498
|
-
type: CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED,
|
|
3499
|
-
interactionId: campaignInteractionId,
|
|
3500
|
-
campaignId: 'campaign-789',
|
|
3501
|
-
reason: 'INTERNAL_ERROR',
|
|
3502
|
-
},
|
|
3503
|
-
};
|
|
3504
|
-
|
|
3505
|
-
webSocketManagerMock.emit('message', JSON.stringify(failPayload));
|
|
3506
|
-
|
|
3507
|
-
expect(taskEmitSpy).toHaveBeenCalledWith(
|
|
3508
|
-
TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_SKIP_FAILED,
|
|
3509
|
-
expect.objectContaining({
|
|
3510
|
-
data: expect.objectContaining({interactionId: campaignInteractionId}),
|
|
3511
|
-
})
|
|
3512
|
-
);
|
|
3513
|
-
expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
|
|
3514
|
-
|
|
3515
|
-
// Failure payload (reason) should be merged into task.data
|
|
3516
|
-
expect(task.data.reason).toBe('INTERNAL_ERROR');
|
|
3517
|
-
|
|
3518
|
-
// Original reservation data must be preserved
|
|
3519
|
-
expect(task.data.interaction).toBeDefined();
|
|
3520
|
-
expect(task.data.interaction.callProcessingDetails.campaignId).toBe('campaign-789');
|
|
3521
|
-
});
|
|
3522
|
-
|
|
3523
|
-
it('should emit TASK_CAMPAIGN_PREVIEW_REMOVE_FAILED when CampaignPreviewRemoveFailed is received', () => {
|
|
3524
|
-
const campaignInteractionId = 'campaign-interaction-123';
|
|
3525
|
-
|
|
3526
|
-
const reservationPayload = {
|
|
3527
|
-
data: {
|
|
3528
|
-
type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
|
|
3529
|
-
interactionId: campaignInteractionId,
|
|
3530
|
-
agentId: taskDataMock.agentId,
|
|
3531
|
-
orgId: taskDataMock.orgId,
|
|
3532
|
-
trackingId: 'campaign-tracking-456',
|
|
3533
|
-
interaction: {
|
|
3534
|
-
mediaType: 'telephony',
|
|
3535
|
-
callProcessingDetails: {
|
|
3536
|
-
campaignId: 'campaign-789',
|
|
3537
|
-
},
|
|
3538
|
-
},
|
|
3539
|
-
},
|
|
3540
|
-
};
|
|
3541
|
-
|
|
3542
|
-
webSocketManagerMock.emit('message', JSON.stringify(reservationPayload));
|
|
3543
|
-
|
|
3544
|
-
const task = taskManager['taskCollection'][campaignInteractionId];
|
|
3545
|
-
expect(task).toBeDefined();
|
|
3546
|
-
|
|
3547
|
-
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
3548
|
-
|
|
3549
|
-
const failPayload = {
|
|
3550
|
-
data: {
|
|
3551
|
-
type: CC_EVENTS.CAMPAIGN_PREVIEW_REMOVE_FAILED,
|
|
3552
|
-
interactionId: campaignInteractionId,
|
|
3553
|
-
campaignId: 'campaign-789',
|
|
3554
|
-
reason: 'INTERNAL_ERROR',
|
|
3555
|
-
},
|
|
3556
|
-
};
|
|
3557
|
-
|
|
3558
|
-
webSocketManagerMock.emit('message', JSON.stringify(failPayload));
|
|
3559
|
-
|
|
3560
|
-
expect(taskEmitSpy).toHaveBeenCalledWith(
|
|
3561
|
-
TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_REMOVE_FAILED,
|
|
3562
|
-
expect.objectContaining({
|
|
3563
|
-
data: expect.objectContaining({interactionId: campaignInteractionId}),
|
|
3564
|
-
})
|
|
3565
|
-
);
|
|
3566
|
-
expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
|
|
3567
|
-
|
|
3568
|
-
// Failure payload (reason) should be merged into task.data
|
|
3569
|
-
expect(task.data.reason).toBe('INTERNAL_ERROR');
|
|
3570
|
-
|
|
3571
|
-
// Original reservation data must be preserved
|
|
3572
|
-
expect(task.data.interaction).toBeDefined();
|
|
3573
|
-
expect(task.data.interaction.callProcessingDetails.campaignId).toBe('campaign-789');
|
|
3574
|
-
});
|
|
3575
|
-
|
|
3576
|
-
it('should not emit campaign preview failure events when task does not exist', () => {
|
|
3577
|
-
const nonExistentId = 'non-existent-interaction';
|
|
3578
|
-
|
|
3579
|
-
const failPayload = {
|
|
3580
|
-
data: {
|
|
3581
|
-
type: CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED,
|
|
3582
|
-
interactionId: nonExistentId,
|
|
3583
|
-
campaignId: 'campaign-789',
|
|
3584
|
-
reason: 'INTERNAL_ERROR',
|
|
3585
|
-
},
|
|
3586
|
-
};
|
|
3587
|
-
|
|
3588
|
-
// Should not throw when task is not found
|
|
3589
|
-
expect(() => {
|
|
3590
|
-
webSocketManagerMock.emit('message', JSON.stringify(failPayload));
|
|
3591
|
-
}).not.toThrow();
|
|
3592
2477
|
|
|
3593
|
-
|
|
2478
|
+
sendStateMachineEventSpy.mockRestore();
|
|
3594
2479
|
});
|
|
3595
2480
|
});
|
|
3596
2481
|
});
|