@webex/contact-center 3.12.0-llmrefactor.1 → 3.12.0-llmrefactor.10

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.
Files changed (198) hide show
  1. package/.sdd/manifest.json +7 -5
  2. package/ai-docs/ARCHITECTURE.md +1 -1
  3. package/ai-docs/CONTRACTS.md +8 -3
  4. package/ai-docs/SECURITY.md +1 -1
  5. package/ai-docs/contact-center-spec.md +22 -4
  6. package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
  7. package/dist/cc.js +544 -12
  8. package/dist/cc.js.map +1 -1
  9. package/dist/config.js +7 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +10 -2
  12. package/dist/constants.js.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/metrics/MetricsManager.js +16 -0
  15. package/dist/metrics/MetricsManager.js.map +1 -1
  16. package/dist/metrics/behavioral-events.js +104 -0
  17. package/dist/metrics/behavioral-events.js.map +1 -1
  18. package/dist/metrics/constants.js +18 -0
  19. package/dist/metrics/constants.js.map +1 -1
  20. package/dist/services/AddressBook.js +18 -15
  21. package/dist/services/AddressBook.js.map +1 -1
  22. package/dist/services/AnswerCallOnWebexService.js +202 -0
  23. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  24. package/dist/services/EntryPoint.js +46 -68
  25. package/dist/services/EntryPoint.js.map +1 -1
  26. package/dist/services/Queue.js +27 -22
  27. package/dist/services/Queue.js.map +1 -1
  28. package/dist/services/WebexCrossClientService.js +222 -0
  29. package/dist/services/WebexCrossClientService.js.map +1 -0
  30. package/dist/services/WxAppTelephonyMercurySync.js +108 -0
  31. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  32. package/dist/services/config/Util.js +3 -0
  33. package/dist/services/config/Util.js.map +1 -1
  34. package/dist/services/config/constants.js +10 -6
  35. package/dist/services/config/constants.js.map +1 -1
  36. package/dist/services/config/types.js +4 -0
  37. package/dist/services/config/types.js.map +1 -1
  38. package/dist/services/core/Err.js.map +1 -1
  39. package/dist/services/core/WebexRequest.js +6 -2
  40. package/dist/services/core/WebexRequest.js.map +1 -1
  41. package/dist/services/core/aqm-reqs.js +28 -14
  42. package/dist/services/core/aqm-reqs.js.map +1 -1
  43. package/dist/services/core/types.js.map +1 -1
  44. package/dist/services/task/Task.js +73 -7
  45. package/dist/services/task/Task.js.map +1 -1
  46. package/dist/services/task/TaskFactory.js +8 -4
  47. package/dist/services/task/TaskFactory.js.map +1 -1
  48. package/dist/services/task/TaskManager.js +490 -21
  49. package/dist/services/task/TaskManager.js.map +1 -1
  50. package/dist/services/task/WebexCallingUtils.js +70 -0
  51. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  52. package/dist/services/task/constants.js +4 -1
  53. package/dist/services/task/constants.js.map +1 -1
  54. package/dist/services/task/contact.js +29 -0
  55. package/dist/services/task/contact.js.map +1 -1
  56. package/dist/services/task/digital/Digital.js +3 -2
  57. package/dist/services/task/digital/Digital.js.map +1 -1
  58. package/dist/services/task/state-machine/TaskStateMachine.js +162 -53
  59. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  60. package/dist/services/task/state-machine/actions.js +6 -1
  61. package/dist/services/task/state-machine/actions.js.map +1 -1
  62. package/dist/services/task/state-machine/guards.js +7 -10
  63. package/dist/services/task/state-machine/guards.js.map +1 -1
  64. package/dist/services/task/state-machine/types.js.map +1 -1
  65. package/dist/services/task/state-machine/uiControlsComputer.js +118 -18
  66. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  67. package/dist/services/task/types.js +16 -1
  68. package/dist/services/task/types.js.map +1 -1
  69. package/dist/services/task/voice/Voice.js +456 -15
  70. package/dist/services/task/voice/Voice.js.map +1 -1
  71. package/dist/services/task/voice/WebRTC.js +4 -1
  72. package/dist/services/task/voice/WebRTC.js.map +1 -1
  73. package/dist/services/task/voice/wxAppVoiceMethods.js +354 -0
  74. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  75. package/dist/services/wxAppDiagnosticLogging.js +122 -0
  76. package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
  77. package/dist/services/wxAppTelephonyUtils.js +19 -0
  78. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  79. package/dist/types/cc.d.ts +78 -4
  80. package/dist/types/config.d.ts +7 -0
  81. package/dist/types/constants.d.ts +8 -0
  82. package/dist/types/index.d.ts +3 -1
  83. package/dist/types/metrics/MetricsManager.d.ts +6 -0
  84. package/dist/types/metrics/constants.d.ts +17 -0
  85. package/dist/types/services/AddressBook.d.ts +2 -1
  86. package/dist/types/services/AnswerCallOnWebexService.d.ts +38 -0
  87. package/dist/types/services/EntryPoint.d.ts +7 -5
  88. package/dist/types/services/Queue.d.ts +5 -3
  89. package/dist/types/services/WebexCrossClientService.d.ts +32 -0
  90. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  91. package/dist/types/services/config/constants.d.ts +8 -5
  92. package/dist/types/services/config/types.d.ts +17 -4
  93. package/dist/types/services/core/Err.d.ts +2 -0
  94. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  95. package/dist/types/services/core/types.d.ts +2 -0
  96. package/dist/types/services/task/Task.d.ts +20 -2
  97. package/dist/types/services/task/TaskFactory.d.ts +2 -1
  98. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  99. package/dist/types/services/task/constants.d.ts +3 -0
  100. package/dist/types/services/task/contact.d.ts +4 -0
  101. package/dist/types/services/task/digital/Digital.d.ts +2 -2
  102. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +304 -56
  103. package/dist/types/services/task/state-machine/guards.d.ts +6 -5
  104. package/dist/types/services/task/state-machine/types.d.ts +9 -1
  105. package/dist/types/services/task/types.d.ts +81 -4
  106. package/dist/types/services/task/voice/Voice.d.ts +52 -9
  107. package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
  108. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +55 -0
  109. package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -0
  110. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  111. package/dist/types/types.d.ts +14 -8
  112. package/dist/types/utils/PageCache.d.ts +20 -3
  113. package/dist/types.js +7 -2
  114. package/dist/types.js.map +1 -1
  115. package/dist/utils/PageCache.js +19 -5
  116. package/dist/utils/PageCache.js.map +1 -1
  117. package/dist/webex.js +1 -1
  118. package/package.json +8 -8
  119. package/src/cc.ts +657 -12
  120. package/src/config.ts +7 -0
  121. package/src/constants.ts +8 -0
  122. package/src/index.ts +2 -0
  123. package/src/metrics/MetricsManager.ts +16 -0
  124. package/src/metrics/ai-docs/metrics-spec.md +78 -10
  125. package/src/metrics/behavioral-events.ts +106 -0
  126. package/src/metrics/constants.ts +20 -0
  127. package/src/services/AddressBook.ts +17 -6
  128. package/src/services/AnswerCallOnWebexService.ts +239 -0
  129. package/src/services/EntryPoint.ts +59 -60
  130. package/src/services/Queue.ts +29 -12
  131. package/src/services/WebexCrossClientService.ts +293 -0
  132. package/src/services/WxAppTelephonyMercurySync.ts +142 -0
  133. package/src/services/agent/ai-docs/agent-spec.md +1 -0
  134. package/src/services/ai-docs/AGENTS.md +10 -10
  135. package/src/services/ai-docs/services-spec.md +6 -1
  136. package/src/services/config/Util.ts +3 -0
  137. package/src/services/config/ai-docs/AGENTS.md +1 -1
  138. package/src/services/config/ai-docs/ARCHITECTURE.md +2 -2
  139. package/src/services/config/ai-docs/config-spec.md +6 -0
  140. package/src/services/config/constants.ts +10 -6
  141. package/src/services/config/types.ts +16 -4
  142. package/src/services/core/Err.ts +1 -0
  143. package/src/services/core/WebexRequest.ts +3 -1
  144. package/src/services/core/ai-docs/core-spec.md +5 -1
  145. package/src/services/core/aqm-reqs.ts +30 -15
  146. package/src/services/core/types.ts +2 -0
  147. package/src/services/task/Task.ts +79 -8
  148. package/src/services/task/TaskFactory.ts +8 -3
  149. package/src/services/task/TaskManager.ts +716 -17
  150. package/src/services/task/WebexCallingUtils.ts +136 -0
  151. package/src/services/task/ai-docs/ARCHITECTURE.md +1 -0
  152. package/src/services/task/ai-docs/task-spec.md +156 -3
  153. package/src/services/task/constants.ts +3 -0
  154. package/src/services/task/contact.ts +30 -0
  155. package/src/services/task/digital/Digital.ts +4 -2
  156. package/src/services/task/state-machine/TaskStateMachine.ts +235 -81
  157. package/src/services/task/state-machine/actions.ts +5 -2
  158. package/src/services/task/state-machine/ai-docs/AGENTS.md +6 -4
  159. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +35 -16
  160. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +103 -33
  161. package/src/services/task/state-machine/guards.ts +7 -10
  162. package/src/services/task/state-machine/types.ts +16 -1
  163. package/src/services/task/state-machine/uiControlsComputer.ts +206 -32
  164. package/src/services/task/types.ts +114 -4
  165. package/src/services/task/voice/Voice.ts +595 -14
  166. package/src/services/task/voice/WebRTC.ts +5 -1
  167. package/src/services/task/voice/wxAppVoiceMethods.ts +516 -0
  168. package/src/services/wxAppDiagnosticLogging.ts +174 -0
  169. package/src/services/wxAppTelephonyUtils.ts +14 -0
  170. package/src/types.ts +33 -9
  171. package/src/utils/AGENTS.md +21 -10
  172. package/src/utils/PageCache.ts +38 -5
  173. package/src/utils/ai-docs/utils-spec.md +21 -11
  174. package/test/unit/spec/cc.ts +1522 -19
  175. package/test/unit/spec/metrics/MetricsManager.ts +25 -0
  176. package/test/unit/spec/metrics/behavioral-events.ts +60 -0
  177. package/test/unit/spec/services/AddressBook.ts +37 -6
  178. package/test/unit/spec/services/AnswerCallOnWebexService.ts +264 -0
  179. package/test/unit/spec/services/EntryPoint.ts +87 -40
  180. package/test/unit/spec/services/Queue.ts +123 -12
  181. package/test/unit/spec/services/WebexCrossClientService.ts +467 -0
  182. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +136 -0
  183. package/test/unit/spec/services/config/Util.ts +85 -0
  184. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  185. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  186. package/test/unit/spec/services/task/Task.ts +274 -6
  187. package/test/unit/spec/services/task/TaskFactory.ts +39 -2
  188. package/test/unit/spec/services/task/TaskManager.ts +1717 -9
  189. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  190. package/test/unit/spec/services/task/contact.ts +33 -0
  191. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +1485 -109
  192. package/test/unit/spec/services/task/state-machine/guards.ts +210 -8
  193. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +445 -7
  194. package/test/unit/spec/services/task/voice/Voice.ts +1412 -0
  195. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +603 -0
  196. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
  197. package/umd/contact-center.min.js +2 -2
  198. package/umd/contact-center.min.js.map +1 -1
