@webex/contact-center 3.12.0-next.9 → 3.12.0-task-refactor.2
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 +65 -123
- package/dist/cc.js.map +1 -1
- package/dist/constants.js +13 -2
- 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 -13
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +7 -6
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/ApiAiAssistant.js +0 -3
- package/dist/services/ApiAiAssistant.js.map +1 -1
- package/dist/services/config/Util.js +2 -3
- package/dist/services/config/Util.js.map +1 -1
- package/dist/services/config/types.js +16 -14
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/constants.js +0 -1
- package/dist/services/constants.js.map +1 -1
- package/dist/services/core/Err.js.map +1 -1
- package/dist/services/core/Utils.js +79 -55
- package/dist/services/core/Utils.js.map +1 -1
- package/dist/services/core/aqm-reqs.js +17 -92
- package/dist/services/core/aqm-reqs.js.map +1 -1
- package/dist/services/core/websocket/WebSocketManager.js +5 -25
- 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 -2
- package/dist/services/index.js.map +1 -1
- package/dist/services/task/Task.js +644 -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 +570 -535
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/TaskUtils.js +132 -28
- package/dist/services/task/TaskUtils.js.map +1 -1
- package/dist/services/task/constants.js +7 -6
- package/dist/services/task/constants.js.map +1 -1
- package/dist/services/task/dialer.js +0 -51
- package/dist/services/task/dialer.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 +634 -0
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
- package/dist/services/task/state-machine/actions.js +372 -0
- package/dist/services/task/state-machine/actions.js.map +1 -0
- package/dist/services/task/state-machine/constants.js +139 -0
- package/dist/services/task/state-machine/constants.js.map +1 -0
- package/dist/services/task/state-machine/guards.js +263 -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 +377 -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 +157 -18
- package/dist/services/task/types.js.map +1 -1
- package/dist/services/task/voice/Voice.js +1031 -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 +4 -33
- package/dist/types/constants.d.ts +13 -2
- package/dist/types/index.d.ts +11 -5
- package/dist/types/metrics/constants.d.ts +5 -3
- package/dist/types/services/ApiAiAssistant.d.ts +1 -1
- package/dist/types/services/config/types.d.ts +97 -25
- package/dist/types/services/core/Err.d.ts +0 -2
- package/dist/types/services/core/Utils.d.ts +25 -23
- package/dist/types/services/core/aqm-reqs.d.ts +0 -49
- package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -1
- package/dist/types/services/core/websocket/connection-service.d.ts +0 -1
- 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 +146 -0
- package/dist/types/services/task/TaskFactory.d.ts +12 -0
- package/dist/types/services/task/TaskUtils.d.ts +39 -8
- package/dist/types/services/task/constants.d.ts +5 -4
- package/dist/types/services/task/dialer.d.ts +0 -15
- package/dist/types/services/task/digital/Digital.d.ts +22 -0
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +906 -0
- package/dist/types/services/task/state-machine/actions.d.ts +8 -0
- package/dist/types/services/task/state-machine/constants.d.ts +91 -0
- package/dist/types/services/task/state-machine/guards.d.ts +78 -0
- package/dist/types/services/task/state-machine/index.d.ts +13 -0
- package/dist/types/services/task/state-machine/types.d.ts +256 -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 +539 -88
- package/dist/types/services/task/voice/Voice.d.ts +183 -0
- package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
- package/dist/types/types.d.ts +68 -0
- package/dist/types/webex.d.ts +1 -0
- package/dist/types.js +70 -0
- package/dist/types.js.map +1 -1
- package/dist/webex.js +14 -2
- package/dist/webex.js.map +1 -1
- package/package.json +14 -11
- package/src/cc.ts +91 -177
- package/src/constants.ts +13 -2
- 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 -14
- package/src/metrics/constants.ts +7 -8
- package/src/services/ApiAiAssistant.ts +2 -4
- 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 -3
- 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 +108 -20
- package/src/services/constants.ts +0 -1
- package/src/services/core/Err.ts +0 -1
- package/src/services/core/Utils.ts +90 -67
- package/src/services/core/ai-docs/AGENTS.md +379 -0
- package/src/services/core/ai-docs/ARCHITECTURE.md +696 -0
- package/src/services/core/aqm-reqs.ts +22 -100
- package/src/services/core/websocket/WebSocketManager.ts +4 -23
- package/src/services/core/websocket/types.ts +1 -1
- package/src/services/index.ts +1 -2
- package/src/services/task/Task.ts +785 -0
- package/src/services/task/TaskFactory.ts +55 -0
- package/src/services/task/TaskManager.ts +579 -633
- package/src/services/task/TaskUtils.ts +175 -31
- package/src/services/task/ai-docs/AGENTS.md +448 -0
- package/src/services/task/ai-docs/ARCHITECTURE.md +573 -0
- package/src/services/task/constants.ts +5 -4
- package/src/services/task/dialer.ts +1 -56
- package/src/services/task/digital/Digital.ts +95 -0
- package/src/services/task/state-machine/TaskStateMachine.ts +793 -0
- package/src/services/task/state-machine/actions.ts +422 -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 +150 -0
- package/src/services/task/state-machine/guards.ts +303 -0
- package/src/services/task/state-machine/index.ts +28 -0
- package/src/services/task/state-machine/types.ts +228 -0
- package/src/services/task/state-machine/uiControlsComputer.ts +542 -0
- package/src/services/task/taskDataNormalizer.ts +137 -0
- package/src/services/task/types.ts +641 -95
- package/src/services/task/voice/Voice.ts +1255 -0
- package/src/services/task/voice/WebRTC.ts +187 -0
- package/src/types.ts +88 -5
- package/src/utils/AGENTS.md +276 -0
- package/src/webex.js +2 -0
- package/test/unit/spec/cc.ts +59 -142
- package/test/unit/spec/logger-proxy.ts +70 -0
- package/test/unit/spec/services/ApiAiAssistant.ts +17 -0
- package/test/unit/spec/services/config/index.ts +26 -55
- package/test/unit/spec/services/core/Utils.ts +103 -52
- package/test/unit/spec/services/core/websocket/WebSocketManager.ts +48 -112
- package/test/unit/spec/services/core/websocket/connection-service.ts +5 -4
- package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
- package/test/unit/spec/services/task/Task.ts +416 -0
- package/test/unit/spec/services/task/TaskFactory.ts +62 -0
- package/test/unit/spec/services/task/TaskManager.ts +781 -1735
- package/test/unit/spec/services/task/TaskUtils.ts +125 -0
- package/test/unit/spec/services/task/dialer.ts +112 -198
- package/test/unit/spec/services/task/digital/Digital.ts +105 -0
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +473 -0
- package/test/unit/spec/services/task/state-machine/guards.ts +288 -0
- package/test/unit/spec/services/task/state-machine/types.ts +18 -0
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +147 -0
- package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
- package/test/unit/spec/services/task/voice/Voice.ts +587 -0
- package/test/unit/spec/services/task/voice/WebRTC.ts +242 -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 -1525
- 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 -1801
- package/test/unit/spec/services/task/index.ts +0 -2184
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import Voice from '../../../../../../src/services/task/voice/Voice';
|
|
2
|
+
import {
|
|
3
|
+
TaskData,
|
|
4
|
+
CONSULT_TRANSFER_DESTINATION_TYPE,
|
|
5
|
+
} from '../../../../../../src/services/task/types';
|
|
6
|
+
import {CC_EVENTS} from '../../../../../../src/services/config/types';
|
|
7
|
+
import {TaskEvent, TaskState} from '../../../../../../src/services/task/state-machine';
|
|
8
|
+
import {computeUIControls} from '../../../../../../src/services/task/state-machine/uiControlsComputer';
|
|
9
|
+
import * as Utils from '../../../../../../src/services/core/Utils';
|
|
10
|
+
import {createTaskData} from '../taskTestUtils';
|
|
11
|
+
|
|
12
|
+
jest.mock('../../../../../../src/services/core/WebexRequest', () => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
default: {
|
|
15
|
+
getInstance: () => ({uploadLogs: jest.fn()}),
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
jest.mock('../../../../../../src/services/core/Utils', () => {
|
|
20
|
+
const actual = jest.requireActual('../../../../../../src/services/core/Utils');
|
|
21
|
+
return {
|
|
22
|
+
__esModule: true,
|
|
23
|
+
...actual,
|
|
24
|
+
// keep tests deterministic; avoid log upload side-effects on failures
|
|
25
|
+
getErrorDetails: (err: any) => ({error: err}),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const dummyContact = {
|
|
30
|
+
hold: jest.fn().mockResolvedValue('held'),
|
|
31
|
+
unHold: jest.fn().mockResolvedValue('resumed'),
|
|
32
|
+
pauseRecording: jest.fn().mockResolvedValue('paused'),
|
|
33
|
+
resumeRecording: jest.fn().mockResolvedValue('resumedRecording'),
|
|
34
|
+
consult: jest.fn().mockResolvedValue('consulted'),
|
|
35
|
+
consultConference: jest.fn().mockResolvedValue('conferenceStarted'),
|
|
36
|
+
consultTransfer: jest.fn().mockResolvedValue('consultTransferred'),
|
|
37
|
+
} as any;
|
|
38
|
+
|
|
39
|
+
const createBaseData = (overrides: Partial<TaskData> = {}): TaskData =>
|
|
40
|
+
createTaskData({
|
|
41
|
+
interactionId: 'int1',
|
|
42
|
+
mediaResourceId: 'media1',
|
|
43
|
+
interaction: {
|
|
44
|
+
...(overrides.interaction || {}),
|
|
45
|
+
media: {
|
|
46
|
+
media1: {mediaResourceId: 'media1', isHold: false},
|
|
47
|
+
...(overrides.interaction as any)?.media,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
...overrides,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const primeConnectedState = (voice: Voice, taskData: TaskData) => {
|
|
54
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
55
|
+
voice.stateMachineService?.send({type: TaskEvent.ASSIGN, taskData});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const primeHeldState = (voice: Voice, taskData: TaskData) => {
|
|
59
|
+
primeConnectedState(voice, taskData);
|
|
60
|
+
voice.stateMachineService?.send({
|
|
61
|
+
type: TaskEvent.HOLD_INITIATED,
|
|
62
|
+
mediaResourceId: taskData.mediaResourceId,
|
|
63
|
+
});
|
|
64
|
+
voice.stateMachineService?.send({
|
|
65
|
+
type: TaskEvent.HOLD_SUCCESS,
|
|
66
|
+
mediaResourceId: taskData.mediaResourceId,
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
describe('Voice Task', () => {
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
jest.clearAllMocks();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('hides end and endConsult when disabled', () => {
|
|
76
|
+
const voice = new Voice(dummyContact, createBaseData(), {
|
|
77
|
+
isEndTaskEnabled: false,
|
|
78
|
+
isEndConsultEnabled: false,
|
|
79
|
+
});
|
|
80
|
+
voice.updateTaskData(createBaseData());
|
|
81
|
+
expect(voice.uiControls.main.end.isVisible).toBe(false);
|
|
82
|
+
expect(voice.uiControls.main.endConsult.isVisible).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('calls contact.hold when media is not held', async () => {
|
|
86
|
+
const taskData = createBaseData() as any;
|
|
87
|
+
const voice = new Voice(dummyContact, taskData, {});
|
|
88
|
+
primeConnectedState(voice, taskData);
|
|
89
|
+
await voice.holdResume();
|
|
90
|
+
expect(dummyContact.hold).toHaveBeenCalledWith({
|
|
91
|
+
interactionId: 'int1',
|
|
92
|
+
data: {mediaResourceId: 'media1'},
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('calls contact.unHold when media is held', async () => {
|
|
97
|
+
const heldData = createBaseData({
|
|
98
|
+
interaction: {
|
|
99
|
+
media: {media1: {mediaResourceId: 'media1', isHold: true}},
|
|
100
|
+
} as any,
|
|
101
|
+
}) as any;
|
|
102
|
+
const voice = new Voice(dummyContact, heldData, {});
|
|
103
|
+
primeHeldState(voice, heldData);
|
|
104
|
+
await voice.holdResume();
|
|
105
|
+
expect(dummyContact.unHold).toHaveBeenCalledWith({
|
|
106
|
+
interactionId: 'int1',
|
|
107
|
+
data: {mediaResourceId: 'media1'},
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('calls contact.unHold when state is held even if task data hold flag is stale', async () => {
|
|
112
|
+
const taskData = createBaseData() as any;
|
|
113
|
+
const voice = new Voice(dummyContact, taskData, {});
|
|
114
|
+
primeHeldState(voice, taskData);
|
|
115
|
+
voice.data.interaction.media.media1.isHold = false;
|
|
116
|
+
|
|
117
|
+
await voice.holdResume();
|
|
118
|
+
|
|
119
|
+
expect(dummyContact.unHold).toHaveBeenCalledWith({
|
|
120
|
+
interactionId: 'int1',
|
|
121
|
+
data: {mediaResourceId: 'media1'},
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('uses the main media resource when stale consult media is left in task data', async () => {
|
|
126
|
+
const heldData = createBaseData({
|
|
127
|
+
mediaResourceId: 'main-media',
|
|
128
|
+
interactionId: 'main-int',
|
|
129
|
+
interaction: {
|
|
130
|
+
mainInteractionId: 'main-int',
|
|
131
|
+
media: {
|
|
132
|
+
'main-int': {mediaResourceId: 'main-media', isHold: true},
|
|
133
|
+
'consult-media': {mediaResourceId: 'consult-media', isHold: false, mType: 'consult'},
|
|
134
|
+
},
|
|
135
|
+
} as any,
|
|
136
|
+
}) as any;
|
|
137
|
+
const voice = new Voice(dummyContact, heldData, {});
|
|
138
|
+
primeHeldState(voice, heldData);
|
|
139
|
+
voice.data.mediaResourceId = 'consult-media';
|
|
140
|
+
|
|
141
|
+
await voice.holdResume();
|
|
142
|
+
|
|
143
|
+
expect(dummyContact.unHold).toHaveBeenCalledWith({
|
|
144
|
+
interactionId: 'main-int',
|
|
145
|
+
data: {mediaResourceId: 'main-media'},
|
|
146
|
+
});
|
|
147
|
+
expect(dummyContact.hold).not.toHaveBeenCalled();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('pauseRecording() calls contact.pauseRecording', async () => {
|
|
151
|
+
const taskData = createBaseData();
|
|
152
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
153
|
+
isEndTaskEnabled: true,
|
|
154
|
+
isEndConsultEnabled: true,
|
|
155
|
+
});
|
|
156
|
+
primeConnectedState(voice, taskData);
|
|
157
|
+
const res = await voice.pauseRecording();
|
|
158
|
+
expect(dummyContact.pauseRecording).toHaveBeenCalledWith({interactionId: 'int1'});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('resumeRecording() with no payload defaults to autoResumed false', async () => {
|
|
162
|
+
const taskData = createBaseData();
|
|
163
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
164
|
+
isEndTaskEnabled: true,
|
|
165
|
+
isEndConsultEnabled: true,
|
|
166
|
+
});
|
|
167
|
+
primeConnectedState(voice, taskData);
|
|
168
|
+
voice.stateMachineService?.send({type: TaskEvent.PAUSE_RECORDING});
|
|
169
|
+
const res = await voice.resumeRecording();
|
|
170
|
+
expect(dummyContact.resumeRecording).toHaveBeenCalledWith({
|
|
171
|
+
interactionId: 'int1',
|
|
172
|
+
data: {autoResumed: false},
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('consult() calls contact.consult with payload', async () => {
|
|
177
|
+
const taskData = createBaseData();
|
|
178
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
179
|
+
isEndTaskEnabled: true,
|
|
180
|
+
isEndConsultEnabled: true,
|
|
181
|
+
});
|
|
182
|
+
primeConnectedState(voice, taskData);
|
|
183
|
+
const payload = {destination: 'agent1', destinationType: 'agent'} as any;
|
|
184
|
+
const res = await voice.consult(payload);
|
|
185
|
+
expect(dummyContact.consult).toHaveBeenCalledWith({
|
|
186
|
+
interactionId: 'int1',
|
|
187
|
+
data: payload,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('transfer()', () => {
|
|
192
|
+
it('calls contact.consultTransfer for consult transfer to agent', async () => {
|
|
193
|
+
const consultTransferMock = jest.fn().mockResolvedValue('consultedA');
|
|
194
|
+
const dataWithState = createBaseData({
|
|
195
|
+
interaction: {state: 'consulting'} as any,
|
|
196
|
+
});
|
|
197
|
+
const voice = new Voice(
|
|
198
|
+
{...dummyContact, consultTransfer: consultTransferMock},
|
|
199
|
+
dataWithState as any,
|
|
200
|
+
{isEndTaskEnabled: true, isEndConsultEnabled: true}
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const result = await voice.transfer({
|
|
204
|
+
to: 'destB',
|
|
205
|
+
destinationType: 'agent',
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(consultTransferMock).toHaveBeenCalledWith({
|
|
209
|
+
interactionId: 'int1',
|
|
210
|
+
data: {to: 'destB', destinationType: 'agent'},
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('throws if consult transfer to QUEUE but no destAgentId set', async () => {
|
|
215
|
+
const dataWithState = createBaseData({
|
|
216
|
+
destAgentId: undefined,
|
|
217
|
+
interaction: {state: 'consulting'} as any,
|
|
218
|
+
});
|
|
219
|
+
const voice = new Voice(dummyContact, dataWithState as any, {
|
|
220
|
+
isEndTaskEnabled: true,
|
|
221
|
+
isEndConsultEnabled: true,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await expect(
|
|
225
|
+
voice.transfer({
|
|
226
|
+
to: 'queue1',
|
|
227
|
+
destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.QUEUE,
|
|
228
|
+
})
|
|
229
|
+
).rejects.toThrow('No agent has accepted this queue consult yet');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('uses data.destAgentId for queue consult transfer', async () => {
|
|
233
|
+
const consultTransferMock = jest.fn().mockResolvedValue('consultedQ');
|
|
234
|
+
const dataWithDest = createBaseData({
|
|
235
|
+
destAgentId: 'agentD',
|
|
236
|
+
interaction: {state: 'consulting'} as any,
|
|
237
|
+
});
|
|
238
|
+
const voice = new Voice(
|
|
239
|
+
{...dummyContact, consultTransfer: consultTransferMock},
|
|
240
|
+
dataWithDest as any,
|
|
241
|
+
{isEndTaskEnabled: true, isEndConsultEnabled: true}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const result = await voice.transfer({
|
|
245
|
+
to: 'queueX',
|
|
246
|
+
destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.QUEUE,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
expect(consultTransferMock).toHaveBeenCalledWith({
|
|
250
|
+
interactionId: 'int1',
|
|
251
|
+
data: {
|
|
252
|
+
to: 'agentD',
|
|
253
|
+
destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('uses preserved consult destination from task data for queue consult transfer', async () => {
|
|
259
|
+
const consultTransferMock = jest.fn().mockResolvedValue('consultedQ');
|
|
260
|
+
const dataWithState = createBaseData({
|
|
261
|
+
destAgentId: undefined,
|
|
262
|
+
destinationType: undefined,
|
|
263
|
+
interaction: {state: 'consulting'} as any,
|
|
264
|
+
});
|
|
265
|
+
const voice = new Voice(
|
|
266
|
+
{...dummyContact, consultTransfer: consultTransferMock},
|
|
267
|
+
dataWithState as any,
|
|
268
|
+
{isEndTaskEnabled: true, isEndConsultEnabled: true}
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
primeConnectedState(voice, dataWithState);
|
|
272
|
+
voice.updateTaskData(
|
|
273
|
+
createBaseData({
|
|
274
|
+
destAgentId: 'agent-preserved',
|
|
275
|
+
interaction: {state: 'consulting'} as any,
|
|
276
|
+
}) as any
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
await voice.transfer({
|
|
280
|
+
to: 'queueX',
|
|
281
|
+
destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.QUEUE,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
expect(consultTransferMock).toHaveBeenCalledWith({
|
|
285
|
+
interactionId: 'int1',
|
|
286
|
+
data: {
|
|
287
|
+
to: 'agent-preserved',
|
|
288
|
+
destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe('endConsult()', () => {
|
|
295
|
+
it('calls contact.consultEnd with correct payload', async () => {
|
|
296
|
+
const consultEndMock = jest.fn().mockResolvedValue('endedC');
|
|
297
|
+
const voice = new Voice({...dummyContact, consultEnd: consultEndMock}, createBaseData(), {
|
|
298
|
+
isEndTaskEnabled: true,
|
|
299
|
+
isEndConsultEnabled: true,
|
|
300
|
+
});
|
|
301
|
+
const payload = {isConsult: true, queueId: 'q1', taskId: 't1'};
|
|
302
|
+
const result = await voice.endConsult(payload);
|
|
303
|
+
|
|
304
|
+
expect(consultEndMock).toHaveBeenCalledWith({
|
|
305
|
+
interactionId: 'int1',
|
|
306
|
+
data: payload,
|
|
307
|
+
});
|
|
308
|
+
expect(result).toBe('endedC');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('uses mainInteractionId when present for consultEnd request', async () => {
|
|
312
|
+
const consultEndMock = jest.fn().mockResolvedValue('endedC');
|
|
313
|
+
const taskData = createBaseData({
|
|
314
|
+
interactionId: 'child-int',
|
|
315
|
+
interaction: {
|
|
316
|
+
mainInteractionId: 'main-int',
|
|
317
|
+
} as any,
|
|
318
|
+
});
|
|
319
|
+
const voice = new Voice({...dummyContact, consultEnd: consultEndMock}, taskData, {
|
|
320
|
+
isEndTaskEnabled: true,
|
|
321
|
+
isEndConsultEnabled: true,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await voice.endConsult({isConsult: true} as any);
|
|
325
|
+
|
|
326
|
+
expect(consultEndMock).toHaveBeenCalledWith({
|
|
327
|
+
interactionId: 'main-int',
|
|
328
|
+
data: {isConsult: true},
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe('UI controls for AGENT_CONTACT_ASSIGNED', () => {
|
|
334
|
+
it('shows main controls and hides accept/decline on AGENT_CONTACT_ASSIGNED', () => {
|
|
335
|
+
const data: any = {...createBaseData(), type: CC_EVENTS.AGENT_CONTACT_ASSIGNED};
|
|
336
|
+
const voice = new Voice(dummyContact, data, {
|
|
337
|
+
isEndTaskEnabled: true,
|
|
338
|
+
isEndConsultEnabled: false,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
voice.updateTaskData(data);
|
|
342
|
+
primeConnectedState(voice, data);
|
|
343
|
+
|
|
344
|
+
expect(voice.uiControls.main.accept.isVisible).toBe(false);
|
|
345
|
+
expect(voice.uiControls.main.decline.isVisible).toBe(false);
|
|
346
|
+
expect(voice.uiControls.main.hold.isVisible).toBe(true);
|
|
347
|
+
expect(voice.uiControls.main.transfer.isVisible).toBe(true);
|
|
348
|
+
expect(voice.uiControls.main.consult.isVisible).toBe(true);
|
|
349
|
+
expect(voice.uiControls.main.recording.isVisible).toBe(true);
|
|
350
|
+
expect(voice.uiControls.main.end.isVisible).toBe(true);
|
|
351
|
+
expect(voice.uiControls.main.endConsult.isVisible).toBe(false);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe('state machine derived controls', () => {
|
|
356
|
+
it('keeps uiControls in sync with state machine context', () => {
|
|
357
|
+
const taskData = createBaseData();
|
|
358
|
+
const voice = new Voice(dummyContact, taskData, {});
|
|
359
|
+
const initialSnapshot = voice.stateMachineService?.getSnapshot();
|
|
360
|
+
const initialExpected = computeUIControls(
|
|
361
|
+
initialSnapshot?.value as TaskState,
|
|
362
|
+
initialSnapshot?.context as any,
|
|
363
|
+
voice.data
|
|
364
|
+
);
|
|
365
|
+
expect(voice.uiControls).toEqual(initialExpected);
|
|
366
|
+
|
|
367
|
+
voice.updateTaskData(taskData);
|
|
368
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
369
|
+
voice.stateMachineService?.send({type: TaskEvent.ASSIGN, taskData});
|
|
370
|
+
|
|
371
|
+
const snapshot = voice.stateMachineService?.getSnapshot();
|
|
372
|
+
const expected = computeUIControls(
|
|
373
|
+
snapshot?.value as TaskState,
|
|
374
|
+
snapshot?.context as any,
|
|
375
|
+
voice.data
|
|
376
|
+
);
|
|
377
|
+
expect(voice.uiControls).toEqual(expected);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe('recording operations', () => {
|
|
382
|
+
const buildRecordingData = (recordingOverrides: Record<string, any>) =>
|
|
383
|
+
createBaseData({
|
|
384
|
+
interaction: {
|
|
385
|
+
callProcessingDetails: recordingOverrides,
|
|
386
|
+
} as any,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('throws when pauseRecording is invoked without active recording', async () => {
|
|
390
|
+
const voice = new Voice(dummyContact, createBaseData(), {});
|
|
391
|
+
await expect(voice.pauseRecording()).rejects.toThrow(
|
|
392
|
+
'Recording is not active or already paused'
|
|
393
|
+
);
|
|
394
|
+
expect(dummyContact.pauseRecording).not.toHaveBeenCalled();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('pauses recording when state machine context indicates active recording', async () => {
|
|
398
|
+
const taskData = buildRecordingData({recordInProgress: true});
|
|
399
|
+
const voice = new Voice(dummyContact, taskData, {});
|
|
400
|
+
primeConnectedState(voice, taskData);
|
|
401
|
+
|
|
402
|
+
await voice.pauseRecording();
|
|
403
|
+
expect(dummyContact.pauseRecording).toHaveBeenCalledWith({interactionId: 'int1'});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('throws if resumeRecording is invoked while recording is not paused', async () => {
|
|
407
|
+
const taskData = buildRecordingData({recordInProgress: true});
|
|
408
|
+
const voice = new Voice(dummyContact, taskData, {});
|
|
409
|
+
primeConnectedState(voice, taskData);
|
|
410
|
+
|
|
411
|
+
await expect(voice.resumeRecording()).rejects.toThrow('Recording is not paused');
|
|
412
|
+
expect(dummyContact.resumeRecording).not.toHaveBeenCalled();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('resumes recording when context shows paused recording', async () => {
|
|
416
|
+
const taskData = buildRecordingData({recordInProgress: true});
|
|
417
|
+
const voice = new Voice(dummyContact, taskData, {});
|
|
418
|
+
primeConnectedState(voice, taskData);
|
|
419
|
+
voice.stateMachineService?.send({type: TaskEvent.PAUSE_RECORDING});
|
|
420
|
+
|
|
421
|
+
await voice.resumeRecording();
|
|
422
|
+
expect(dummyContact.resumeRecording).toHaveBeenCalledWith({
|
|
423
|
+
interactionId: 'int1',
|
|
424
|
+
data: {autoResumed: false},
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
describe('switchCall()', () => {
|
|
430
|
+
const buildConsultingTaskData = () =>
|
|
431
|
+
createBaseData({
|
|
432
|
+
agentId: 'agent1',
|
|
433
|
+
consultMediaResourceId: 'consultMedia1',
|
|
434
|
+
interaction: {
|
|
435
|
+
media: {
|
|
436
|
+
media1: {
|
|
437
|
+
mediaResourceId: 'media1',
|
|
438
|
+
isHold: true,
|
|
439
|
+
mType: 'mainCall',
|
|
440
|
+
participants: ['agent1', 'customer1'],
|
|
441
|
+
},
|
|
442
|
+
consultMedia1: {
|
|
443
|
+
mediaResourceId: 'consultMedia1',
|
|
444
|
+
isHold: false,
|
|
445
|
+
mType: 'consult',
|
|
446
|
+
participants: ['agent1', 'agent2'],
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
participants: {
|
|
450
|
+
agent1: {id: 'agent1', pType: 'Agent', type: 'Agent', hasLeft: false},
|
|
451
|
+
agent2: {id: 'agent2', pType: 'Agent', type: 'Agent', hasLeft: false},
|
|
452
|
+
customer1: {id: 'customer1', pType: 'Customer', type: 'Customer', hasLeft: false},
|
|
453
|
+
},
|
|
454
|
+
} as any,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const primeConsultingState = (voice: Voice, taskData: TaskData) => {
|
|
458
|
+
primeConnectedState(voice, taskData);
|
|
459
|
+
voice.stateMachineService?.send({
|
|
460
|
+
type: TaskEvent.CONSULT,
|
|
461
|
+
destination: 'agent2',
|
|
462
|
+
destinationType: 'agent' as any,
|
|
463
|
+
});
|
|
464
|
+
voice.stateMachineService?.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
it('switches from consult leg to main leg by unholding main media', async () => {
|
|
468
|
+
const taskData = buildConsultingTaskData();
|
|
469
|
+
const voice = new Voice(dummyContact, taskData, {});
|
|
470
|
+
primeConsultingState(voice, taskData);
|
|
471
|
+
|
|
472
|
+
await voice.switchCall();
|
|
473
|
+
|
|
474
|
+
expect(dummyContact.unHold).toHaveBeenCalledTimes(1);
|
|
475
|
+
expect(dummyContact.unHold).toHaveBeenCalledWith({
|
|
476
|
+
interactionId: 'int1',
|
|
477
|
+
data: {mediaResourceId: 'media1'},
|
|
478
|
+
});
|
|
479
|
+
expect(dummyContact.hold).not.toHaveBeenCalled();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('switches from main leg to consult leg by holding main media', async () => {
|
|
483
|
+
const taskData = buildConsultingTaskData();
|
|
484
|
+
taskData.interaction.media.media1.isHold = false;
|
|
485
|
+
taskData.interaction.media.consultMedia1.isHold = true;
|
|
486
|
+
const voice = new Voice(dummyContact, taskData, {});
|
|
487
|
+
primeConsultingState(voice, taskData);
|
|
488
|
+
voice.stateMachineService?.send({type: TaskEvent.SWITCH_TO_MAIN_CALL});
|
|
489
|
+
|
|
490
|
+
await voice.switchCall();
|
|
491
|
+
|
|
492
|
+
expect(dummyContact.hold).toHaveBeenCalledTimes(1);
|
|
493
|
+
expect(dummyContact.hold).toHaveBeenCalledWith({
|
|
494
|
+
interactionId: 'int1',
|
|
495
|
+
data: {mediaResourceId: 'media1'},
|
|
496
|
+
});
|
|
497
|
+
expect(dummyContact.unHold).not.toHaveBeenCalled();
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
describe('consultConference()', () => {
|
|
502
|
+
afterEach(() => {
|
|
503
|
+
jest.restoreAllMocks();
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('uses cached consult destination when task data destination is cleared', async () => {
|
|
507
|
+
const taskData = createBaseData({
|
|
508
|
+
agentId: 'agent1',
|
|
509
|
+
destAgentId: undefined,
|
|
510
|
+
destinationType: undefined,
|
|
511
|
+
interaction: {
|
|
512
|
+
media: {
|
|
513
|
+
media1: {mediaResourceId: 'media1', isHold: true},
|
|
514
|
+
},
|
|
515
|
+
participants: {
|
|
516
|
+
agent1: {id: 'agent1', pType: 'Agent', type: 'Agent', hasLeft: false},
|
|
517
|
+
},
|
|
518
|
+
} as any,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const voice = new Voice(dummyContact, taskData, {});
|
|
522
|
+
primeConnectedState(voice, taskData);
|
|
523
|
+
voice.stateMachineService?.send({
|
|
524
|
+
type: TaskEvent.CONSULT,
|
|
525
|
+
destination: 'agent2',
|
|
526
|
+
destAgentId: 'agent2',
|
|
527
|
+
destinationType: 'agent' as any,
|
|
528
|
+
});
|
|
529
|
+
voice.updateTaskData(
|
|
530
|
+
createBaseData({
|
|
531
|
+
agentId: 'agent1',
|
|
532
|
+
destAgentId: undefined,
|
|
533
|
+
destinationType: undefined,
|
|
534
|
+
interaction: {state: 'consulting'} as any,
|
|
535
|
+
}) as any
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
await voice.consultConference();
|
|
539
|
+
|
|
540
|
+
expect(dummyContact.consultConference).toHaveBeenCalledWith({
|
|
541
|
+
interactionId: 'int1',
|
|
542
|
+
data: expect.objectContaining({
|
|
543
|
+
to: 'agent2',
|
|
544
|
+
destinationType: 'agent',
|
|
545
|
+
}),
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('falls back to derived destination when context and task destination are unavailable', async () => {
|
|
550
|
+
jest.spyOn(Utils, 'calculateDestAgentId').mockReturnValueOnce('derivedAgent');
|
|
551
|
+
jest.spyOn(Utils, 'calculateDestType').mockReturnValueOnce('agent');
|
|
552
|
+
|
|
553
|
+
const taskData = createBaseData({
|
|
554
|
+
agentId: 'agent1',
|
|
555
|
+
destAgentId: undefined,
|
|
556
|
+
destinationType: undefined,
|
|
557
|
+
interaction: {
|
|
558
|
+
media: {
|
|
559
|
+
media1: {mediaResourceId: 'media1', isHold: true},
|
|
560
|
+
},
|
|
561
|
+
participants: {
|
|
562
|
+
agent1: {id: 'agent1', pType: 'Agent', type: 'Agent', hasLeft: false},
|
|
563
|
+
},
|
|
564
|
+
} as any,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
const voice = new Voice(dummyContact, taskData, {});
|
|
568
|
+
primeConnectedState(voice, taskData);
|
|
569
|
+
voice.stateMachineService?.send({
|
|
570
|
+
type: TaskEvent.CONSULT,
|
|
571
|
+
destination: '',
|
|
572
|
+
destAgentId: undefined,
|
|
573
|
+
destinationType: 'agent' as any,
|
|
574
|
+
});
|
|
575
|
+
voice.stateMachineService?.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
|
|
576
|
+
|
|
577
|
+
await voice.consultConference();
|
|
578
|
+
|
|
579
|
+
expect(dummyContact.consultConference).toHaveBeenCalledWith({
|
|
580
|
+
interactionId: 'int1',
|
|
581
|
+
data: expect.objectContaining({
|
|
582
|
+
to: 'derivedAgent',
|
|
583
|
+
}),
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
});
|