@@ -5,6 +5,7 @@ import {
5
5
  TaskState,
6
6
  } from '../../../../../../src/services/task/state-machine';
7
7
  import {createTaskData} from '../taskTestUtils';
8
+ import {guards} from '../../../../../../src/services/task/state-machine/guards';
8
9
 
9
10
  const createConfig = () => ({
10
11
  channelType: 'voice' as const,
@@ -14,6 +15,18 @@ const createConfig = () => ({
14
15
  isRecordingEnabled: true,
15
16
  });
16
17
 
18
+ /** Mirrors Task.emitTaskWrapup: publishWrapup spy only runs when context would emit task:wrapup. */
19
+ function createPublishAwareEmitTaskWrapupMock() {
20
+ const publishWrapup = jest.fn();
21
+ const emitTaskWrapup = ({context}: {context: {taskData?: {wrapUpRequired?: boolean}}}) => {
22
+ if (context.taskData?.wrapUpRequired) {
23
+ publishWrapup();
24
+ }
25
+ };
26
+
27
+ return {emitTaskWrapup, publishWrapup};
28
+ }
29
+
17
30
  describe('Task state machine', () => {
18
31
  const startMachine = () => {
19
32
  const actor = createActor(createTaskStateMachine(createConfig()));
@@ -106,6 +119,234 @@ describe('Task state machine', () => {
106
119
  });
107
120
  });
108
121
 
122
+ describe('partial lifecycle payloads', () => {
123
+ const createActiveMainCallTaskData = (isHold = false) =>
124
+ createTaskData({
125
+ agentId: 'agent-1',
126
+ interactionId: 'interaction-1',
127
+ mediaResourceId: 'main-media',
128
+ interaction: {
129
+ state: 'connected',
130
+ participants: {
131
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
132
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
133
+ },
134
+ media: {
135
+ 'main-media': {
136
+ mediaResourceId: 'main-media',
137
+ mType: 'mainCall',
138
+ participants: ['agent-1', 'customer-1'],
139
+ isHold,
140
+ },
141
+ },
142
+ } as any,
143
+ });
144
+
145
+ const createPartialMainCallTaskData = (isHold = false, omitSelf = false) =>
146
+ createTaskData({
147
+ agentId: 'agent-1',
148
+ interactionId: 'interaction-1',
149
+ mediaResourceId: 'main-media',
150
+ interaction: {
151
+ state: 'connected',
152
+ participants: omitSelf
153
+ ? {'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false}}
154
+ : {
155
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
156
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
157
+ },
158
+ media: {
159
+ 'main-media': {
160
+ mediaResourceId: 'main-media',
161
+ mType: 'mainCall',
162
+ participants: ['customer-1'],
163
+ isHold,
164
+ },
165
+ },
166
+ } as any,
167
+ });
168
+
169
+ const startMachineWithEndSpy = () => {
170
+ const emitTaskEnd = jest.fn();
171
+ const service = createActor(
172
+ createTaskStateMachine({...createConfig(), agentId: 'agent-1'}, {actions: {emitTaskEnd}})
173
+ );
174
+ service.start();
175
+
176
+ return {service, emitTaskEnd};
177
+ };
178
+
179
+ const enterTransientHoldState = (
180
+ service: ReturnType<typeof startMachine>,
181
+ state: TaskState.HOLD_INITIATING | TaskState.RESUME_INITIATING,
182
+ taskData: ReturnType<typeof createActiveMainCallTaskData>
183
+ ) => {
184
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
185
+ service.send({type: TaskEvent.ASSIGN, taskData});
186
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: 'main-media'});
187
+
188
+ if (state === TaskState.RESUME_INITIATING) {
189
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: 'main-media'});
190
+ service.send({type: TaskEvent.UNHOLD_INITIATED, mediaResourceId: 'main-media'});
191
+ }
192
+
193
+ expect(service.getSnapshot().value).toBe(state);
194
+ };
195
+
196
+ it('keeps CONNECTED when another participant leave event has a partial mainCall', () => {
197
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
198
+ const taskData = createActiveMainCallTaskData();
199
+
200
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
201
+ service.send({type: TaskEvent.ASSIGN, taskData});
202
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
203
+
204
+ service.send({
205
+ type: TaskEvent.PARTICIPANT_LEAVE,
206
+ participantId: 'agent-2',
207
+ taskData: createPartialMainCallTaskData(false, true),
208
+ });
209
+
210
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
211
+ expect(emitTaskEnd).not.toHaveBeenCalled();
212
+ });
213
+
214
+ it('ignores PARTICIPANT_LEAVE in CONNECTED even when it identifies the current agent', () => {
215
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
216
+ const taskData = createActiveMainCallTaskData();
217
+
218
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
219
+ service.send({type: TaskEvent.ASSIGN, taskData});
220
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
221
+
222
+ service.send({
223
+ type: TaskEvent.PARTICIPANT_LEAVE,
224
+ participantId: 'agent-1',
225
+ taskData: createPartialMainCallTaskData(false, true),
226
+ });
227
+
228
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
229
+ expect(emitTaskEnd).not.toHaveBeenCalled();
230
+ });
231
+
232
+ it('enters WRAPPING_UP on TASK_WRAPUP while HOLD_INITIATING and emits wrap-up once', () => {
233
+ const emitTaskWrapup = jest.fn();
234
+ const service = createActor(
235
+ createTaskStateMachine({...createConfig(), agentId: 'agent-1'}, {actions: {emitTaskWrapup}})
236
+ );
237
+ service.start();
238
+ const taskData = createActiveMainCallTaskData();
239
+
240
+ enterTransientHoldState(service, TaskState.HOLD_INITIATING, taskData);
241
+ service.send({
242
+ type: TaskEvent.TASK_WRAPUP,
243
+ taskData: {...taskData, wrapUpRequired: true},
244
+ });
245
+
246
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
247
+ expect(emitTaskWrapup).toHaveBeenCalledTimes(1);
248
+ });
249
+
250
+ it('ignores PARTICIPANT_LEAVE in HOLD_INITIATING when a remote Drop identifies the current agent', () => {
251
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
252
+ const taskData = createActiveMainCallTaskData();
253
+
254
+ enterTransientHoldState(service, TaskState.HOLD_INITIATING, taskData);
255
+ service.send({
256
+ type: TaskEvent.PARTICIPANT_LEAVE,
257
+ participantId: 'agent-1',
258
+ taskData: createPartialMainCallTaskData(false, true),
259
+ });
260
+
261
+ expect(service.getSnapshot().value).toBe(TaskState.HOLD_INITIATING);
262
+ expect(emitTaskEnd).not.toHaveBeenCalled();
263
+ });
264
+
265
+ it('terminates RESUME_INITIATING when a remote Drop identifies the current agent', () => {
266
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
267
+ const taskData = createActiveMainCallTaskData(true);
268
+
269
+ enterTransientHoldState(service, TaskState.RESUME_INITIATING, taskData);
270
+ service.send({
271
+ type: TaskEvent.PARTICIPANT_LEAVE,
272
+ participantId: 'agent-1',
273
+ taskData: createPartialMainCallTaskData(true, true),
274
+ });
275
+
276
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
277
+ expect(emitTaskEnd).toHaveBeenCalledTimes(1);
278
+ });
279
+
280
+ it('keeps HOLD_INITIATING when ParticipantLeftConference identifies another participant', () => {
281
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
282
+ const taskData = createActiveMainCallTaskData();
283
+
284
+ enterTransientHoldState(service, TaskState.HOLD_INITIATING, taskData);
285
+ service.send({
286
+ type: TaskEvent.PARTICIPANT_LEAVE,
287
+ participantId: 'agent-2',
288
+ taskData: createPartialMainCallTaskData(false, true),
289
+ });
290
+
291
+ expect(service.getSnapshot().value).toBe(TaskState.HOLD_INITIATING);
292
+ expect(emitTaskEnd).not.toHaveBeenCalled();
293
+ });
294
+
295
+ it('terminates RESUME_INITIATING when another participant leave omits self from the roster', () => {
296
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
297
+ const taskData = createActiveMainCallTaskData(true);
298
+
299
+ enterTransientHoldState(service, TaskState.RESUME_INITIATING, taskData);
300
+ service.send({
301
+ type: TaskEvent.PARTICIPANT_LEAVE,
302
+ participantId: 'agent-2',
303
+ taskData: createPartialMainCallTaskData(true, true),
304
+ });
305
+
306
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
307
+ expect(emitTaskEnd).toHaveBeenCalledTimes(1);
308
+ });
309
+
310
+ it('terminates HELD when another participant leave event omits self from its roster', () => {
311
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
312
+ const taskData = createActiveMainCallTaskData(true);
313
+
314
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
315
+ service.send({type: TaskEvent.ASSIGN, taskData});
316
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: 'main-media'});
317
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: 'main-media'});
318
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
319
+
320
+ service.send({
321
+ type: TaskEvent.PARTICIPANT_LEAVE,
322
+ participantId: 'agent-2',
323
+ taskData: createPartialMainCallTaskData(true, true),
324
+ });
325
+
326
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
327
+ expect(emitTaskEnd).toHaveBeenCalledTimes(1);
328
+ });
329
+
330
+ it('keeps HELD when CONSULT_END has a partial mainCall', () => {
331
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
332
+ const taskData = createActiveMainCallTaskData(true);
333
+
334
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
335
+ service.send({type: TaskEvent.ASSIGN, taskData});
336
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: 'main-media'});
337
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: 'main-media'});
338
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
339
+
340
+ service.send({
341
+ type: TaskEvent.CONSULT_END,
342
+ taskData: createPartialMainCallTaskData(true),
343
+ });
344
+
345
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
346
+ expect(emitTaskEnd).not.toHaveBeenCalled();
347
+ });
348
+ });
349
+
109
350
  describe('recording pause/resume events', () => {
110
351
  it('toggles recordingPaused flag based on events', () => {
111
352
  const service = startMachine();
@@ -180,6 +421,32 @@ describe('Task state machine', () => {
180
421
  service.send({type: TaskEvent.WRAPUP_COMPLETE});
181
422
  expect(service.getSnapshot().value).toBe(TaskState.COMPLETED);
182
423
  });
424
+
425
+ it('emits task wrappedup before cleanup on COMPLETED entry', () => {
426
+ const actionOrder: string[] = [];
427
+ const actor = createActor(
428
+ createTaskStateMachine(createConfig(), {
429
+ actions: {
430
+ emitTaskWrappedup: () => {
431
+ actionOrder.push('emitTaskWrappedup');
432
+ },
433
+ cleanupResources: () => {
434
+ actionOrder.push('cleanupResources');
435
+ },
436
+ },
437
+ })
438
+ );
439
+ actor.start();
440
+ const taskData = createTaskData();
441
+
442
+ actor.send({type: TaskEvent.TASK_INCOMING, taskData});
443
+ actor.send({type: TaskEvent.ASSIGN, taskData});
444
+ actor.send({type: TaskEvent.TASK_WRAPUP});
445
+ actor.send({type: TaskEvent.WRAPUP_COMPLETE});
446
+
447
+ expect(actor.getSnapshot().value).toBe(TaskState.COMPLETED);
448
+ expect(actionOrder).toEqual(['emitTaskWrappedup', 'cleanupResources']);
449
+ });
183
450
  });
184
451
 
185
452
  describe('consult and conference flows', () => {
@@ -343,53 +610,53 @@ describe('Task state machine', () => {
343
610
  expect(snapshot.context.consultFromConference).toBe(true);
344
611
  });
345
612
 
346
- it('hydrates consulted agent to CONSULTING when self consultState is consulting and main leg is held', () => {
347
- const service = startMachine();
348
- const taskData = createTaskData({
349
- agentId: 'agent-2',
350
- isConsulted: true,
351
- interaction: {
352
- state: 'conference',
353
- mainInteractionId: 'interaction-1',
354
- interactionId: 'interaction-1',
355
- participants: {
356
- 'agent-1': {
357
- id: 'agent-1',
358
- pType: 'Agent',
359
- hasLeft: false,
360
- consultState: 'consulting',
361
- isConsulted: false,
362
- },
363
- 'agent-2': {
364
- id: 'agent-2',
365
- pType: 'Agent',
366
- hasLeft: false,
367
- consultState: 'consulting',
368
- isConsulted: true,
369
- },
370
- 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
371
- },
372
- media: {
373
- 'interaction-1': {
374
- mediaResourceId: 'interaction-1',
375
- mType: 'mainCall',
376
- participants: ['agent-1', 'customer-1'],
377
- isHold: true,
378
- },
379
- 'consult-media-1': {
380
- mediaResourceId: 'consult-media-1',
381
- mType: 'consult',
382
- participants: ['agent-1', 'agent-2'],
383
- isHold: false,
613
+ it('hydrates consulted agent to CONSULTING when self consultState is consulting and main leg is held', () => {
614
+ const service = startMachine();
615
+ const taskData = createTaskData({
616
+ agentId: 'agent-2',
617
+ isConsulted: true,
618
+ interaction: {
619
+ state: 'conference',
620
+ mainInteractionId: 'interaction-1',
621
+ interactionId: 'interaction-1',
622
+ participants: {
623
+ 'agent-1': {
624
+ id: 'agent-1',
625
+ pType: 'Agent',
626
+ hasLeft: false,
627
+ consultState: 'consulting',
628
+ isConsulted: false,
629
+ },
630
+ 'agent-2': {
631
+ id: 'agent-2',
632
+ pType: 'Agent',
633
+ hasLeft: false,
634
+ consultState: 'consulting',
635
+ isConsulted: true,
636
+ },
637
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
384
638
  },
385
- },
386
- } as any,
387
- });
639
+ media: {
640
+ 'interaction-1': {
641
+ mediaResourceId: 'interaction-1',
642
+ mType: 'mainCall',
643
+ participants: ['agent-1', 'customer-1'],
644
+ isHold: true,
645
+ },
646
+ 'consult-media-1': {
647
+ mediaResourceId: 'consult-media-1',
648
+ mType: 'consult',
649
+ participants: ['agent-1', 'agent-2'],
650
+ isHold: false,
651
+ },
652
+ },
653
+ } as any,
654
+ });
388
655
 
389
- service.send({type: TaskEvent.HYDRATE, taskData});
656
+ service.send({type: TaskEvent.HYDRATE, taskData});
390
657
 
391
- expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
392
- });
658
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
659
+ });
393
660
 
394
661
  it('hydrates to CONSULTING when consult is pending (self consultState is consultInitiated)', () => {
395
662
  const service = startMachine();
@@ -1587,13 +1854,25 @@ describe('Task state machine', () => {
1587
1854
  expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
1588
1855
  });
1589
1856
 
1590
- it('terminates when EXIT_CONFERENCE_SUCCESS is received in conferencing and wrapup is not required', () => {
1591
- const service = startMachine();
1592
- const taskData = createTaskData({
1593
- isConsulted: true,
1594
- wrapUpRequired: false,
1857
+ const primeConferencing = (service: ReturnType<typeof startMachine>, taskData: ReturnType<typeof createTaskData>) => {
1858
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1859
+ service.send({type: TaskEvent.ASSIGN, taskData});
1860
+ service.send({
1861
+ type: TaskEvent.CONSULT,
1862
+ destination: 'agent-42',
1863
+ destinationType: 'agent',
1864
+ });
1865
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
1866
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
1867
+ service.send({type: TaskEvent.CONFERENCE_START, taskData});
1868
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1869
+ };
1870
+
1871
+ const conferenceInProgressTaskData = (overrides: Parameters<typeof createTaskData>[0] = {}) =>
1872
+ createTaskData({
1873
+ wrapUpRequired: true,
1595
1874
  interaction: {
1596
- owner: 'other-agent',
1875
+ owner: 'agent-1',
1597
1876
  state: 'conference',
1598
1877
  mainInteractionId: 'interaction-1',
1599
1878
  interactionId: 'interaction-1',
@@ -1604,54 +1883,485 @@ describe('Task state machine', () => {
1604
1883
  type: 'Agent',
1605
1884
  hasJoined: true,
1606
1885
  hasLeft: false,
1607
- isInPredial: false,
1608
- isWrapUp: false,
1609
1886
  },
1610
- c1: {
1611
- id: 'c1',
1612
- pType: 'Customer',
1613
- type: 'Customer',
1887
+ 'agent-2': {
1888
+ id: 'agent-2',
1889
+ pType: 'Agent',
1890
+ type: 'Agent',
1614
1891
  hasJoined: true,
1615
1892
  hasLeft: false,
1616
- isInPredial: false,
1617
1893
  },
1618
1894
  },
1619
1895
  media: {
1620
1896
  'interaction-1': {
1621
1897
  mediaResourceId: 'interaction-1',
1622
1898
  mediaType: 'telephony',
1623
- mediaMgr: 'mm',
1624
- participants: ['agent-1', 'c1'],
1899
+ participants: ['agent-1', 'agent-2'],
1625
1900
  mType: 'mainCall',
1626
1901
  isHold: false,
1627
- holdTimestamp: null,
1628
1902
  },
1629
1903
  },
1630
1904
  } as any,
1905
+ ...overrides,
1631
1906
  });
1632
1907
 
1908
+ const primeConnectedWithConferenceTaskData = (
1909
+ service: ReturnType<typeof startMachine>,
1910
+ taskData: ReturnType<typeof createTaskData>
1911
+ ) => {
1633
1912
  service.send({type: TaskEvent.TASK_INCOMING, taskData});
1634
1913
  service.send({type: TaskEvent.ASSIGN, taskData});
1635
- service.send({
1636
- type: TaskEvent.CONSULT,
1637
- destination: 'agent-42',
1638
- destinationType: 'agent',
1639
- });
1640
- service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
1641
- service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
1642
- service.send({type: TaskEvent.CONFERENCE_START, taskData});
1643
- expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1914
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
1915
+ };
1916
+
1917
+ it('enters WRAPPING_UP on EXIT_CONFERENCE_SUCCESS while CONNECTED with conference in task data', () => {
1918
+ const service = startMachine();
1919
+ const taskData = conferenceInProgressTaskData();
1644
1920
 
1921
+ primeConnectedWithConferenceTaskData(service, taskData);
1645
1922
  service.send({type: TaskEvent.EXIT_CONFERENCE_SUCCESS, taskData});
1646
- expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
1923
+
1924
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
1925
+ expect(service.getSnapshot().context.taskData?.wrapUpRequired).toBe(true);
1647
1926
  });
1648
1927
 
1649
- it('downgrades to HELD on HOLD_SUCCESS when no other agents remain on task', () => {
1650
- const service = startMachine();
1651
- const conferenceTaskData = createSingleAgentConferenceTaskData('conference');
1652
- const heldTaskData = createSingleAgentConferenceTaskData('hold', true);
1928
+ it('emits wrap-up once on EXIT_CONFERENCE_SUCCESS while CONNECTED', () => {
1929
+ const emitTaskWrapup = jest.fn();
1930
+ const service = createActor(
1931
+ createTaskStateMachine(createConfig(), {actions: {emitTaskWrapup}})
1932
+ );
1933
+ service.start();
1934
+ const taskData = conferenceInProgressTaskData();
1653
1935
 
1654
- service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
1936
+ primeConnectedWithConferenceTaskData(service, taskData);
1937
+ service.send({type: TaskEvent.EXIT_CONFERENCE_SUCCESS, taskData});
1938
+
1939
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
1940
+ expect(emitTaskWrapup).toHaveBeenCalledTimes(1);
1941
+ });
1942
+
1943
+ it('terminates on EXIT_CONFERENCE_SUCCESS while CONNECTED when wrap-up is not required', () => {
1944
+ const service = startMachine();
1945
+ const taskData = conferenceInProgressTaskData({
1946
+ wrapUpRequired: false,
1947
+ interaction: {
1948
+ owner: 'other-agent',
1949
+ state: 'conference',
1950
+ mainInteractionId: 'interaction-1',
1951
+ interactionId: 'interaction-1',
1952
+ participants: {
1953
+ 'agent-1': {
1954
+ id: 'agent-1',
1955
+ pType: 'Agent',
1956
+ type: 'Agent',
1957
+ hasJoined: true,
1958
+ hasLeft: false,
1959
+ isWrapUp: false,
1960
+ },
1961
+ 'agent-2': {
1962
+ id: 'agent-2',
1963
+ pType: 'Agent',
1964
+ type: 'Agent',
1965
+ hasJoined: true,
1966
+ hasLeft: false,
1967
+ },
1968
+ },
1969
+ media: {
1970
+ 'interaction-1': {
1971
+ mediaResourceId: 'interaction-1',
1972
+ mediaType: 'telephony',
1973
+ participants: ['agent-1', 'agent-2'],
1974
+ mType: 'mainCall',
1975
+ isHold: false,
1976
+ },
1977
+ },
1978
+ } as any,
1979
+ });
1980
+
1981
+ primeConnectedWithConferenceTaskData(service, taskData);
1982
+ service.send({type: TaskEvent.EXIT_CONFERENCE_SUCCESS, taskData});
1983
+
1984
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
1985
+ });
1986
+
1987
+ it('does not re-emit wrap-up on late TASK_WRAPUP when wrap-up was already published', () => {
1988
+ const {emitTaskWrapup, publishWrapup} = createPublishAwareEmitTaskWrapupMock();
1989
+ const service = createActor(
1990
+ createTaskStateMachine(createConfig(), {actions: {emitTaskWrapup}})
1991
+ );
1992
+ service.start();
1993
+ const taskData = conferenceInProgressTaskData();
1994
+
1995
+ primeConferencing(service, taskData);
1996
+ service.send({
1997
+ type: TaskEvent.EXIT_CONFERENCE_SUCCESS,
1998
+ taskData: {...taskData, wrapUpRequired: true},
1999
+ });
2000
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2001
+ expect(service.getSnapshot().context.taskData?.wrapUpRequired).toBe(true);
2002
+
2003
+ const emitCountAfterExit = publishWrapup.mock.calls.length;
2004
+ service.send({
2005
+ type: TaskEvent.TASK_WRAPUP,
2006
+ taskData: {...taskData, wrapUpRequired: true},
2007
+ });
2008
+
2009
+ expect(publishWrapup.mock.calls.length).toBe(emitCountAfterExit);
2010
+ });
2011
+
2012
+ it('emits wrap-up on late TASK_WRAPUP when wrap-up was not yet published', () => {
2013
+ const {emitTaskWrapup, publishWrapup} = createPublishAwareEmitTaskWrapupMock();
2014
+ const service = createActor(
2015
+ createTaskStateMachine(createConfig(), {actions: {emitTaskWrapup}})
2016
+ );
2017
+ service.start();
2018
+ const taskData = createTaskData({
2019
+ wrapUpRequired: false,
2020
+ interaction: {
2021
+ owner: 'agent-1',
2022
+ state: 'connected',
2023
+ mainInteractionId: 'interaction-1',
2024
+ interactionId: 'interaction-1',
2025
+ participants: {
2026
+ 'agent-1': {
2027
+ id: 'agent-1',
2028
+ pType: 'Agent',
2029
+ type: 'Agent',
2030
+ hasJoined: true,
2031
+ hasLeft: false,
2032
+ },
2033
+ },
2034
+ } as any,
2035
+ });
2036
+
2037
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2038
+ service.send({type: TaskEvent.ASSIGN, taskData});
2039
+ service.send({type: TaskEvent.TASK_WRAPUP, taskData});
2040
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2041
+ expect(service.getSnapshot().context.taskData?.wrapUpRequired).not.toBe(true);
2042
+ expect(publishWrapup).not.toHaveBeenCalled();
2043
+
2044
+ service.send({
2045
+ type: TaskEvent.TASK_WRAPUP,
2046
+ taskData: {...taskData, wrapUpRequired: true},
2047
+ });
2048
+
2049
+ expect(publishWrapup).toHaveBeenCalledTimes(1);
2050
+ });
2051
+
2052
+ it('terminates when EXIT_CONFERENCE_SUCCESS is received in conferencing and wrapup is not required', () => {
2053
+ const service = startMachine();
2054
+ const taskData = createTaskData({
2055
+ isConsulted: true,
2056
+ wrapUpRequired: false,
2057
+ interaction: {
2058
+ owner: 'other-agent',
2059
+ state: 'conference',
2060
+ mainInteractionId: 'interaction-1',
2061
+ interactionId: 'interaction-1',
2062
+ participants: {
2063
+ 'agent-1': {
2064
+ id: 'agent-1',
2065
+ pType: 'Agent',
2066
+ type: 'Agent',
2067
+ hasJoined: true,
2068
+ hasLeft: false,
2069
+ isInPredial: false,
2070
+ isWrapUp: false,
2071
+ },
2072
+ c1: {
2073
+ id: 'c1',
2074
+ pType: 'Customer',
2075
+ type: 'Customer',
2076
+ hasJoined: true,
2077
+ hasLeft: false,
2078
+ isInPredial: false,
2079
+ },
2080
+ },
2081
+ media: {
2082
+ 'interaction-1': {
2083
+ mediaResourceId: 'interaction-1',
2084
+ mediaType: 'telephony',
2085
+ mediaMgr: 'mm',
2086
+ participants: ['agent-1', 'c1'],
2087
+ mType: 'mainCall',
2088
+ isHold: false,
2089
+ holdTimestamp: null,
2090
+ },
2091
+ },
2092
+ } as any,
2093
+ });
2094
+
2095
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2096
+ service.send({type: TaskEvent.ASSIGN, taskData});
2097
+ service.send({
2098
+ type: TaskEvent.CONSULT,
2099
+ destination: 'agent-42',
2100
+ destinationType: 'agent',
2101
+ });
2102
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
2103
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
2104
+ service.send({type: TaskEvent.CONFERENCE_START, taskData});
2105
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2106
+
2107
+ service.send({type: TaskEvent.EXIT_CONFERENCE_SUCCESS, taskData});
2108
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2109
+ });
2110
+
2111
+ it('enters WRAPPING_UP on CONFERENCE_END when wrapUpRequired is stamped', () => {
2112
+ const service = startMachine();
2113
+ const taskData = createTaskData({
2114
+ wrapUpRequired: true,
2115
+ interaction: {
2116
+ owner: 'agent-1',
2117
+ state: 'conference',
2118
+ mainInteractionId: 'interaction-1',
2119
+ interactionId: 'interaction-1',
2120
+ participants: {
2121
+ 'agent-1': {
2122
+ id: 'agent-1',
2123
+ pType: 'Agent',
2124
+ type: 'Agent',
2125
+ isWrapUp: true,
2126
+ hasJoined: true,
2127
+ hasLeft: true,
2128
+ },
2129
+ },
2130
+ } as any,
2131
+ });
2132
+
2133
+ primeConferencing(service, taskData);
2134
+ service.send({type: TaskEvent.CONFERENCE_END, taskData});
2135
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2136
+ });
2137
+
2138
+ it('emits wrap-up once on CONFERENCE_END when wrap-up is required', () => {
2139
+ const {emitTaskWrapup, publishWrapup} = createPublishAwareEmitTaskWrapupMock();
2140
+ const service = createActor(
2141
+ createTaskStateMachine(createConfig(), {actions: {emitTaskWrapup}})
2142
+ );
2143
+ service.start();
2144
+ const taskData = createTaskData({
2145
+ wrapUpRequired: true,
2146
+ interaction: {
2147
+ owner: 'agent-1',
2148
+ state: 'conference',
2149
+ mainInteractionId: 'interaction-1',
2150
+ interactionId: 'interaction-1',
2151
+ participants: {
2152
+ 'agent-1': {
2153
+ id: 'agent-1',
2154
+ pType: 'Agent',
2155
+ type: 'Agent',
2156
+ isWrapUp: true,
2157
+ hasJoined: true,
2158
+ hasLeft: true,
2159
+ },
2160
+ },
2161
+ } as any,
2162
+ });
2163
+
2164
+ primeConferencing(service, taskData);
2165
+ service.send({type: TaskEvent.CONFERENCE_END, taskData});
2166
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2167
+ expect(publishWrapup).toHaveBeenCalledTimes(1);
2168
+ });
2169
+
2170
+ it('emits wrap-up once on TASK_WRAPUP from CONSULTING when wrap-up is required', () => {
2171
+ const {emitTaskWrapup, publishWrapup} = createPublishAwareEmitTaskWrapupMock();
2172
+ const service = createActor(
2173
+ createTaskStateMachine(createConfig(), {actions: {emitTaskWrapup}})
2174
+ );
2175
+ service.start();
2176
+ const taskData = createTaskData({
2177
+ interaction: {owner: 'agent-1', state: 'connected'} as any,
2178
+ });
2179
+
2180
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2181
+ service.send({type: TaskEvent.ASSIGN, taskData});
2182
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-2', destinationType: 'agent'});
2183
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
2184
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
2185
+
2186
+ service.send({
2187
+ type: TaskEvent.TASK_WRAPUP,
2188
+ taskData: {...taskData, wrapUpRequired: true},
2189
+ });
2190
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2191
+ expect(publishWrapup).toHaveBeenCalledTimes(1);
2192
+ });
2193
+
2194
+ it('emits wrap-up once on TASK_WRAPUP from CONFERENCING when wrap-up is required', () => {
2195
+ const {emitTaskWrapup, publishWrapup} = createPublishAwareEmitTaskWrapupMock();
2196
+ const service = createActor(
2197
+ createTaskStateMachine(createConfig(), {actions: {emitTaskWrapup}})
2198
+ );
2199
+ service.start();
2200
+ const taskData = createTaskData({
2201
+ interaction: {
2202
+ owner: 'agent-1',
2203
+ state: 'conference',
2204
+ mainInteractionId: 'interaction-1',
2205
+ interactionId: 'interaction-1',
2206
+ } as any,
2207
+ });
2208
+
2209
+ primeConferencing(service, taskData);
2210
+ service.send({
2211
+ type: TaskEvent.TASK_WRAPUP,
2212
+ taskData: {...taskData, wrapUpRequired: true},
2213
+ });
2214
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2215
+ expect(publishWrapup).toHaveBeenCalledTimes(1);
2216
+ });
2217
+
2218
+ it('enters WRAPPING_UP on PARTICIPANT_LEAVE when this agent left and wrap-up is required', () => {
2219
+ const service = startMachine();
2220
+ const conferenceData = createTaskData({
2221
+ interaction: {
2222
+ owner: 'agent-1',
2223
+ state: 'conference',
2224
+ mainInteractionId: 'interaction-1',
2225
+ interactionId: 'interaction-1',
2226
+ participants: {
2227
+ 'agent-1': {id: 'agent-1', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2228
+ 'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2229
+ },
2230
+ } as any,
2231
+ });
2232
+ const leaveData = createTaskData({
2233
+ wrapUpRequired: true,
2234
+ participantId: 'agent-1',
2235
+ interaction: {
2236
+ owner: 'agent-1',
2237
+ state: 'conference',
2238
+ mainInteractionId: 'interaction-1',
2239
+ interactionId: 'interaction-1',
2240
+ participants: {
2241
+ 'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2242
+ },
2243
+ } as any,
2244
+ });
2245
+
2246
+ primeConferencing(service, conferenceData);
2247
+ service.send({
2248
+ type: TaskEvent.PARTICIPANT_LEAVE,
2249
+ participantId: 'agent-1',
2250
+ taskData: leaveData,
2251
+ });
2252
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2253
+ });
2254
+
2255
+ it('emits wrap-up once on PARTICIPANT_LEAVE when wrap-up is required', () => {
2256
+ const emitTaskWrapup = jest.fn();
2257
+ const service = createActor(
2258
+ createTaskStateMachine(createConfig(), {actions: {emitTaskWrapup}})
2259
+ );
2260
+ service.start();
2261
+ const conferenceData = createTaskData({
2262
+ interaction: {
2263
+ owner: 'agent-1',
2264
+ state: 'conference',
2265
+ mainInteractionId: 'interaction-1',
2266
+ interactionId: 'interaction-1',
2267
+ participants: {
2268
+ 'agent-1': {id: 'agent-1', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2269
+ 'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2270
+ },
2271
+ } as any,
2272
+ });
2273
+ const leaveData = createTaskData({
2274
+ wrapUpRequired: true,
2275
+ participantId: 'agent-1',
2276
+ interaction: {
2277
+ owner: 'agent-1',
2278
+ state: 'conference',
2279
+ mainInteractionId: 'interaction-1',
2280
+ interactionId: 'interaction-1',
2281
+ participants: {
2282
+ 'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2283
+ },
2284
+ } as any,
2285
+ });
2286
+
2287
+ primeConferencing(service, conferenceData);
2288
+ service.send({
2289
+ type: TaskEvent.PARTICIPANT_LEAVE,
2290
+ participantId: 'agent-1',
2291
+ taskData: leaveData,
2292
+ });
2293
+
2294
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2295
+ expect(emitTaskWrapup).toHaveBeenCalledTimes(1);
2296
+ });
2297
+
2298
+ it('does not wrap remaining agent on PARTICIPANT_LEAVE', () => {
2299
+ const service = startMachine();
2300
+ const conferenceData = createTaskData({
2301
+ interaction: {
2302
+ owner: 'agent-1',
2303
+ state: 'conference',
2304
+ mainInteractionId: 'interaction-1',
2305
+ interactionId: 'interaction-1',
2306
+ participants: {
2307
+ 'agent-1': {id: 'agent-1', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2308
+ 'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2309
+ c1: {id: 'c1', pType: 'Customer', type: 'Customer', hasJoined: true, hasLeft: false},
2310
+ },
2311
+ media: {
2312
+ 'interaction-1': {
2313
+ mediaResourceId: 'interaction-1',
2314
+ mediaType: 'telephony',
2315
+ mediaMgr: 'mm',
2316
+ participants: ['agent-1', 'agent-2', 'c1'],
2317
+ mType: 'mainCall',
2318
+ isHold: false,
2319
+ holdTimestamp: null,
2320
+ },
2321
+ },
2322
+ } as any,
2323
+ });
2324
+ const otherLeft = createTaskData({
2325
+ wrapUpRequired: false,
2326
+ participantId: 'agent-2',
2327
+ interaction: {
2328
+ owner: 'agent-1',
2329
+ state: 'conference',
2330
+ mainInteractionId: 'interaction-1',
2331
+ interactionId: 'interaction-1',
2332
+ participants: {
2333
+ 'agent-1': {id: 'agent-1', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2334
+ c1: {id: 'c1', pType: 'Customer', type: 'Customer', hasJoined: true, hasLeft: false},
2335
+ },
2336
+ media: {
2337
+ 'interaction-1': {
2338
+ mediaResourceId: 'interaction-1',
2339
+ mediaType: 'telephony',
2340
+ mediaMgr: 'mm',
2341
+ participants: ['agent-1', 'c1'],
2342
+ mType: 'mainCall',
2343
+ isHold: false,
2344
+ holdTimestamp: null,
2345
+ },
2346
+ },
2347
+ } as any,
2348
+ });
2349
+
2350
+ primeConferencing(service, conferenceData);
2351
+ service.send({
2352
+ type: TaskEvent.PARTICIPANT_LEAVE,
2353
+ participantId: 'agent-2',
2354
+ taskData: otherLeft,
2355
+ });
2356
+ expect(service.getSnapshot().value).not.toBe(TaskState.WRAPPING_UP);
2357
+ });
2358
+
2359
+ it('downgrades to HELD on HOLD_SUCCESS when no other agents remain on task', () => {
2360
+ const service = startMachine();
2361
+ const conferenceTaskData = createSingleAgentConferenceTaskData('conference');
2362
+ const heldTaskData = createSingleAgentConferenceTaskData('hold', true);
2363
+
2364
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
1655
2365
  service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
1656
2366
  service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
1657
2367
  expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
@@ -1684,6 +2394,47 @@ describe('Task state machine', () => {
1684
2394
  expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
1685
2395
  });
1686
2396
 
2397
+ it('downgrades to CONNECTED on PARTICIPANT_LEAVE when the current agent remains', () => {
2398
+ const service = startMachine();
2399
+ const conferenceTaskData = createSingleAgentConferenceTaskData('conference');
2400
+ const connectedTaskData = createSingleAgentConferenceTaskData('connected');
2401
+
2402
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
2403
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
2404
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
2405
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2406
+
2407
+ service.send({
2408
+ type: TaskEvent.PARTICIPANT_LEAVE,
2409
+ participantId: 'agent-2',
2410
+ taskData: connectedTaskData,
2411
+ });
2412
+
2413
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
2414
+ });
2415
+
2416
+ it('terminates instead of downgrading when PARTICIPANT_LEAVE identifies the current agent', () => {
2417
+ const service = startMachine();
2418
+ const conferenceTaskData = createSingleAgentConferenceTaskData('conference');
2419
+ const connectedTaskData = {
2420
+ ...createSingleAgentConferenceTaskData('connected'),
2421
+ wrapUpRequired: false,
2422
+ };
2423
+
2424
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
2425
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
2426
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
2427
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2428
+
2429
+ service.send({
2430
+ type: TaskEvent.PARTICIPANT_LEAVE,
2431
+ participantId: 'agent-1',
2432
+ taskData: connectedTaskData,
2433
+ });
2434
+
2435
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2436
+ });
2437
+
1687
2438
  it('returns to CONNECTED when CTQ cancel arrives before queue connects', () => {
1688
2439
  const service = startMachine();
1689
2440
  const taskData = createTaskData();
@@ -1798,13 +2549,21 @@ describe('Task state machine', () => {
1798
2549
  owner: 'agent-1',
1799
2550
  } as any,
1800
2551
  });
1801
- service.send({type: TaskEvent.CONSULT_END, taskData: terminatedTaskData});
1802
- expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
1803
- });
1804
-
1805
- it('transitions to CONNECTED on CONSULT_END without isTerminated during CONF_INITIATING', () => {
1806
- const service = startMachine();
1807
- const taskData = createTaskData({consultingAgentId: 'agent-1'});
2552
+ service.send({type: TaskEvent.CONSULT_END, taskData: terminatedTaskData});
2553
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2554
+ });
2555
+
2556
+ it('transitions to CONNECTED on CONSULT_END without isTerminated during CONF_INITIATING', () => {
2557
+ const service = startMachine();
2558
+ const taskData = createTaskData({
2559
+ consultingAgentId: 'agent-1',
2560
+ interaction: {
2561
+ participants: {
2562
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
2563
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
2564
+ },
2565
+ } as any,
2566
+ });
1808
2567
 
1809
2568
  service.send({type: TaskEvent.TASK_INCOMING, taskData});
1810
2569
  service.send({type: TaskEvent.ASSIGN, taskData});
@@ -1820,10 +2579,8 @@ describe('Task state machine', () => {
1820
2579
  service.send({type: TaskEvent.CONSULT_END, taskData});
1821
2580
  expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
1822
2581
  });
1823
-
1824
2582
  });
1825
2583
 
1826
-
1827
2584
  describe('CONFERENCING state CONSULT_FAILED ANOTHER_CONSULT_IN_PROGRESS (CAI-8329)', () => {
1828
2585
  it('keeps hideBlindTransfer flag and hides transfer after secondary consult rejected from Agent Desktop', () => {
1829
2586
  const service = startMachine();
@@ -1848,7 +2605,12 @@ describe('Task state machine', () => {
1848
2605
  consultState: 'conferencing',
1849
2606
  },
1850
2607
  '+13159998059': {id: '+13159998059', pType: 'EP-DN', hasLeft: false},
1851
- '+14696762938': {id: '+14696762938', pType: 'Customer', hasJoined: true, hasLeft: false},
2608
+ '+14696762938': {
2609
+ id: '+14696762938',
2610
+ pType: 'Customer',
2611
+ hasJoined: true,
2612
+ hasLeft: false,
2613
+ },
1852
2614
  },
1853
2615
  media: {
1854
2616
  'interaction-1': {
@@ -1902,7 +2664,12 @@ describe('Task state machine', () => {
1902
2664
  consultState: 'conferencing',
1903
2665
  },
1904
2666
  '+13159998059': {id: '+13159998059', pType: 'EP-DN', hasLeft: false},
1905
- '+14696762938': {id: '+14696762938', pType: 'Customer', hasJoined: true, hasLeft: false},
2667
+ '+14696762938': {
2668
+ id: '+14696762938',
2669
+ pType: 'Customer',
2670
+ hasJoined: true,
2671
+ hasLeft: false,
2672
+ },
1906
2673
  },
1907
2674
  media: {
1908
2675
  'interaction-1': {
@@ -1920,12 +2687,19 @@ describe('Task state machine', () => {
1920
2687
  },
1921
2688
  } as any,
1922
2689
  });
1923
- service.send({type: TaskEvent.CONSULT_FAILED, taskData: consultFailedTaskData, reason: 'ANOTHER_CONSULT_IN_PROGRESS'});
2690
+ service.send({
2691
+ type: TaskEvent.CONSULT_FAILED,
2692
+ taskData: consultFailedTaskData,
2693
+ reason: 'ANOTHER_CONSULT_IN_PROGRESS',
2694
+ });
1924
2695
 
1925
2696
  const snapshot = service.getSnapshot();
1926
2697
  expect(snapshot.value).toBe(TaskState.CONFERENCING);
1927
2698
  expect(snapshot.context.hideBlindTransferForEpDnPendingMerge).toBe(true);
1928
- expect(snapshot.context.uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
2699
+ expect(snapshot.context.uiControls.main.transfer).toEqual({
2700
+ isVisible: false,
2701
+ isEnabled: false,
2702
+ });
1929
2703
  expect(snapshot.context.uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
1930
2704
  });
1931
2705
  });
@@ -2126,6 +2900,72 @@ describe('Task state machine', () => {
2126
2900
  service.send({type: TaskEvent.CONSULT_FAILED, taskData});
2127
2901
  expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2128
2902
  });
2903
+
2904
+ it('emits only consult-end when an unaccepted consulted offer is ended', () => {
2905
+ const emitTaskConsultEnd = jest.fn();
2906
+ const emitTaskEnd = jest.fn();
2907
+ const service = createActor(
2908
+ createTaskStateMachine(createConfig(), {
2909
+ actions: {emitTaskConsultEnd, emitTaskEnd},
2910
+ })
2911
+ );
2912
+ service.start();
2913
+ const taskData = createTaskData({isConsulted: true, consultingAgentId: 'agent-1'});
2914
+
2915
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2916
+ service.send({type: TaskEvent.CONSULT_END, taskData});
2917
+
2918
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2919
+ expect(emitTaskConsultEnd).toHaveBeenCalledTimes(1);
2920
+ expect(emitTaskEnd).not.toHaveBeenCalled();
2921
+ });
2922
+
2923
+ it('transitions to WRAPPING_UP on TASK_WRAPUP when wrapup is required', () => {
2924
+ const service = startMachine();
2925
+ const offerTaskData = createTaskData({
2926
+ interaction: {
2927
+ outboundType: 'OUTDIAL',
2928
+ } as any,
2929
+ });
2930
+
2931
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
2932
+ expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
2933
+
2934
+ const wrapupTaskData = createTaskData({
2935
+ agentId: 'agent-1',
2936
+ agentsPendingWrapUp: ['agent-1'],
2937
+ wrapUpRequired: true,
2938
+ interaction: {
2939
+ outboundType: 'OUTDIAL',
2940
+ state: 'wrapUp',
2941
+ isTerminated: true,
2942
+ } as any,
2943
+ });
2944
+ service.send({type: TaskEvent.TASK_WRAPUP, taskData: wrapupTaskData});
2945
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2946
+ });
2947
+
2948
+ it('emits wrap-up once on TASK_WRAPUP from OFFERED when wrapup is required', () => {
2949
+ const {emitTaskWrapup, publishWrapup} = createPublishAwareEmitTaskWrapupMock();
2950
+ const service = createActor(
2951
+ createTaskStateMachine(createConfig(), {actions: {emitTaskWrapup}})
2952
+ );
2953
+ service.start();
2954
+ const offerTaskData = createTaskData({
2955
+ interaction: {outboundType: 'OUTDIAL'} as any,
2956
+ });
2957
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
2958
+ expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
2959
+
2960
+ const wrapupTaskData = createTaskData({
2961
+ agentId: 'agent-1',
2962
+ wrapUpRequired: true,
2963
+ interaction: {outboundType: 'OUTDIAL', state: 'wrapUp', isTerminated: true} as any,
2964
+ });
2965
+ service.send({type: TaskEvent.TASK_WRAPUP, taskData: wrapupTaskData});
2966
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2967
+ expect(publishWrapup).toHaveBeenCalledTimes(1);
2968
+ });
2129
2969
  });
2130
2970
 
2131
2971
  describe('OUTBOUND_FAILED handling', () => {
@@ -2152,29 +2992,247 @@ describe('Task state machine', () => {
2152
2992
  } as any,
2153
2993
  });
2154
2994
 
2155
- service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
2156
- expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
2995
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
2996
+ expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
2997
+
2998
+ const failedTaskData = createTaskData({
2999
+ interaction: {
3000
+ outboundType: 'OUTDIAL',
3001
+ isTerminated: true,
3002
+ } as any,
3003
+ });
3004
+ service.send({
3005
+ type: TaskEvent.OUTBOUND_FAILED,
3006
+ taskData: failedTaskData,
3007
+ reason: 'CUSTOMER_BUSY',
3008
+ });
3009
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3010
+ });
3011
+
3012
+ it('transitions from OFFERED to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3013
+ const service = startMachine();
3014
+ const offerTaskData = createTaskData({
3015
+ interaction: {
3016
+ outboundType: 'OUTDIAL',
3017
+ } as any,
3018
+ });
3019
+
3020
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
3021
+ expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
3022
+
3023
+ const failedTaskData = createTaskData({
3024
+ agentId: 'agent-1',
3025
+ agentsPendingWrapUp: ['agent-1'],
3026
+ interaction: {
3027
+ outboundType: 'OUTDIAL',
3028
+ isTerminated: true,
3029
+ } as any,
3030
+ });
3031
+ service.send({
3032
+ type: TaskEvent.OUTBOUND_FAILED,
3033
+ taskData: failedTaskData,
3034
+ reason: 'CUSTOMER_BUSY',
3035
+ });
3036
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3037
+ });
3038
+
3039
+ it('transitions from CONNECTED to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3040
+ const service = startMachine();
3041
+ const offerTaskData = createTaskData({
3042
+ interaction: {
3043
+ outboundType: 'OUTDIAL',
3044
+ } as any,
3045
+ });
3046
+
3047
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
3048
+ service.send({type: TaskEvent.ASSIGN, taskData: offerTaskData});
3049
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
3050
+
3051
+ const failedTaskData = createTaskData({
3052
+ agentId: 'agent-1',
3053
+ agentsPendingWrapUp: ['agent-1'],
3054
+ interaction: {
3055
+ outboundType: 'OUTDIAL',
3056
+ isTerminated: true,
3057
+ } as any,
3058
+ });
3059
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
3060
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3061
+ });
3062
+
3063
+ it('transitions from HELD to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3064
+ const service = startMachine();
3065
+ const taskData = createTaskData({
3066
+ interaction: {
3067
+ outboundType: 'OUTDIAL',
3068
+ } as any,
3069
+ });
3070
+
3071
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3072
+ service.send({type: TaskEvent.ASSIGN, taskData});
3073
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
3074
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: taskData.mediaResourceId});
3075
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
3076
+
3077
+ const failedTaskData = createTaskData({
3078
+ agentId: 'agent-1',
3079
+ agentsPendingWrapUp: ['agent-1'],
3080
+ interaction: {
3081
+ outboundType: 'OUTDIAL',
3082
+ isTerminated: true,
3083
+ } as any,
3084
+ });
3085
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
3086
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3087
+ });
3088
+
3089
+ it('transitions from CONSULTING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3090
+ const service = startMachine();
3091
+ const taskData = createTaskData({
3092
+ interaction: {
3093
+ outboundType: 'OUTDIAL',
3094
+ } as any,
3095
+ });
3096
+
3097
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3098
+ service.send({type: TaskEvent.ASSIGN, taskData});
3099
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-42', destinationType: 'agent'});
3100
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
3101
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
3102
+
3103
+ const failedTaskData = createTaskData({
3104
+ agentId: 'agent-1',
3105
+ agentsPendingWrapUp: ['agent-1'],
3106
+ interaction: {
3107
+ outboundType: 'OUTDIAL',
3108
+ isTerminated: true,
3109
+ } as any,
3110
+ });
3111
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
3112
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3113
+ });
3114
+
3115
+ it('transitions from CONFERENCING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3116
+ const service = startMachine();
3117
+ const taskData = createTaskData({
3118
+ consultingAgentId: 'agent-1',
3119
+ interaction: {
3120
+ outboundType: 'OUTDIAL',
3121
+ } as any,
3122
+ });
3123
+
3124
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3125
+ service.send({type: TaskEvent.ASSIGN, taskData});
3126
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-42', destinationType: 'agent'});
3127
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
3128
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
3129
+ service.send({type: TaskEvent.CONFERENCE_START});
3130
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
3131
+
3132
+ const failedTaskData = createTaskData({
3133
+ agentId: 'agent-1',
3134
+ agentsPendingWrapUp: ['agent-1'],
3135
+ interaction: {
3136
+ outboundType: 'OUTDIAL',
3137
+ isTerminated: true,
3138
+ } as any,
3139
+ });
3140
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
3141
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3142
+ });
3143
+
3144
+ it('transitions from HOLD_INITIATING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3145
+ const service = startMachine();
3146
+ const taskData = createTaskData({
3147
+ interaction: {
3148
+ outboundType: 'OUTDIAL',
3149
+ } as any,
3150
+ });
3151
+
3152
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3153
+ service.send({type: TaskEvent.ASSIGN, taskData});
3154
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
3155
+ expect(service.getSnapshot().value).toBe(TaskState.HOLD_INITIATING);
3156
+
3157
+ const failedTaskData = createTaskData({
3158
+ agentId: 'agent-1',
3159
+ agentsPendingWrapUp: ['agent-1'],
3160
+ interaction: {
3161
+ outboundType: 'OUTDIAL',
3162
+ isTerminated: true,
3163
+ } as any,
3164
+ });
3165
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
3166
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3167
+ });
3168
+
3169
+ it('transitions from RESUME_INITIATING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3170
+ const service = startMachine();
3171
+ const taskData = createTaskData({
3172
+ interaction: {
3173
+ outboundType: 'OUTDIAL',
3174
+ } as any,
3175
+ });
3176
+
3177
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3178
+ service.send({type: TaskEvent.ASSIGN, taskData});
3179
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
3180
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: taskData.mediaResourceId});
3181
+ service.send({type: TaskEvent.UNHOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
3182
+ expect(service.getSnapshot().value).toBe(TaskState.RESUME_INITIATING);
3183
+
3184
+ const failedTaskData = createTaskData({
3185
+ agentId: 'agent-1',
3186
+ agentsPendingWrapUp: ['agent-1'],
3187
+ interaction: {
3188
+ outboundType: 'OUTDIAL',
3189
+ isTerminated: true,
3190
+ } as any,
3191
+ });
3192
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
3193
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3194
+ });
3195
+
3196
+ it('transitions from CONSULT_INITIATING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3197
+ const service = startMachine();
3198
+ const taskData = createTaskData({
3199
+ interaction: {
3200
+ outboundType: 'OUTDIAL',
3201
+ } as any,
3202
+ });
3203
+
3204
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3205
+ service.send({type: TaskEvent.ASSIGN, taskData});
3206
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-42', destinationType: 'agent'});
3207
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
2157
3208
 
2158
3209
  const failedTaskData = createTaskData({
3210
+ agentId: 'agent-1',
3211
+ agentsPendingWrapUp: ['agent-1'],
2159
3212
  interaction: {
2160
3213
  outboundType: 'OUTDIAL',
2161
3214
  isTerminated: true,
2162
3215
  } as any,
2163
3216
  });
2164
- service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'CUSTOMER_BUSY'});
2165
- expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3217
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
3218
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2166
3219
  });
2167
3220
 
2168
- it('transitions from OFFERED to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3221
+ it('transitions from CONF_INITIATING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
2169
3222
  const service = startMachine();
2170
- const offerTaskData = createTaskData({
3223
+ const taskData = createTaskData({
3224
+ consultingAgentId: 'agent-1',
2171
3225
  interaction: {
2172
3226
  outboundType: 'OUTDIAL',
2173
3227
  } as any,
2174
3228
  });
2175
3229
 
2176
- service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
2177
- expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
3230
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3231
+ service.send({type: TaskEvent.ASSIGN, taskData});
3232
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-42', destinationType: 'agent'});
3233
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
3234
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
3235
+ expect(service.getSnapshot().value).toBe(TaskState.CONF_INITIATING);
2178
3236
 
2179
3237
  const failedTaskData = createTaskData({
2180
3238
  agentId: 'agent-1',
@@ -2184,16 +3242,14 @@ describe('Task state machine', () => {
2184
3242
  isTerminated: true,
2185
3243
  } as any,
2186
3244
  });
2187
- service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'CUSTOMER_BUSY'});
3245
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
2188
3246
  expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2189
3247
  });
2190
3248
  });
2191
3249
 
2192
3250
  describe('CONSULTING state TRANSFER_CONFERENCE_SUCCESS desktop-initiated (CAI-8329)', () => {
2193
3251
  const startAgent2Machine = () => {
2194
- const actor = createActor(
2195
- createTaskStateMachine({...createConfig(), agentId: 'agent-2'})
2196
- );
3252
+ const actor = createActor(createTaskStateMachine({...createConfig(), agentId: 'agent-2'}));
2197
3253
  actor.start();
2198
3254
 
2199
3255
  return actor;
@@ -2292,7 +3348,54 @@ describe('Task state machine', () => {
2292
3348
  } as any,
2293
3349
  });
2294
3350
 
2295
- const setupSecondaryAgentConsultingFromConference = (service: ReturnType<typeof startAgent2Machine>) => {
3351
+ const createSecondaryAgentRemovedTaskData = () =>
3352
+ createTaskData({
3353
+ agentId: 'agent-2',
3354
+ isConsulted: true,
3355
+ interactionId: 'interaction-1',
3356
+ mediaResourceId: 'interaction-1',
3357
+ consultMediaResourceId: 'consult-media-1',
3358
+ interaction: {
3359
+ state: 'conference',
3360
+ owner: 'agent-1',
3361
+ mainInteractionId: 'interaction-1',
3362
+ interactionId: 'interaction-1',
3363
+ participants: {
3364
+ 'agent-1': {
3365
+ id: 'agent-1',
3366
+ pType: 'Agent',
3367
+ hasLeft: false,
3368
+ consultState: 'conferencing',
3369
+ },
3370
+ 'agent-3': {
3371
+ id: 'agent-3',
3372
+ pType: 'Agent',
3373
+ hasLeft: false,
3374
+ isConsulted: true,
3375
+ consultState: 'consulting',
3376
+ },
3377
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
3378
+ },
3379
+ media: {
3380
+ 'arbitrary-main-media-key': {
3381
+ mediaResourceId: 'interaction-1',
3382
+ mType: 'mainCall',
3383
+ participants: ['agent-1', 'customer-1'],
3384
+ isHold: false,
3385
+ },
3386
+ 'consult-media-1': {
3387
+ mediaResourceId: 'consult-media-1',
3388
+ mType: 'consult',
3389
+ participants: ['agent-2', 'agent-3'],
3390
+ isHold: false,
3391
+ },
3392
+ },
3393
+ } as any,
3394
+ });
3395
+
3396
+ const setupSecondaryAgentConsultingFromConference = (
3397
+ service: ReturnType<typeof startAgent2Machine>
3398
+ ) => {
2296
3399
  const conferenceTaskData = createSecondaryAgentConferenceBaseTaskData();
2297
3400
  const consultingTaskData = createSecondaryAgentConsultingTaskData();
2298
3401
 
@@ -2339,7 +3442,10 @@ describe('Task state machine', () => {
2339
3442
  setupSecondaryAgentDesktopConsultingFromConference(service);
2340
3443
 
2341
3444
  const transferSuccessTaskData = createSecondaryAgentConsultingTaskData();
2342
- service.send({type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: transferSuccessTaskData});
3445
+ service.send({
3446
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3447
+ taskData: transferSuccessTaskData,
3448
+ });
2343
3449
 
2344
3450
  expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2345
3451
  });
@@ -2349,7 +3455,10 @@ describe('Task state machine', () => {
2349
3455
  setupSecondaryAgentConsultingFromConference(service);
2350
3456
 
2351
3457
  const transferSuccessTaskData = createSecondaryAgentConsultingTaskData();
2352
- service.send({type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: transferSuccessTaskData});
3458
+ service.send({
3459
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3460
+ taskData: transferSuccessTaskData,
3461
+ });
2353
3462
 
2354
3463
  const snapshot = service.getSnapshot();
2355
3464
  expect(snapshot.value).toBe(TaskState.TERMINATED);
@@ -2359,9 +3468,7 @@ describe('Task state machine', () => {
2359
3468
 
2360
3469
  it('moves consultee to CONFERENCING when primary agent conference-transfers', () => {
2361
3470
  const startAgent3Machine = () => {
2362
- const actor = createActor(
2363
- createTaskStateMachine({...createConfig(), agentId: 'agent-3'})
2364
- );
3471
+ const actor = createActor(createTaskStateMachine({...createConfig(), agentId: 'agent-3'}));
2365
3472
  actor.start();
2366
3473
 
2367
3474
  return actor;
@@ -2524,7 +3631,10 @@ describe('Task state machine', () => {
2524
3631
  expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
2525
3632
  expect(service.getSnapshot().context.consultInitiator).toBe(false);
2526
3633
 
2527
- service.send({type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: transferSuccessTaskData});
3634
+ service.send({
3635
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3636
+ taskData: transferSuccessTaskData,
3637
+ });
2528
3638
  expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2529
3639
  });
2530
3640
 
@@ -2582,7 +3692,10 @@ describe('Task state machine', () => {
2582
3692
  expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2583
3693
  expect(service.getSnapshot().context.consultInitiator).toBe(false);
2584
3694
 
2585
- service.send({type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: transferSuccessTaskData});
3695
+ service.send({
3696
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3697
+ taskData: transferSuccessTaskData,
3698
+ });
2586
3699
  expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2587
3700
  });
2588
3701
 
@@ -2592,11 +3705,87 @@ describe('Task state machine', () => {
2592
3705
 
2593
3706
  const transferSuccessTaskData = createSecondaryAgentConsultingTaskData();
2594
3707
  service.send({type: TaskEvent.TRANSFER_CONFERENCE});
2595
- service.send({type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: transferSuccessTaskData});
3708
+ service.send({
3709
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3710
+ taskData: transferSuccessTaskData,
3711
+ });
2596
3712
 
2597
3713
  expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2598
3714
  });
2599
3715
 
3716
+ it('emits wrap-up once on TRANSFER_CONFERENCE_SUCCESS from CONSULTING when wrap-up is required', () => {
3717
+ const {emitTaskWrapup, publishWrapup} = createPublishAwareEmitTaskWrapupMock();
3718
+ const service = createActor(
3719
+ createTaskStateMachine({...createConfig(), agentId: 'agent-1'}, {actions: {emitTaskWrapup}})
3720
+ );
3721
+ service.start();
3722
+ const taskData = createTaskData({
3723
+ agentId: 'agent-1',
3724
+ consultingAgentId: 'agent-1',
3725
+ interaction: {
3726
+ owner: 'agent-1',
3727
+ state: 'conference',
3728
+ mainInteractionId: 'interaction-1',
3729
+ interactionId: 'interaction-1',
3730
+ participants: {
3731
+ 'agent-1': {id: 'agent-1', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
3732
+ 'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
3733
+ },
3734
+ } as any,
3735
+ });
3736
+
3737
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3738
+ service.send({type: TaskEvent.ASSIGN, taskData});
3739
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-2', destinationType: 'agent'});
3740
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
3741
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
3742
+
3743
+ service.send({type: TaskEvent.TRANSFER_CONFERENCE});
3744
+ service.send({
3745
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3746
+ taskData: {...taskData, wrapUpRequired: true},
3747
+ });
3748
+
3749
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3750
+ expect(publishWrapup).toHaveBeenCalledTimes(1);
3751
+ });
3752
+
3753
+ it('emits wrap-up once on TRANSFER_CONFERENCE_SUCCESS from CONFERENCING when wrap-up is required', () => {
3754
+ const {emitTaskWrapup, publishWrapup} = createPublishAwareEmitTaskWrapupMock();
3755
+ const service = createActor(
3756
+ createTaskStateMachine({...createConfig(), agentId: 'agent-1'}, {actions: {emitTaskWrapup}})
3757
+ );
3758
+ service.start();
3759
+ const taskData = createTaskData({
3760
+ agentId: 'agent-1',
3761
+ interaction: {
3762
+ owner: 'agent-1',
3763
+ state: 'conference',
3764
+ mainInteractionId: 'interaction-1',
3765
+ interactionId: 'interaction-1',
3766
+ participants: {
3767
+ 'agent-1': {id: 'agent-1', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
3768
+ 'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
3769
+ },
3770
+ } as any,
3771
+ });
3772
+
3773
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3774
+ service.send({type: TaskEvent.ASSIGN, taskData});
3775
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
3776
+ service.send({type: TaskEvent.CONFERENCE_START, taskData});
3777
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
3778
+
3779
+ service.send({type: TaskEvent.TRANSFER_CONFERENCE});
3780
+ service.send({
3781
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3782
+ taskData: {...taskData, wrapUpRequired: true},
3783
+ });
3784
+
3785
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3786
+ expect(publishWrapup).toHaveBeenCalledTimes(1);
3787
+ });
3788
+
2600
3789
  it('terminates secondary agent on PARTICIPANT_LEAVE while in CONSULTING during conference', () => {
2601
3790
  const service = startAgent2Machine();
2602
3791
  setupSecondaryAgentDesktopConsultingFromConference(service);
@@ -2647,5 +3836,192 @@ describe('Task state machine', () => {
2647
3836
 
2648
3837
  expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2649
3838
  });
3839
+
3840
+ it('terminates secondary agent on PARTICIPANT_LEAVE while consult is initiating', () => {
3841
+ const service = startAgent2Machine();
3842
+ const conferenceTaskData = createSecondaryAgentConferenceBaseTaskData();
3843
+
3844
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
3845
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
3846
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
3847
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
3848
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
3849
+ expect(service.getSnapshot().context.taskData.interaction).toBe(
3850
+ conferenceTaskData.interaction
3851
+ );
3852
+ expect(service.getSnapshot().context.taskData.destAgentId).toBe('agent-3');
3853
+
3854
+ const participantLeaveEvent = {
3855
+ type: TaskEvent.PARTICIPANT_LEAVE,
3856
+ taskData: createSecondaryAgentRemovedTaskData(),
3857
+ } as const;
3858
+ expect(
3859
+ guards.didCurrentAgentLeaveMainInteraction({
3860
+ context: service.getSnapshot().context,
3861
+ event: participantLeaveEvent,
3862
+ })
3863
+ ).toBe(true);
3864
+
3865
+ service.send(participantLeaveEvent);
3866
+
3867
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3868
+ });
3869
+
3870
+ it('terminates secondary agent on CONSULT_END while consult is initiating', () => {
3871
+ const service = startAgent2Machine();
3872
+ const conferenceTaskData = createSecondaryAgentConferenceBaseTaskData();
3873
+
3874
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
3875
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
3876
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
3877
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
3878
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
3879
+
3880
+ const consultEndEvent = {
3881
+ type: TaskEvent.CONSULT_END,
3882
+ taskData: createSecondaryAgentRemovedTaskData(),
3883
+ } as const;
3884
+ expect(
3885
+ guards.didCurrentAgentLeaveMainInteraction({
3886
+ context: service.getSnapshot().context,
3887
+ event: consultEndEvent,
3888
+ })
3889
+ ).toBe(true);
3890
+
3891
+ service.send(consultEndEvent);
3892
+
3893
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3894
+ });
3895
+
3896
+ it('terminates a consulting secondary initiator when CONSULT_END removes it from mainCall', () => {
3897
+ const service = startAgent2Machine();
3898
+ setupSecondaryAgentConsultingFromConference(service);
3899
+
3900
+ service.send({
3901
+ type: TaskEvent.CONSULT_END,
3902
+ taskData: createSecondaryAgentRemovedTaskData(),
3903
+ });
3904
+
3905
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3906
+ expect(service.getSnapshot().context.consultInitiator).toBe(false);
3907
+ expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(false);
3908
+ });
3909
+
3910
+ it('does not infer departure from a partial CONSULT_END payload', () => {
3911
+ const service = startAgent2Machine();
3912
+ setupSecondaryAgentConsultingFromConference(service);
3913
+ const partialTaskData = {
3914
+ ...createTaskData({agentId: 'agent-2', interactionId: 'interaction-1'}),
3915
+ interaction: {
3916
+ state: 'conference',
3917
+ interactionId: 'interaction-1',
3918
+ mainInteractionId: 'interaction-1',
3919
+ },
3920
+ } as any;
3921
+
3922
+ const consultEndEvent = {type: TaskEvent.CONSULT_END, taskData: partialTaskData} as const;
3923
+ expect(service.getSnapshot().context.consultInitiator).toBe(true);
3924
+ expect(
3925
+ guards.didCurrentAgentLeaveMainInteraction({
3926
+ context: service.getSnapshot().context,
3927
+ event: consultEndEvent,
3928
+ })
3929
+ ).toBe(false);
3930
+
3931
+ service.send(consultEndEvent);
3932
+
3933
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
3934
+ });
3935
+
3936
+ it('emits consult-end and task-end when AgentConsultEnded terminates the consulted agent', () => {
3937
+ const emitTaskConsultEnd = jest.fn();
3938
+ const emitTaskEnd = jest.fn();
3939
+ const cleanupResources = jest.fn();
3940
+ const service = createActor(
3941
+ createTaskStateMachine(
3942
+ {...createConfig(), agentId: 'agent-3'},
3943
+ {
3944
+ actions: {emitTaskConsultEnd, emitTaskEnd, cleanupResources},
3945
+ }
3946
+ )
3947
+ );
3948
+ service.start();
3949
+
3950
+ const consultingTaskData = createTaskData({
3951
+ agentId: 'agent-3',
3952
+ isConsulted: true,
3953
+ interactionId: 'consult-child',
3954
+ interaction: {
3955
+ state: 'consulting',
3956
+ owner: 'agent-1',
3957
+ interactionId: 'consult-child',
3958
+ mainInteractionId: 'interaction-1',
3959
+ participants: {
3960
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
3961
+ 'agent-2': {
3962
+ id: 'agent-2',
3963
+ pType: 'Agent',
3964
+ hasLeft: false,
3965
+ consultState: 'consulting',
3966
+ },
3967
+ 'agent-3': {
3968
+ id: 'agent-3',
3969
+ pType: 'Agent',
3970
+ hasLeft: false,
3971
+ isConsulted: true,
3972
+ consultState: 'consulting',
3973
+ },
3974
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
3975
+ },
3976
+ media: {
3977
+ 'interaction-1': {
3978
+ mediaResourceId: 'interaction-1',
3979
+ mType: 'mainCall',
3980
+ participants: ['agent-1', 'agent-2', 'customer-1'],
3981
+ isHold: true,
3982
+ },
3983
+ 'consult-child': {
3984
+ mediaResourceId: 'consult-child',
3985
+ mType: 'consult',
3986
+ participants: ['agent-2', 'agent-3'],
3987
+ isHold: false,
3988
+ },
3989
+ },
3990
+ } as any,
3991
+ });
3992
+ service.send({type: TaskEvent.HYDRATE, taskData: consultingTaskData});
3993
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
3994
+
3995
+ const consultEndedTaskData = createTaskData({
3996
+ ...consultingTaskData,
3997
+ type: 'AgentConsultEnded' as any,
3998
+ interaction: {
3999
+ ...consultingTaskData.interaction,
4000
+ state: 'conference',
4001
+ participants: {
4002
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
4003
+ 'agent-2': {id: 'agent-2', pType: 'Agent', hasLeft: false},
4004
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
4005
+ },
4006
+ media: {
4007
+ 'interaction-1': {
4008
+ mediaResourceId: 'interaction-1',
4009
+ mType: 'mainCall',
4010
+ participants: ['agent-1', 'agent-2', 'customer-1'],
4011
+ isHold: false,
4012
+ },
4013
+ },
4014
+ } as any,
4015
+ });
4016
+
4017
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
4018
+
4019
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
4020
+ expect(emitTaskConsultEnd).toHaveBeenCalledTimes(1);
4021
+ expect(emitTaskEnd).toHaveBeenCalledTimes(1);
4022
+ expect(cleanupResources).toHaveBeenCalledTimes(1);
4023
+ expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(false);
4024
+ expect(service.getSnapshot().context.consultInitiator).toBe(false);
4025
+ });
2650
4026
  });
2651
4027
  });