@webex/contact-center 3.12.0-next.13 → 3.12.0-next.130
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.sdd/manifest.json +884 -0
- package/AGENTS.md +94 -0
- package/ai-docs/ARCHITECTURE.md +168 -0
- package/ai-docs/CONTRACTS.md +53 -0
- package/ai-docs/GETTING_STARTED.md +168 -0
- package/ai-docs/GLOSSARY.md +43 -0
- package/ai-docs/README.md +138 -0
- package/ai-docs/REVIEW_CHECKLIST.md +41 -0
- package/ai-docs/RULES.md +444 -0
- package/ai-docs/SECURITY.md +52 -0
- package/ai-docs/SERVICE_STATE.md +52 -0
- package/ai-docs/SPEC_INDEX.md +65 -0
- package/ai-docs/adr/0001-spec-source-policy.md +55 -0
- package/ai-docs/adr/README.md +8 -0
- package/ai-docs/adr/_adr-template.md +31 -0
- package/ai-docs/contact-center-spec.md +382 -0
- package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
- package/ai-docs/features/contact-owner-change-refactor.md +78 -0
- package/ai-docs/features/generated-spec-conformance-fidelity-remediation/spec/feature-spec.md +117 -0
- package/ai-docs/features/residual-warning-coverage-completion/spec/feature-spec.md +203 -0
- package/ai-docs/features/validator-code-fidelity-drift-fix/spec/feature-spec.md +315 -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 +971 -42
- package/dist/cc.js.map +1 -1
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +31 -3
- package/dist/constants.js.map +1 -1
- package/dist/index.js +45 -15
- package/dist/index.js.map +1 -1
- package/dist/metrics/MetricsManager.js +16 -0
- package/dist/metrics/MetricsManager.js.map +1 -1
- package/dist/metrics/behavioral-events.js +205 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +48 -4
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/AddressBook.js +18 -15
- package/dist/services/AddressBook.js.map +1 -1
- package/dist/services/AnswerCallOnWebexService.js +202 -0
- package/dist/services/AnswerCallOnWebexService.js.map +1 -0
- package/dist/services/ApiAiAssistant.js +269 -9
- package/dist/services/ApiAiAssistant.js.map +1 -1
- package/dist/services/EntryPoint.js +46 -68
- package/dist/services/EntryPoint.js.map +1 -1
- package/dist/services/Queue.js +27 -22
- package/dist/services/Queue.js.map +1 -1
- package/dist/services/UserPreference.js +427 -0
- package/dist/services/UserPreference.js.map +1 -0
- package/dist/services/WebexCrossClientService.js +222 -0
- package/dist/services/WebexCrossClientService.js.map +1 -0
- package/dist/services/WxAppTelephonyMercurySync.js +108 -0
- package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
- package/dist/services/config/Util.js +11 -4
- package/dist/services/config/Util.js.map +1 -1
- package/dist/services/config/constants.js +41 -7
- package/dist/services/config/constants.js.map +1 -1
- package/dist/services/config/index.js +51 -0
- package/dist/services/config/index.js.map +1 -1
- package/dist/services/config/types.js +57 -12
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/core/Err.js.map +1 -1
- package/dist/services/core/Utils.js +111 -32
- package/dist/services/core/Utils.js.map +1 -1
- package/dist/services/core/WebexRequest.js +6 -2
- package/dist/services/core/WebexRequest.js.map +1 -1
- package/dist/services/core/aqm-reqs.js +28 -14
- package/dist/services/core/aqm-reqs.js.map +1 -1
- package/dist/services/core/types.js.map +1 -1
- package/dist/services/core/websocket/WebSocketManager.js +2 -1
- package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
- package/dist/services/core/websocket/types.js.map +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/index.js.map +1 -1
- package/dist/services/task/Task.js +754 -0
- package/dist/services/task/Task.js.map +1 -0
- package/dist/services/task/TaskFactory.js +49 -0
- package/dist/services/task/TaskFactory.js.map +1 -0
- package/dist/services/task/TaskManager.js +1172 -521
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/TaskUtils.js +211 -27
- package/dist/services/task/TaskUtils.js.map +1 -1
- package/dist/services/task/WebexCallingUtils.js +70 -0
- package/dist/services/task/WebexCallingUtils.js.map +1 -0
- package/dist/services/task/constants.js +12 -2
- package/dist/services/task/constants.js.map +1 -1
- package/dist/services/task/contact.js +29 -0
- package/dist/services/task/contact.js.map +1 -1
- package/dist/services/task/dialer.js +78 -0
- package/dist/services/task/dialer.js.map +1 -1
- package/dist/services/task/digital/Digital.js +78 -0
- package/dist/services/task/digital/Digital.js.map +1 -0
- package/dist/services/task/state-machine/TaskStateMachine.js +982 -0
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
- package/dist/services/task/state-machine/actions.js +572 -0
- package/dist/services/task/state-machine/actions.js.map +1 -0
- package/dist/services/task/state-machine/constants.js +161 -0
- package/dist/services/task/state-machine/constants.js.map +1 -0
- package/dist/services/task/state-machine/guards.js +379 -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 +703 -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 +228 -5
- package/dist/services/task/types.js.map +1 -1
- package/dist/services/task/voice/Voice.js +1485 -0
- package/dist/services/task/voice/Voice.js.map +1 -0
- package/dist/services/task/voice/WebRTC.js +152 -0
- package/dist/services/task/voice/WebRTC.js.map +1 -0
- package/dist/services/task/voice/wxAppVoiceMethods.js +354 -0
- package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
- package/dist/services/wxAppDiagnosticLogging.js +122 -0
- package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
- package/dist/services/wxAppTelephonyUtils.js +19 -0
- package/dist/services/wxAppTelephonyUtils.js.map +1 -0
- package/dist/types/cc.d.ts +193 -6
- package/dist/types/config.d.ts +13 -0
- package/dist/types/constants.d.ts +29 -1
- package/dist/types/index.d.ts +35 -8
- package/dist/types/metrics/MetricsManager.d.ts +6 -0
- package/dist/types/metrics/constants.d.ts +43 -1
- package/dist/types/services/AddressBook.d.ts +2 -1
- package/dist/types/services/AnswerCallOnWebexService.d.ts +38 -0
- package/dist/types/services/ApiAiAssistant.d.ts +56 -5
- package/dist/types/services/EntryPoint.d.ts +7 -5
- package/dist/types/services/Queue.d.ts +5 -3
- package/dist/types/services/UserPreference.d.ts +118 -0
- package/dist/types/services/WebexCrossClientService.d.ts +32 -0
- package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
- package/dist/types/services/config/constants.d.ts +34 -5
- package/dist/types/services/config/types.d.ts +197 -12
- package/dist/types/services/core/Err.d.ts +6 -0
- package/dist/types/services/core/Utils.d.ts +35 -13
- package/dist/types/services/core/WebexRequest.d.ts +1 -0
- package/dist/types/services/core/types.d.ts +2 -0
- package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
- package/dist/types/services/core/websocket/types.d.ts +1 -1
- package/dist/types/services/index.d.ts +1 -1
- package/dist/types/services/task/Task.d.ts +175 -0
- package/dist/types/services/task/TaskFactory.d.ts +13 -0
- package/dist/types/services/task/TaskUtils.d.ts +48 -2
- package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
- package/dist/types/services/task/constants.d.ts +10 -0
- package/dist/types/services/task/contact.d.ts +4 -0
- package/dist/types/services/task/dialer.d.ts +30 -0
- package/dist/types/services/task/digital/Digital.d.ts +22 -0
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1442 -0
- package/dist/types/services/task/state-machine/actions.d.ts +10 -0
- package/dist/types/services/task/state-machine/constants.d.ts +107 -0
- package/dist/types/services/task/state-machine/guards.d.ts +103 -0
- package/dist/types/services/task/state-machine/index.d.ts +13 -0
- package/dist/types/services/task/state-machine/types.d.ts +277 -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 +680 -66
- package/dist/types/services/task/voice/Voice.d.ts +227 -0
- package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
- package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +55 -0
- package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -0
- package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
- package/dist/types/types.d.ts +205 -9
- package/dist/types/utils/PageCache.d.ts +20 -3
- package/dist/types/webex.d.ts +1 -0
- package/dist/types.js +162 -4
- package/dist/types.js.map +1 -1
- package/dist/utils/PageCache.js +19 -5
- package/dist/utils/PageCache.js.map +1 -1
- package/dist/webex.js +22 -2
- package/dist/webex.js.map +1 -1
- package/package.json +16 -12
- package/src/cc.ts +1289 -112
- package/src/config.ts +13 -0
- package/src/constants.ts +29 -1
- package/src/index.ts +36 -5
- package/src/metrics/MetricsManager.ts +16 -0
- package/src/metrics/ai-docs/AGENTS.md +350 -0
- package/src/metrics/ai-docs/ARCHITECTURE.md +338 -0
- package/src/metrics/ai-docs/metrics-spec.md +921 -0
- package/src/metrics/behavioral-events.ts +212 -0
- package/src/metrics/constants.ts +52 -4
- package/src/services/AddressBook.ts +17 -6
- package/src/services/AnswerCallOnWebexService.ts +239 -0
- package/src/services/ApiAiAssistant.ts +356 -9
- package/src/services/EntryPoint.ts +59 -60
- package/src/services/Queue.ts +29 -12
- package/src/services/UserPreference.ts +509 -0
- package/src/services/WebexCrossClientService.ts +293 -0
- package/src/services/WxAppTelephonyMercurySync.ts +142 -0
- package/src/services/agent/ai-docs/AGENTS.md +240 -0
- package/src/services/agent/ai-docs/ARCHITECTURE.md +304 -0
- package/src/services/agent/ai-docs/agent-spec.md +505 -0
- package/src/services/ai-docs/AGENTS.md +386 -0
- package/src/services/ai-docs/services-spec.md +505 -0
- package/src/services/config/Util.ts +12 -3
- package/src/services/config/ai-docs/AGENTS.md +255 -0
- package/src/services/config/ai-docs/ARCHITECTURE.md +426 -0
- package/src/services/config/ai-docs/config-spec.md +686 -0
- package/src/services/config/constants.ts +46 -6
- package/src/services/config/index.ts +65 -0
- package/src/services/config/types.ts +197 -13
- package/src/services/core/Err.ts +3 -0
- package/src/services/core/Utils.ts +127 -37
- package/src/services/core/WebexRequest.ts +3 -1
- package/src/services/core/ai-docs/AGENTS.md +381 -0
- package/src/services/core/ai-docs/ARCHITECTURE.md +698 -0
- package/src/services/core/ai-docs/core-spec.md +787 -0
- package/src/services/core/aqm-reqs.ts +30 -15
- package/src/services/core/types.ts +2 -0
- package/src/services/core/websocket/WebSocketManager.ts +2 -0
- package/src/services/core/websocket/types.ts +1 -1
- package/src/services/index.ts +1 -1
- package/src/services/task/Task.ts +908 -0
- package/src/services/task/TaskFactory.ts +60 -0
- package/src/services/task/TaskManager.ts +1419 -608
- package/src/services/task/TaskUtils.ts +303 -25
- package/src/services/task/WebexCallingUtils.ts +136 -0
- package/src/services/task/ai-docs/AGENTS.md +457 -0
- package/src/services/task/ai-docs/ARCHITECTURE.md +595 -0
- package/src/services/task/ai-docs/task-spec.md +1474 -0
- package/src/services/task/constants.ts +10 -0
- package/src/services/task/contact.ts +30 -0
- package/src/services/task/dialer.ts +80 -0
- package/src/services/task/digital/Digital.ts +97 -0
- package/src/services/task/state-machine/TaskStateMachine.ts +1320 -0
- package/src/services/task/state-machine/actions.ts +741 -0
- package/src/services/task/state-machine/ai-docs/AGENTS.md +460 -0
- package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1156 -0
- package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +2247 -0
- package/src/services/task/state-machine/constants.ts +172 -0
- package/src/services/task/state-machine/guards.ts +442 -0
- package/src/services/task/state-machine/index.ts +28 -0
- package/src/services/task/state-machine/types.ts +258 -0
- package/src/services/task/state-machine/uiControlsComputer.ts +1135 -0
- package/src/services/task/taskDataNormalizer.ts +137 -0
- package/src/services/task/types.ts +819 -70
- package/src/services/task/voice/Voice.ts +1851 -0
- package/src/services/task/voice/WebRTC.ts +191 -0
- package/src/services/task/voice/wxAppVoiceMethods.ts +516 -0
- package/src/services/wxAppDiagnosticLogging.ts +174 -0
- package/src/services/wxAppTelephonyUtils.ts +14 -0
- package/src/types.ts +262 -11
- package/src/utils/AGENTS.md +289 -0
- package/src/utils/PageCache.ts +38 -5
- package/src/utils/ai-docs/utils-spec.md +391 -0
- package/src/webex.js +10 -0
- package/test/unit/spec/cc.ts +2210 -119
- package/test/unit/spec/logger-proxy.ts +70 -0
- package/test/unit/spec/metrics/MetricsManager.ts +25 -0
- package/test/unit/spec/metrics/behavioral-events.ts +60 -0
- package/test/unit/spec/services/AddressBook.ts +37 -6
- package/test/unit/spec/services/AnswerCallOnWebexService.ts +264 -0
- package/test/unit/spec/services/ApiAiAssistant.ts +311 -21
- package/test/unit/spec/services/EntryPoint.ts +87 -40
- package/test/unit/spec/services/Queue.ts +123 -12
- package/test/unit/spec/services/UserPreference.ts +401 -0
- package/test/unit/spec/services/WebCallingService.ts +7 -1
- package/test/unit/spec/services/WebexCrossClientService.ts +467 -0
- package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +136 -0
- package/test/unit/spec/services/config/Util.ts +129 -0
- package/test/unit/spec/services/config/index.ts +97 -29
- package/test/unit/spec/services/core/Utils.ts +425 -8
- package/test/unit/spec/services/core/WebexRequest.ts +3 -1
- package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
- package/test/unit/spec/services/core/websocket/WebSocketManager.ts +66 -40
- package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
- package/test/unit/spec/services/task/Task.ts +745 -0
- package/test/unit/spec/services/task/TaskFactory.ts +99 -0
- package/test/unit/spec/services/task/TaskManager.ts +2518 -1494
- package/test/unit/spec/services/task/TaskUtils.ts +235 -0
- package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
- package/test/unit/spec/services/task/contact.ts +33 -0
- package/test/unit/spec/services/task/dialer.ts +190 -0
- package/test/unit/spec/services/task/digital/Digital.ts +105 -0
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +4060 -0
- package/test/unit/spec/services/task/state-machine/guards.ts +839 -0
- package/test/unit/spec/services/task/state-machine/types.ts +18 -0
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +3101 -0
- package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
- package/test/unit/spec/services/task/voice/Voice.ts +2061 -0
- package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
- package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +603 -0
- package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
- package/test/unit/spec/wellnessSampleUtils.ts +288 -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
|
@@ -5,16 +5,21 @@ import {CALL_EVENT_KEYS, CallingClientConfig, LINE_EVENTS} from '@webex/calling'
|
|
|
5
5
|
import {CC_AGENT_EVENTS, CC_EVENTS} from '../../../../../src/services/config/types';
|
|
6
6
|
import TaskManager from '../../../../../src/services/task/TaskManager';
|
|
7
7
|
import * as contact from '../../../../../src/services/task/contact';
|
|
8
|
-
import Task from '../../../../../src/services/task';
|
|
8
|
+
import Task from '../../../../../src/services/task/Task';
|
|
9
9
|
import {TASK_EVENTS} from '../../../../../src/services/task/types';
|
|
10
|
+
import {TaskEvent, TaskState} from '../../../../../src/services/task/state-machine';
|
|
11
|
+
import WebRTC from '../../../../../src/services/task/voice/WebRTC';
|
|
12
|
+
import {Profile} from '../../../../../src/services/config/types';
|
|
10
13
|
import WebCallingService from '../../../../../src/services/WebCallingService';
|
|
11
14
|
import config from '../../../../../src/config';
|
|
12
15
|
import {CC_TASK_EVENTS} from '../../../../../src/services/config/types';
|
|
16
|
+
import TaskFactory from '../../../../../src/services/task/TaskFactory';
|
|
13
17
|
|
|
14
18
|
describe('TaskManager', () => {
|
|
15
19
|
let mockCall;
|
|
16
20
|
let mockApiAIAssistant;
|
|
17
21
|
let webSocketManagerMock;
|
|
22
|
+
let rtdWebSocketManagerMock;
|
|
18
23
|
let onSpy;
|
|
19
24
|
let offSpy;
|
|
20
25
|
let taskManager;
|
|
@@ -24,6 +29,111 @@ describe('TaskManager', () => {
|
|
|
24
29
|
let webex: WebexSDK;
|
|
25
30
|
const taskId = '0ae913a4-c857-4705-8d49-76dd3dde75e4';
|
|
26
31
|
|
|
32
|
+
const createMockTask = (data = taskDataMock) => {
|
|
33
|
+
const task = new EventEmitter() as any;
|
|
34
|
+
|
|
35
|
+
const updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
36
|
+
task.data = {...task.data, ...newData};
|
|
37
|
+
return task;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
Object.assign(task, {
|
|
41
|
+
data,
|
|
42
|
+
accept: jest.fn(),
|
|
43
|
+
decline: jest.fn(),
|
|
44
|
+
updateTaskData,
|
|
45
|
+
unregisterWebCallListeners: jest.fn(),
|
|
46
|
+
cancelAutoWrapupTimer: jest.fn(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const taskEventMap: Partial<Record<TaskEvent, string>> = {
|
|
50
|
+
[TaskEvent.TASK_INCOMING]: TASK_EVENTS.TASK_INCOMING,
|
|
51
|
+
[TaskEvent.TASK_OFFERED]: TASK_EVENTS.TASK_OFFER_CONTACT,
|
|
52
|
+
[TaskEvent.OFFER_CONSULT]: TASK_EVENTS.TASK_OFFER_CONSULT,
|
|
53
|
+
[TaskEvent.HYDRATE]: TASK_EVENTS.TASK_HYDRATE,
|
|
54
|
+
[TaskEvent.CONTACT_OWNER_CHANGED]: TASK_EVENTS.TASK_HYDRATE,
|
|
55
|
+
[TaskEvent.ASSIGN]: TASK_EVENTS.TASK_ASSIGNED,
|
|
56
|
+
[TaskEvent.HOLD_SUCCESS]: TASK_EVENTS.TASK_HOLD,
|
|
57
|
+
[TaskEvent.UNHOLD_SUCCESS]: TASK_EVENTS.TASK_RESUME,
|
|
58
|
+
[TaskEvent.CONSULT_CREATED]: TASK_EVENTS.TASK_CONSULT_CREATED,
|
|
59
|
+
[TaskEvent.CONSULTING_ACTIVE]: TASK_EVENTS.TASK_CONSULT_ACCEPTED,
|
|
60
|
+
[TaskEvent.CONSULT_END]: TASK_EVENTS.TASK_CONSULT_END,
|
|
61
|
+
[TaskEvent.CONSULT_FAILED]: CC_EVENTS.AGENT_CONSULT_FAILED,
|
|
62
|
+
[TaskEvent.CTQ_CANCEL]: TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED,
|
|
63
|
+
[TaskEvent.CTQ_CANCEL_FAILED]: TASK_EVENTS.TASK_CONSULT_QUEUE_FAILED,
|
|
64
|
+
[TaskEvent.END]: TASK_EVENTS.TASK_END,
|
|
65
|
+
[TaskEvent.CONTACT_ENDED]: TASK_EVENTS.TASK_END,
|
|
66
|
+
[TaskEvent.ASSIGN_FAILED]: TASK_EVENTS.TASK_REJECT,
|
|
67
|
+
[TaskEvent.INVITE_FAILED]: TASK_EVENTS.TASK_REJECT,
|
|
68
|
+
[TaskEvent.RONA]: TASK_EVENTS.TASK_REJECT,
|
|
69
|
+
[TaskEvent.OUTBOUND_FAILED]: TASK_EVENTS.TASK_OUTDIAL_FAILED,
|
|
70
|
+
[TaskEvent.RECORDING_STARTED]: TASK_EVENTS.TASK_RECORDING_STARTED,
|
|
71
|
+
[TaskEvent.PAUSE_RECORDING]: TASK_EVENTS.TASK_RECORDING_PAUSED,
|
|
72
|
+
[TaskEvent.RESUME_RECORDING]: TASK_EVENTS.TASK_RECORDING_RESUMED,
|
|
73
|
+
[TaskEvent.WRAPUP_COMPLETE]: TASK_EVENTS.TASK_WRAPPEDUP,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
task.sendStateMachineEvent = jest.fn().mockImplementation((event) => {
|
|
77
|
+
if (event.taskData) {
|
|
78
|
+
task.updateTaskData(event.taskData);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const mappedEvent = taskEventMap[event.type as TaskEvent];
|
|
82
|
+
if (mappedEvent) {
|
|
83
|
+
if (
|
|
84
|
+
[TaskEvent.ASSIGN_FAILED, TaskEvent.RONA, TaskEvent.INVITE_FAILED].includes(
|
|
85
|
+
event.type as TaskEvent
|
|
86
|
+
)
|
|
87
|
+
) {
|
|
88
|
+
task.emit(mappedEvent, event.reason ?? event.taskData?.reason);
|
|
89
|
+
} else if (event.type === TaskEvent.OUTBOUND_FAILED) {
|
|
90
|
+
if (!event.taskData?.suppressOutdialFailedPopup) {
|
|
91
|
+
task.emit(mappedEvent, event.reason);
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
task.emit(mappedEvent, task);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if ([TaskEvent.ASSIGN, TaskEvent.CONSULTING_ACTIVE].includes(event.type as TaskEvent)) {
|
|
99
|
+
task.emit(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, task);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Auto-answer is now handled at the Task layer (triggered by state machine actions)
|
|
103
|
+
if (
|
|
104
|
+
[TaskEvent.TASK_OFFERED, TaskEvent.OFFER_CONSULT].includes(event.type as TaskEvent) &&
|
|
105
|
+
(event.taskData?.isAutoAnswering === true || event.taskData?.isAutoAnswering === 'true')
|
|
106
|
+
) {
|
|
107
|
+
Promise.resolve(task.accept())
|
|
108
|
+
.then(() => {
|
|
109
|
+
task.emit(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
|
|
110
|
+
})
|
|
111
|
+
.catch(() => undefined);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Cleanup is now emitted by state machine actions (Task layer).
|
|
115
|
+
// Simulate the TASK_CLEANUP emission for unit tests using mock tasks.
|
|
116
|
+
const eventType = event.type as TaskEvent;
|
|
117
|
+
const shouldCleanup =
|
|
118
|
+
eventType === TaskEvent.CONTACT_ENDED ||
|
|
119
|
+
eventType === TaskEvent.END ||
|
|
120
|
+
eventType === TaskEvent.TASK_WRAPUP ||
|
|
121
|
+
eventType === TaskEvent.WRAPUP_COMPLETE ||
|
|
122
|
+
eventType === TaskEvent.ASSIGN_FAILED ||
|
|
123
|
+
eventType === TaskEvent.INVITE_FAILED ||
|
|
124
|
+
eventType === TaskEvent.RONA ||
|
|
125
|
+
eventType === TaskEvent.OUTBOUND_FAILED ||
|
|
126
|
+
(eventType === TaskEvent.CONSULT_END && task.data?.isConsulted === true);
|
|
127
|
+
|
|
128
|
+
if (shouldCleanup) {
|
|
129
|
+
const removeFromCollection = eventType !== TaskEvent.CONTACT_ENDED;
|
|
130
|
+
task.emit(TASK_EVENTS.TASK_CLEANUP, task, {removeFromCollection});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return task;
|
|
135
|
+
};
|
|
136
|
+
|
|
27
137
|
taskDataMock = {
|
|
28
138
|
type: CC_EVENTS.AGENT_CONTACT_RESERVED,
|
|
29
139
|
agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
|
|
@@ -43,18 +153,21 @@ describe('TaskManager', () => {
|
|
|
43
153
|
data: taskDataMock,
|
|
44
154
|
};
|
|
45
155
|
|
|
156
|
+
const expectLastStateMachineEvent = (
|
|
157
|
+
spy: jest.SpyInstance | jest.Mock,
|
|
158
|
+
expectedType: TaskEvent
|
|
159
|
+
) => {
|
|
160
|
+
expect(spy).toHaveBeenCalled();
|
|
161
|
+
const lastCall = spy.mock.calls[spy.mock.calls.length - 1] || [];
|
|
162
|
+
const event = lastCall[3]?.type ? lastCall[3] : lastCall[0];
|
|
163
|
+
expect(event?.type).toBe(expectedType);
|
|
164
|
+
return event;
|
|
165
|
+
};
|
|
166
|
+
|
|
46
167
|
beforeEach(() => {
|
|
47
168
|
contactMock = contact;
|
|
48
169
|
webSocketManagerMock = new EventEmitter();
|
|
49
|
-
|
|
50
|
-
sendEvent: jest.fn().mockResolvedValue({}),
|
|
51
|
-
setAIFeatureFlags: jest.fn(),
|
|
52
|
-
aiFeature: {
|
|
53
|
-
realtimeTranscripts: {
|
|
54
|
-
enable: true,
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
};
|
|
170
|
+
rtdWebSocketManagerMock = new EventEmitter();
|
|
58
171
|
|
|
59
172
|
webex = {
|
|
60
173
|
logger: {
|
|
@@ -84,20 +197,25 @@ describe('TaskManager', () => {
|
|
|
84
197
|
onSpy = jest.spyOn(webCallingService, 'on');
|
|
85
198
|
offSpy = jest.spyOn(webCallingService, 'off');
|
|
86
199
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
emit: jest.fn(),
|
|
90
|
-
accept: jest.fn(),
|
|
91
|
-
decline: jest.fn(),
|
|
92
|
-
updateTaskData: jest.fn().mockImplementation((updatedData) => {
|
|
93
|
-
taskMock.data = {...taskMock.data, ...updatedData};
|
|
94
|
-
return taskMock;
|
|
95
|
-
}),
|
|
96
|
-
data: taskDataMock,
|
|
200
|
+
mockApiAIAssistant = {
|
|
201
|
+
sendEvent: jest.fn().mockResolvedValue({}),
|
|
97
202
|
};
|
|
98
|
-
|
|
99
|
-
taskManager
|
|
203
|
+
|
|
204
|
+
taskManager = new TaskManager(
|
|
205
|
+
mockApiAIAssistant as any,
|
|
206
|
+
contactMock,
|
|
207
|
+
webCallingService,
|
|
208
|
+
webSocketManagerMock as any,
|
|
209
|
+
rtdWebSocketManagerMock as any
|
|
210
|
+
);
|
|
211
|
+
taskManager.taskCollection[taskId] = createMockTask(taskDataMock);
|
|
212
|
+
(taskManager as any).setupTaskListeners?.(taskManager.taskCollection[taskId]);
|
|
100
213
|
taskManager.call = mockCall;
|
|
214
|
+
taskManager.setAgentId('test-agent-id');
|
|
215
|
+
|
|
216
|
+
jest
|
|
217
|
+
.spyOn(TaskFactory, 'createTask')
|
|
218
|
+
.mockImplementation((contact, webCallingService, data, configFlags) => createMockTask(data));
|
|
101
219
|
});
|
|
102
220
|
|
|
103
221
|
afterEach(() => {
|
|
@@ -108,7 +226,8 @@ describe('TaskManager', () => {
|
|
|
108
226
|
it('should initialize TaskManager and register listeners', () => {
|
|
109
227
|
webSocketManagerMock.emit('message', JSON.stringify({data: taskDataMock}));
|
|
110
228
|
const incomingCallCb = onSpy.mock.calls[0][1];
|
|
111
|
-
const
|
|
229
|
+
const incomingHandler = jest.fn();
|
|
230
|
+
taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
|
|
112
231
|
|
|
113
232
|
expect(taskManager).toBeInstanceOf(TaskManager);
|
|
114
233
|
expect(webCallingService.listenerCount(LINE_EVENTS.INCOMING_CALL)).toBe(1);
|
|
@@ -117,10 +236,8 @@ describe('TaskManager', () => {
|
|
|
117
236
|
|
|
118
237
|
incomingCallCb(mockCall);
|
|
119
238
|
|
|
120
|
-
expect(
|
|
121
|
-
|
|
122
|
-
taskManager.getTask(taskId)
|
|
123
|
-
);
|
|
239
|
+
expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(taskId));
|
|
240
|
+
taskManager.off(TASK_EVENTS.TASK_INCOMING, incomingHandler);
|
|
124
241
|
});
|
|
125
242
|
|
|
126
243
|
it('should re-emit task related events', () => {
|
|
@@ -136,15 +253,32 @@ describe('TaskManager', () => {
|
|
|
136
253
|
|
|
137
254
|
webSocketManagerMock.emit('message', JSON.stringify(dummyPayload));
|
|
138
255
|
|
|
139
|
-
expect(taskEmitSpy).toHaveBeenCalledWith(
|
|
256
|
+
expect(taskEmitSpy).toHaveBeenCalledWith(
|
|
257
|
+
TASK_EVENTS.TASK_CONSULT_ACCEPTED,
|
|
258
|
+
taskManager.getTask(taskId)
|
|
259
|
+
);
|
|
140
260
|
});
|
|
141
261
|
|
|
142
262
|
it('should invoke sendEvent for configured start/stop backend events', () => {
|
|
263
|
+
taskManager.setConfigFlags({
|
|
264
|
+
isEndTaskEnabled: true,
|
|
265
|
+
isEndConsultEnabled: true,
|
|
266
|
+
webRtcEnabled: true,
|
|
267
|
+
autoWrapup: false,
|
|
268
|
+
aiFeature: {
|
|
269
|
+
id: 'ai-feature-1',
|
|
270
|
+
realtimeTranscripts: {
|
|
271
|
+
enable: true,
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const interactionId = taskId;
|
|
143
277
|
const message = (type: CC_EVENTS) =>
|
|
144
278
|
JSON.stringify({
|
|
145
279
|
data: {
|
|
146
280
|
...taskDataMock,
|
|
147
|
-
|
|
281
|
+
interactionId,
|
|
148
282
|
type,
|
|
149
283
|
},
|
|
150
284
|
});
|
|
@@ -153,67 +287,114 @@ describe('TaskManager', () => {
|
|
|
153
287
|
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
|
|
154
288
|
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_CONFERENCED));
|
|
155
289
|
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_ENDED));
|
|
156
|
-
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_WRAPUP));
|
|
157
290
|
webSocketManagerMock.emit('message', message(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE));
|
|
291
|
+
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_WRAPUP));
|
|
158
292
|
|
|
159
293
|
expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledTimes(6);
|
|
160
294
|
expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledWith(
|
|
161
295
|
'test-agent-id',
|
|
162
|
-
|
|
296
|
+
interactionId,
|
|
163
297
|
'CUSTOM_EVENT',
|
|
164
298
|
'GET_TRANSCRIPTS',
|
|
165
|
-
'START'
|
|
299
|
+
{action: 'START'}
|
|
166
300
|
);
|
|
167
301
|
expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledWith(
|
|
168
302
|
'test-agent-id',
|
|
169
|
-
|
|
303
|
+
interactionId,
|
|
170
304
|
'CUSTOM_EVENT',
|
|
171
305
|
'GET_TRANSCRIPTS',
|
|
172
|
-
'STOP'
|
|
306
|
+
{action: 'STOP'}
|
|
173
307
|
);
|
|
174
308
|
});
|
|
175
309
|
|
|
176
|
-
it('should not invoke sendEvent
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
310
|
+
it('should not invoke sendEvent when realtime transcripts are disabled in aiFeature', () => {
|
|
311
|
+
taskManager.setConfigFlags({
|
|
312
|
+
isEndTaskEnabled: true,
|
|
313
|
+
isEndConsultEnabled: true,
|
|
314
|
+
webRtcEnabled: true,
|
|
315
|
+
autoWrapup: false,
|
|
316
|
+
aiFeature: {
|
|
317
|
+
id: 'ai-feature-1',
|
|
318
|
+
realtimeTranscripts: {
|
|
319
|
+
enable: false,
|
|
320
|
+
},
|
|
180
321
|
},
|
|
181
|
-
};
|
|
182
|
-
mockApiAIAssistant.setAIFeatureFlags(mockApiAIAssistant.aiFeature);
|
|
322
|
+
});
|
|
183
323
|
|
|
184
324
|
const message = (type: CC_EVENTS) =>
|
|
185
325
|
JSON.stringify({
|
|
186
326
|
data: {
|
|
187
327
|
...taskDataMock,
|
|
188
|
-
taskId,
|
|
328
|
+
interactionId: taskId,
|
|
329
|
+
type,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONTACT_ASSIGNED));
|
|
334
|
+
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
|
|
335
|
+
|
|
336
|
+
expect(mockApiAIAssistant.sendEvent).not.toHaveBeenCalled();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should not invoke sendEvent when realtime transcripts config is missing', () => {
|
|
340
|
+
taskManager.setConfigFlags({
|
|
341
|
+
isEndTaskEnabled: true,
|
|
342
|
+
isEndConsultEnabled: true,
|
|
343
|
+
webRtcEnabled: true,
|
|
344
|
+
autoWrapup: false,
|
|
345
|
+
aiFeature: {
|
|
346
|
+
id: 'ai-feature-1',
|
|
347
|
+
suggestedResponses: {
|
|
348
|
+
enable: true,
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const message = (type: CC_EVENTS) =>
|
|
354
|
+
JSON.stringify({
|
|
355
|
+
data: {
|
|
356
|
+
...taskDataMock,
|
|
357
|
+
interactionId: taskId,
|
|
189
358
|
type,
|
|
190
359
|
},
|
|
191
360
|
});
|
|
192
361
|
|
|
193
362
|
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONTACT_ASSIGNED));
|
|
194
363
|
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
|
|
195
|
-
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_CONFERENCED));
|
|
196
|
-
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_ENDED));
|
|
197
|
-
webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_WRAPUP));
|
|
198
|
-
webSocketManagerMock.emit('message', message(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE));
|
|
199
364
|
|
|
200
365
|
expect(mockApiAIAssistant.sendEvent).not.toHaveBeenCalled();
|
|
201
366
|
});
|
|
202
367
|
|
|
203
|
-
it('should emit REAL_TIME_TRANSCRIPTION from
|
|
368
|
+
it('should emit REAL_TIME_TRANSCRIPTION from RTD websocket payload', () => {
|
|
204
369
|
const task = taskManager.getTask(taskId);
|
|
205
370
|
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
206
371
|
const realtimePayload = {
|
|
207
372
|
data: {
|
|
208
|
-
|
|
209
|
-
type: CC_EVENTS.REAL_TIME_TRANSCRIPTION,
|
|
373
|
+
agentId: 'test-agent-id',
|
|
210
374
|
data: {
|
|
211
|
-
content: '
|
|
375
|
+
content: 'Thank you. Okay.',
|
|
376
|
+
conversationId: taskId,
|
|
377
|
+
isFinal: true,
|
|
378
|
+
languageCode: 'en-US',
|
|
379
|
+
messageId: '1',
|
|
380
|
+
orgId: 'org-id',
|
|
381
|
+
publishTimestamp: 1773807297475,
|
|
382
|
+
role: 'AGENT',
|
|
383
|
+
trackingId: 'tracking-id',
|
|
384
|
+
utteranceId: 'utterance-id',
|
|
212
385
|
},
|
|
386
|
+
notifDetails: {
|
|
387
|
+
actionEvent: 'REAL_TIME_TRANSCRIPTION',
|
|
388
|
+
},
|
|
389
|
+
notifType: 'REAL_TIME_TRANSCRIPTION',
|
|
390
|
+
orgId: 'org-id',
|
|
213
391
|
},
|
|
392
|
+
orgId: 'org-id',
|
|
393
|
+
trackingId: 'notifs_tracking-id',
|
|
394
|
+
type: 'REAL_TIME_TRANSCRIPTION',
|
|
214
395
|
};
|
|
215
396
|
|
|
216
|
-
|
|
397
|
+
taskManager.handleRealtimeWebsocketEvent(JSON.stringify(realtimePayload));
|
|
217
398
|
|
|
218
399
|
expect(taskEmitSpy).toHaveBeenCalledWith(
|
|
219
400
|
CC_EVENTS.REAL_TIME_TRANSCRIPTION,
|
|
@@ -221,22 +402,37 @@ describe('TaskManager', () => {
|
|
|
221
402
|
);
|
|
222
403
|
});
|
|
223
404
|
|
|
224
|
-
it('should
|
|
225
|
-
const task = taskManager.getTask(taskId);
|
|
226
|
-
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
405
|
+
it('should ignore RTD transcript events when task is not found', () => {
|
|
227
406
|
const realtimePayload = {
|
|
228
407
|
data: {
|
|
229
|
-
notifType: CC_EVENTS.REAL_TIME_TRANSCRIPTION,
|
|
230
408
|
data: {
|
|
231
|
-
|
|
232
|
-
|
|
409
|
+
content: 'Thank you. Okay.',
|
|
410
|
+
conversationId: 'missing-task-id',
|
|
411
|
+
isFinal: true,
|
|
412
|
+
languageCode: 'en-US',
|
|
413
|
+
messageId: '1',
|
|
414
|
+
orgId: 'org-id',
|
|
415
|
+
publishTimestamp: 1773807297475,
|
|
416
|
+
role: 'AGENT',
|
|
417
|
+
trackingId: 'tracking-id',
|
|
418
|
+
utteranceId: 'utterance-id',
|
|
419
|
+
},
|
|
420
|
+
notifDetails: {
|
|
421
|
+
actionEvent: 'REAL_TIME_TRANSCRIPTION',
|
|
233
422
|
},
|
|
423
|
+
notifType: 'REAL_TIME_TRANSCRIPTION',
|
|
424
|
+
orgId: 'org-id',
|
|
234
425
|
},
|
|
426
|
+
orgId: 'org-id',
|
|
427
|
+
trackingId: 'notifs_tracking-id',
|
|
428
|
+
type: 'REAL_TIME_TRANSCRIPTION',
|
|
235
429
|
};
|
|
236
430
|
|
|
431
|
+
const existingTaskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
|
|
432
|
+
|
|
237
433
|
taskManager.handleRealtimeWebsocketEvent(JSON.stringify(realtimePayload));
|
|
238
434
|
|
|
239
|
-
expect(
|
|
435
|
+
expect(existingTaskEmitSpy).not.toHaveBeenCalled();
|
|
240
436
|
});
|
|
241
437
|
|
|
242
438
|
it('should not re-emit agent related events', () => {
|
|
@@ -254,7 +450,7 @@ describe('TaskManager', () => {
|
|
|
254
450
|
|
|
255
451
|
webSocketManagerMock.emit('message', JSON.stringify(dummyPayload));
|
|
256
452
|
|
|
257
|
-
expect(taskEmitSpy).not.
|
|
453
|
+
expect(taskEmitSpy).not.toHaveBeenCalled();
|
|
258
454
|
});
|
|
259
455
|
|
|
260
456
|
it('should handle WebSocket message for AGENT_CONTACT_RESERVED and emit task:incoming for browser case', () => {
|
|
@@ -275,14 +471,12 @@ describe('TaskManager', () => {
|
|
|
275
471
|
},
|
|
276
472
|
};
|
|
277
473
|
|
|
278
|
-
const
|
|
474
|
+
const incomingHandler = jest.fn();
|
|
475
|
+
taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
|
|
279
476
|
|
|
280
477
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
281
478
|
|
|
282
|
-
expect(
|
|
283
|
-
TASK_EVENTS.TASK_INCOMING,
|
|
284
|
-
taskManager.getTask(payload.data.interactionId)
|
|
285
|
-
);
|
|
479
|
+
expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(payload.data.interactionId));
|
|
286
480
|
expect(taskManager.getTask(payload.data.interactionId)).toBe(taskManager.getTask(taskId));
|
|
287
481
|
expect(taskManager.getAllTasks()).toHaveProperty(payload.data.interactionId);
|
|
288
482
|
|
|
@@ -307,6 +501,7 @@ describe('TaskManager', () => {
|
|
|
307
501
|
taskManager.getTask(payload.data.interactionId),
|
|
308
502
|
'emit'
|
|
309
503
|
);
|
|
504
|
+
const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
|
|
310
505
|
|
|
311
506
|
webSocketManagerMock.emit('message', JSON.stringify(assignedPayload));
|
|
312
507
|
|
|
@@ -314,6 +509,10 @@ describe('TaskManager', () => {
|
|
|
314
509
|
TASK_EVENTS.TASK_ASSIGNED,
|
|
315
510
|
taskManager.getTask(taskId)
|
|
316
511
|
);
|
|
512
|
+
expect(taskManagerEmitSpy).toHaveBeenCalledWith(
|
|
513
|
+
TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
|
|
514
|
+
taskManager.getTask(taskId)
|
|
515
|
+
);
|
|
317
516
|
});
|
|
318
517
|
|
|
319
518
|
it('should handle WebSocket message for AGENT_CONTACT_RESERVED and emit task:incoming for extension case', () => {
|
|
@@ -335,16 +534,64 @@ describe('TaskManager', () => {
|
|
|
335
534
|
},
|
|
336
535
|
};
|
|
337
536
|
|
|
338
|
-
const
|
|
537
|
+
const incomingHandler = jest.fn();
|
|
538
|
+
taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
|
|
339
539
|
|
|
340
540
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
341
541
|
|
|
342
|
-
expect(
|
|
343
|
-
TASK_EVENTS.TASK_INCOMING,
|
|
344
|
-
taskManager.getTask(taskId)
|
|
345
|
-
);
|
|
542
|
+
expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(taskId));
|
|
346
543
|
expect(taskManager.getTask(payload.data.interactionId)).toBe(taskManager.getTask(taskId));
|
|
347
544
|
expect(taskManager.getAllTasks()).toHaveProperty(payload.data.interactionId);
|
|
545
|
+
taskManager.off(TASK_EVENTS.TASK_INCOMING, incomingHandler);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('should send mapped events through the state machine without duplicate updates', () => {
|
|
549
|
+
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
550
|
+
const task = taskManager.getTask(taskId);
|
|
551
|
+
const updateSpy = task.updateTaskData as jest.Mock;
|
|
552
|
+
updateSpy.mockClear();
|
|
553
|
+
const sendSpy = task.sendStateMachineEvent as jest.Mock;
|
|
554
|
+
sendSpy.mockClear();
|
|
555
|
+
const cleanupSpy = jest.spyOn(taskManager as any, 'handleTaskCleanup');
|
|
556
|
+
|
|
557
|
+
const assignFailedPayload = {
|
|
558
|
+
data: {
|
|
559
|
+
...initalPayload.data,
|
|
560
|
+
type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED,
|
|
561
|
+
reason: 'ASSIGN_FAILED',
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
webSocketManagerMock.emit('message', JSON.stringify(assignFailedPayload));
|
|
566
|
+
|
|
567
|
+
const stateMachineEvent = expectLastStateMachineEvent(sendSpy, TaskEvent.ASSIGN_FAILED);
|
|
568
|
+
expect(stateMachineEvent).toEqual({
|
|
569
|
+
type: TaskEvent.ASSIGN_FAILED,
|
|
570
|
+
reason: assignFailedPayload.data.reason,
|
|
571
|
+
});
|
|
572
|
+
expect(updateSpy).toHaveBeenCalledWith(assignFailedPayload.data);
|
|
573
|
+
expect(cleanupSpy).toHaveBeenCalledWith(task);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('should update task data directly when no state machine mapping exists', () => {
|
|
577
|
+
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
578
|
+
const task = taskManager.getTask(taskId);
|
|
579
|
+
const updateSpy = task.updateTaskData as jest.Mock;
|
|
580
|
+
updateSpy.mockClear();
|
|
581
|
+
const sendSpy = task.sendStateMachineEvent as jest.Mock;
|
|
582
|
+
sendSpy.mockClear();
|
|
583
|
+
|
|
584
|
+
const participantMovedPayload = {
|
|
585
|
+
data: {
|
|
586
|
+
...initalPayload.data,
|
|
587
|
+
type: CC_EVENTS.CONSULTED_PARTICIPANT_MOVING,
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
webSocketManagerMock.emit('message', JSON.stringify(participantMovedPayload));
|
|
592
|
+
|
|
593
|
+
expect(sendSpy).not.toHaveBeenCalled();
|
|
594
|
+
expect(updateSpy).toHaveBeenCalledWith(participantMovedPayload.data);
|
|
348
595
|
});
|
|
349
596
|
|
|
350
597
|
it('should return task by ID', () => {
|
|
@@ -429,12 +676,30 @@ describe('TaskManager', () => {
|
|
|
429
676
|
it('test call listeners being switched off on call end', () => {
|
|
430
677
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
431
678
|
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
);
|
|
679
|
+
const webrtcTask = new WebRTC(contactMock, webCallingService, taskDataMock, {
|
|
680
|
+
isEndTaskEnabled: true,
|
|
681
|
+
isEndConsultEnabled: true,
|
|
682
|
+
});
|
|
683
|
+
(taskManager as any).taskCollection[taskId] = webrtcTask;
|
|
684
|
+
// TaskManager must listen to task-level cleanup events emitted by the state machine.
|
|
685
|
+
// This is normally wired when TaskManager creates the task via TaskFactory.
|
|
686
|
+
(taskManager as any).setupTaskListeners(webrtcTask);
|
|
687
|
+
|
|
688
|
+
const task = taskManager.getTask(taskId)!;
|
|
689
|
+
// This test doesn't validate UI controls; avoid requiring full interaction.media
|
|
690
|
+
// shape for WebRTC UI controls computation.
|
|
691
|
+
jest.spyOn(task as any, 'updateUiControls').mockImplementation(() => undefined);
|
|
692
|
+
const originalEmit = task.emit;
|
|
693
|
+
jest.spyOn(task, 'emit').mockImplementation((event, arg) => {
|
|
694
|
+
if (event === CC_EVENTS.CONTACT_ENDED) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
return originalEmit.call(task, event, arg);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
const webCallListenerSpy = jest.spyOn(task, 'unregisterWebCallListeners');
|
|
437
701
|
const callOffSpy = jest.spyOn(mockCall, 'off');
|
|
702
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
438
703
|
const payload = {
|
|
439
704
|
data: {
|
|
440
705
|
type: CC_EVENTS.CONTACT_ENDED,
|
|
@@ -452,10 +717,25 @@ describe('TaskManager', () => {
|
|
|
452
717
|
},
|
|
453
718
|
};
|
|
454
719
|
|
|
720
|
+
// Ensure the state machine is hydrated into a connected state before CONTACT_ENDED
|
|
721
|
+
const hydratePayload = {
|
|
722
|
+
data: {
|
|
723
|
+
...payload.data,
|
|
724
|
+
type: CC_EVENTS.AGENT_CONTACT,
|
|
725
|
+
interaction: {state: 'connected', mediaType: 'telephony'},
|
|
726
|
+
},
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
taskManager.getTask(taskId).data = hydratePayload.data;
|
|
730
|
+
webSocketManagerMock.emit('message', JSON.stringify(hydratePayload));
|
|
731
|
+
|
|
455
732
|
taskManager.getTask(taskId).data = payload.data;
|
|
456
|
-
const task = taskManager.getTask(taskId);
|
|
457
733
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
458
|
-
|
|
734
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
735
|
+
sendStateMachineEventSpy,
|
|
736
|
+
TaskEvent.CONTACT_ENDED
|
|
737
|
+
);
|
|
738
|
+
expect(stateMachineEvent?.taskData.wrapUpRequired).toBe(false);
|
|
459
739
|
expect(webCallListenerSpy).toHaveBeenCalledWith();
|
|
460
740
|
expect(callOffSpy).toHaveBeenCalledWith(
|
|
461
741
|
CALL_EVENT_KEYS.REMOTE_MEDIA,
|
|
@@ -466,12 +746,13 @@ describe('TaskManager', () => {
|
|
|
466
746
|
expect(offSpy.mock.calls.length).toBe(2); // 1 for incoming call and 1 for remote media
|
|
467
747
|
expect(offSpy).toHaveBeenCalledWith(CALL_EVENT_KEYS.REMOTE_MEDIA, offSpy.mock.calls[0][1]);
|
|
468
748
|
expect(offSpy).toHaveBeenCalledWith(LINE_EVENTS.INCOMING_CALL, offSpy.mock.calls[1][1]);
|
|
749
|
+
sendStateMachineEventSpy.mockRestore();
|
|
469
750
|
});
|
|
470
751
|
|
|
471
752
|
it('should emit TASK_END event with wrapupRequired on regular call end', () => {
|
|
472
753
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
473
|
-
|
|
474
|
-
const
|
|
754
|
+
const task = taskManager.getTask(taskId);
|
|
755
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
475
756
|
const payload = {
|
|
476
757
|
data: {
|
|
477
758
|
type: CC_EVENTS.CONTACT_ENDED,
|
|
@@ -486,20 +767,25 @@ describe('TaskManager', () => {
|
|
|
486
767
|
destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
|
|
487
768
|
owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
|
|
488
769
|
queueMgr: 'aqm',
|
|
770
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
489
771
|
},
|
|
490
772
|
};
|
|
491
773
|
|
|
492
|
-
|
|
774
|
+
task.updateTaskData(payload.data);
|
|
493
775
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
494
|
-
|
|
495
|
-
|
|
776
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
777
|
+
sendStateMachineEventSpy,
|
|
778
|
+
TaskEvent.CONTACT_ENDED
|
|
779
|
+
);
|
|
780
|
+
expect(stateMachineEvent?.taskData.wrapUpRequired).toBe(true);
|
|
781
|
+
sendStateMachineEventSpy.mockRestore();
|
|
496
782
|
});
|
|
497
783
|
|
|
498
784
|
it('should emit TASK_REJECT event on AGENT_INVITE_FAILED event', () => {
|
|
499
785
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
500
786
|
|
|
501
|
-
const
|
|
502
|
-
const
|
|
787
|
+
const task = taskManager.getTask(taskId);
|
|
788
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
503
789
|
const payload = {
|
|
504
790
|
data: {
|
|
505
791
|
type: CC_EVENTS.AGENT_INVITE_FAILED,
|
|
@@ -518,35 +804,40 @@ describe('TaskManager', () => {
|
|
|
518
804
|
},
|
|
519
805
|
};
|
|
520
806
|
|
|
521
|
-
|
|
807
|
+
task.updateTaskData(payload.data);
|
|
522
808
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
expect(
|
|
809
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
810
|
+
sendStateMachineEventSpy,
|
|
811
|
+
TaskEvent.INVITE_FAILED
|
|
812
|
+
);
|
|
813
|
+
expect(stateMachineEvent?.reason).toBe(payload.data.reason);
|
|
814
|
+
sendStateMachineEventSpy.mockRestore();
|
|
528
815
|
});
|
|
529
816
|
|
|
530
|
-
it('should
|
|
817
|
+
it('should emit TASK_HYDRATE even if task is already present in taskManager', () => {
|
|
531
818
|
const payload = {
|
|
532
819
|
data: {
|
|
533
820
|
...initalPayload.data,
|
|
534
821
|
type: CC_EVENTS.AGENT_CONTACT,
|
|
535
822
|
},
|
|
536
823
|
};
|
|
537
|
-
const
|
|
824
|
+
const existingTask = taskManager.getTask(taskId);
|
|
825
|
+
const sendStateMachineEventSpy = jest.spyOn(existingTask, 'sendStateMachineEvent');
|
|
538
826
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
539
827
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
828
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
829
|
+
sendStateMachineEventSpy,
|
|
830
|
+
TaskEvent.HYDRATE
|
|
543
831
|
);
|
|
832
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
833
|
+
expect(existingTask).toBe(taskManager.getTask(taskId));
|
|
544
834
|
expect(taskManager.taskCollection[payload.data.interactionId]).toBe(
|
|
545
835
|
taskManager.getTask(taskId)
|
|
546
836
|
);
|
|
837
|
+
sendStateMachineEventSpy.mockRestore();
|
|
547
838
|
});
|
|
548
839
|
|
|
549
|
-
it('should emit
|
|
840
|
+
it('should emit TASK_HYDRATE event on AGENT_CONTACT when task is created from payload', () => {
|
|
550
841
|
taskManager.taskCollection = [];
|
|
551
842
|
const payload = {
|
|
552
843
|
data: {
|
|
@@ -556,13 +847,15 @@ describe('TaskManager', () => {
|
|
|
556
847
|
},
|
|
557
848
|
};
|
|
558
849
|
|
|
559
|
-
const taskEmitSpy = jest.spyOn(taskManager, 'emit');
|
|
560
850
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
561
851
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
852
|
+
const createdTask = taskManager.getTask(taskId);
|
|
853
|
+
const sendStateMachineEventSpy = createdTask.sendStateMachineEvent as jest.Mock;
|
|
854
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
855
|
+
sendStateMachineEventSpy,
|
|
856
|
+
TaskEvent.HYDRATE
|
|
565
857
|
);
|
|
858
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
566
859
|
expect(taskManager.taskCollection[payload.data.interactionId]).toBe(
|
|
567
860
|
taskManager.getTask(taskId)
|
|
568
861
|
);
|
|
@@ -577,10 +870,15 @@ describe('TaskManager', () => {
|
|
|
577
870
|
},
|
|
578
871
|
};
|
|
579
872
|
|
|
580
|
-
const taskEmitSpy = jest.spyOn(taskManager, 'emit');
|
|
581
873
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
582
874
|
|
|
583
|
-
|
|
875
|
+
const createdTask = taskManager.getTask(taskId);
|
|
876
|
+
const sendStateMachineEventSpy = createdTask.sendStateMachineEvent as jest.Mock;
|
|
877
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
878
|
+
sendStateMachineEventSpy,
|
|
879
|
+
TaskEvent.HYDRATE
|
|
880
|
+
);
|
|
881
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
584
882
|
expect(taskManager.taskCollection[payload.data.interactionId]).toBe(
|
|
585
883
|
taskManager.getTask(taskId)
|
|
586
884
|
);
|
|
@@ -653,7 +951,7 @@ describe('TaskManager', () => {
|
|
|
653
951
|
expect(createdTask.data.isConferenceInProgress).toBe(false);
|
|
654
952
|
});
|
|
655
953
|
|
|
656
|
-
it('should emit
|
|
954
|
+
it('should emit TASK_WRAPUP event on AGENT_WRAPUP event', () => {
|
|
657
955
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
658
956
|
|
|
659
957
|
const wrapupPayload = {
|
|
@@ -675,13 +973,15 @@ describe('TaskManager', () => {
|
|
|
675
973
|
};
|
|
676
974
|
|
|
677
975
|
const task = taskManager.getTask(taskId);
|
|
678
|
-
const updateTaskDataSpy = jest.
|
|
679
|
-
|
|
976
|
+
const updateTaskDataSpy = task.updateTaskData as jest.Mock;
|
|
977
|
+
updateTaskDataSpy.mockClear();
|
|
978
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
680
979
|
|
|
681
980
|
webSocketManagerMock.emit('message', JSON.stringify(wrapupPayload));
|
|
682
981
|
|
|
683
982
|
expect(updateTaskDataSpy).toHaveBeenCalledWith(wrapupPayload.data);
|
|
684
|
-
|
|
983
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TASK_WRAPUP);
|
|
984
|
+
sendStateMachineEventSpy.mockRestore();
|
|
685
985
|
});
|
|
686
986
|
|
|
687
987
|
it('should emit TASK_HOLD event on AGENT_CONTACT_HELD event', () => {
|
|
@@ -704,13 +1004,20 @@ describe('TaskManager', () => {
|
|
|
704
1004
|
},
|
|
705
1005
|
};
|
|
706
1006
|
|
|
707
|
-
const
|
|
708
|
-
const taskUpdateTaskDataSpy = jest.
|
|
1007
|
+
const task = taskManager.getTask(taskId);
|
|
1008
|
+
const taskUpdateTaskDataSpy = task.updateTaskData as jest.Mock;
|
|
1009
|
+
taskUpdateTaskDataSpy.mockClear();
|
|
1010
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
709
1011
|
|
|
710
1012
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
711
1013
|
|
|
712
1014
|
expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
|
|
713
|
-
|
|
1015
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1016
|
+
sendStateMachineEventSpy,
|
|
1017
|
+
TaskEvent.HOLD_SUCCESS
|
|
1018
|
+
);
|
|
1019
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
1020
|
+
sendStateMachineEventSpy.mockRestore();
|
|
714
1021
|
});
|
|
715
1022
|
|
|
716
1023
|
it('should emit TASK_RESUME event on AGENT_CONTACT_UNHELD event', () => {
|
|
@@ -733,11 +1040,18 @@ describe('TaskManager', () => {
|
|
|
733
1040
|
},
|
|
734
1041
|
};
|
|
735
1042
|
|
|
736
|
-
const
|
|
737
|
-
const taskUpdateTaskDataSpy = jest.
|
|
1043
|
+
const task = taskManager.getTask(taskId);
|
|
1044
|
+
const taskUpdateTaskDataSpy = task.updateTaskData as jest.Mock;
|
|
1045
|
+
taskUpdateTaskDataSpy.mockClear();
|
|
1046
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
738
1047
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
739
1048
|
expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
|
|
740
|
-
|
|
1049
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1050
|
+
sendStateMachineEventSpy,
|
|
1051
|
+
TaskEvent.UNHOLD_SUCCESS
|
|
1052
|
+
);
|
|
1053
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
1054
|
+
sendStateMachineEventSpy.mockRestore();
|
|
741
1055
|
});
|
|
742
1056
|
|
|
743
1057
|
it('handle AGENT_CONSULT_CREATED event', () => {
|
|
@@ -750,17 +1064,16 @@ describe('TaskManager', () => {
|
|
|
750
1064
|
|
|
751
1065
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
752
1066
|
const task = taskManager.getTask(taskId);
|
|
753
|
-
const
|
|
754
|
-
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
1067
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
755
1068
|
|
|
756
1069
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
757
1070
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
expect(
|
|
763
|
-
|
|
1071
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1072
|
+
sendStateMachineEventSpy,
|
|
1073
|
+
TaskEvent.CONSULT_CREATED
|
|
1074
|
+
);
|
|
1075
|
+
expect(stateMachineEvent?.taskData).toEqual({...payload.data, isConsulted: false});
|
|
1076
|
+
sendStateMachineEventSpy.mockRestore();
|
|
764
1077
|
});
|
|
765
1078
|
|
|
766
1079
|
it('handle AGENT_OFFER_CONTACT event', () => {
|
|
@@ -773,11 +1086,17 @@ describe('TaskManager', () => {
|
|
|
773
1086
|
|
|
774
1087
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
775
1088
|
|
|
776
|
-
const
|
|
1089
|
+
const task = taskManager.getTask(taskId);
|
|
1090
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
777
1091
|
|
|
778
1092
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
779
1093
|
|
|
780
|
-
|
|
1094
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1095
|
+
sendStateMachineEventSpy,
|
|
1096
|
+
TaskEvent.TASK_OFFERED
|
|
1097
|
+
);
|
|
1098
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
1099
|
+
sendStateMachineEventSpy.mockRestore();
|
|
781
1100
|
});
|
|
782
1101
|
|
|
783
1102
|
describe('Auto-Answer Functionality', () => {
|
|
@@ -787,8 +1106,8 @@ describe('TaskManager', () => {
|
|
|
787
1106
|
|
|
788
1107
|
const task = taskManager.getTask(taskId);
|
|
789
1108
|
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
790
|
-
const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
|
|
791
1109
|
const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
|
|
1110
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
792
1111
|
|
|
793
1112
|
// Step 2: Trigger AGENT_OFFER_CONTACT with auto-answer
|
|
794
1113
|
const autoAnswerPayload = {
|
|
@@ -812,9 +1131,14 @@ describe('TaskManager', () => {
|
|
|
812
1131
|
// Verify accept was called
|
|
813
1132
|
expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
|
|
814
1133
|
|
|
815
|
-
|
|
816
|
-
|
|
1134
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1135
|
+
sendStateMachineEventSpy,
|
|
1136
|
+
TaskEvent.TASK_OFFERED
|
|
1137
|
+
);
|
|
1138
|
+
expect(stateMachineEvent?.taskData).toEqual(autoAnswerPayload.data);
|
|
1139
|
+
// Verify task auto-answer event was emitted
|
|
817
1140
|
expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
|
|
1141
|
+
sendStateMachineEventSpy.mockRestore();
|
|
818
1142
|
});
|
|
819
1143
|
|
|
820
1144
|
it('should NOT emit TASK_AUTO_ANSWERED event when auto-answer fails', async () => {
|
|
@@ -860,6 +1184,7 @@ describe('TaskManager', () => {
|
|
|
860
1184
|
const task = taskManager.getTask(taskId);
|
|
861
1185
|
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
862
1186
|
const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
|
|
1187
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
863
1188
|
|
|
864
1189
|
// Step 2: Trigger AGENT_OFFER_CONSULT with auto-answer
|
|
865
1190
|
const consultAutoAnswerPayload = {
|
|
@@ -884,12 +1209,20 @@ describe('TaskManager', () => {
|
|
|
884
1209
|
// Verify accept was called
|
|
885
1210
|
expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
|
|
886
1211
|
|
|
887
|
-
|
|
888
|
-
|
|
1212
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1213
|
+
sendStateMachineEventSpy,
|
|
1214
|
+
TaskEvent.OFFER_CONSULT
|
|
1215
|
+
);
|
|
1216
|
+
expect(stateMachineEvent?.taskData).toEqual({
|
|
1217
|
+
...consultAutoAnswerPayload.data,
|
|
1218
|
+
isConsulted: true,
|
|
1219
|
+
});
|
|
1220
|
+
// Verify task auto-answer event was emitted
|
|
889
1221
|
expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
|
|
890
1222
|
|
|
891
1223
|
// Verify isConsulted flag is set correctly
|
|
892
1224
|
expect(task.data.isConsulted).toBe(true);
|
|
1225
|
+
sendStateMachineEventSpy.mockRestore();
|
|
893
1226
|
});
|
|
894
1227
|
|
|
895
1228
|
it('should NOT emit TASK_AUTO_ANSWERED when isAutoAnswering is false', async () => {
|
|
@@ -930,24 +1263,20 @@ describe('TaskManager', () => {
|
|
|
930
1263
|
});
|
|
931
1264
|
});
|
|
932
1265
|
|
|
933
|
-
it('should
|
|
1266
|
+
it('should remove OUTDIAL task from taskCollection on AGENT_OUTBOUND_FAILED when terminated', () => {
|
|
934
1267
|
const task = taskManager.getTask(taskId);
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
...task.data,
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
state: 'new',
|
|
944
|
-
isTerminated: true,
|
|
945
|
-
},
|
|
946
|
-
};
|
|
947
|
-
return task;
|
|
1268
|
+
Object.assign(task.data, {
|
|
1269
|
+
interaction: {
|
|
1270
|
+
...task.data.interaction,
|
|
1271
|
+
outboundType: 'OUTDIAL',
|
|
1272
|
+
state: 'new',
|
|
1273
|
+
isTerminated: true,
|
|
1274
|
+
},
|
|
1275
|
+
agentsPendingWrapUp: ['agent-123'],
|
|
948
1276
|
});
|
|
949
1277
|
task.unregisterWebCallListeners = jest.fn();
|
|
950
1278
|
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
1279
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
951
1280
|
|
|
952
1281
|
const payload = {
|
|
953
1282
|
data: {
|
|
@@ -960,6 +1289,7 @@ describe('TaskManager', () => {
|
|
|
960
1289
|
state: 'new',
|
|
961
1290
|
isTerminated: true,
|
|
962
1291
|
},
|
|
1292
|
+
agentsPendingWrapUp: ['agent-123'],
|
|
963
1293
|
interactionId: taskId,
|
|
964
1294
|
orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
|
|
965
1295
|
trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
|
|
@@ -974,23 +1304,61 @@ describe('TaskManager', () => {
|
|
|
974
1304
|
|
|
975
1305
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
976
1306
|
|
|
977
|
-
|
|
978
|
-
|
|
1307
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1308
|
+
|
|
1309
|
+
expect(taskManager.getTask(taskId)).toBeUndefined();
|
|
1310
|
+
expect(removeTaskSpy).toHaveBeenCalled();
|
|
1311
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1312
|
+
sendStateMachineEventSpy,
|
|
1313
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1314
|
+
);
|
|
1315
|
+
expect(stateMachineEvent?.reason).toBe('CUSTOMER_BUSY');
|
|
1316
|
+
sendStateMachineEventSpy.mockRestore();
|
|
979
1317
|
});
|
|
980
1318
|
|
|
981
1319
|
it('should emit TASK_OUTDIAL_FAILED event on AGENT_OUTBOUND_FAILED', () => {
|
|
982
1320
|
const task = taskManager.getTask(taskId);
|
|
983
|
-
|
|
984
|
-
const
|
|
1321
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1322
|
+
const payload = {
|
|
1323
|
+
data: {
|
|
1324
|
+
type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
|
|
1325
|
+
interactionId: taskId,
|
|
1326
|
+
reason: 'CUSTOMER_BUSY',
|
|
1327
|
+
},
|
|
1328
|
+
};
|
|
1329
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1330
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1331
|
+
sendStateMachineEventSpy,
|
|
1332
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1333
|
+
);
|
|
1334
|
+
expect(stateMachineEvent?.reason).toBe('CUSTOMER_BUSY');
|
|
1335
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
it('should pass taskData in OUTBOUND_FAILED event for shouldWrapUp guard evaluation', () => {
|
|
1339
|
+
const task = taskManager.getTask(taskId);
|
|
1340
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
985
1341
|
const payload = {
|
|
986
1342
|
data: {
|
|
987
1343
|
type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
|
|
988
1344
|
interactionId: taskId,
|
|
989
1345
|
reason: 'CUSTOMER_BUSY',
|
|
1346
|
+
agentsPendingWrapUp: ['agent-123'],
|
|
1347
|
+
interaction: {
|
|
1348
|
+
outboundType: 'OUTDIAL',
|
|
1349
|
+
isTerminated: true,
|
|
1350
|
+
},
|
|
990
1351
|
},
|
|
991
1352
|
};
|
|
992
1353
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
993
|
-
|
|
1354
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1355
|
+
sendStateMachineEventSpy,
|
|
1356
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1357
|
+
);
|
|
1358
|
+
expect(stateMachineEvent?.taskData).toBeDefined();
|
|
1359
|
+
expect(stateMachineEvent?.taskData?.agentsPendingWrapUp).toEqual(['agent-123']);
|
|
1360
|
+
expect(stateMachineEvent?.taskData?.interaction?.outboundType).toBe('OUTDIAL');
|
|
1361
|
+
sendStateMachineEventSpy.mockRestore();
|
|
994
1362
|
});
|
|
995
1363
|
|
|
996
1364
|
it('should handle AGENT_OUTBOUND_FAILED gracefully when task is undefined', () => {
|
|
@@ -1008,23 +1376,15 @@ describe('TaskManager', () => {
|
|
|
1008
1376
|
});
|
|
1009
1377
|
|
|
1010
1378
|
it('should NOT remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp exists', () => {
|
|
1011
|
-
const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
|
|
1012
|
-
taskManager.setAgentId(agentId);
|
|
1013
|
-
|
|
1014
1379
|
const task = taskManager.getTask(taskId);
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
...task.data,
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
mediaType: 'telephony',
|
|
1024
|
-
},
|
|
1025
|
-
agentsPendingWrapUp: [agentId],
|
|
1026
|
-
};
|
|
1027
|
-
return task;
|
|
1380
|
+
Object.assign(task.data, {
|
|
1381
|
+
interaction: {
|
|
1382
|
+
...task.data.interaction,
|
|
1383
|
+
outboundType: 'OUTDIAL',
|
|
1384
|
+
state: 'new',
|
|
1385
|
+
mediaType: 'telephony',
|
|
1386
|
+
},
|
|
1387
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
1028
1388
|
});
|
|
1029
1389
|
task.unregisterWebCallListeners = jest.fn();
|
|
1030
1390
|
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
@@ -1038,7 +1398,7 @@ describe('TaskManager', () => {
|
|
|
1038
1398
|
state: 'new',
|
|
1039
1399
|
mediaType: 'telephony',
|
|
1040
1400
|
},
|
|
1041
|
-
agentsPendingWrapUp: [
|
|
1401
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
1042
1402
|
},
|
|
1043
1403
|
};
|
|
1044
1404
|
|
|
@@ -1050,19 +1410,14 @@ describe('TaskManager', () => {
|
|
|
1050
1410
|
|
|
1051
1411
|
it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is empty', () => {
|
|
1052
1412
|
const task = taskManager.getTask(taskId);
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
...task.data,
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
mediaType: 'telephony',
|
|
1062
|
-
},
|
|
1063
|
-
agentsPendingWrapUp: [],
|
|
1064
|
-
};
|
|
1065
|
-
return task;
|
|
1413
|
+
Object.assign(task.data, {
|
|
1414
|
+
interaction: {
|
|
1415
|
+
...task.data.interaction,
|
|
1416
|
+
outboundType: 'OUTDIAL',
|
|
1417
|
+
state: 'new',
|
|
1418
|
+
mediaType: 'telephony',
|
|
1419
|
+
},
|
|
1420
|
+
agentsPendingWrapUp: [],
|
|
1066
1421
|
});
|
|
1067
1422
|
task.unregisterWebCallListeners = jest.fn();
|
|
1068
1423
|
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
@@ -1087,19 +1442,13 @@ describe('TaskManager', () => {
|
|
|
1087
1442
|
|
|
1088
1443
|
it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is undefined', () => {
|
|
1089
1444
|
const task = taskManager.getTask(taskId);
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
...task.data,
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
state: 'new',
|
|
1098
|
-
mediaType: 'telephony',
|
|
1099
|
-
},
|
|
1100
|
-
// agentsPendingWrapUp is undefined
|
|
1101
|
-
};
|
|
1102
|
-
return task;
|
|
1445
|
+
Object.assign(task.data, {
|
|
1446
|
+
interaction: {
|
|
1447
|
+
...task.data.interaction,
|
|
1448
|
+
outboundType: 'OUTDIAL',
|
|
1449
|
+
state: 'new',
|
|
1450
|
+
mediaType: 'telephony',
|
|
1451
|
+
},
|
|
1103
1452
|
});
|
|
1104
1453
|
task.unregisterWebCallListeners = jest.fn();
|
|
1105
1454
|
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
@@ -1122,300 +1471,393 @@ describe('TaskManager', () => {
|
|
|
1122
1471
|
expect(removeTaskSpy).toHaveBeenCalled();
|
|
1123
1472
|
});
|
|
1124
1473
|
|
|
1125
|
-
it('should
|
|
1474
|
+
it('should map non-wxApp agent-terminated OUTDIAL ContactEnded to CONTACT_ENDED (not OUTBOUND_FAILED)', () => {
|
|
1475
|
+
const task = taskManager.getTask(taskId);
|
|
1476
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1477
|
+
const outdialFailedSpy = jest.fn();
|
|
1478
|
+
task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
|
|
1479
|
+
|
|
1126
1480
|
const payload = {
|
|
1127
1481
|
data: {
|
|
1128
1482
|
type: CC_EVENTS.CONTACT_ENDED,
|
|
1129
|
-
interactionId:
|
|
1483
|
+
interactionId: taskId,
|
|
1484
|
+
terminatingParty: 'Agent',
|
|
1130
1485
|
interaction: {
|
|
1131
|
-
|
|
1486
|
+
outboundType: 'OUTDIAL',
|
|
1487
|
+
state: 'wrapUp',
|
|
1488
|
+
mediaType: 'telephony',
|
|
1132
1489
|
},
|
|
1490
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
1133
1491
|
},
|
|
1134
1492
|
};
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1493
|
+
|
|
1494
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1495
|
+
|
|
1496
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONTACT_ENDED);
|
|
1497
|
+
expect(outdialFailedSpy).not.toHaveBeenCalled();
|
|
1498
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1139
1499
|
});
|
|
1140
1500
|
|
|
1141
|
-
|
|
1142
|
-
const
|
|
1501
|
+
it('should map non-wxApp agent-terminated OUTDIAL AgentOutboundFailed to OUTBOUND_FAILED with popup suppressed', () => {
|
|
1502
|
+
const task = taskManager.getTask(taskId);
|
|
1503
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1504
|
+
const outdialFailedSpy = jest.fn();
|
|
1505
|
+
task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
|
|
1143
1506
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1507
|
+
const payload = {
|
|
1508
|
+
data: {
|
|
1509
|
+
type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
|
|
1510
|
+
interactionId: taskId,
|
|
1511
|
+
terminatingParty: 'Agent',
|
|
1512
|
+
reason: 'AGENT_ENDS',
|
|
1513
|
+
interaction: {
|
|
1514
|
+
outboundType: 'OUTDIAL',
|
|
1515
|
+
state: 'wrapUp',
|
|
1516
|
+
mediaType: 'telephony',
|
|
1517
|
+
},
|
|
1518
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
1519
|
+
},
|
|
1520
|
+
};
|
|
1148
1521
|
|
|
1149
|
-
|
|
1150
|
-
const task = taskManager.getTask(taskId);
|
|
1151
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
1152
|
-
task.data = {
|
|
1153
|
-
...task.data,
|
|
1154
|
-
...newData,
|
|
1155
|
-
};
|
|
1156
|
-
return task;
|
|
1157
|
-
});
|
|
1158
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
1522
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1159
1523
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1524
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1525
|
+
sendStateMachineEventSpy,
|
|
1526
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1527
|
+
);
|
|
1528
|
+
expect(stateMachineEvent?.reason).toBe('AGENT_ENDS');
|
|
1529
|
+
expect(stateMachineEvent?.taskData?.suppressOutdialFailedPopup).toBe(true);
|
|
1530
|
+
expect(outdialFailedSpy).not.toHaveBeenCalled();
|
|
1531
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
it('should map wxApp agent-terminated OUTDIAL AgentOutboundFailed to OUTBOUND_FAILED with AGENT_ENDS', () => {
|
|
1535
|
+
const task = taskManager.getTask(taskId);
|
|
1536
|
+
task.uiControlConfig = {enableWxBetterTogether: true};
|
|
1537
|
+
task.data = {
|
|
1538
|
+
...task.data,
|
|
1539
|
+
agentId: 'test-agent-id',
|
|
1540
|
+
interaction: {
|
|
1541
|
+
...task.data.interaction,
|
|
1542
|
+
outboundType: 'OUTDIAL',
|
|
1543
|
+
participants: {
|
|
1544
|
+
'test-agent-id': {
|
|
1545
|
+
id: 'test-agent-id',
|
|
1546
|
+
deviceType: 'wxApp',
|
|
1547
|
+
deviceId: 'device-1',
|
|
1548
|
+
deviceCallId: 'call-1',
|
|
1167
1549
|
},
|
|
1168
|
-
agentsPendingWrapUp: [agentId, 'other-agent-id'],
|
|
1169
1550
|
},
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
expect.objectContaining({
|
|
1176
|
-
wrapUpRequired: true,
|
|
1177
|
-
})
|
|
1178
|
-
);
|
|
1179
|
-
});
|
|
1180
|
-
|
|
1181
|
-
it('should set wrapUpRequired to false when agent is not in agentsPendingWrapUp array', () => {
|
|
1182
|
-
const task = taskManager.getTask(taskId);
|
|
1183
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
1184
|
-
task.data = {
|
|
1185
|
-
...task.data,
|
|
1186
|
-
...newData,
|
|
1187
|
-
};
|
|
1188
|
-
return task;
|
|
1189
|
-
});
|
|
1190
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
1551
|
+
},
|
|
1552
|
+
};
|
|
1553
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1554
|
+
const outdialFailedSpy = jest.fn();
|
|
1555
|
+
task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
|
|
1191
1556
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1557
|
+
const payload = {
|
|
1558
|
+
data: {
|
|
1559
|
+
type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
|
|
1560
|
+
interactionId: taskId,
|
|
1561
|
+
terminatingParty: 'Agent',
|
|
1562
|
+
reason: 'AGENT_ENDS',
|
|
1563
|
+
interaction: {
|
|
1564
|
+
outboundType: 'OUTDIAL',
|
|
1565
|
+
state: 'wrapUp',
|
|
1566
|
+
mediaType: 'telephony',
|
|
1567
|
+
participants: {
|
|
1568
|
+
'test-agent-id': {
|
|
1569
|
+
id: 'test-agent-id',
|
|
1570
|
+
deviceType: 'wxApp',
|
|
1571
|
+
deviceId: 'device-1',
|
|
1572
|
+
deviceCallId: 'call-1',
|
|
1573
|
+
},
|
|
1199
1574
|
},
|
|
1200
|
-
agentsPendingWrapUp: ['other-agent-id', 'another-agent-id'],
|
|
1201
1575
|
},
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1576
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
1577
|
+
},
|
|
1578
|
+
};
|
|
1205
1579
|
|
|
1206
|
-
|
|
1207
|
-
expect.objectContaining({
|
|
1208
|
-
wrapUpRequired: false,
|
|
1209
|
-
})
|
|
1210
|
-
);
|
|
1211
|
-
});
|
|
1580
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1212
1581
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
});
|
|
1222
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
1582
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1583
|
+
sendStateMachineEventSpy,
|
|
1584
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1585
|
+
);
|
|
1586
|
+
expect(stateMachineEvent?.reason).toBe('AGENT_ENDS');
|
|
1587
|
+
expect(outdialFailedSpy).toHaveBeenCalledWith('AGENT_ENDS');
|
|
1588
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1589
|
+
});
|
|
1223
1590
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1591
|
+
it('should map wxApp agent-terminated OUTDIAL ContactEnded to OUTBOUND_FAILED with AGENT_ENDS', () => {
|
|
1592
|
+
const task = taskManager.getTask(taskId);
|
|
1593
|
+
task.uiControlConfig = {enableWxBetterTogether: true};
|
|
1594
|
+
task.data = {
|
|
1595
|
+
...task.data,
|
|
1596
|
+
agentId: 'test-agent-id',
|
|
1597
|
+
interaction: {
|
|
1598
|
+
...task.data.interaction,
|
|
1599
|
+
outboundType: 'OUTDIAL',
|
|
1600
|
+
participants: {
|
|
1601
|
+
'test-agent-id': {
|
|
1602
|
+
id: 'test-agent-id',
|
|
1603
|
+
deviceType: 'wxApp',
|
|
1604
|
+
deviceId: 'device-1',
|
|
1605
|
+
deviceCallId: 'call-1',
|
|
1231
1606
|
},
|
|
1232
|
-
agentsPendingWrapUp: [],
|
|
1233
1607
|
},
|
|
1234
|
-
}
|
|
1608
|
+
},
|
|
1609
|
+
};
|
|
1610
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1611
|
+
const outdialFailedSpy = jest.fn();
|
|
1612
|
+
task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
|
|
1235
1613
|
|
|
1236
|
-
|
|
1614
|
+
const payload = {
|
|
1615
|
+
data: {
|
|
1616
|
+
type: CC_EVENTS.CONTACT_ENDED,
|
|
1617
|
+
interactionId: taskId,
|
|
1618
|
+
terminatingParty: 'Agent',
|
|
1619
|
+
interaction: {
|
|
1620
|
+
outboundType: 'OUTDIAL',
|
|
1621
|
+
state: 'wrapUp',
|
|
1622
|
+
mediaType: 'telephony',
|
|
1623
|
+
participants: {
|
|
1624
|
+
'test-agent-id': {
|
|
1625
|
+
id: 'test-agent-id',
|
|
1626
|
+
deviceType: 'wxApp',
|
|
1627
|
+
deviceId: 'device-1',
|
|
1628
|
+
deviceCallId: 'call-1',
|
|
1629
|
+
},
|
|
1630
|
+
},
|
|
1631
|
+
},
|
|
1632
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
1633
|
+
},
|
|
1634
|
+
};
|
|
1237
1635
|
|
|
1238
|
-
|
|
1239
|
-
expect.objectContaining({
|
|
1240
|
-
wrapUpRequired: false,
|
|
1241
|
-
})
|
|
1242
|
-
);
|
|
1243
|
-
});
|
|
1636
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1244
1637
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
1638
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1639
|
+
sendStateMachineEventSpy,
|
|
1640
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1641
|
+
);
|
|
1642
|
+
expect(stateMachineEvent?.reason).toBe('AGENT_ENDS');
|
|
1643
|
+
expect(stateMachineEvent?.taskData?.wrapUpRequired).toBe(true);
|
|
1644
|
+
expect(outdialFailedSpy).toHaveBeenCalledWith('AGENT_ENDS');
|
|
1645
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1646
|
+
});
|
|
1255
1647
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1648
|
+
it('should map offer-stage OUTDIAL decline ContactEnded to CONTACT_ENDED (not wrapup)', () => {
|
|
1649
|
+
const task = taskManager.getTask(taskId);
|
|
1650
|
+
task.stateMachineService = {
|
|
1651
|
+
getSnapshot: () => ({value: 'OFFERED'}),
|
|
1652
|
+
};
|
|
1653
|
+
task.uiControlConfig = {enableWxBetterTogether: true, wxAppAnswerPending: false};
|
|
1654
|
+
task.data = {
|
|
1655
|
+
...task.data,
|
|
1656
|
+
agentId: 'test-agent-id',
|
|
1657
|
+
interaction: {
|
|
1658
|
+
...task.data.interaction,
|
|
1659
|
+
outboundType: 'OUTDIAL',
|
|
1660
|
+
participants: {
|
|
1661
|
+
'test-agent-id': {
|
|
1662
|
+
id: 'test-agent-id',
|
|
1663
|
+
deviceType: 'wxApp',
|
|
1664
|
+
deviceId: 'device-1',
|
|
1665
|
+
deviceCallId: 'call-1',
|
|
1263
1666
|
},
|
|
1264
|
-
// agentsPendingWrapUp is not defined
|
|
1265
1667
|
},
|
|
1266
|
-
}
|
|
1668
|
+
},
|
|
1669
|
+
};
|
|
1670
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1671
|
+
const outdialFailedSpy = jest.fn();
|
|
1672
|
+
task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
|
|
1267
1673
|
|
|
1268
|
-
|
|
1674
|
+
const payload = {
|
|
1675
|
+
data: {
|
|
1676
|
+
type: CC_EVENTS.CONTACT_ENDED,
|
|
1677
|
+
interactionId: taskId,
|
|
1678
|
+
terminatingParty: 'Agent',
|
|
1679
|
+
interaction: {
|
|
1680
|
+
outboundType: 'OUTDIAL',
|
|
1681
|
+
state: 'wrapUp',
|
|
1682
|
+
mediaType: 'telephony',
|
|
1683
|
+
},
|
|
1684
|
+
agentsPendingWrapUp: [],
|
|
1685
|
+
},
|
|
1686
|
+
};
|
|
1269
1687
|
|
|
1270
|
-
|
|
1271
|
-
expect.objectContaining({
|
|
1272
|
-
wrapUpRequired: false,
|
|
1273
|
-
})
|
|
1274
|
-
);
|
|
1275
|
-
});
|
|
1688
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1276
1689
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
...task.data,
|
|
1282
|
-
...newData,
|
|
1283
|
-
};
|
|
1284
|
-
return task;
|
|
1285
|
-
});
|
|
1286
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
1690
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONTACT_ENDED);
|
|
1691
|
+
expect(outdialFailedSpy).not.toHaveBeenCalled();
|
|
1692
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1693
|
+
});
|
|
1287
1694
|
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1695
|
+
it('should map post-accept OUTDIAL ContactEnded to OUTBOUND_FAILED when still OFFERED with wxAppAnswerPending', () => {
|
|
1696
|
+
const task = taskManager.getTask(taskId);
|
|
1697
|
+
task.stateMachineService = {
|
|
1698
|
+
getSnapshot: () => ({value: 'OFFERED'}),
|
|
1699
|
+
};
|
|
1700
|
+
task.uiControlConfig = {enableWxBetterTogether: true, wxAppAnswerPending: true};
|
|
1701
|
+
task.data = {
|
|
1702
|
+
...task.data,
|
|
1703
|
+
agentId: 'test-agent-id',
|
|
1704
|
+
interaction: {
|
|
1705
|
+
...task.data.interaction,
|
|
1706
|
+
outboundType: 'OUTDIAL',
|
|
1707
|
+
participants: {
|
|
1708
|
+
'test-agent-id': {
|
|
1709
|
+
id: 'test-agent-id',
|
|
1710
|
+
deviceType: 'wxApp',
|
|
1711
|
+
deviceId: 'device-1',
|
|
1712
|
+
deviceCallId: 'call-1',
|
|
1295
1713
|
},
|
|
1296
|
-
agentsPendingWrapUp: null,
|
|
1297
1714
|
},
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
expect.objectContaining({
|
|
1304
|
-
wrapUpRequired: false,
|
|
1305
|
-
})
|
|
1306
|
-
);
|
|
1307
|
-
});
|
|
1308
|
-
|
|
1309
|
-
it('should set wrapUpRequired correctly when agent is the only one in agentsPendingWrapUp', () => {
|
|
1310
|
-
const task = taskManager.getTask(taskId);
|
|
1311
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
1312
|
-
task.data = {
|
|
1313
|
-
...task.data,
|
|
1314
|
-
...newData,
|
|
1315
|
-
};
|
|
1316
|
-
return task;
|
|
1317
|
-
});
|
|
1318
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
1715
|
+
},
|
|
1716
|
+
};
|
|
1717
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1718
|
+
const outdialFailedSpy = jest.fn();
|
|
1719
|
+
task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
|
|
1319
1720
|
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1721
|
+
const payload = {
|
|
1722
|
+
data: {
|
|
1723
|
+
type: CC_EVENTS.CONTACT_ENDED,
|
|
1724
|
+
interactionId: taskId,
|
|
1725
|
+
terminatingParty: 'Agent',
|
|
1726
|
+
interaction: {
|
|
1727
|
+
outboundType: 'OUTDIAL',
|
|
1728
|
+
state: 'offered',
|
|
1729
|
+
mediaType: 'telephony',
|
|
1730
|
+
participants: {
|
|
1731
|
+
'test-agent-id': {
|
|
1732
|
+
id: 'test-agent-id',
|
|
1733
|
+
deviceType: 'wxApp',
|
|
1734
|
+
deviceId: 'device-1',
|
|
1735
|
+
deviceCallId: 'call-1',
|
|
1736
|
+
},
|
|
1327
1737
|
},
|
|
1328
|
-
agentsPendingWrapUp: [agentId],
|
|
1329
1738
|
},
|
|
1330
|
-
}
|
|
1739
|
+
},
|
|
1740
|
+
};
|
|
1331
1741
|
|
|
1332
|
-
|
|
1742
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1333
1743
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1744
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1745
|
+
sendStateMachineEventSpy,
|
|
1746
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1747
|
+
);
|
|
1748
|
+
expect(stateMachineEvent?.reason).toBe('AGENT_ENDS');
|
|
1749
|
+
expect(outdialFailedSpy).toHaveBeenCalledWith('AGENT_ENDS');
|
|
1750
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1751
|
+
});
|
|
1340
1752
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1753
|
+
it('should map post-accept agent-terminated OUTDIAL ContactEnded to OUTBOUND_FAILED when CONNECTED', () => {
|
|
1754
|
+
const task = taskManager.getTask(taskId);
|
|
1755
|
+
task.stateMachineService = {
|
|
1756
|
+
getSnapshot: () => ({value: 'CONNECTED'}),
|
|
1757
|
+
};
|
|
1758
|
+
task.uiControlConfig = {enableWxBetterTogether: true};
|
|
1759
|
+
task.data = {
|
|
1760
|
+
...task.data,
|
|
1761
|
+
agentId: 'test-agent-id',
|
|
1762
|
+
interaction: {
|
|
1763
|
+
...task.data.interaction,
|
|
1764
|
+
outboundType: 'OUTDIAL',
|
|
1765
|
+
participants: {
|
|
1766
|
+
'test-agent-id': {
|
|
1767
|
+
id: 'test-agent-id',
|
|
1768
|
+
deviceType: 'wxApp',
|
|
1769
|
+
deviceId: 'device-1',
|
|
1770
|
+
deviceCallId: 'call-1',
|
|
1350
1771
|
},
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1772
|
+
},
|
|
1773
|
+
},
|
|
1774
|
+
};
|
|
1775
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1776
|
+
const outdialFailedSpy = jest.fn();
|
|
1777
|
+
task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
|
|
1355
1778
|
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1779
|
+
const payload = {
|
|
1780
|
+
data: {
|
|
1781
|
+
type: CC_EVENTS.CONTACT_ENDED,
|
|
1782
|
+
interactionId: taskId,
|
|
1783
|
+
terminatingParty: 'Agent',
|
|
1784
|
+
interaction: {
|
|
1785
|
+
outboundType: 'OUTDIAL',
|
|
1786
|
+
state: 'wrapUp',
|
|
1787
|
+
mediaType: 'telephony',
|
|
1788
|
+
participants: {
|
|
1789
|
+
'test-agent-id': {
|
|
1790
|
+
id: 'test-agent-id',
|
|
1791
|
+
deviceType: 'wxApp',
|
|
1792
|
+
deviceId: 'device-1',
|
|
1793
|
+
deviceCallId: 'call-1',
|
|
1794
|
+
},
|
|
1364
1795
|
},
|
|
1365
|
-
agentsPendingWrapUp: [agentId],
|
|
1366
1796
|
},
|
|
1367
|
-
|
|
1797
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
1798
|
+
},
|
|
1799
|
+
};
|
|
1368
1800
|
|
|
1369
|
-
|
|
1801
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1370
1802
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1803
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1804
|
+
sendStateMachineEventSpy,
|
|
1805
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1806
|
+
);
|
|
1807
|
+
expect(stateMachineEvent?.reason).toBe('AGENT_ENDS');
|
|
1808
|
+
expect(outdialFailedSpy).toHaveBeenCalledWith('AGENT_ENDS');
|
|
1809
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1810
|
+
});
|
|
1378
1811
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1812
|
+
it('should map customer-terminated OUTDIAL ContactEnded to CONTACT_ENDED (no AGENT_ENDS remap)', () => {
|
|
1813
|
+
const task = taskManager.getTask(taskId);
|
|
1814
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1815
|
+
|
|
1816
|
+
const payload = {
|
|
1817
|
+
data: {
|
|
1818
|
+
type: CC_EVENTS.CONTACT_ENDED,
|
|
1819
|
+
interactionId: taskId,
|
|
1820
|
+
terminatingParty: 'Customer',
|
|
1821
|
+
interaction: {
|
|
1822
|
+
outboundType: 'OUTDIAL',
|
|
1823
|
+
state: 'new',
|
|
1824
|
+
mediaType: 'telephony',
|
|
1389
1825
|
},
|
|
1390
|
-
|
|
1826
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
1827
|
+
},
|
|
1828
|
+
};
|
|
1391
1829
|
|
|
1392
|
-
|
|
1830
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1393
1831
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
2,
|
|
1397
|
-
expect.objectContaining({
|
|
1398
|
-
wrapUpRequired: true,
|
|
1399
|
-
})
|
|
1400
|
-
);
|
|
1401
|
-
});
|
|
1832
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONTACT_ENDED);
|
|
1833
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1402
1834
|
});
|
|
1403
1835
|
|
|
1404
|
-
it('should
|
|
1405
|
-
const
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
...newData,
|
|
1836
|
+
it('should handle CONTACT_ENDED gracefully when task is undefined', () => {
|
|
1837
|
+
const payload = {
|
|
1838
|
+
data: {
|
|
1839
|
+
type: CC_EVENTS.CONTACT_ENDED,
|
|
1840
|
+
interactionId: 'non-existent-task-id',
|
|
1410
1841
|
interaction: {
|
|
1411
|
-
...task.data.interaction,
|
|
1412
|
-
...newData.interaction,
|
|
1413
|
-
outboundType: 'OUTDIAL',
|
|
1414
1842
|
state: 'new',
|
|
1415
|
-
isTerminated: false,
|
|
1416
1843
|
},
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1844
|
+
},
|
|
1845
|
+
};
|
|
1846
|
+
// Should not throw error when task doesn't exist
|
|
1847
|
+
expect(() => {
|
|
1848
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1849
|
+
}).not.toThrow();
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
it('should remove OUTDIAL task from taskCollection on AGENT_CONTACT_ASSIGN_FAILED when NOT terminated (user-declined)', () => {
|
|
1853
|
+
const task = taskManager.getTask(taskId);
|
|
1854
|
+
Object.assign(task.data, {
|
|
1855
|
+
interaction: {
|
|
1856
|
+
...task.data.interaction,
|
|
1857
|
+
outboundType: 'OUTDIAL',
|
|
1858
|
+
state: 'new',
|
|
1859
|
+
isTerminated: false,
|
|
1860
|
+
},
|
|
1419
1861
|
});
|
|
1420
1862
|
task.unregisterWebCallListeners = jest.fn();
|
|
1421
1863
|
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
@@ -1459,20 +1901,16 @@ describe('TaskManager', () => {
|
|
|
1459
1901
|
|
|
1460
1902
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1461
1903
|
const task = taskManager.getTask(taskId);
|
|
1462
|
-
|
|
1463
|
-
task.data = {...newData, isConsulted: true};
|
|
1464
|
-
return task;
|
|
1465
|
-
});
|
|
1466
|
-
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
1904
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1467
1905
|
|
|
1468
1906
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1469
1907
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
expect(
|
|
1475
|
-
|
|
1908
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1909
|
+
sendStateMachineEventSpy,
|
|
1910
|
+
TaskEvent.OFFER_CONSULT
|
|
1911
|
+
);
|
|
1912
|
+
expect(stateMachineEvent?.taskData).toEqual({...payload.data, isConsulted: true});
|
|
1913
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1476
1914
|
});
|
|
1477
1915
|
|
|
1478
1916
|
it('should emit TASK_CONSULT_ACCEPTED event on AGENT_CONSULTING event', () => {
|
|
@@ -1491,19 +1929,18 @@ describe('TaskManager', () => {
|
|
|
1491
1929
|
};
|
|
1492
1930
|
|
|
1493
1931
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1494
|
-
taskManager.getTask(taskId).updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
1495
|
-
taskManager.getTask(taskId).data = {...newData, isConsulted: true};
|
|
1496
|
-
return taskManager.getTask(taskId);
|
|
1497
|
-
});
|
|
1498
1932
|
|
|
1499
|
-
const
|
|
1933
|
+
const task = taskManager.getTask(taskId);
|
|
1934
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1935
|
+
|
|
1500
1936
|
webSocketManagerMock.emit('message', JSON.stringify(initialConsultingPayload));
|
|
1501
1937
|
webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
taskManager.getTask(taskId)
|
|
1938
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1939
|
+
sendStateMachineEventSpy,
|
|
1940
|
+
TaskEvent.CONSULTING_ACTIVE
|
|
1506
1941
|
);
|
|
1942
|
+
expect(stateMachineEvent?.taskData).toEqual(consultingPayload.data);
|
|
1943
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1507
1944
|
});
|
|
1508
1945
|
|
|
1509
1946
|
it('should emit TASK_CONSULT_ENDED event on AGENT_CONSULT_ENDED event', () => {
|
|
@@ -1515,14 +1952,16 @@ describe('TaskManager', () => {
|
|
|
1515
1952
|
};
|
|
1516
1953
|
|
|
1517
1954
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1518
|
-
|
|
1519
|
-
const
|
|
1955
|
+
taskManager.getTask(taskId).data.isConsulted = true;
|
|
1956
|
+
const task = taskManager.getTask(taskId);
|
|
1957
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1520
1958
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
taskManager.getTask(taskId)
|
|
1959
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1960
|
+
sendStateMachineEventSpy,
|
|
1961
|
+
TaskEvent.CONSULT_END
|
|
1525
1962
|
);
|
|
1963
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
1964
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1526
1965
|
});
|
|
1527
1966
|
|
|
1528
1967
|
it('should emit TASK_CONSULT_ENDED event and remove currentTask when on AGENT_CONSULT_ENDED event when requested for a consult', () => {
|
|
@@ -1535,19 +1974,14 @@ describe('TaskManager', () => {
|
|
|
1535
1974
|
|
|
1536
1975
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1537
1976
|
|
|
1538
|
-
taskManager.getTask(taskId).
|
|
1539
|
-
taskManager.getTask(taskId).data = {...newData, isConsulted: true};
|
|
1540
|
-
return taskManager.getTask(taskId);
|
|
1541
|
-
});
|
|
1977
|
+
taskManager.getTask(taskId).data.isConsulted = true;
|
|
1542
1978
|
const task = taskManager.getTask(taskId);
|
|
1543
1979
|
|
|
1544
|
-
const
|
|
1545
|
-
const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
|
|
1980
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1546
1981
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1547
|
-
|
|
1548
|
-
expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.AGENT_CONSULT_ENDED, payload.data);
|
|
1549
|
-
expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONSULT_END, task);
|
|
1982
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONSULT_END);
|
|
1550
1983
|
expect(taskManager.getTask(taskId)).toBeUndefined(); // Ensure task is removed from the task collection after the consult ends
|
|
1984
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1551
1985
|
});
|
|
1552
1986
|
|
|
1553
1987
|
it('should emit TASK_CANCELLED event on AGENT_CTQ_CANCELLED event', () => {
|
|
@@ -1559,14 +1993,15 @@ describe('TaskManager', () => {
|
|
|
1559
1993
|
};
|
|
1560
1994
|
|
|
1561
1995
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1562
|
-
const
|
|
1563
|
-
const
|
|
1996
|
+
const task = taskManager.getTask(taskId);
|
|
1997
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1564
1998
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
taskManager.getTask(taskId)
|
|
1999
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
2000
|
+
sendStateMachineEventSpy,
|
|
2001
|
+
TaskEvent.CTQ_CANCEL
|
|
1569
2002
|
);
|
|
2003
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
2004
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1570
2005
|
});
|
|
1571
2006
|
|
|
1572
2007
|
it('should handle AGENT_CONSULT_FAILED event', () => {
|
|
@@ -1580,9 +2015,15 @@ describe('TaskManager', () => {
|
|
|
1580
2015
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1581
2016
|
|
|
1582
2017
|
// Always spy on the updated task object after CONTACT_RESERVED is emitted
|
|
1583
|
-
const
|
|
2018
|
+
const task = taskManager.getTask(taskId);
|
|
2019
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1584
2020
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1585
|
-
|
|
2021
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
2022
|
+
sendStateMachineEventSpy,
|
|
2023
|
+
TaskEvent.CONSULT_FAILED
|
|
2024
|
+
);
|
|
2025
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
2026
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1586
2027
|
});
|
|
1587
2028
|
|
|
1588
2029
|
it('should emit TASK_CONSULT_QUEUE_FAILED on AGENT_CTQ_CANCEL_FAILED event', () => {
|
|
@@ -1594,14 +2035,15 @@ describe('TaskManager', () => {
|
|
|
1594
2035
|
};
|
|
1595
2036
|
|
|
1596
2037
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1597
|
-
const
|
|
1598
|
-
const
|
|
2038
|
+
const task = taskManager.getTask(taskId);
|
|
2039
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1599
2040
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
taskManager.getTask(taskId)
|
|
2041
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
2042
|
+
sendStateMachineEventSpy,
|
|
2043
|
+
TaskEvent.CTQ_CANCEL_FAILED
|
|
1604
2044
|
);
|
|
2045
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
2046
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1605
2047
|
});
|
|
1606
2048
|
|
|
1607
2049
|
it('should emit TASK_REJECT event on AGENT_CONTACT_OFFER_RONA event', () => {
|
|
@@ -1644,15 +2086,14 @@ describe('TaskManager', () => {
|
|
|
1644
2086
|
};
|
|
1645
2087
|
|
|
1646
2088
|
taskManager.taskCollection[taskId] = taskManager.getTask(taskId);
|
|
1647
|
-
const
|
|
1648
|
-
const
|
|
2089
|
+
const task = taskManager.getTask(taskId);
|
|
2090
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1649
2091
|
|
|
1650
2092
|
webSocketManagerMock.emit('message', JSON.stringify(ronaPayload));
|
|
1651
2093
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
expect(metricsTrackSpy.mock.calls[0][0]).toBe('Agent RONA');
|
|
2094
|
+
const stateMachineEvent = expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.RONA);
|
|
2095
|
+
expect(stateMachineEvent?.reason).toBe(ronaPayload.data.reason);
|
|
2096
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1656
2097
|
});
|
|
1657
2098
|
|
|
1658
2099
|
it('should emit TASK_REJECT event on AGENT_CONTACT_ASSIGN_FAILED event', () => {
|
|
@@ -1696,20 +2137,16 @@ describe('TaskManager', () => {
|
|
|
1696
2137
|
|
|
1697
2138
|
taskManager.taskCollection[taskId] = taskManager.getTask(taskId);
|
|
1698
2139
|
const task = taskManager.getTask(taskId);
|
|
1699
|
-
const
|
|
1700
|
-
const taskUpdateDataSpy = jest.spyOn(task, 'updateTaskData');
|
|
1701
|
-
const metricsTrackSpy = jest.spyOn(taskManager.metricsManager, 'trackEvent');
|
|
2140
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1702
2141
|
|
|
1703
2142
|
webSocketManagerMock.emit('message', JSON.stringify(assignFailedPayload));
|
|
1704
2143
|
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
assignFailedPayload.data.reason
|
|
2144
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
2145
|
+
sendStateMachineEventSpy,
|
|
2146
|
+
TaskEvent.ASSIGN_FAILED
|
|
1709
2147
|
);
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
expect(metricsTrackSpy.mock.calls[0][0]).toBe('Agent Contact Assign Failed');
|
|
2148
|
+
expect(stateMachineEvent?.reason).toBe(assignFailedPayload.data.reason);
|
|
2149
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1713
2150
|
});
|
|
1714
2151
|
|
|
1715
2152
|
it('should remove currentTask from taskCollection on AGENT_WRAPPEDUP event', () => {
|
|
@@ -1732,11 +2169,12 @@ describe('TaskManager', () => {
|
|
|
1732
2169
|
|
|
1733
2170
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1734
2171
|
const task = taskManager.getTask(taskId);
|
|
1735
|
-
const
|
|
2172
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1736
2173
|
|
|
1737
2174
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1738
2175
|
|
|
1739
|
-
|
|
2176
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.WRAPUP_COMPLETE);
|
|
2177
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1740
2178
|
expect(taskManager.getTask(taskId)).toBeUndefined();
|
|
1741
2179
|
});
|
|
1742
2180
|
|
|
@@ -1762,7 +2200,8 @@ describe('TaskManager', () => {
|
|
|
1762
2200
|
};
|
|
1763
2201
|
|
|
1764
2202
|
const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
|
|
1765
|
-
const taskUpdateTaskDataSpy =
|
|
2203
|
+
const taskUpdateTaskDataSpy = taskManager.getTask(taskId).updateTaskData as jest.Mock;
|
|
2204
|
+
taskUpdateTaskDataSpy.mockClear();
|
|
1766
2205
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1767
2206
|
expect(taskEmitSpy).not.toHaveBeenCalled();
|
|
1768
2207
|
expect(taskUpdateTaskDataSpy).not.toHaveBeenCalled();
|
|
@@ -1771,7 +2210,9 @@ describe('TaskManager', () => {
|
|
|
1771
2210
|
it('should emit TASK_CONSULTING event when agent is consulting', () => {
|
|
1772
2211
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1773
2212
|
taskManager.getTask(taskId).data.isConsulted = false;
|
|
1774
|
-
const
|
|
2213
|
+
const task = taskManager.getTask(taskId);
|
|
2214
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
2215
|
+
const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
|
|
1775
2216
|
const consultingPayload = {
|
|
1776
2217
|
data: {
|
|
1777
2218
|
...initalPayload.data,
|
|
@@ -1780,16 +2221,21 @@ describe('TaskManager', () => {
|
|
|
1780
2221
|
},
|
|
1781
2222
|
};
|
|
1782
2223
|
webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
|
|
1783
|
-
|
|
1784
|
-
expect(
|
|
1785
|
-
TASK_EVENTS.
|
|
2224
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONSULTING_ACTIVE);
|
|
2225
|
+
expect(taskManagerEmitSpy).toHaveBeenCalledWith(
|
|
2226
|
+
TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
|
|
1786
2227
|
taskManager.getTask(taskId)
|
|
1787
2228
|
);
|
|
2229
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1788
2230
|
});
|
|
1789
2231
|
|
|
1790
|
-
it('should
|
|
2232
|
+
it('should update task data on AGENT_CONTACT_UNASSIGNED', () => {
|
|
1791
2233
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
1792
|
-
const
|
|
2234
|
+
const task = taskManager.getTask(taskId);
|
|
2235
|
+
const sendStateMachineEventSpy = task.sendStateMachineEvent as jest.Mock;
|
|
2236
|
+
sendStateMachineEventSpy.mockClear();
|
|
2237
|
+
const updateTaskDataSpy = task.updateTaskData as jest.Mock;
|
|
2238
|
+
updateTaskDataSpy.mockClear();
|
|
1793
2239
|
const unassignedPayload = {
|
|
1794
2240
|
data: {
|
|
1795
2241
|
type: CC_EVENTS.AGENT_CONTACT_UNASSIGNED,
|
|
@@ -1807,11 +2253,76 @@ describe('TaskManager', () => {
|
|
|
1807
2253
|
},
|
|
1808
2254
|
};
|
|
1809
2255
|
webSocketManagerMock.emit('message', JSON.stringify(unassignedPayload));
|
|
1810
|
-
expect(
|
|
1811
|
-
|
|
1812
|
-
|
|
2256
|
+
expect(sendStateMachineEventSpy).not.toHaveBeenCalled();
|
|
2257
|
+
expect(updateTaskDataSpy).toHaveBeenCalledWith(unassignedPayload.data);
|
|
2258
|
+
});
|
|
2259
|
+
|
|
2260
|
+
it('preserves consult fields from state context during consulting payload refresh', () => {
|
|
2261
|
+
const task = createMockTask({
|
|
2262
|
+
...taskDataMock,
|
|
2263
|
+
interaction: {state: 'consulting'} as any,
|
|
2264
|
+
interactionId: taskId,
|
|
2265
|
+
});
|
|
2266
|
+
task.stateMachineService = {
|
|
2267
|
+
getSnapshot: () => ({
|
|
2268
|
+
value: 'CONSULTING',
|
|
2269
|
+
context: {
|
|
2270
|
+
consultDestinationAgentId: 'agent-preserved',
|
|
2271
|
+
consultDestinationType: 'agent',
|
|
2272
|
+
},
|
|
2273
|
+
}),
|
|
2274
|
+
};
|
|
2275
|
+
|
|
2276
|
+
const incomingTaskData = {
|
|
2277
|
+
...taskDataMock,
|
|
2278
|
+
interaction: {state: 'consulting'} as any,
|
|
2279
|
+
interactionId: taskId,
|
|
2280
|
+
destAgentId: null,
|
|
2281
|
+
destinationType: null,
|
|
2282
|
+
};
|
|
2283
|
+
|
|
2284
|
+
(taskManager as any).updateTaskData(task, incomingTaskData);
|
|
2285
|
+
|
|
2286
|
+
expect(task.updateTaskData).toHaveBeenCalledWith(
|
|
2287
|
+
expect.objectContaining({
|
|
2288
|
+
destAgentId: 'agent-preserved',
|
|
2289
|
+
destinationType: 'agent',
|
|
2290
|
+
})
|
|
2291
|
+
);
|
|
2292
|
+
});
|
|
2293
|
+
|
|
2294
|
+
it('does not preserve stale consult fields once consult is no longer active', () => {
|
|
2295
|
+
const task = createMockTask({
|
|
2296
|
+
...taskDataMock,
|
|
2297
|
+
interaction: {state: 'connected'} as any,
|
|
2298
|
+
interactionId: taskId,
|
|
2299
|
+
});
|
|
2300
|
+
task.stateMachineService = {
|
|
2301
|
+
getSnapshot: () => ({
|
|
2302
|
+
value: 'CONNECTED',
|
|
2303
|
+
context: {
|
|
2304
|
+
consultDestinationAgentId: 'agent-stale',
|
|
2305
|
+
consultDestinationType: 'agent',
|
|
2306
|
+
},
|
|
2307
|
+
}),
|
|
2308
|
+
};
|
|
2309
|
+
|
|
2310
|
+
const incomingTaskData = {
|
|
2311
|
+
...taskDataMock,
|
|
2312
|
+
interaction: {state: 'connected'} as any,
|
|
2313
|
+
interactionId: taskId,
|
|
2314
|
+
destAgentId: null,
|
|
2315
|
+
destinationType: null,
|
|
2316
|
+
};
|
|
2317
|
+
|
|
2318
|
+
(taskManager as any).updateTaskData(task, incomingTaskData);
|
|
2319
|
+
|
|
2320
|
+
expect(task.updateTaskData).toHaveBeenCalledWith(
|
|
2321
|
+
expect.objectContaining({
|
|
2322
|
+
destAgentId: null,
|
|
2323
|
+
destinationType: null,
|
|
2324
|
+
})
|
|
1813
2325
|
);
|
|
1814
|
-
expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, taskManager.getTask(taskId));
|
|
1815
2326
|
});
|
|
1816
2327
|
|
|
1817
2328
|
it('should handle chat interaction and emit TASK_INCOMING immediately', () => {
|
|
@@ -1823,16 +2334,16 @@ describe('TaskManager', () => {
|
|
|
1823
2334
|
},
|
|
1824
2335
|
};
|
|
1825
2336
|
|
|
1826
|
-
const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
|
|
1827
|
-
|
|
1828
2337
|
// Simulate receiving a chat task
|
|
1829
2338
|
webSocketManagerMock.emit('message', JSON.stringify(chatPayload));
|
|
1830
2339
|
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
2340
|
+
const chatTask = taskManager.getTask(chatPayload.data.interactionId);
|
|
2341
|
+
const sendStateMachineEventSpy = chatTask.sendStateMachineEvent as jest.Mock;
|
|
2342
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
2343
|
+
sendStateMachineEventSpy,
|
|
2344
|
+
TaskEvent.TASK_INCOMING
|
|
1835
2345
|
);
|
|
2346
|
+
expect(stateMachineEvent?.taskData).toEqual(chatPayload.data);
|
|
1836
2347
|
expect(taskManager.getAllTasks()).toHaveProperty(chatPayload.data.interactionId);
|
|
1837
2348
|
});
|
|
1838
2349
|
|
|
@@ -1845,16 +2356,16 @@ describe('TaskManager', () => {
|
|
|
1845
2356
|
},
|
|
1846
2357
|
};
|
|
1847
2358
|
|
|
1848
|
-
const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
|
|
1849
|
-
|
|
1850
2359
|
// Simulate receiving an email task
|
|
1851
2360
|
webSocketManagerMock.emit('message', JSON.stringify(emailPayload));
|
|
1852
2361
|
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2362
|
+
const emailTask = taskManager.getTask(emailPayload.data.interactionId);
|
|
2363
|
+
const sendStateMachineEventSpy = emailTask.sendStateMachineEvent as jest.Mock;
|
|
2364
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
2365
|
+
sendStateMachineEventSpy,
|
|
2366
|
+
TaskEvent.TASK_INCOMING
|
|
1857
2367
|
);
|
|
2368
|
+
expect(stateMachineEvent?.taskData).toEqual(emailPayload.data);
|
|
1858
2369
|
expect(taskManager.getAllTasks()).toHaveProperty(emailPayload.data.interactionId);
|
|
1859
2370
|
});
|
|
1860
2371
|
|
|
@@ -1868,13 +2379,11 @@ describe('TaskManager', () => {
|
|
|
1868
2379
|
},
|
|
1869
2380
|
};
|
|
1870
2381
|
|
|
1871
|
-
const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
|
|
1872
2382
|
webSocketManagerMock.emit('message', JSON.stringify(chatReservedPayload));
|
|
2383
|
+
const task = taskManager.getTask(chatReservedPayload.data.interactionId);
|
|
2384
|
+
const sendStateMachineEventSpy = task.sendStateMachineEvent as jest.Mock;
|
|
1873
2385
|
|
|
1874
|
-
|
|
1875
|
-
TASK_EVENTS.TASK_INCOMING,
|
|
1876
|
-
taskManager.getTask(chatReservedPayload.data.interactionId)
|
|
1877
|
-
);
|
|
2386
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TASK_INCOMING);
|
|
1878
2387
|
|
|
1879
2388
|
// 2. Chat task is assigned
|
|
1880
2389
|
const chatAssignedPayload = {
|
|
@@ -1884,12 +2393,9 @@ describe('TaskManager', () => {
|
|
|
1884
2393
|
},
|
|
1885
2394
|
};
|
|
1886
2395
|
|
|
1887
|
-
const task = taskManager.getTask(chatReservedPayload.data.interactionId);
|
|
1888
|
-
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
1889
|
-
|
|
1890
2396
|
webSocketManagerMock.emit('message', JSON.stringify(chatAssignedPayload));
|
|
1891
2397
|
|
|
1892
|
-
|
|
2398
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.ASSIGN);
|
|
1893
2399
|
|
|
1894
2400
|
// 3. Chat task is ended with state 'new' to trigger cleanup
|
|
1895
2401
|
const chatEndedPayload = {
|
|
@@ -1897,12 +2403,15 @@ describe('TaskManager', () => {
|
|
|
1897
2403
|
...chatReservedPayload.data,
|
|
1898
2404
|
type: CC_EVENTS.CONTACT_ENDED,
|
|
1899
2405
|
interaction: {mediaType: 'chat', state: 'new'}, // Change to 'new' state
|
|
2406
|
+
wrapUpRequired: false,
|
|
1900
2407
|
},
|
|
1901
2408
|
};
|
|
1902
2409
|
|
|
2410
|
+
// Simulate state on the task to allow cleanup logic
|
|
2411
|
+
task.data.interaction.state = 'new';
|
|
1903
2412
|
webSocketManagerMock.emit('message', JSON.stringify(chatEndedPayload));
|
|
1904
2413
|
|
|
1905
|
-
|
|
2414
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONTACT_ENDED);
|
|
1906
2415
|
// Verify task is removed since it was in a 'new' state
|
|
1907
2416
|
expect(taskManager.getTask(chatReservedPayload.data.interactionId)).toBeUndefined();
|
|
1908
2417
|
});
|
|
@@ -1991,13 +2500,8 @@ describe('TaskManager', () => {
|
|
|
1991
2500
|
expect(taskManager.getAllTasks()).toHaveProperty(task2Payload.data.interactionId);
|
|
1992
2501
|
expect(taskManager.getAllTasks()).toHaveProperty(task3Payload.data.interactionId);
|
|
1993
2502
|
|
|
1994
|
-
// Create spies for all tasks
|
|
1995
|
-
const task1EmitSpy = jest.spyOn(taskManager.getTask(task1Payload.data.interactionId), 'emit');
|
|
1996
|
-
const task2EmitSpy = jest.spyOn(taskManager.getTask(task2Payload.data.interactionId), 'emit');
|
|
1997
|
-
const task3EmitSpy = jest.spyOn(taskManager.getTask(task3Payload.data.interactionId), 'emit');
|
|
1998
|
-
|
|
1999
|
-
// Store reference to task2 before it gets removed
|
|
2000
2503
|
const task2 = taskManager.getTask(task2Payload.data.interactionId);
|
|
2504
|
+
const task2SendStateMachineEventSpy = task2.sendStateMachineEvent as jest.Mock;
|
|
2001
2505
|
|
|
2002
2506
|
// End only the second task (chat task)
|
|
2003
2507
|
const chatEndedPayload = {
|
|
@@ -2005,15 +2509,18 @@ describe('TaskManager', () => {
|
|
|
2005
2509
|
...task2Payload.data,
|
|
2006
2510
|
type: CC_EVENTS.CONTACT_ENDED,
|
|
2007
2511
|
interaction: {mediaType: 'chat', state: 'new'}, // Using 'new' to trigger cleanup
|
|
2512
|
+
wrapUpRequired: false,
|
|
2008
2513
|
},
|
|
2009
2514
|
};
|
|
2010
2515
|
|
|
2516
|
+
task2.data.interaction.state = 'new';
|
|
2011
2517
|
webSocketManagerMock.emit('message', JSON.stringify(chatEndedPayload));
|
|
2012
2518
|
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2519
|
+
const firstEndEvent = expectLastStateMachineEvent(
|
|
2520
|
+
task2SendStateMachineEventSpy,
|
|
2521
|
+
TaskEvent.CONTACT_ENDED
|
|
2522
|
+
);
|
|
2523
|
+
expect(firstEndEvent?.taskData).toEqual(chatEndedPayload.data);
|
|
2017
2524
|
|
|
2018
2525
|
// Verify task2 was removed from collection (since state was 'new')
|
|
2019
2526
|
expect(taskManager.getTask(task2Payload.data.interactionId)).toBeUndefined();
|
|
@@ -2024,6 +2531,7 @@ describe('TaskManager', () => {
|
|
|
2024
2531
|
|
|
2025
2532
|
// Store reference to task3 before we end it
|
|
2026
2533
|
const task3 = taskManager.getTask(task3Payload.data.interactionId);
|
|
2534
|
+
const task3SendStateMachineEventSpy = task3.sendStateMachineEvent as jest.Mock;
|
|
2027
2535
|
|
|
2028
2536
|
// Now end task3 with a state that doesn't trigger cleanup
|
|
2029
2537
|
const emailEndedPayload = {
|
|
@@ -2031,31 +2539,32 @@ describe('TaskManager', () => {
|
|
|
2031
2539
|
...task3Payload.data,
|
|
2032
2540
|
type: CC_EVENTS.CONTACT_ENDED,
|
|
2033
2541
|
interaction: {mediaType: 'email', state: 'connected'}, // Using 'connected' to NOT trigger cleanup
|
|
2542
|
+
wrapUpRequired: true,
|
|
2543
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
2034
2544
|
},
|
|
2035
2545
|
};
|
|
2036
2546
|
|
|
2547
|
+
task3.data.interaction.state = 'connected';
|
|
2037
2548
|
webSocketManagerMock.emit('message', JSON.stringify(emailEndedPayload));
|
|
2038
2549
|
|
|
2039
|
-
|
|
2040
|
-
|
|
2550
|
+
const secondEndEvent = expectLastStateMachineEvent(
|
|
2551
|
+
task3SendStateMachineEventSpy,
|
|
2552
|
+
TaskEvent.CONTACT_ENDED
|
|
2553
|
+
);
|
|
2554
|
+
expect(secondEndEvent?.taskData).toEqual(emailEndedPayload.data);
|
|
2041
2555
|
|
|
2042
2556
|
// Verify task3 is still in collection (since state was 'connected')
|
|
2043
2557
|
expect(taskManager.getTask(task3Payload.data.interactionId)).toBeDefined();
|
|
2044
2558
|
|
|
2045
2559
|
// Verify task1 remains unaffected
|
|
2046
|
-
expect(task1EmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END);
|
|
2047
2560
|
expect(taskManager.getTask(task1Payload.data.interactionId)).toBeDefined();
|
|
2048
2561
|
});
|
|
2049
2562
|
|
|
2050
|
-
it('should emit
|
|
2563
|
+
it('should emit TRANSFER_SUCCESS event on AGENT_VTEAM_TRANSFERRED event', () => {
|
|
2051
2564
|
// First create a task by emitting the initial payload
|
|
2052
2565
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
2053
|
-
|
|
2054
|
-
// Get a reference to the task from taskCollection
|
|
2055
2566
|
const task = taskManager.getTask(taskId);
|
|
2056
|
-
|
|
2057
|
-
// Now spy on the task's emit method
|
|
2058
|
-
const taskEmitSpy = jest.spyOn(task, 'emit');
|
|
2567
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
2059
2568
|
|
|
2060
2569
|
const vteamTransferredPayload = {
|
|
2061
2570
|
data: {
|
|
@@ -2079,8 +2588,9 @@ describe('TaskManager', () => {
|
|
|
2079
2588
|
|
|
2080
2589
|
webSocketManagerMock.emit('message', JSON.stringify(vteamTransferredPayload));
|
|
2081
2590
|
|
|
2082
|
-
// Check that
|
|
2083
|
-
|
|
2591
|
+
// Check that the state machine received the END event
|
|
2592
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TRANSFER_SUCCESS);
|
|
2593
|
+
sendStateMachineEventSpy.mockRestore();
|
|
2084
2594
|
|
|
2085
2595
|
// The task should still exist in the collection based on current implementation
|
|
2086
2596
|
expect(taskManager.getTask(taskId)).toBeDefined();
|
|
@@ -2095,12 +2605,32 @@ describe('TaskManager', () => {
|
|
|
2095
2605
|
},
|
|
2096
2606
|
};
|
|
2097
2607
|
const task = taskManager.getTask(taskId);
|
|
2098
|
-
const
|
|
2099
|
-
|
|
2100
|
-
|
|
2608
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
2609
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2610
|
+
expect(sendStateMachineEventSpy).toHaveBeenCalled();
|
|
2611
|
+
const stateMachineEvent = sendStateMachineEventSpy.mock.calls.at(-1)?.[0];
|
|
2612
|
+
expect(stateMachineEvent?.type).toBe(TaskEvent.TASK_WRAPUP);
|
|
2613
|
+
expect(stateMachineEvent?.taskData).toEqual({
|
|
2614
|
+
...payload.data,
|
|
2615
|
+
wrapUpRequired: true,
|
|
2101
2616
|
});
|
|
2617
|
+
sendStateMachineEventSpy.mockRestore();
|
|
2618
|
+
});
|
|
2619
|
+
|
|
2620
|
+
it('stamps wrapUpRequired on AGENT_WRAPUP when the payload omits it', () => {
|
|
2621
|
+
const payload = {
|
|
2622
|
+
data: {
|
|
2623
|
+
type: CC_EVENTS.AGENT_WRAPUP,
|
|
2624
|
+
interactionId: taskId,
|
|
2625
|
+
},
|
|
2626
|
+
};
|
|
2627
|
+
const task = taskManager.getTask(taskId);
|
|
2102
2628
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2103
|
-
|
|
2629
|
+
const stateMachineEvent = task.sendStateMachineEvent.mock.calls.at(-1)?.[0];
|
|
2630
|
+
|
|
2631
|
+
expect(stateMachineEvent?.type).toBe(TaskEvent.TASK_WRAPUP);
|
|
2632
|
+
expect(stateMachineEvent?.taskData.wrapUpRequired).toBe(true);
|
|
2633
|
+
expect(task.data.wrapUpRequired).toBe(true);
|
|
2104
2634
|
});
|
|
2105
2635
|
|
|
2106
2636
|
it('should not attempt cleanup twice when AGENT_CONTACT_UNASSIGNED is followed by AGENT_WRAPUP', () => {
|
|
@@ -2176,1176 +2706,1670 @@ describe('TaskManager', () => {
|
|
|
2176
2706
|
});
|
|
2177
2707
|
|
|
2178
2708
|
describe('should emit appropriate task events for recording events', () => {
|
|
2179
|
-
|
|
2709
|
+
const eventMap: Record<string, TaskEvent | null> = {
|
|
2710
|
+
STARTED: TaskEvent.RECORDING_STARTED,
|
|
2711
|
+
PAUSED: TaskEvent.PAUSE_RECORDING,
|
|
2712
|
+
PAUSE_FAILED: null,
|
|
2713
|
+
RESUMED: TaskEvent.RESUME_RECORDING,
|
|
2714
|
+
RESUME_FAILED: null,
|
|
2715
|
+
};
|
|
2716
|
+
|
|
2717
|
+
['STARTED', 'PAUSED', 'PAUSE_FAILED', 'RESUMED', 'RESUME_FAILED'].forEach((suffix) => {
|
|
2180
2718
|
const ccEvent = CC_EVENTS[`CONTACT_RECORDING_${suffix}`];
|
|
2181
|
-
const
|
|
2182
|
-
it(`should
|
|
2719
|
+
const expectedTaskEvent = eventMap[suffix];
|
|
2720
|
+
it(`should ${expectedTaskEvent ? 'send' : 'not send'} ${
|
|
2721
|
+
expectedTaskEvent ?? 'a'
|
|
2722
|
+
} state machine event on ${ccEvent} event`, () => {
|
|
2183
2723
|
const payload = {data: {...initalPayload.data, type: ccEvent}};
|
|
2184
2724
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
2185
2725
|
const task = taskManager.getTask(taskId);
|
|
2186
|
-
const
|
|
2726
|
+
const sendStateMachineEventSpy = task.sendStateMachineEvent as jest.Mock;
|
|
2727
|
+
sendStateMachineEventSpy.mockClear();
|
|
2187
2728
|
|
|
2188
2729
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2189
|
-
|
|
2730
|
+
if (expectedTaskEvent) {
|
|
2731
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
2732
|
+
sendStateMachineEventSpy,
|
|
2733
|
+
expectedTaskEvent
|
|
2734
|
+
);
|
|
2735
|
+
expect(stateMachineEvent?.taskData).toEqual(payload.data);
|
|
2736
|
+
} else {
|
|
2737
|
+
expect(sendStateMachineEventSpy).not.toHaveBeenCalled();
|
|
2738
|
+
}
|
|
2190
2739
|
});
|
|
2191
2740
|
});
|
|
2192
2741
|
});
|
|
2193
2742
|
|
|
2194
|
-
describe('
|
|
2195
|
-
|
|
2196
|
-
const
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2743
|
+
describe('Contact owner propagation', () => {
|
|
2744
|
+
const currentAgentId = 'test-agent-id';
|
|
2745
|
+
const previousOwnerId = 'previous-owner-id';
|
|
2746
|
+
const promotedOwnerId = 'promoted-owner-id';
|
|
2747
|
+
|
|
2748
|
+
const createConferenceTaskData = (
|
|
2749
|
+
interactionId: string,
|
|
2750
|
+
owner: string,
|
|
2751
|
+
mainInteractionId = taskId
|
|
2752
|
+
) => ({
|
|
2753
|
+
...taskDataMock,
|
|
2754
|
+
agentId: currentAgentId,
|
|
2755
|
+
interactionId,
|
|
2756
|
+
owner,
|
|
2757
|
+
interaction: {
|
|
2758
|
+
mediaType: 'telephony',
|
|
2759
|
+
interactionId,
|
|
2760
|
+
mainInteractionId,
|
|
2761
|
+
owner,
|
|
2762
|
+
participants: {
|
|
2763
|
+
[currentAgentId]: {
|
|
2764
|
+
id: currentAgentId,
|
|
2765
|
+
pType: 'Agent',
|
|
2766
|
+
hasJoined: true,
|
|
2767
|
+
hasLeft: false,
|
|
2768
|
+
},
|
|
2769
|
+
[previousOwnerId]: {
|
|
2770
|
+
id: previousOwnerId,
|
|
2771
|
+
pType: 'Agent',
|
|
2772
|
+
hasJoined: true,
|
|
2773
|
+
hasLeft: false,
|
|
2774
|
+
},
|
|
2775
|
+
[promotedOwnerId]: {
|
|
2776
|
+
id: promotedOwnerId,
|
|
2777
|
+
pType: 'Agent',
|
|
2778
|
+
hasJoined: true,
|
|
2779
|
+
hasLeft: false,
|
|
2780
|
+
},
|
|
2781
|
+
},
|
|
2782
|
+
media: {
|
|
2783
|
+
[mainInteractionId]: {
|
|
2784
|
+
mediaResourceId: mainInteractionId,
|
|
2785
|
+
mediaType: 'telephony',
|
|
2786
|
+
mType: 'mainCall',
|
|
2787
|
+
participants: [currentAgentId, previousOwnerId, promotedOwnerId],
|
|
2788
|
+
},
|
|
2789
|
+
},
|
|
2790
|
+
},
|
|
2212
2791
|
});
|
|
2213
2792
|
|
|
2214
|
-
|
|
2215
|
-
const
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
interactionId: taskId,
|
|
2219
|
-
isConferencing: true,
|
|
2220
|
-
},
|
|
2221
|
-
};
|
|
2793
|
+
const installTask = (data) => {
|
|
2794
|
+
const task = createMockTask(data);
|
|
2795
|
+
taskManager.taskCollection = {[data.interactionId]: task};
|
|
2796
|
+
(taskManager as any).setupTaskListeners(task);
|
|
2222
2797
|
|
|
2223
|
-
|
|
2798
|
+
return task;
|
|
2799
|
+
};
|
|
2224
2800
|
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2801
|
+
const createPromotedAgentOwnerChangeData = (
|
|
2802
|
+
interactionId = 'promoted-child-interaction-id',
|
|
2803
|
+
mainInteractionId = taskId
|
|
2804
|
+
) => {
|
|
2805
|
+
const data = createConferenceTaskData(interactionId, currentAgentId, mainInteractionId);
|
|
2228
2806
|
|
|
2229
|
-
|
|
2230
|
-
const payload = {
|
|
2231
|
-
data: {
|
|
2232
|
-
type: CC_EVENTS.AGENT_CONSULT_CONFERENCING,
|
|
2233
|
-
interactionId: taskId,
|
|
2234
|
-
isConferencing: true,
|
|
2235
|
-
},
|
|
2236
|
-
};
|
|
2807
|
+
data.interaction.state = 'conference';
|
|
2237
2808
|
|
|
2238
|
-
|
|
2809
|
+
return {...data, type: CC_EVENTS.CONTACT_OWNER_CHANGED};
|
|
2810
|
+
};
|
|
2239
2811
|
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
}
|
|
2812
|
+
const clearTasksAndFactoryHistory = () => {
|
|
2813
|
+
taskManager.taskCollection = {};
|
|
2814
|
+
(TaskFactory.createTask as jest.Mock).mockClear();
|
|
2815
|
+
};
|
|
2244
2816
|
|
|
2245
|
-
it('
|
|
2246
|
-
const
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
interactionId: taskId,
|
|
2250
|
-
reason: 'Network error',
|
|
2251
|
-
},
|
|
2252
|
-
};
|
|
2817
|
+
it('emits one hydrate when ContactUpdated changes interaction.owner', () => {
|
|
2818
|
+
const task = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
2819
|
+
const hydrateHandler = jest.fn();
|
|
2820
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
2253
2821
|
|
|
2254
|
-
webSocketManagerMock.emit(
|
|
2822
|
+
webSocketManagerMock.emit(
|
|
2823
|
+
'message',
|
|
2824
|
+
JSON.stringify({
|
|
2825
|
+
data: {
|
|
2826
|
+
...createConferenceTaskData(taskId, promotedOwnerId),
|
|
2827
|
+
type: CC_EVENTS.CONTACT_UPDATED,
|
|
2828
|
+
},
|
|
2829
|
+
})
|
|
2830
|
+
);
|
|
2255
2831
|
|
|
2256
|
-
|
|
2257
|
-
|
|
2832
|
+
expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
|
|
2833
|
+
expect(task.data.interaction.owner).toBe(promotedOwnerId);
|
|
2834
|
+
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
2835
|
+
expect(hydrateHandler).toHaveBeenCalledWith(task);
|
|
2258
2836
|
});
|
|
2259
2837
|
|
|
2260
|
-
it(
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2838
|
+
it.each([
|
|
2839
|
+
['the same owner', previousOwnerId],
|
|
2840
|
+
['no owner', undefined],
|
|
2841
|
+
['a blank owner', ' '],
|
|
2842
|
+
])('keeps ContactUpdated data-only when it carries %s', (_description, owner) => {
|
|
2843
|
+
const task = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
2844
|
+
const hydrateHandler = jest.fn();
|
|
2845
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
2846
|
+
const interaction = createConferenceTaskData(taskId, previousOwnerId).interaction;
|
|
2847
|
+
if (owner === undefined) {
|
|
2848
|
+
delete interaction.owner;
|
|
2849
|
+
} else {
|
|
2850
|
+
interaction.owner = owner;
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
webSocketManagerMock.emit(
|
|
2854
|
+
'message',
|
|
2855
|
+
JSON.stringify({
|
|
2856
|
+
data: {
|
|
2857
|
+
...createConferenceTaskData(taskId, previousOwnerId),
|
|
2858
|
+
type: CC_EVENTS.CONTACT_UPDATED,
|
|
2859
|
+
interaction,
|
|
2278
2860
|
},
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2861
|
+
})
|
|
2862
|
+
);
|
|
2281
2863
|
|
|
2282
|
-
|
|
2864
|
+
expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_UPDATED);
|
|
2865
|
+
expect(hydrateHandler).not.toHaveBeenCalled();
|
|
2866
|
+
});
|
|
2867
|
+
|
|
2868
|
+
it('correlates child-keyed ContactOwnerChanged and keeps the main task identity', () => {
|
|
2869
|
+
const childTaskId = 'surviving-child-id';
|
|
2870
|
+
const promotedChildId = 'promoted-child-id';
|
|
2871
|
+
const task = installTask(
|
|
2872
|
+
createConferenceTaskData(childTaskId, previousOwnerId, taskId)
|
|
2873
|
+
);
|
|
2874
|
+
const hydrateHandler = jest.fn();
|
|
2875
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
2876
|
+
|
|
2877
|
+
const ownerChangeData = createConferenceTaskData(
|
|
2878
|
+
promotedChildId,
|
|
2879
|
+
promotedOwnerId,
|
|
2880
|
+
taskId
|
|
2881
|
+
);
|
|
2882
|
+
ownerChangeData.interaction.interactionId = taskId;
|
|
2883
|
+
webSocketManagerMock.emit(
|
|
2884
|
+
'message',
|
|
2885
|
+
JSON.stringify({
|
|
2886
|
+
data: {...ownerChangeData, type: CC_EVENTS.CONTACT_OWNER_CHANGED},
|
|
2887
|
+
})
|
|
2888
|
+
);
|
|
2283
2889
|
|
|
2284
|
-
|
|
2285
|
-
expect(task.data.
|
|
2286
|
-
|
|
2890
|
+
expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
|
|
2891
|
+
expect(task.data.interactionId).toBe(taskId);
|
|
2892
|
+
expect(task.data.interaction.owner).toBe(promotedOwnerId);
|
|
2893
|
+
expect(taskManager.taskCollection[childTaskId]).toBeUndefined();
|
|
2894
|
+
expect(taskManager.taskCollection[promotedChildId]).toBeUndefined();
|
|
2895
|
+
expect(taskManager.taskCollection[taskId]).toBe(task);
|
|
2896
|
+
expect(Object.values(taskManager.taskCollection).filter((entry) => entry === task)).toHaveLength(
|
|
2897
|
+
1
|
|
2898
|
+
);
|
|
2899
|
+
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
2287
2900
|
});
|
|
2288
2901
|
|
|
2289
|
-
it('
|
|
2290
|
-
const
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
'agent-2': {pType: 'Agent', hasLeft: false},
|
|
2299
|
-
'new-agent-789': {pType: 'Agent', hasLeft: false},
|
|
2300
|
-
'customer-1': {pType: 'Customer', hasLeft: false},
|
|
2301
|
-
},
|
|
2302
|
-
media: {
|
|
2303
|
-
[taskId]: {
|
|
2304
|
-
mType: 'mainCall',
|
|
2305
|
-
participants: [agentId, 'agent-2', 'new-agent-789', 'customer-1'],
|
|
2306
|
-
},
|
|
2307
|
-
},
|
|
2308
|
-
},
|
|
2902
|
+
it('uses the incoming promotion snapshot to correlate a stale child-keyed task', () => {
|
|
2903
|
+
const childTaskId = 'stale-child-interaction-id';
|
|
2904
|
+
const staleTaskData = createConferenceTaskData(childTaskId, previousOwnerId, taskId);
|
|
2905
|
+
staleTaskData.interaction.media = {
|
|
2906
|
+
[childTaskId]: {
|
|
2907
|
+
mediaResourceId: childTaskId,
|
|
2908
|
+
mediaType: 'telephony',
|
|
2909
|
+
mType: 'consult',
|
|
2910
|
+
participants: [currentAgentId, promotedOwnerId],
|
|
2309
2911
|
},
|
|
2310
2912
|
};
|
|
2913
|
+
const task = installTask(staleTaskData);
|
|
2914
|
+
const hydrateHandler = jest.fn();
|
|
2915
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
2311
2916
|
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2917
|
+
webSocketManagerMock.emit(
|
|
2918
|
+
'message',
|
|
2919
|
+
JSON.stringify({data: createPromotedAgentOwnerChangeData()})
|
|
2920
|
+
);
|
|
2315
2921
|
|
|
2316
|
-
|
|
2317
|
-
expect(
|
|
2922
|
+
expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
|
|
2923
|
+
expect(task.data.interaction.owner).toBe(currentAgentId);
|
|
2924
|
+
expect(taskManager.taskCollection[childTaskId]).toBeUndefined();
|
|
2925
|
+
expect(taskManager.taskCollection[taskId]).toBe(task);
|
|
2926
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
2927
|
+
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
2928
|
+
});
|
|
2318
2929
|
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2930
|
+
it('reuses a task identified by the incoming main-call media key', () => {
|
|
2931
|
+
const mainMediaInteractionId = 'main-media-interaction-id';
|
|
2932
|
+
const promotedChildId = 'promoted-child-without-main-id';
|
|
2933
|
+
const task = installTask(
|
|
2934
|
+
createConferenceTaskData(
|
|
2935
|
+
mainMediaInteractionId,
|
|
2936
|
+
previousOwnerId,
|
|
2937
|
+
mainMediaInteractionId
|
|
2938
|
+
)
|
|
2325
2939
|
);
|
|
2940
|
+
const hydrateHandler = jest.fn();
|
|
2941
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
2942
|
+
const ownerChangeData = createPromotedAgentOwnerChangeData(
|
|
2943
|
+
promotedChildId,
|
|
2944
|
+
mainMediaInteractionId
|
|
2945
|
+
);
|
|
2946
|
+
delete ownerChangeData.interaction.mainInteractionId;
|
|
2947
|
+
|
|
2948
|
+
webSocketManagerMock.emit('message', JSON.stringify({data: ownerChangeData}));
|
|
2949
|
+
|
|
2950
|
+
expect(task.sendStateMachineEvent).toHaveBeenCalledTimes(1);
|
|
2951
|
+
expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
|
|
2952
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
2953
|
+
expect(taskManager.taskCollection[mainMediaInteractionId]).toBe(task);
|
|
2954
|
+
expect(taskManager.taskCollection[promotedChildId]).toBeUndefined();
|
|
2955
|
+
expect(Object.values(taskManager.taskCollection)).toEqual([task]);
|
|
2956
|
+
expect(task.listenerCount(TASK_EVENTS.TASK_HYDRATE)).toBe(1);
|
|
2957
|
+
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
2958
|
+
expect(hydrateHandler).toHaveBeenCalledWith(task);
|
|
2959
|
+
});
|
|
2326
2960
|
|
|
2327
|
-
|
|
2961
|
+
it('reuses a stale task through its collection key and the incoming main-call media key', () => {
|
|
2962
|
+
const mainMediaInteractionId = 'collection-main-interaction-id';
|
|
2963
|
+
const staleChildId = 'stale-collection-child-id';
|
|
2964
|
+
const promotedChildId = 'promoted-collection-child-id';
|
|
2965
|
+
const staleTaskData = createConferenceTaskData(
|
|
2966
|
+
staleChildId,
|
|
2967
|
+
previousOwnerId,
|
|
2968
|
+
staleChildId
|
|
2969
|
+
);
|
|
2970
|
+
staleTaskData.interaction.media[staleChildId].mType = 'consult';
|
|
2971
|
+
const task = createMockTask(staleTaskData);
|
|
2972
|
+
taskManager.taskCollection = {[mainMediaInteractionId]: task};
|
|
2973
|
+
(taskManager as any).setupTaskListeners(task);
|
|
2974
|
+
const hydrateHandler = jest.fn();
|
|
2975
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
2976
|
+
const ownerChangeData = createPromotedAgentOwnerChangeData(
|
|
2977
|
+
promotedChildId,
|
|
2978
|
+
mainMediaInteractionId
|
|
2979
|
+
);
|
|
2980
|
+
delete ownerChangeData.interaction.mainInteractionId;
|
|
2981
|
+
|
|
2982
|
+
webSocketManagerMock.emit('message', JSON.stringify({data: ownerChangeData}));
|
|
2983
|
+
|
|
2984
|
+
expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
|
|
2985
|
+
expect(task.data.interactionId).toBe(mainMediaInteractionId);
|
|
2986
|
+
expect(task.data.interaction.owner).toBe(currentAgentId);
|
|
2987
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
2988
|
+
expect(taskManager.taskCollection[mainMediaInteractionId]).toBe(task);
|
|
2989
|
+
expect(taskManager.taskCollection[staleChildId]).toBeUndefined();
|
|
2990
|
+
expect(taskManager.taskCollection[promotedChildId]).toBeUndefined();
|
|
2991
|
+
expect(Object.values(taskManager.taskCollection)).toEqual([task]);
|
|
2992
|
+
expect(task.listenerCount(TASK_EVENTS.TASK_HYDRATE)).toBe(1);
|
|
2993
|
+
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
2328
2994
|
});
|
|
2329
2995
|
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2996
|
+
it('prefers the exact task for ContactOwnerChanged over related child tasks', () => {
|
|
2997
|
+
const exactTask = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
2998
|
+
const childTaskId = 'related-child-id';
|
|
2999
|
+
const childTask = createMockTask(
|
|
3000
|
+
createConferenceTaskData(childTaskId, previousOwnerId, taskId)
|
|
3001
|
+
);
|
|
3002
|
+
taskManager.taskCollection[childTaskId] = childTask;
|
|
3003
|
+
|
|
3004
|
+
webSocketManagerMock.emit(
|
|
3005
|
+
'message',
|
|
3006
|
+
JSON.stringify({
|
|
2333
3007
|
data: {
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
interaction: {
|
|
2337
|
-
participants: {
|
|
2338
|
-
[agentId]: {pType: 'Agent', hasLeft: false},
|
|
2339
|
-
'agent-2': {pType: 'Agent', hasLeft: true}, // This agent left
|
|
2340
|
-
'customer-1': {pType: 'Customer', hasLeft: false},
|
|
2341
|
-
},
|
|
2342
|
-
media: {
|
|
2343
|
-
[taskId]: {
|
|
2344
|
-
mType: 'mainCall',
|
|
2345
|
-
participants: [agentId, 'customer-1'], // agent-2 removed from participants
|
|
2346
|
-
},
|
|
2347
|
-
},
|
|
2348
|
-
},
|
|
3008
|
+
...createConferenceTaskData(taskId, promotedOwnerId),
|
|
3009
|
+
type: CC_EVENTS.CONTACT_OWNER_CHANGED,
|
|
2349
3010
|
},
|
|
2350
|
-
}
|
|
3011
|
+
})
|
|
3012
|
+
);
|
|
2351
3013
|
|
|
2352
|
-
|
|
3014
|
+
expectLastStateMachineEvent(exactTask.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
|
|
3015
|
+
expect(childTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3016
|
+
});
|
|
2353
3017
|
|
|
2354
|
-
|
|
3018
|
+
it('does not replace a distinct main task when an exact child task also exists', () => {
|
|
3019
|
+
const childTaskId = 'exact-child-id';
|
|
3020
|
+
const mainTask = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
3021
|
+
const childTask = createMockTask(
|
|
3022
|
+
createConferenceTaskData(childTaskId, previousOwnerId, taskId)
|
|
3023
|
+
);
|
|
3024
|
+
taskManager.taskCollection[childTaskId] = childTask;
|
|
2355
3025
|
|
|
2356
|
-
|
|
2357
|
-
|
|
3026
|
+
webSocketManagerMock.emit(
|
|
3027
|
+
'message',
|
|
3028
|
+
JSON.stringify({
|
|
3029
|
+
data: {
|
|
3030
|
+
...createConferenceTaskData(childTaskId, promotedOwnerId, taskId),
|
|
3031
|
+
type: CC_EVENTS.CONTACT_OWNER_CHANGED,
|
|
3032
|
+
},
|
|
3033
|
+
})
|
|
3034
|
+
);
|
|
2358
3035
|
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
);
|
|
3036
|
+
expect(childTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3037
|
+
expect(mainTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3038
|
+
expect(taskManager.taskCollection[taskId]).toBe(mainTask);
|
|
3039
|
+
expect(taskManager.taskCollection[childTaskId]).toBe(childTask);
|
|
3040
|
+
});
|
|
2365
3041
|
|
|
2366
|
-
|
|
2367
|
-
|
|
3042
|
+
it('ignores a related owner change with conflicting explicit main interaction IDs', () => {
|
|
3043
|
+
const childTaskId = 'conflicting-main-child-id';
|
|
3044
|
+
const existingMainId = 'existing-main-id';
|
|
3045
|
+
const incomingMainId = 'incoming-main-id';
|
|
3046
|
+
const sharedParentId = 'shared-parent-id';
|
|
3047
|
+
const taskData = createConferenceTaskData(
|
|
3048
|
+
childTaskId,
|
|
3049
|
+
previousOwnerId,
|
|
3050
|
+
existingMainId
|
|
3051
|
+
);
|
|
3052
|
+
taskData.interaction.parentInteractionId = sharedParentId;
|
|
3053
|
+
const task = installTask(taskData);
|
|
3054
|
+
const ownerChangeData = createConferenceTaskData(
|
|
3055
|
+
'different-child-id',
|
|
3056
|
+
promotedOwnerId,
|
|
3057
|
+
incomingMainId
|
|
3058
|
+
);
|
|
3059
|
+
ownerChangeData.interaction.parentInteractionId = sharedParentId;
|
|
2368
3060
|
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
participants: {
|
|
2376
|
-
[agentId]: {
|
|
2377
|
-
hasLeft: false,
|
|
2378
|
-
},
|
|
2379
|
-
},
|
|
2380
|
-
},
|
|
2381
|
-
},
|
|
2382
|
-
};
|
|
3061
|
+
webSocketManagerMock.emit(
|
|
3062
|
+
'message',
|
|
3063
|
+
JSON.stringify({
|
|
3064
|
+
data: {...ownerChangeData, type: CC_EVENTS.CONTACT_OWNER_CHANGED},
|
|
3065
|
+
})
|
|
3066
|
+
);
|
|
2383
3067
|
|
|
2384
|
-
|
|
3068
|
+
expect(task.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3069
|
+
expect(taskManager.taskCollection[childTaskId]).toBe(task);
|
|
3070
|
+
expect(taskManager.taskCollection[existingMainId]).toBeUndefined();
|
|
3071
|
+
expect(taskManager.taskCollection[incomingMainId]).toBeUndefined();
|
|
3072
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3073
|
+
});
|
|
2385
3074
|
|
|
2386
|
-
|
|
2387
|
-
|
|
3075
|
+
it('does not replace a distinct task already stored at the canonical main key', () => {
|
|
3076
|
+
const childTaskId = 'related-active-child-id';
|
|
3077
|
+
const canonicalMainId = 'occupied-canonical-main-id';
|
|
3078
|
+
const candidate = createMockTask(
|
|
3079
|
+
createConferenceTaskData(childTaskId, previousOwnerId, canonicalMainId)
|
|
3080
|
+
);
|
|
3081
|
+
const occupiedData = createConferenceTaskData(
|
|
3082
|
+
'occupied-task-child-id',
|
|
3083
|
+
previousOwnerId,
|
|
3084
|
+
canonicalMainId
|
|
3085
|
+
);
|
|
3086
|
+
occupiedData.interaction.media[canonicalMainId].mType = 'consult';
|
|
3087
|
+
const occupiedTask = createMockTask(occupiedData);
|
|
3088
|
+
taskManager.taskCollection = {
|
|
3089
|
+
[childTaskId]: candidate,
|
|
3090
|
+
[canonicalMainId]: occupiedTask,
|
|
3091
|
+
};
|
|
2388
3092
|
|
|
2389
|
-
|
|
2390
|
-
|
|
3093
|
+
webSocketManagerMock.emit(
|
|
3094
|
+
'message',
|
|
3095
|
+
JSON.stringify({
|
|
2391
3096
|
data: {
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
},
|
|
2399
|
-
},
|
|
2400
|
-
},
|
|
3097
|
+
...createConferenceTaskData(
|
|
3098
|
+
'incoming-related-child-id',
|
|
3099
|
+
promotedOwnerId,
|
|
3100
|
+
canonicalMainId
|
|
3101
|
+
),
|
|
3102
|
+
type: CC_EVENTS.CONTACT_OWNER_CHANGED,
|
|
2401
3103
|
},
|
|
2402
|
-
}
|
|
3104
|
+
})
|
|
3105
|
+
);
|
|
2403
3106
|
|
|
2404
|
-
|
|
3107
|
+
expect(candidate.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3108
|
+
expect(occupiedTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3109
|
+
expect(taskManager.taskCollection[childTaskId]).toBe(candidate);
|
|
3110
|
+
expect(taskManager.taskCollection[canonicalMainId]).toBe(occupiedTask);
|
|
3111
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3112
|
+
});
|
|
2405
3113
|
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
3114
|
+
it('ignores a related ContactOwnerChanged task whose current agent is consult-only', () => {
|
|
3115
|
+
const childTaskId = 'consult-only-child-id';
|
|
3116
|
+
const data = createConferenceTaskData(childTaskId, previousOwnerId, taskId);
|
|
3117
|
+
data.interaction.media[taskId].participants = [previousOwnerId, promotedOwnerId];
|
|
3118
|
+
data.interaction.media[childTaskId] = {
|
|
3119
|
+
mediaResourceId: childTaskId,
|
|
3120
|
+
mediaType: 'telephony',
|
|
3121
|
+
mType: 'consult',
|
|
3122
|
+
participants: [currentAgentId, promotedOwnerId],
|
|
3123
|
+
};
|
|
3124
|
+
const task = installTask(data);
|
|
2410
3125
|
|
|
2411
|
-
|
|
2412
|
-
|
|
3126
|
+
webSocketManagerMock.emit(
|
|
3127
|
+
'message',
|
|
3128
|
+
JSON.stringify({
|
|
2413
3129
|
data: {
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
interaction: {
|
|
2417
|
-
participants: {
|
|
2418
|
-
[agentId]: {
|
|
2419
|
-
hasLeft: true,
|
|
2420
|
-
},
|
|
2421
|
-
},
|
|
2422
|
-
media: {
|
|
2423
|
-
[taskId]: {
|
|
2424
|
-
mType: 'mainCall',
|
|
2425
|
-
participants: [agentId],
|
|
2426
|
-
},
|
|
2427
|
-
},
|
|
2428
|
-
},
|
|
3130
|
+
...createConferenceTaskData('unmatched-child-id', promotedOwnerId, taskId),
|
|
3131
|
+
type: CC_EVENTS.CONTACT_OWNER_CHANGED,
|
|
2429
3132
|
},
|
|
2430
|
-
}
|
|
3133
|
+
})
|
|
3134
|
+
);
|
|
2431
3135
|
|
|
2432
|
-
|
|
3136
|
+
expect(task.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3137
|
+
expect(taskManager.taskCollection[childTaskId]).toBe(task);
|
|
3138
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3139
|
+
});
|
|
2433
3140
|
|
|
2434
|
-
|
|
3141
|
+
it.each([
|
|
3142
|
+
[
|
|
3143
|
+
'the current agent is marked departed',
|
|
3144
|
+
(data) => {
|
|
3145
|
+
data.interaction.participants[currentAgentId].hasLeft = true;
|
|
3146
|
+
},
|
|
3147
|
+
],
|
|
3148
|
+
[
|
|
3149
|
+
'the current agent participant is missing',
|
|
3150
|
+
(data) => {
|
|
3151
|
+
delete data.interaction.participants[currentAgentId];
|
|
3152
|
+
},
|
|
3153
|
+
],
|
|
3154
|
+
[
|
|
3155
|
+
'the current agent is present only on a consult leg',
|
|
3156
|
+
(data) => {
|
|
3157
|
+
data.interaction.media[taskId].mType = 'consult';
|
|
3158
|
+
},
|
|
3159
|
+
],
|
|
3160
|
+
])('does not recover a promoted task when %s', (_description, mutatePayload) => {
|
|
3161
|
+
clearTasksAndFactoryHistory();
|
|
3162
|
+
const ownerChangeData = createPromotedAgentOwnerChangeData();
|
|
3163
|
+
mutatePayload(ownerChangeData);
|
|
2435
3164
|
|
|
2436
|
-
|
|
2437
|
-
expect(removeTaskSpy).not.toHaveBeenCalled();
|
|
2438
|
-
expect(taskManager.getTask(taskId)).toBeDefined();
|
|
2439
|
-
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
|
|
2440
|
-
});
|
|
3165
|
+
webSocketManagerMock.emit('message', JSON.stringify({data: ownerChangeData}));
|
|
2441
3166
|
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
2446
|
-
interactionId: taskId,
|
|
2447
|
-
interaction: {
|
|
2448
|
-
participants: {
|
|
2449
|
-
[agentId]: {
|
|
2450
|
-
hasLeft: true,
|
|
2451
|
-
},
|
|
2452
|
-
},
|
|
2453
|
-
owner: agentId,
|
|
2454
|
-
media: {
|
|
2455
|
-
[taskId]: {
|
|
2456
|
-
mType: 'consultCall',
|
|
2457
|
-
participants: ['other-agent'],
|
|
2458
|
-
},
|
|
2459
|
-
},
|
|
2460
|
-
},
|
|
2461
|
-
},
|
|
2462
|
-
};
|
|
3167
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3168
|
+
expect(taskManager.getAllTasks()).toEqual({});
|
|
3169
|
+
});
|
|
2463
3170
|
|
|
2464
|
-
|
|
3171
|
+
it('ignores ambiguous active main-call matches for ContactOwnerChanged', () => {
|
|
3172
|
+
const firstTask = createMockTask(
|
|
3173
|
+
createConferenceTaskData('first-child-id', previousOwnerId, taskId)
|
|
3174
|
+
);
|
|
3175
|
+
const secondTask = createMockTask(
|
|
3176
|
+
createConferenceTaskData('second-child-id', previousOwnerId, taskId)
|
|
3177
|
+
);
|
|
3178
|
+
taskManager.taskCollection = {
|
|
3179
|
+
'first-child-id': firstTask,
|
|
3180
|
+
'second-child-id': secondTask,
|
|
3181
|
+
};
|
|
2465
3182
|
|
|
2466
|
-
|
|
3183
|
+
const ownerChangeData = createConferenceTaskData(
|
|
3184
|
+
'unmatched-child-id',
|
|
3185
|
+
promotedOwnerId,
|
|
3186
|
+
taskId
|
|
3187
|
+
);
|
|
3188
|
+
ownerChangeData.interaction.interactionId = 'unmatched-child-id';
|
|
3189
|
+
webSocketManagerMock.emit(
|
|
3190
|
+
'message',
|
|
3191
|
+
JSON.stringify({
|
|
3192
|
+
data: {...ownerChangeData, type: CC_EVENTS.CONTACT_OWNER_CHANGED},
|
|
3193
|
+
})
|
|
3194
|
+
);
|
|
2467
3195
|
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
|
|
2472
|
-
});
|
|
3196
|
+
expect(firstTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3197
|
+
expect(secondTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3198
|
+
});
|
|
2473
3199
|
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
},
|
|
2485
|
-
owner: 'another-agent-id',
|
|
2486
|
-
media: {
|
|
2487
|
-
[taskId]: {
|
|
2488
|
-
mType: 'mainCall',
|
|
2489
|
-
participants: ['another-agent-id'],
|
|
2490
|
-
},
|
|
2491
|
-
},
|
|
2492
|
-
},
|
|
3200
|
+
it('ignores ambiguous main-call media-key matches without creating a task', () => {
|
|
3201
|
+
const mainMediaInteractionId = 'ambiguous-main-media-id';
|
|
3202
|
+
const createMediaRelatedTask = (interactionId: string) => {
|
|
3203
|
+
const data = createConferenceTaskData(interactionId, previousOwnerId, interactionId);
|
|
3204
|
+
data.interaction.media = {
|
|
3205
|
+
[mainMediaInteractionId]: {
|
|
3206
|
+
mediaResourceId: mainMediaInteractionId,
|
|
3207
|
+
mediaType: 'telephony',
|
|
3208
|
+
mType: 'mainCall',
|
|
3209
|
+
participants: [currentAgentId, previousOwnerId, promotedOwnerId],
|
|
2493
3210
|
},
|
|
2494
3211
|
};
|
|
2495
3212
|
|
|
2496
|
-
|
|
3213
|
+
return createMockTask(data);
|
|
3214
|
+
};
|
|
3215
|
+
const firstTask = createMediaRelatedTask('first-media-child-id');
|
|
3216
|
+
const secondTask = createMediaRelatedTask('second-media-child-id');
|
|
3217
|
+
taskManager.taskCollection = {
|
|
3218
|
+
'first-media-child-id': firstTask,
|
|
3219
|
+
'second-media-child-id': secondTask,
|
|
3220
|
+
};
|
|
3221
|
+
(TaskFactory.createTask as jest.Mock).mockClear();
|
|
3222
|
+
const ownerChangeData = createPromotedAgentOwnerChangeData(
|
|
3223
|
+
'promoted-media-child-id',
|
|
3224
|
+
mainMediaInteractionId
|
|
3225
|
+
);
|
|
3226
|
+
delete ownerChangeData.interaction.mainInteractionId;
|
|
2497
3227
|
|
|
2498
|
-
|
|
3228
|
+
webSocketManagerMock.emit('message', JSON.stringify({data: ownerChangeData}));
|
|
2499
3229
|
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
3230
|
+
expect(firstTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3231
|
+
expect(secondTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3232
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3233
|
+
expect(Object.values(taskManager.taskCollection)).toEqual([firstTask, secondTask]);
|
|
3234
|
+
});
|
|
2504
3235
|
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
interactionId: taskId,
|
|
2510
|
-
interaction: {
|
|
2511
|
-
participants: {
|
|
2512
|
-
'other-agent-id': {
|
|
2513
|
-
hasLeft: false,
|
|
2514
|
-
},
|
|
2515
|
-
},
|
|
2516
|
-
owner: 'another-agent-id',
|
|
2517
|
-
},
|
|
2518
|
-
},
|
|
2519
|
-
};
|
|
3236
|
+
it('does not recover a new task when multiple stale related tasks exist', () => {
|
|
3237
|
+
const createStaleRelatedTask = (interactionId: string) => {
|
|
3238
|
+
const data = createConferenceTaskData(interactionId, previousOwnerId, taskId);
|
|
3239
|
+
data.interaction.media[taskId].participants = [previousOwnerId, promotedOwnerId];
|
|
2520
3240
|
|
|
2521
|
-
|
|
3241
|
+
return createMockTask(data);
|
|
3242
|
+
};
|
|
3243
|
+
const firstTask = createStaleRelatedTask('first-stale-child-id');
|
|
3244
|
+
const secondTask = createStaleRelatedTask('second-stale-child-id');
|
|
3245
|
+
taskManager.taskCollection = {
|
|
3246
|
+
'first-stale-child-id': firstTask,
|
|
3247
|
+
'second-stale-child-id': secondTask,
|
|
3248
|
+
};
|
|
3249
|
+
(TaskFactory.createTask as jest.Mock).mockClear();
|
|
2522
3250
|
|
|
2523
|
-
|
|
3251
|
+
webSocketManagerMock.emit(
|
|
3252
|
+
'message',
|
|
3253
|
+
JSON.stringify({data: createPromotedAgentOwnerChangeData()})
|
|
3254
|
+
);
|
|
2524
3255
|
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
3256
|
+
expect(firstTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3257
|
+
expect(secondTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3258
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3259
|
+
expect(Object.keys(taskManager.taskCollection)).toEqual([
|
|
3260
|
+
'first-stale-child-id',
|
|
3261
|
+
'second-stale-child-id',
|
|
3262
|
+
]);
|
|
3263
|
+
});
|
|
2529
3264
|
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
pType: 'Agent',
|
|
2544
|
-
},
|
|
2545
|
-
'customer-1': {
|
|
2546
|
-
hasLeft: false,
|
|
2547
|
-
pType: 'Customer',
|
|
2548
|
-
},
|
|
2549
|
-
},
|
|
2550
|
-
media: {
|
|
2551
|
-
[taskId]: {
|
|
2552
|
-
mType: 'mainCall',
|
|
2553
|
-
participants: [agentId, 'agent-2', 'customer-1'],
|
|
2554
|
-
},
|
|
2555
|
-
},
|
|
2556
|
-
},
|
|
2557
|
-
},
|
|
2558
|
-
};
|
|
3265
|
+
it('ignores ContactOwnerChanged when different direct nested IDs match active tasks', () => {
|
|
3266
|
+
const firstTaskId = 'direct-main-id';
|
|
3267
|
+
const secondTaskId = 'direct-parent-id';
|
|
3268
|
+
const firstTask = createMockTask(
|
|
3269
|
+
createConferenceTaskData(firstTaskId, previousOwnerId, firstTaskId)
|
|
3270
|
+
);
|
|
3271
|
+
const secondTask = createMockTask(
|
|
3272
|
+
createConferenceTaskData(secondTaskId, previousOwnerId, secondTaskId)
|
|
3273
|
+
);
|
|
3274
|
+
taskManager.taskCollection = {
|
|
3275
|
+
[firstTaskId]: firstTask,
|
|
3276
|
+
[secondTaskId]: secondTask,
|
|
3277
|
+
};
|
|
2559
3278
|
|
|
2560
|
-
|
|
3279
|
+
const ownerChangeData = createConferenceTaskData(
|
|
3280
|
+
'unmatched-child-id',
|
|
3281
|
+
promotedOwnerId,
|
|
3282
|
+
firstTaskId
|
|
3283
|
+
);
|
|
3284
|
+
ownerChangeData.interaction.interactionId = secondTaskId;
|
|
3285
|
+
webSocketManagerMock.emit(
|
|
3286
|
+
'message',
|
|
3287
|
+
JSON.stringify({
|
|
3288
|
+
data: {...ownerChangeData, type: CC_EVENTS.CONTACT_OWNER_CHANGED},
|
|
3289
|
+
})
|
|
3290
|
+
);
|
|
2561
3291
|
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
3292
|
+
expect(firstTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3293
|
+
expect(secondTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3294
|
+
});
|
|
3295
|
+
|
|
3296
|
+
it('ignores ContactOwnerChanged without an interaction correlation identifier', () => {
|
|
3297
|
+
const task = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
2566
3298
|
|
|
2567
|
-
|
|
2568
|
-
|
|
3299
|
+
webSocketManagerMock.emit(
|
|
3300
|
+
'message',
|
|
3301
|
+
JSON.stringify({
|
|
2569
3302
|
data: {
|
|
2570
|
-
type: CC_EVENTS.
|
|
2571
|
-
|
|
2572
|
-
interaction: {
|
|
2573
|
-
participants: {
|
|
2574
|
-
[agentId]: {
|
|
2575
|
-
hasLeft: false,
|
|
2576
|
-
pType: 'Agent',
|
|
2577
|
-
},
|
|
2578
|
-
'agent-2': {
|
|
2579
|
-
hasLeft: true,
|
|
2580
|
-
pType: 'Agent',
|
|
2581
|
-
},
|
|
2582
|
-
'customer-1': {
|
|
2583
|
-
hasLeft: false,
|
|
2584
|
-
pType: 'Customer',
|
|
2585
|
-
},
|
|
2586
|
-
},
|
|
2587
|
-
media: {
|
|
2588
|
-
[taskId]: {
|
|
2589
|
-
mType: 'mainCall',
|
|
2590
|
-
participants: [agentId, 'customer-1'],
|
|
2591
|
-
},
|
|
2592
|
-
},
|
|
2593
|
-
},
|
|
3303
|
+
type: CC_EVENTS.CONTACT_OWNER_CHANGED,
|
|
3304
|
+
interaction: {owner: promotedOwnerId},
|
|
2594
3305
|
},
|
|
2595
|
-
}
|
|
2596
|
-
|
|
2597
|
-
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
3306
|
+
})
|
|
3307
|
+
);
|
|
2598
3308
|
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
|
|
2602
|
-
});
|
|
3309
|
+
expect(task.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3310
|
+
});
|
|
2603
3311
|
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
},
|
|
2611
|
-
};
|
|
3312
|
+
it('recovers a missing promoted-agent task under the main interaction and emits one hydrate', () => {
|
|
3313
|
+
clearTasksAndFactoryHistory();
|
|
3314
|
+
const hydrateHandler = jest.fn();
|
|
3315
|
+
const incomingHandler = jest.fn();
|
|
3316
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
3317
|
+
taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
|
|
2612
3318
|
|
|
2613
|
-
|
|
3319
|
+
webSocketManagerMock.emit(
|
|
3320
|
+
'message',
|
|
3321
|
+
JSON.stringify({data: createPromotedAgentOwnerChangeData()})
|
|
3322
|
+
);
|
|
2614
3323
|
|
|
2615
|
-
|
|
3324
|
+
expect(TaskFactory.createTask).toHaveBeenCalledTimes(1);
|
|
3325
|
+
expect(TaskFactory.createTask).toHaveBeenCalledWith(
|
|
3326
|
+
contactMock,
|
|
3327
|
+
webCallingService,
|
|
3328
|
+
expect.objectContaining({
|
|
3329
|
+
interactionId: taskId,
|
|
3330
|
+
isAutoAnswering: false,
|
|
3331
|
+
isConsulted: false,
|
|
3332
|
+
isConferenceInProgress: true,
|
|
3333
|
+
wrapUpRequired: false,
|
|
3334
|
+
}),
|
|
3335
|
+
undefined,
|
|
3336
|
+
undefined,
|
|
3337
|
+
currentAgentId,
|
|
3338
|
+
undefined
|
|
3339
|
+
);
|
|
2616
3340
|
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
3341
|
+
const recoveredTask = (TaskFactory.createTask as jest.Mock).mock.results[0].value;
|
|
3342
|
+
expect(recoveredTask.sendStateMachineEvent).toHaveBeenCalledTimes(2);
|
|
3343
|
+
expect(recoveredTask.sendStateMachineEvent).toHaveBeenNthCalledWith(
|
|
3344
|
+
1,
|
|
3345
|
+
expect.objectContaining({
|
|
3346
|
+
type: TaskEvent.HYDRATE,
|
|
3347
|
+
agentId: currentAgentId,
|
|
3348
|
+
taskData: expect.objectContaining({
|
|
3349
|
+
interactionId: taskId,
|
|
3350
|
+
interaction: expect.objectContaining({state: 'conference'}),
|
|
3351
|
+
}),
|
|
3352
|
+
})
|
|
3353
|
+
);
|
|
3354
|
+
expect(recoveredTask.sendStateMachineEvent).toHaveBeenNthCalledWith(
|
|
3355
|
+
2,
|
|
3356
|
+
expect.objectContaining({
|
|
3357
|
+
type: TaskEvent.CONTACT_OWNER_CHANGED,
|
|
3358
|
+
taskData: expect.objectContaining({interactionId: taskId}),
|
|
3359
|
+
})
|
|
3360
|
+
);
|
|
3361
|
+
expect(taskManager.taskCollection[taskId]).toBe(recoveredTask);
|
|
3362
|
+
expect(taskManager.taskCollection['promoted-child-interaction-id']).toBeUndefined();
|
|
3363
|
+
expect(Object.values(taskManager.taskCollection)).toEqual([recoveredTask]);
|
|
3364
|
+
expect(recoveredTask.data.interaction.owner).toBe(currentAgentId);
|
|
3365
|
+
expect(recoveredTask.listenerCount(TASK_EVENTS.TASK_HYDRATE)).toBe(1);
|
|
3366
|
+
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
3367
|
+
expect(hydrateHandler).toHaveBeenCalledWith(recoveredTask);
|
|
3368
|
+
expect(incomingHandler).not.toHaveBeenCalled();
|
|
2622
3369
|
});
|
|
2623
3370
|
|
|
2624
|
-
it(
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
3371
|
+
it.each([
|
|
3372
|
+
[
|
|
3373
|
+
'another agent owns the interaction',
|
|
3374
|
+
(data) => {
|
|
3375
|
+
data.interaction.owner = promotedOwnerId;
|
|
3376
|
+
data.owner = promotedOwnerId;
|
|
2630
3377
|
},
|
|
2631
|
-
|
|
3378
|
+
],
|
|
3379
|
+
[
|
|
3380
|
+
'the payload has no main-call media',
|
|
3381
|
+
(data) => {
|
|
3382
|
+
data.interaction.media = {};
|
|
3383
|
+
},
|
|
3384
|
+
],
|
|
3385
|
+
[
|
|
3386
|
+
'the interaction is explicitly terminated',
|
|
3387
|
+
(data) => {
|
|
3388
|
+
data.interaction.isTerminated = true;
|
|
3389
|
+
},
|
|
3390
|
+
],
|
|
3391
|
+
[
|
|
3392
|
+
'the interaction is not telephony',
|
|
3393
|
+
(data) => {
|
|
3394
|
+
data.interaction.mediaType = 'chat';
|
|
3395
|
+
},
|
|
3396
|
+
],
|
|
3397
|
+
[
|
|
3398
|
+
'the payload has no stable interaction identifier',
|
|
3399
|
+
(data) => {
|
|
3400
|
+
data.interactionId = undefined;
|
|
3401
|
+
data.interaction.interactionId = undefined;
|
|
3402
|
+
delete data.interaction.mainInteractionId;
|
|
3403
|
+
data.interaction.media = {};
|
|
3404
|
+
},
|
|
3405
|
+
],
|
|
3406
|
+
])('does not create a missing task when %s', (_description, mutatePayload) => {
|
|
3407
|
+
clearTasksAndFactoryHistory();
|
|
3408
|
+
const ownerChangeData = createPromotedAgentOwnerChangeData();
|
|
3409
|
+
mutatePayload(ownerChangeData);
|
|
2632
3410
|
|
|
2633
|
-
webSocketManagerMock.emit('message', JSON.stringify(
|
|
3411
|
+
webSocketManagerMock.emit('message', JSON.stringify({data: ownerChangeData}));
|
|
2634
3412
|
|
|
2635
|
-
expect(
|
|
2636
|
-
|
|
3413
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3414
|
+
expect(taskManager.getAllTasks()).toEqual({});
|
|
2637
3415
|
});
|
|
2638
3416
|
|
|
2639
|
-
it('
|
|
2640
|
-
|
|
2641
|
-
const
|
|
2642
|
-
|
|
2643
|
-
|
|
3417
|
+
it('does not create a missing task for an owner-changing ContactUpdated event', () => {
|
|
3418
|
+
clearTasksAndFactoryHistory();
|
|
3419
|
+
const contactUpdatedData = {
|
|
3420
|
+
...createPromotedAgentOwnerChangeData(),
|
|
3421
|
+
type: CC_EVENTS.CONTACT_UPDATED,
|
|
2644
3422
|
};
|
|
2645
|
-
taskManager.taskCollection[otherTaskId] = otherTask;
|
|
2646
3423
|
|
|
2647
|
-
|
|
2648
|
-
data: {
|
|
2649
|
-
type: CC_EVENTS.AGENT_CONSULT_CONFERENCED,
|
|
2650
|
-
interactionId: taskId,
|
|
2651
|
-
isConferencing: true,
|
|
2652
|
-
},
|
|
2653
|
-
};
|
|
3424
|
+
webSocketManagerMock.emit('message', JSON.stringify({data: contactUpdatedData}));
|
|
2654
3425
|
|
|
2655
|
-
|
|
3426
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3427
|
+
expect(taskManager.getAllTasks()).toEqual({});
|
|
3428
|
+
});
|
|
2656
3429
|
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
3430
|
+
it('reuses a recovered task for later AgentContact and ContactMerged events', () => {
|
|
3431
|
+
clearTasksAndFactoryHistory();
|
|
3432
|
+
const hydrateHandler = jest.fn();
|
|
3433
|
+
const mergedHandler = jest.fn();
|
|
3434
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
3435
|
+
taskManager.on(TASK_EVENTS.TASK_MERGED, mergedHandler);
|
|
2660
3436
|
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
});
|
|
3437
|
+
webSocketManagerMock.emit(
|
|
3438
|
+
'message',
|
|
3439
|
+
JSON.stringify({data: createPromotedAgentOwnerChangeData()})
|
|
3440
|
+
);
|
|
2666
3441
|
|
|
2667
|
-
|
|
2668
|
-
|
|
3442
|
+
const recoveredTask = taskManager.taskCollection[taskId];
|
|
3443
|
+
expect(recoveredTask).toBeDefined();
|
|
3444
|
+
expect(recoveredTask.listenerCount(TASK_EVENTS.TASK_HYDRATE)).toBe(1);
|
|
2669
3445
|
|
|
2670
|
-
|
|
2671
|
-
|
|
3446
|
+
webSocketManagerMock.emit(
|
|
3447
|
+
'message',
|
|
3448
|
+
JSON.stringify({
|
|
3449
|
+
data: {
|
|
3450
|
+
...createConferenceTaskData(taskId, currentAgentId),
|
|
3451
|
+
type: CC_EVENTS.AGENT_CONTACT,
|
|
3452
|
+
},
|
|
3453
|
+
})
|
|
3454
|
+
);
|
|
3455
|
+
webSocketManagerMock.emit(
|
|
3456
|
+
'message',
|
|
3457
|
+
JSON.stringify({
|
|
3458
|
+
data: {
|
|
3459
|
+
...createConferenceTaskData(taskId, currentAgentId),
|
|
3460
|
+
type: CC_EVENTS.CONTACT_MERGED,
|
|
3461
|
+
childInteractionId: 'promoted-child-interaction-id',
|
|
3462
|
+
},
|
|
3463
|
+
})
|
|
3464
|
+
);
|
|
3465
|
+
|
|
3466
|
+
expect(TaskFactory.createTask).toHaveBeenCalledTimes(1);
|
|
3467
|
+
expect(taskManager.taskCollection[taskId]).toBe(recoveredTask);
|
|
3468
|
+
expect(taskManager.taskCollection['promoted-child-interaction-id']).toBeUndefined();
|
|
3469
|
+
expect(Object.values(taskManager.taskCollection)).toEqual([recoveredTask]);
|
|
3470
|
+
expect(recoveredTask.listenerCount(TASK_EVENTS.TASK_HYDRATE)).toBe(1);
|
|
3471
|
+
expect(hydrateHandler).toHaveBeenCalledTimes(2);
|
|
3472
|
+
expect(mergedHandler).toHaveBeenCalledTimes(1);
|
|
3473
|
+
expect(mergedHandler).toHaveBeenCalledWith(recoveredTask);
|
|
2672
3474
|
});
|
|
2673
3475
|
|
|
2674
|
-
it('
|
|
2675
|
-
const task =
|
|
2676
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
2677
|
-
task.data = {
|
|
2678
|
-
...task.data,
|
|
2679
|
-
...newData,
|
|
2680
|
-
interaction: {
|
|
2681
|
-
...task.data.interaction,
|
|
2682
|
-
outboundType: 'OUTDIAL',
|
|
2683
|
-
state: 'new',
|
|
2684
|
-
mediaType: 'telephony',
|
|
2685
|
-
},
|
|
2686
|
-
agentsPendingWrapUp: ['different-agent-123'], // Current agent not in the list
|
|
2687
|
-
};
|
|
2688
|
-
return task;
|
|
2689
|
-
});
|
|
2690
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
2691
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
3476
|
+
it('does not let a late ParticipantLeftConference restore a departed owner', () => {
|
|
3477
|
+
const task = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
2692
3478
|
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
state: 'new',
|
|
2700
|
-
mediaType: 'telephony',
|
|
3479
|
+
webSocketManagerMock.emit(
|
|
3480
|
+
'message',
|
|
3481
|
+
JSON.stringify({
|
|
3482
|
+
data: {
|
|
3483
|
+
...createConferenceTaskData(taskId, promotedOwnerId),
|
|
3484
|
+
type: CC_EVENTS.CONTACT_UPDATED,
|
|
2701
3485
|
},
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
};
|
|
3486
|
+
})
|
|
3487
|
+
);
|
|
2705
3488
|
|
|
2706
|
-
|
|
3489
|
+
const participantLeftData = createConferenceTaskData(taskId, previousOwnerId);
|
|
3490
|
+
participantLeftData.participantId = previousOwnerId;
|
|
3491
|
+
participantLeftData.interaction.participants[previousOwnerId].hasLeft = true;
|
|
3492
|
+
participantLeftData.interaction.media[taskId].participants = [
|
|
3493
|
+
currentAgentId,
|
|
3494
|
+
promotedOwnerId,
|
|
3495
|
+
];
|
|
3496
|
+
webSocketManagerMock.emit(
|
|
3497
|
+
'message',
|
|
3498
|
+
JSON.stringify({
|
|
3499
|
+
data: {...participantLeftData, type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE},
|
|
3500
|
+
})
|
|
3501
|
+
);
|
|
2707
3502
|
|
|
2708
|
-
|
|
2709
|
-
|
|
3503
|
+
const participantLeaveEvent = expectLastStateMachineEvent(
|
|
3504
|
+
task.sendStateMachineEvent,
|
|
3505
|
+
TaskEvent.PARTICIPANT_LEAVE
|
|
3506
|
+
);
|
|
3507
|
+
expect(participantLeaveEvent.taskData.interaction.owner).toBe(promotedOwnerId);
|
|
3508
|
+
expect(task.data.interaction.owner).toBe(promotedOwnerId);
|
|
2710
3509
|
});
|
|
2711
3510
|
|
|
2712
|
-
it('
|
|
2713
|
-
const task =
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
3511
|
+
it('does not infer that an incoming owner departed from a partial main-call roster', () => {
|
|
3512
|
+
const task = installTask(createConferenceTaskData(taskId, promotedOwnerId));
|
|
3513
|
+
const partialParticipantLeftData = createConferenceTaskData(taskId, previousOwnerId);
|
|
3514
|
+
partialParticipantLeftData.participantId = 'another-agent-id';
|
|
3515
|
+
partialParticipantLeftData.interaction.media[taskId].participants = [
|
|
3516
|
+
currentAgentId,
|
|
3517
|
+
promotedOwnerId,
|
|
3518
|
+
];
|
|
3519
|
+
|
|
3520
|
+
webSocketManagerMock.emit(
|
|
3521
|
+
'message',
|
|
3522
|
+
JSON.stringify({
|
|
3523
|
+
data: {
|
|
3524
|
+
...partialParticipantLeftData,
|
|
3525
|
+
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
2723
3526
|
},
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
return task;
|
|
2727
|
-
});
|
|
2728
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
2729
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
3527
|
+
})
|
|
3528
|
+
);
|
|
2730
3529
|
|
|
2731
|
-
const
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
3530
|
+
const participantLeaveEvent = expectLastStateMachineEvent(
|
|
3531
|
+
task.sendStateMachineEvent,
|
|
3532
|
+
TaskEvent.PARTICIPANT_LEAVE
|
|
3533
|
+
);
|
|
3534
|
+
expect(participantLeaveEvent.taskData.interaction.owner).toBe(previousOwnerId);
|
|
3535
|
+
expect(task.data.interaction.owner).toBe(previousOwnerId);
|
|
3536
|
+
});
|
|
3537
|
+
|
|
3538
|
+
it('applies the promoted owner when ParticipantLeftConference arrives first', () => {
|
|
3539
|
+
const task = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
3540
|
+
const participantLeftData = createConferenceTaskData(taskId, previousOwnerId);
|
|
3541
|
+
participantLeftData.participantId = previousOwnerId;
|
|
3542
|
+
participantLeftData.interaction.participants[previousOwnerId].hasLeft = true;
|
|
3543
|
+
participantLeftData.interaction.media[taskId].participants = [
|
|
3544
|
+
currentAgentId,
|
|
3545
|
+
promotedOwnerId,
|
|
3546
|
+
];
|
|
3547
|
+
|
|
3548
|
+
webSocketManagerMock.emit(
|
|
3549
|
+
'message',
|
|
3550
|
+
JSON.stringify({
|
|
3551
|
+
data: {...participantLeftData, type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE},
|
|
3552
|
+
})
|
|
3553
|
+
);
|
|
3554
|
+
webSocketManagerMock.emit(
|
|
3555
|
+
'message',
|
|
3556
|
+
JSON.stringify({
|
|
3557
|
+
data: {
|
|
3558
|
+
...createConferenceTaskData(taskId, promotedOwnerId),
|
|
3559
|
+
type: CC_EVENTS.CONTACT_UPDATED,
|
|
2739
3560
|
},
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
};
|
|
3561
|
+
})
|
|
3562
|
+
);
|
|
2743
3563
|
|
|
2744
|
-
|
|
3564
|
+
expect(task.data.interaction.owner).toBe(promotedOwnerId);
|
|
3565
|
+
expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
|
|
3566
|
+
});
|
|
3567
|
+
});
|
|
3568
|
+
|
|
3569
|
+
describe('Conference event handling', () => {
|
|
3570
|
+
let task;
|
|
2745
3571
|
|
|
2746
|
-
|
|
2747
|
-
|
|
3572
|
+
beforeEach(() => {
|
|
3573
|
+
task = {
|
|
3574
|
+
data: {interactionId: taskId},
|
|
3575
|
+
emit: jest.fn(),
|
|
3576
|
+
updateTaskData: jest.fn(),
|
|
3577
|
+
sendStateMachineEvent: jest.fn(),
|
|
3578
|
+
};
|
|
3579
|
+
taskManager.taskCollection[taskId] = task as any;
|
|
2748
3580
|
});
|
|
2749
3581
|
|
|
2750
|
-
it('
|
|
2751
|
-
const
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
state: 'WRAPUP', // Not 'new' state
|
|
2760
|
-
mediaType: 'telephony',
|
|
2761
|
-
},
|
|
2762
|
-
agentsPendingWrapUp: [], // No agents pending wrap-up
|
|
2763
|
-
};
|
|
2764
|
-
return task;
|
|
2765
|
-
});
|
|
2766
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
2767
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
3582
|
+
it('sends AGENT_CONSULT_CONFERENCED to state machine as CONFERENCE_START', () => {
|
|
3583
|
+
const payload = {
|
|
3584
|
+
data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCED, interactionId: taskId},
|
|
3585
|
+
};
|
|
3586
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
3587
|
+
expect(task.sendStateMachineEvent).toHaveBeenCalled();
|
|
3588
|
+
const call = task.sendStateMachineEvent.mock.calls[0][0];
|
|
3589
|
+
expect(call.type).toBe(TaskEvent.CONFERENCE_START);
|
|
3590
|
+
});
|
|
2768
3591
|
|
|
3592
|
+
it('sends PARTICIPANT_JOINED_CONFERENCE to state machine as CONFERENCE_START', () => {
|
|
2769
3593
|
const payload = {
|
|
2770
|
-
data: {
|
|
2771
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
2772
|
-
interactionId: taskId,
|
|
2773
|
-
interaction: {
|
|
2774
|
-
outboundType: 'OUTDIAL',
|
|
2775
|
-
state: 'WRAPUP', // Not 'new' state
|
|
2776
|
-
mediaType: 'telephony',
|
|
2777
|
-
},
|
|
2778
|
-
agentsPendingWrapUp: [], // No agents pending wrap-up
|
|
2779
|
-
},
|
|
3594
|
+
data: {type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE, interactionId: taskId},
|
|
2780
3595
|
};
|
|
3596
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
3597
|
+
expect(task.sendStateMachineEvent).toHaveBeenCalled();
|
|
3598
|
+
const call = task.sendStateMachineEvent.mock.calls[0][0];
|
|
3599
|
+
expect(call.type).toBe(TaskEvent.CONFERENCE_START);
|
|
3600
|
+
});
|
|
2781
3601
|
|
|
3602
|
+
it('sends AGENT_CONSULT_CONFERENCE_FAILED to state machine as CONFERENCE_FAILED', () => {
|
|
3603
|
+
const payload = {
|
|
3604
|
+
data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED, interactionId: taskId},
|
|
3605
|
+
};
|
|
2782
3606
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
3607
|
+
expect(task.sendStateMachineEvent).toHaveBeenCalled();
|
|
3608
|
+
const call = task.sendStateMachineEvent.mock.calls[0][0];
|
|
3609
|
+
expect(call.type).toBe(TaskEvent.CONFERENCE_FAILED);
|
|
3610
|
+
});
|
|
2783
3611
|
|
|
2784
|
-
|
|
2785
|
-
|
|
3612
|
+
it('sends PARTICIPANT_LEFT_CONFERENCE to state machine as PARTICIPANT_LEAVE', () => {
|
|
3613
|
+
const payload = {
|
|
3614
|
+
data: {type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, interactionId: taskId},
|
|
3615
|
+
};
|
|
3616
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
3617
|
+
expect(task.sendStateMachineEvent).toHaveBeenCalled();
|
|
3618
|
+
const call = task.sendStateMachineEvent.mock.calls[0][0];
|
|
3619
|
+
expect(call.type).toBe(TaskEvent.PARTICIPANT_LEAVE);
|
|
2786
3620
|
});
|
|
2787
3621
|
|
|
2788
|
-
it('
|
|
2789
|
-
const
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
},
|
|
2800
|
-
agentsPendingWrapUp: undefined, // No agentsPendingWrapUp field
|
|
2801
|
-
};
|
|
2802
|
-
return task;
|
|
3622
|
+
it('routes participant-left from a main interaction to a child-keyed task', () => {
|
|
3623
|
+
const childTaskId = 'child-interaction-id';
|
|
3624
|
+
const childTask = createMockTask({
|
|
3625
|
+
...taskDataMock,
|
|
3626
|
+
interactionId: childTaskId,
|
|
3627
|
+
isConsulted: true,
|
|
3628
|
+
interaction: {
|
|
3629
|
+
mediaType: 'telephony',
|
|
3630
|
+
interactionId: childTaskId,
|
|
3631
|
+
parentInteractionId: taskId,
|
|
3632
|
+
},
|
|
2803
3633
|
});
|
|
2804
|
-
|
|
2805
|
-
|
|
3634
|
+
delete taskManager.taskCollection[taskId];
|
|
3635
|
+
taskManager.taskCollection[childTaskId] = childTask;
|
|
2806
3636
|
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
3637
|
+
webSocketManagerMock.emit(
|
|
3638
|
+
'message',
|
|
3639
|
+
JSON.stringify({
|
|
3640
|
+
data: {
|
|
3641
|
+
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3642
|
+
interactionId: taskId,
|
|
3643
|
+
participantId: 'test-agent-id',
|
|
3644
|
+
interaction: {
|
|
3645
|
+
mediaType: 'telephony',
|
|
3646
|
+
interactionId: taskId,
|
|
3647
|
+
mainInteractionId: taskId,
|
|
3648
|
+
},
|
|
2815
3649
|
},
|
|
2816
|
-
|
|
3650
|
+
})
|
|
3651
|
+
);
|
|
3652
|
+
|
|
3653
|
+
expect(childTask.sendStateMachineEvent).toHaveBeenCalledWith(
|
|
3654
|
+
expect.objectContaining({
|
|
3655
|
+
type: TaskEvent.PARTICIPANT_LEAVE,
|
|
3656
|
+
participantId: 'test-agent-id',
|
|
3657
|
+
})
|
|
3658
|
+
);
|
|
3659
|
+
expect(taskManager.taskCollection[childTaskId]).toBeUndefined();
|
|
3660
|
+
expect(taskManager.taskCollection[taskId]).toBe(childTask);
|
|
3661
|
+
expect(
|
|
3662
|
+
Object.values(taskManager.taskCollection).filter((entry) => entry === childTask)
|
|
3663
|
+
).toHaveLength(1);
|
|
3664
|
+
});
|
|
3665
|
+
|
|
3666
|
+
it('routes consult-end from a main interaction to a child-keyed consulted task', () => {
|
|
3667
|
+
const childTaskId = 'consult-child-interaction-id';
|
|
3668
|
+
const childTask = createMockTask({
|
|
3669
|
+
...taskDataMock,
|
|
3670
|
+
interactionId: childTaskId,
|
|
3671
|
+
isConsulted: true,
|
|
3672
|
+
interaction: {
|
|
3673
|
+
mediaType: 'telephony',
|
|
3674
|
+
interactionId: childTaskId,
|
|
3675
|
+
callProcessingDetails: {parentInteractionId: taskId},
|
|
2817
3676
|
},
|
|
2818
|
-
};
|
|
3677
|
+
});
|
|
3678
|
+
delete taskManager.taskCollection[taskId];
|
|
3679
|
+
taskManager.taskCollection[childTaskId] = childTask;
|
|
2819
3680
|
|
|
2820
|
-
webSocketManagerMock.emit(
|
|
3681
|
+
webSocketManagerMock.emit(
|
|
3682
|
+
'message',
|
|
3683
|
+
JSON.stringify({
|
|
3684
|
+
data: {
|
|
3685
|
+
type: CC_EVENTS.AGENT_CONSULT_ENDED,
|
|
3686
|
+
interactionId: taskId,
|
|
3687
|
+
interaction: {
|
|
3688
|
+
mediaType: 'telephony',
|
|
3689
|
+
interactionId: taskId,
|
|
3690
|
+
mainInteractionId: taskId,
|
|
3691
|
+
},
|
|
3692
|
+
},
|
|
3693
|
+
})
|
|
3694
|
+
);
|
|
2821
3695
|
|
|
2822
|
-
expect(
|
|
2823
|
-
|
|
3696
|
+
expect(childTask.sendStateMachineEvent).toHaveBeenCalledWith(
|
|
3697
|
+
expect.objectContaining({type: TaskEvent.CONSULT_END})
|
|
3698
|
+
);
|
|
2824
3699
|
});
|
|
2825
3700
|
|
|
2826
|
-
it('
|
|
2827
|
-
const
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
mediaType: 'telephony',
|
|
2837
|
-
},
|
|
2838
|
-
agentsPendingWrapUp: [agentId], // Current agent needs wrap-up
|
|
2839
|
-
};
|
|
2840
|
-
return task;
|
|
3701
|
+
it('deduplicates aliases of the same child task during related-interaction lookup', () => {
|
|
3702
|
+
const childTaskId = 'aliased-child-interaction-id';
|
|
3703
|
+
const childTask = createMockTask({
|
|
3704
|
+
...taskDataMock,
|
|
3705
|
+
interactionId: childTaskId,
|
|
3706
|
+
interaction: {
|
|
3707
|
+
mediaType: 'telephony',
|
|
3708
|
+
interactionId: childTaskId,
|
|
3709
|
+
mainInteractionId: taskId,
|
|
3710
|
+
},
|
|
2841
3711
|
});
|
|
2842
|
-
|
|
2843
|
-
|
|
3712
|
+
delete taskManager.taskCollection[taskId];
|
|
3713
|
+
taskManager.taskCollection[childTaskId] = childTask;
|
|
3714
|
+
taskManager.taskCollection['child-task-alias'] = childTask;
|
|
2844
3715
|
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
mediaType: 'telephony',
|
|
3716
|
+
webSocketManagerMock.emit(
|
|
3717
|
+
'message',
|
|
3718
|
+
JSON.stringify({
|
|
3719
|
+
data: {
|
|
3720
|
+
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3721
|
+
interactionId: taskId,
|
|
3722
|
+
participantId: 'test-agent-id',
|
|
2853
3723
|
},
|
|
2854
|
-
|
|
3724
|
+
})
|
|
3725
|
+
);
|
|
3726
|
+
|
|
3727
|
+
expect(childTask.sendStateMachineEvent).toHaveBeenCalledTimes(1);
|
|
3728
|
+
expect(
|
|
3729
|
+
Object.values(taskManager.taskCollection).filter((entry) => entry === childTask)
|
|
3730
|
+
).toHaveLength(1);
|
|
3731
|
+
});
|
|
3732
|
+
|
|
3733
|
+
it('prefers an exact task match over a child task with the same main interaction', () => {
|
|
3734
|
+
const childTaskId = 'child-interaction-id';
|
|
3735
|
+
const childTask = createMockTask({
|
|
3736
|
+
...taskDataMock,
|
|
3737
|
+
interactionId: childTaskId,
|
|
3738
|
+
interaction: {
|
|
3739
|
+
mediaType: 'telephony',
|
|
3740
|
+
interactionId: childTaskId,
|
|
3741
|
+
mainInteractionId: taskId,
|
|
2855
3742
|
},
|
|
2856
|
-
};
|
|
3743
|
+
});
|
|
3744
|
+
taskManager.taskCollection[childTaskId] = childTask;
|
|
2857
3745
|
|
|
2858
|
-
webSocketManagerMock.emit(
|
|
3746
|
+
webSocketManagerMock.emit(
|
|
3747
|
+
'message',
|
|
3748
|
+
JSON.stringify({
|
|
3749
|
+
data: {
|
|
3750
|
+
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3751
|
+
interactionId: taskId,
|
|
3752
|
+
participantId: 'another-agent',
|
|
3753
|
+
},
|
|
3754
|
+
})
|
|
3755
|
+
);
|
|
2859
3756
|
|
|
2860
|
-
expect(
|
|
2861
|
-
expect(
|
|
3757
|
+
expect(task.sendStateMachineEvent).toHaveBeenCalled();
|
|
3758
|
+
expect(childTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
2862
3759
|
});
|
|
2863
3760
|
|
|
2864
|
-
it('
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
...
|
|
2869
|
-
|
|
3761
|
+
it('does not route a main-interaction event when multiple unique child tasks match', () => {
|
|
3762
|
+
delete taskManager.taskCollection[taskId];
|
|
3763
|
+
const createChildTask = (interactionId: string) =>
|
|
3764
|
+
createMockTask({
|
|
3765
|
+
...taskDataMock,
|
|
3766
|
+
interactionId,
|
|
2870
3767
|
interaction: {
|
|
2871
|
-
...task.data.interaction,
|
|
2872
|
-
outboundType: 'PREVIEW', // Not OUTDIAL
|
|
2873
|
-
state: 'new',
|
|
2874
3768
|
mediaType: 'telephony',
|
|
3769
|
+
interactionId,
|
|
3770
|
+
mainInteractionId: taskId,
|
|
2875
3771
|
},
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
3772
|
+
});
|
|
3773
|
+
const firstChildTask = createChildTask('child-1');
|
|
3774
|
+
const secondChildTask = createChildTask('child-2');
|
|
3775
|
+
taskManager.taskCollection['child-1'] = firstChildTask;
|
|
3776
|
+
taskManager.taskCollection['child-2'] = secondChildTask;
|
|
3777
|
+
|
|
3778
|
+
webSocketManagerMock.emit(
|
|
3779
|
+
'message',
|
|
3780
|
+
JSON.stringify({
|
|
3781
|
+
data: {
|
|
3782
|
+
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3783
|
+
interactionId: taskId,
|
|
3784
|
+
},
|
|
3785
|
+
})
|
|
3786
|
+
);
|
|
2882
3787
|
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
3788
|
+
expect(firstChildTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3789
|
+
expect(secondChildTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3790
|
+
});
|
|
3791
|
+
|
|
3792
|
+
it('does not route a lifecycle event without a correlation identifier', () => {
|
|
3793
|
+
webSocketManagerMock.emit(
|
|
3794
|
+
'message',
|
|
3795
|
+
JSON.stringify({
|
|
3796
|
+
data: {
|
|
3797
|
+
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3798
|
+
participantId: 'another-agent',
|
|
2891
3799
|
},
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
};
|
|
3800
|
+
})
|
|
3801
|
+
);
|
|
2895
3802
|
|
|
2896
|
-
|
|
3803
|
+
expect(task.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3804
|
+
});
|
|
3805
|
+
|
|
3806
|
+
it('removes every collection alias for a task during terminal cleanup', () => {
|
|
3807
|
+
taskManager.taskCollection['task-alias'] = task;
|
|
3808
|
+
|
|
3809
|
+
(taskManager as any).removeTaskFromCollection(task);
|
|
3810
|
+
|
|
3811
|
+
expect(taskManager.taskCollection[taskId]).toBeUndefined();
|
|
3812
|
+
expect(taskManager.taskCollection['task-alias']).toBeUndefined();
|
|
3813
|
+
});
|
|
3814
|
+
|
|
3815
|
+
it('replaces an EP-DN child task with a hydrated main task on CONTACT_MERGED', () => {
|
|
3816
|
+
const childTaskId = 'ep-dn-child-interaction-id';
|
|
3817
|
+
const childTask = createMockTask({
|
|
3818
|
+
...taskDataMock,
|
|
3819
|
+
interactionId: childTaskId,
|
|
3820
|
+
interaction: {
|
|
3821
|
+
mediaType: 'telephony',
|
|
3822
|
+
interactionId: childTaskId,
|
|
3823
|
+
parentInteractionId: taskId,
|
|
3824
|
+
},
|
|
3825
|
+
});
|
|
3826
|
+
const mergedHandler = jest.fn();
|
|
3827
|
+
delete taskManager.taskCollection[taskId];
|
|
3828
|
+
taskManager.taskCollection[childTaskId] = childTask;
|
|
3829
|
+
taskManager.on(TASK_EVENTS.TASK_MERGED, mergedHandler);
|
|
3830
|
+
|
|
3831
|
+
webSocketManagerMock.emit(
|
|
3832
|
+
'message',
|
|
3833
|
+
JSON.stringify({
|
|
3834
|
+
data: {
|
|
3835
|
+
type: CC_EVENTS.CONTACT_MERGED,
|
|
3836
|
+
interactionId: taskId,
|
|
3837
|
+
childInteractionId: childTaskId,
|
|
3838
|
+
interaction: {
|
|
3839
|
+
mediaType: 'telephony',
|
|
3840
|
+
interactionId: taskId,
|
|
3841
|
+
mainInteractionId: taskId,
|
|
3842
|
+
state: 'conference',
|
|
3843
|
+
owner: 'test-agent-id',
|
|
3844
|
+
participants: {
|
|
3845
|
+
'test-agent-id': {id: 'test-agent-id', pType: 'Agent', hasLeft: false},
|
|
3846
|
+
},
|
|
3847
|
+
media: {
|
|
3848
|
+
[taskId]: {
|
|
3849
|
+
mediaResourceId: taskId,
|
|
3850
|
+
mediaType: 'telephony',
|
|
3851
|
+
mType: 'mainCall',
|
|
3852
|
+
participants: ['test-agent-id'],
|
|
3853
|
+
},
|
|
3854
|
+
},
|
|
3855
|
+
},
|
|
3856
|
+
},
|
|
3857
|
+
})
|
|
3858
|
+
);
|
|
2897
3859
|
|
|
2898
|
-
|
|
2899
|
-
expect(taskManager.
|
|
3860
|
+
const mainTask = taskManager.taskCollection[taskId];
|
|
3861
|
+
expect(taskManager.taskCollection[childTaskId]).toBeUndefined();
|
|
3862
|
+
expect(mainTask).toBeDefined();
|
|
3863
|
+
expect(mainTask).not.toBe(childTask);
|
|
3864
|
+
expect(mainTask.sendStateMachineEvent).toHaveBeenCalledWith(
|
|
3865
|
+
expect.objectContaining({type: TaskEvent.HYDRATE})
|
|
3866
|
+
);
|
|
3867
|
+
expect(mergedHandler).toHaveBeenCalledWith(mainTask);
|
|
2900
3868
|
});
|
|
2901
3869
|
|
|
2902
|
-
it('
|
|
2903
|
-
const task = taskManager.getTask(taskId);
|
|
2904
|
-
task.updateTaskData = jest.fn().mockImplementation((newData) => {
|
|
2905
|
-
task.data = {
|
|
2906
|
-
...task.data,
|
|
2907
|
-
...newData,
|
|
2908
|
-
interaction: {
|
|
2909
|
-
...task.data.interaction,
|
|
2910
|
-
outboundType: 'OUTDIAL',
|
|
2911
|
-
state: 'new',
|
|
2912
|
-
mediaType: 'telephony',
|
|
2913
|
-
},
|
|
2914
|
-
agentsPendingWrapUp: ['agent-1', 'agent-2', 'agent-3'], // Current agent not in the list
|
|
2915
|
-
};
|
|
2916
|
-
return task;
|
|
2917
|
-
});
|
|
2918
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
2919
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
2920
|
-
|
|
3870
|
+
it('handles AGENT_CONSULT_CONFERENCING event without errors', () => {
|
|
2921
3871
|
const payload = {
|
|
2922
|
-
data: {
|
|
2923
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
2924
|
-
interactionId: taskId,
|
|
2925
|
-
interaction: {
|
|
2926
|
-
outboundType: 'OUTDIAL',
|
|
2927
|
-
state: 'new',
|
|
2928
|
-
mediaType: 'telephony',
|
|
2929
|
-
},
|
|
2930
|
-
agentsPendingWrapUp: ['agent-1', 'agent-2', 'agent-3'],
|
|
2931
|
-
},
|
|
3872
|
+
data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCING, interactionId: taskId},
|
|
2932
3873
|
};
|
|
3874
|
+
expect(() => {
|
|
3875
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
3876
|
+
}).not.toThrow();
|
|
3877
|
+
});
|
|
2933
3878
|
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
3879
|
+
it('handles PARTICIPANT_LEFT_CONFERENCE_FAILED event without errors', () => {
|
|
3880
|
+
const payload = {
|
|
3881
|
+
data: {type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE_FAILED, interactionId: taskId},
|
|
3882
|
+
};
|
|
3883
|
+
expect(() => {
|
|
3884
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
3885
|
+
}).not.toThrow();
|
|
2938
3886
|
});
|
|
2939
3887
|
|
|
2940
|
-
it('
|
|
2941
|
-
const
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
state: 'new',
|
|
2950
|
-
mediaType: 'telephony',
|
|
2951
|
-
},
|
|
2952
|
-
agentsPendingWrapUp: ['agent-1', agentId, 'agent-3'], // Current agent IS in the list
|
|
2953
|
-
};
|
|
2954
|
-
return task;
|
|
2955
|
-
});
|
|
2956
|
-
task.unregisterWebCallListeners = jest.fn();
|
|
2957
|
-
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
|
|
3888
|
+
it('only routes conference events to matching tasks', () => {
|
|
3889
|
+
const otherTaskId = 'other-task-id';
|
|
3890
|
+
const otherTask: any = {
|
|
3891
|
+
data: {interactionId: otherTaskId},
|
|
3892
|
+
emit: jest.fn(),
|
|
3893
|
+
updateTaskData: jest.fn(),
|
|
3894
|
+
sendStateMachineEvent: jest.fn(),
|
|
3895
|
+
};
|
|
3896
|
+
taskManager.taskCollection[otherTaskId] = otherTask;
|
|
2958
3897
|
|
|
2959
3898
|
const payload = {
|
|
2960
|
-
data: {
|
|
2961
|
-
type: CC_EVENTS.CONTACT_ENDED,
|
|
2962
|
-
interactionId: taskId,
|
|
2963
|
-
interaction: {
|
|
2964
|
-
outboundType: 'OUTDIAL',
|
|
2965
|
-
state: 'new',
|
|
2966
|
-
mediaType: 'telephony',
|
|
2967
|
-
},
|
|
2968
|
-
agentsPendingWrapUp: ['agent-1', agentId, 'agent-3'],
|
|
2969
|
-
},
|
|
3899
|
+
data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCED, interactionId: taskId},
|
|
2970
3900
|
};
|
|
2971
|
-
|
|
2972
3901
|
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2973
3902
|
|
|
2974
|
-
expect(
|
|
2975
|
-
expect(
|
|
3903
|
+
expect(task.sendStateMachineEvent).toHaveBeenCalled();
|
|
3904
|
+
expect(otherTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
2976
3905
|
});
|
|
2977
3906
|
});
|
|
2978
3907
|
|
|
2979
|
-
describe('
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3908
|
+
describe('state machine integration', () => {
|
|
3909
|
+
it('maps CC events to task state machine events using normalized payload', () => {
|
|
3910
|
+
const mapped = (TaskManager as any).mapEventToTaskStateMachineEvent(
|
|
3911
|
+
CC_EVENTS.AGENT_CONTACT_ASSIGNED,
|
|
3912
|
+
{...taskDataMock, type: CC_EVENTS.AGENT_CONTACT_ASSIGNED}
|
|
3913
|
+
);
|
|
2983
3914
|
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
taskEmitSpy = jest.spyOn(task, 'emit');
|
|
2989
|
-
managerEmitSpy = jest.spyOn(taskManager, 'emit');
|
|
3915
|
+
expect(mapped).toEqual({
|
|
3916
|
+
type: TaskEvent.ASSIGN,
|
|
3917
|
+
taskData: {...taskDataMock, type: CC_EVENTS.AGENT_CONTACT_ASSIGNED},
|
|
3918
|
+
});
|
|
2990
3919
|
});
|
|
2991
3920
|
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3921
|
+
const stampWrapUp = (
|
|
3922
|
+
eventType: CC_EVENTS,
|
|
3923
|
+
payload: Record<string, unknown>,
|
|
3924
|
+
agentId = 'test-agent-id',
|
|
3925
|
+
task?: unknown
|
|
3926
|
+
) => (TaskManager as any).stampWrapUpRequiredForEvent(eventType, payload, agentId, task);
|
|
3927
|
+
|
|
3928
|
+
it('maps conference wrap-up events without restamping wrapUpRequired', () => {
|
|
3929
|
+
const unstamped = {...taskDataMock, wrapUpRequired: false};
|
|
3930
|
+
const conferenceEnded = (TaskManager as any).mapEventToTaskStateMachineEvent(
|
|
3931
|
+
CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED,
|
|
3932
|
+
unstamped,
|
|
3933
|
+
'test-agent-id'
|
|
3934
|
+
);
|
|
3935
|
+
const participantLeft = (TaskManager as any).mapEventToTaskStateMachineEvent(
|
|
3936
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3937
|
+
{...unstamped, participantId: 'test-agent-id'},
|
|
3938
|
+
'test-agent-id'
|
|
3939
|
+
);
|
|
3940
|
+
const transferred = (TaskManager as any).mapEventToTaskStateMachineEvent(
|
|
3941
|
+
CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED,
|
|
3942
|
+
unstamped,
|
|
3943
|
+
'test-agent-id'
|
|
3944
|
+
);
|
|
3945
|
+
|
|
3946
|
+
expect(conferenceEnded.taskData).toBe(unstamped);
|
|
3947
|
+
expect(participantLeft.taskData.wrapUpRequired).toBe(false);
|
|
3948
|
+
expect(participantLeft.participantId).toBe('test-agent-id');
|
|
3949
|
+
expect(transferred.taskData).toBe(unstamped);
|
|
3950
|
+
});
|
|
3005
3951
|
|
|
3006
|
-
|
|
3952
|
+
it('stamps wrapUpRequired on AGENT_CONSULT_CONFERENCE_ENDED only from explicit signals', () => {
|
|
3953
|
+
const wrapping = stampWrapUp(CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED, {
|
|
3954
|
+
...taskDataMock,
|
|
3955
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
3956
|
+
});
|
|
3957
|
+
const remaining = stampWrapUp(CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED, {
|
|
3958
|
+
...taskDataMock,
|
|
3959
|
+
owner: 'test-agent-id',
|
|
3960
|
+
isConsulted: false,
|
|
3961
|
+
});
|
|
3007
3962
|
|
|
3008
|
-
|
|
3009
|
-
expect(
|
|
3010
|
-
expect(updatedTask.data.interaction.customField).toBe('updated-value');
|
|
3011
|
-
expect(updatedTask.data.interaction.state).toBe('merged');
|
|
3012
|
-
expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, updatedTask);
|
|
3963
|
+
expect(wrapping.wrapUpRequired).toBe(true);
|
|
3964
|
+
expect(remaining.wrapUpRequired).toBe(false);
|
|
3013
3965
|
});
|
|
3014
3966
|
|
|
3015
|
-
it('
|
|
3016
|
-
const
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3967
|
+
it('stamps wrapUpRequired on PARTICIPANT_LEFT_CONFERENCE only when this agent left', () => {
|
|
3968
|
+
const selfLeft = stampWrapUp(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, {
|
|
3969
|
+
...taskDataMock,
|
|
3970
|
+
participantId: 'test-agent-id',
|
|
3971
|
+
interaction: {
|
|
3972
|
+
...taskDataMock.interaction,
|
|
3973
|
+
owner: 'test-agent-id',
|
|
3974
|
+
participants: {},
|
|
3975
|
+
},
|
|
3976
|
+
});
|
|
3977
|
+
const otherLeft = stampWrapUp(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, {
|
|
3978
|
+
...taskDataMock,
|
|
3979
|
+
participantId: 'other-agent-id',
|
|
3980
|
+
interaction: {
|
|
3981
|
+
...taskDataMock.interaction,
|
|
3982
|
+
owner: 'test-agent-id',
|
|
3983
|
+
participants: {
|
|
3984
|
+
'test-agent-id': {id: 'test-agent-id', pType: 'Agent', hasLeft: false},
|
|
3031
3985
|
},
|
|
3032
3986
|
},
|
|
3033
|
-
};
|
|
3987
|
+
});
|
|
3034
3988
|
|
|
3035
|
-
|
|
3036
|
-
expect(
|
|
3989
|
+
expect(selfLeft.wrapUpRequired).toBe(true);
|
|
3990
|
+
expect(otherLeft.wrapUpRequired).toBe(false);
|
|
3991
|
+
});
|
|
3037
3992
|
|
|
3038
|
-
|
|
3993
|
+
it('stamps wrapUpRequired true when this agent left and pending is empty or absent', () => {
|
|
3994
|
+
const emptyPending = stampWrapUp(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, {
|
|
3995
|
+
...taskDataMock,
|
|
3996
|
+
participantId: 'test-agent-id',
|
|
3997
|
+
agentsPendingWrapUp: [],
|
|
3998
|
+
interaction: {
|
|
3999
|
+
...taskDataMock.interaction,
|
|
4000
|
+
participants: {},
|
|
4001
|
+
},
|
|
4002
|
+
});
|
|
4003
|
+
const absentPending = stampWrapUp(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, {
|
|
4004
|
+
...taskDataMock,
|
|
4005
|
+
participantId: 'test-agent-id',
|
|
4006
|
+
interaction: {
|
|
4007
|
+
...taskDataMock.interaction,
|
|
4008
|
+
participants: {},
|
|
4009
|
+
},
|
|
4010
|
+
});
|
|
3039
4011
|
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
expect(newTask).toBeDefined();
|
|
3043
|
-
expect(newTask.data.interactionId).toBe(newMergedTaskId);
|
|
3044
|
-
expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, newTask);
|
|
4012
|
+
expect(emptyPending.wrapUpRequired).toBe(true);
|
|
4013
|
+
expect(absentPending.wrapUpRequired).toBe(true);
|
|
3045
4014
|
});
|
|
3046
4015
|
|
|
3047
|
-
it('
|
|
3048
|
-
const
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
interaction: {mediaType: 'telephony'},
|
|
4016
|
+
it('does not stamp wrapUpRequired when this agent left but pending excludes self', () => {
|
|
4017
|
+
const leftExcluded = stampWrapUp(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, {
|
|
4018
|
+
...taskDataMock,
|
|
4019
|
+
participantId: 'test-agent-id',
|
|
4020
|
+
wrapUpRequired: true,
|
|
4021
|
+
agentsPendingWrapUp: ['other-agent-id'],
|
|
4022
|
+
interaction: {
|
|
4023
|
+
...taskDataMock.interaction,
|
|
4024
|
+
owner: 'test-agent-id',
|
|
4025
|
+
participants: {},
|
|
3058
4026
|
},
|
|
3059
|
-
};
|
|
3060
|
-
webSocketManagerMock.emit('message', JSON.stringify(childPayload));
|
|
4027
|
+
});
|
|
3061
4028
|
|
|
3062
|
-
|
|
3063
|
-
|
|
4029
|
+
expect(leftExcluded.wrapUpRequired).toBe(false);
|
|
4030
|
+
});
|
|
3064
4031
|
|
|
3065
|
-
|
|
3066
|
-
const
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
interaction
|
|
4032
|
+
it('does not infer self-leave from a partial participant map when another participant is named', () => {
|
|
4033
|
+
const remaining = stampWrapUp(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, {
|
|
4034
|
+
...taskDataMock,
|
|
4035
|
+
participantId: 'other-agent-id',
|
|
4036
|
+
agentsPendingWrapUp: [],
|
|
4037
|
+
interaction: {
|
|
4038
|
+
...taskDataMock.interaction,
|
|
4039
|
+
owner: 'test-agent-id',
|
|
4040
|
+
participants: {
|
|
4041
|
+
'other-agent-id': {id: 'other-agent-id', pType: 'Agent', hasLeft: true},
|
|
4042
|
+
},
|
|
3072
4043
|
},
|
|
3073
|
-
};
|
|
3074
|
-
webSocketManagerMock.emit('message', JSON.stringify(parentPayload));
|
|
4044
|
+
});
|
|
3075
4045
|
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
4046
|
+
expect(remaining.wrapUpRequired).toBe(false);
|
|
4047
|
+
|
|
4048
|
+
class TimerTask extends Task {
|
|
4049
|
+
public accept() {
|
|
4050
|
+
return Promise.resolve({} as any);
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
const remainingTask = new TimerTask(
|
|
4054
|
+
{},
|
|
4055
|
+
{...taskDataMock, wrapUpRequired: false},
|
|
4056
|
+
{channelType: 'voice', isEndTaskEnabled: true, isEndConsultEnabled: true},
|
|
4057
|
+
{
|
|
4058
|
+
wrapUpProps: {
|
|
4059
|
+
autoWrapup: true,
|
|
4060
|
+
autoWrapupInterval: 1000,
|
|
4061
|
+
wrapUpReasonList: [{id: 'code-1', name: 'Default', isDefault: true, isSystem: false}],
|
|
3086
4062
|
},
|
|
3087
4063
|
},
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
4064
|
+
'test-agent-id'
|
|
4065
|
+
);
|
|
4066
|
+
try {
|
|
4067
|
+
remainingTask.updateTaskData(remaining);
|
|
4068
|
+
expect(remainingTask.autoWrapup).toBeUndefined();
|
|
4069
|
+
} finally {
|
|
4070
|
+
remainingTask.cancelAutoWrapupTimer();
|
|
4071
|
+
}
|
|
4072
|
+
});
|
|
3097
4073
|
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
4074
|
+
it('does not stamp wrapUpRequired when a consulted non-owner leaves with empty pending', () => {
|
|
4075
|
+
const consulted = stampWrapUp(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, {
|
|
4076
|
+
...taskDataMock,
|
|
4077
|
+
participantId: 'test-agent-id',
|
|
4078
|
+
isConsulted: true,
|
|
4079
|
+
agentsPendingWrapUp: [],
|
|
4080
|
+
interaction: {
|
|
4081
|
+
...taskDataMock.interaction,
|
|
4082
|
+
owner: 'other-agent-id',
|
|
4083
|
+
participants: {},
|
|
4084
|
+
},
|
|
4085
|
+
});
|
|
4086
|
+
const consultedFromTask = stampWrapUp(
|
|
4087
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
4088
|
+
{
|
|
4089
|
+
...taskDataMock,
|
|
4090
|
+
participantId: 'test-agent-id',
|
|
4091
|
+
agentsPendingWrapUp: [],
|
|
4092
|
+
interaction: {
|
|
4093
|
+
...taskDataMock.interaction,
|
|
4094
|
+
participants: {},
|
|
4095
|
+
},
|
|
4096
|
+
},
|
|
4097
|
+
'test-agent-id',
|
|
4098
|
+
{
|
|
4099
|
+
data: {
|
|
4100
|
+
isConsulted: true,
|
|
4101
|
+
interaction: {owner: 'other-agent-id'},
|
|
4102
|
+
},
|
|
4103
|
+
}
|
|
3106
4104
|
);
|
|
4105
|
+
|
|
4106
|
+
expect(consulted.wrapUpRequired).toBe(false);
|
|
4107
|
+
expect(consultedFromTask.wrapUpRequired).toBe(false);
|
|
3107
4108
|
});
|
|
3108
4109
|
|
|
3109
|
-
it('
|
|
3110
|
-
const
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
agentId: taskDataMock.agentId,
|
|
4110
|
+
it('stamps wrapUpRequired when EP-DN omits participantId and previous task included self', () => {
|
|
4111
|
+
const epdn = stampWrapUp(
|
|
4112
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
4113
|
+
{
|
|
4114
|
+
...taskDataMock,
|
|
4115
|
+
agentsPendingWrapUp: [],
|
|
3116
4116
|
interaction: {
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
participants: {
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
isWrapUp: false,
|
|
4117
|
+
...taskDataMock.interaction,
|
|
4118
|
+
owner: 'test-agent-id',
|
|
4119
|
+
participants: {},
|
|
4120
|
+
},
|
|
4121
|
+
},
|
|
4122
|
+
'test-agent-id',
|
|
4123
|
+
{
|
|
4124
|
+
data: {
|
|
4125
|
+
interaction: {
|
|
4126
|
+
participants: {
|
|
4127
|
+
'test-agent-id': {id: 'test-agent-id', pType: 'Agent', hasLeft: false},
|
|
3129
4128
|
},
|
|
3130
4129
|
},
|
|
3131
4130
|
},
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
|
|
4131
|
+
}
|
|
4132
|
+
);
|
|
3136
4133
|
|
|
3137
|
-
|
|
3138
|
-
expect(mergedTask).toBeDefined();
|
|
3139
|
-
expect(mergedTask.data.interaction.participants['epdn-participant']).toBeDefined();
|
|
3140
|
-
expect(mergedTask.data.interaction.participants['epdn-participant'].type).toBe('EpDn');
|
|
3141
|
-
expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, mergedTask);
|
|
4134
|
+
expect(epdn.wrapUpRequired).toBe(true);
|
|
3142
4135
|
});
|
|
3143
4136
|
|
|
3144
|
-
it('
|
|
3145
|
-
const
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
4137
|
+
it('does not stamp wrapUpRequired on PARTICIPANT_LEFT_CONFERENCE when the actor ignores PARTICIPANT_LEAVE', () => {
|
|
4138
|
+
const selfLeave = {
|
|
4139
|
+
...taskDataMock,
|
|
4140
|
+
participantId: 'test-agent-id',
|
|
4141
|
+
agentsPendingWrapUp: [],
|
|
4142
|
+
interaction: {
|
|
4143
|
+
...taskDataMock.interaction,
|
|
4144
|
+
owner: 'test-agent-id',
|
|
4145
|
+
participants: {},
|
|
3152
4146
|
},
|
|
3153
4147
|
};
|
|
3154
|
-
|
|
4148
|
+
const snapshotTask = (state: TaskState) => ({
|
|
4149
|
+
stateMachineService: {getSnapshot: () => ({value: state})},
|
|
4150
|
+
});
|
|
3155
4151
|
|
|
3156
|
-
const
|
|
3157
|
-
|
|
4152
|
+
const holdInitiating = stampWrapUp(
|
|
4153
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
4154
|
+
selfLeave,
|
|
4155
|
+
'test-agent-id',
|
|
4156
|
+
snapshotTask(TaskState.HOLD_INITIATING)
|
|
4157
|
+
);
|
|
4158
|
+
const connected = stampWrapUp(
|
|
4159
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
4160
|
+
selfLeave,
|
|
4161
|
+
'test-agent-id',
|
|
4162
|
+
snapshotTask(TaskState.CONNECTED)
|
|
4163
|
+
);
|
|
4164
|
+
const confInitiating = stampWrapUp(
|
|
4165
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
4166
|
+
selfLeave,
|
|
4167
|
+
'test-agent-id',
|
|
4168
|
+
snapshotTask(TaskState.CONF_INITIATING)
|
|
4169
|
+
);
|
|
4170
|
+
const conferencing = stampWrapUp(
|
|
4171
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
4172
|
+
selfLeave,
|
|
4173
|
+
'test-agent-id',
|
|
4174
|
+
snapshotTask(TaskState.CONFERENCING)
|
|
4175
|
+
);
|
|
3158
4176
|
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
4177
|
+
expect(holdInitiating).toBe(selfLeave);
|
|
4178
|
+
expect(holdInitiating.wrapUpRequired).toBeUndefined();
|
|
4179
|
+
expect(connected.wrapUpRequired).toBeUndefined();
|
|
4180
|
+
expect(confInitiating.wrapUpRequired).toBeUndefined();
|
|
4181
|
+
expect(conferencing.wrapUpRequired).toBe(true);
|
|
4182
|
+
|
|
4183
|
+
class TimerTask extends Task {
|
|
4184
|
+
public accept() {
|
|
4185
|
+
return Promise.resolve({} as any);
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
const holdTask = new TimerTask(
|
|
4189
|
+
{},
|
|
4190
|
+
{...taskDataMock, wrapUpRequired: false},
|
|
4191
|
+
{channelType: 'voice', isEndTaskEnabled: true, isEndConsultEnabled: true},
|
|
4192
|
+
{
|
|
4193
|
+
wrapUpProps: {
|
|
4194
|
+
autoWrapup: true,
|
|
4195
|
+
autoWrapupInterval: 1000,
|
|
4196
|
+
wrapUpReasonList: [{id: 'code-1', name: 'Default', isDefault: true, isSystem: false}],
|
|
3168
4197
|
},
|
|
3169
4198
|
},
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
4199
|
+
'test-agent-id'
|
|
4200
|
+
);
|
|
4201
|
+
try {
|
|
4202
|
+
holdTask.updateTaskData(holdInitiating);
|
|
4203
|
+
expect(holdTask.autoWrapup).toBeUndefined();
|
|
4204
|
+
} finally {
|
|
4205
|
+
holdTask.cancelAutoWrapupTimer();
|
|
4206
|
+
}
|
|
4207
|
+
});
|
|
3173
4208
|
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
4209
|
+
it('stamps wrapUpRequired on AGENT_CONFERENCE_TRANSFERRED for the initiator only', () => {
|
|
4210
|
+
const initiator = stampWrapUp(CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED, {
|
|
4211
|
+
...taskDataMock,
|
|
4212
|
+
consultingAgentId: 'test-agent-id',
|
|
4213
|
+
});
|
|
4214
|
+
const observer = stampWrapUp(CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED, {
|
|
4215
|
+
...taskDataMock,
|
|
4216
|
+
consultingAgentId: 'other-agent-id',
|
|
4217
|
+
owner: 'test-agent-id',
|
|
4218
|
+
});
|
|
3177
4219
|
|
|
3178
|
-
|
|
3179
|
-
expect(
|
|
3180
|
-
TASK_EVENTS.TASK_MERGED,
|
|
3181
|
-
expect.objectContaining({
|
|
3182
|
-
data: expect.objectContaining({
|
|
3183
|
-
interactionId: taskId,
|
|
3184
|
-
}),
|
|
3185
|
-
})
|
|
3186
|
-
);
|
|
4220
|
+
expect(initiator.wrapUpRequired).toBe(true);
|
|
4221
|
+
expect(observer.wrapUpRequired).toBe(false);
|
|
3187
4222
|
});
|
|
3188
|
-
});
|
|
3189
4223
|
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
},
|
|
3204
|
-
},
|
|
4224
|
+
it('does not let transfer initiator override a nonempty pending list that excludes self', () => {
|
|
4225
|
+
const consultingInitiator = stampWrapUp(CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED, {
|
|
4226
|
+
...taskDataMock,
|
|
4227
|
+
wrapUpRequired: true,
|
|
4228
|
+
agentsPendingWrapUp: ['other-agent-id'],
|
|
4229
|
+
consultingAgentId: 'test-agent-id',
|
|
4230
|
+
});
|
|
4231
|
+
const flaggedInitiator = stampWrapUp(
|
|
4232
|
+
CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED,
|
|
4233
|
+
{
|
|
4234
|
+
...taskDataMock,
|
|
4235
|
+
wrapUpRequired: true,
|
|
4236
|
+
agentsPendingWrapUp: ['other-agent-id'],
|
|
3205
4237
|
},
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
4238
|
+
'test-agent-id',
|
|
4239
|
+
{
|
|
4240
|
+
stateMachineService: {
|
|
4241
|
+
getSnapshot: () => ({context: {transferConferenceRequested: true}}),
|
|
4242
|
+
},
|
|
4243
|
+
}
|
|
4244
|
+
);
|
|
3209
4245
|
|
|
3210
|
-
|
|
4246
|
+
expect(consultingInitiator.wrapUpRequired).toBe(false);
|
|
4247
|
+
expect(flaggedInitiator.wrapUpRequired).toBe(false);
|
|
4248
|
+
});
|
|
3211
4249
|
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
})
|
|
4250
|
+
it('honors transferConferenceRequested when pending is empty and consultingAgentId is omitted', () => {
|
|
4251
|
+
const flagged = stampWrapUp(
|
|
4252
|
+
CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED,
|
|
4253
|
+
{...taskDataMock},
|
|
4254
|
+
'test-agent-id',
|
|
4255
|
+
{
|
|
4256
|
+
stateMachineService: {
|
|
4257
|
+
getSnapshot: () => ({context: {transferConferenceRequested: true}}),
|
|
4258
|
+
},
|
|
4259
|
+
}
|
|
3223
4260
|
);
|
|
4261
|
+
const unflagged = stampWrapUp(CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED, {...taskDataMock});
|
|
3224
4262
|
|
|
3225
|
-
|
|
3226
|
-
expect(
|
|
4263
|
+
expect(flagged.wrapUpRequired).toBe(true);
|
|
4264
|
+
expect(unflagged.wrapUpRequired).toBe(false);
|
|
3227
4265
|
});
|
|
3228
4266
|
|
|
3229
|
-
it('
|
|
3230
|
-
const
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
agentId: taskDataMock.agentId,
|
|
3235
|
-
orgId: taskDataMock.orgId,
|
|
3236
|
-
trackingId: 'campaign-tracking-456',
|
|
3237
|
-
interaction: {
|
|
3238
|
-
mediaType: 'telephony',
|
|
3239
|
-
callProcessingDetails: {
|
|
3240
|
-
campaignId: 'campaign-789',
|
|
3241
|
-
},
|
|
3242
|
-
},
|
|
3243
|
-
},
|
|
4267
|
+
it('does not stamp wrapUpRequired when a nonempty pending list excludes this agent', () => {
|
|
4268
|
+
const conflictingPayload = {
|
|
4269
|
+
...taskDataMock,
|
|
4270
|
+
wrapUpRequired: true,
|
|
4271
|
+
agentsPendingWrapUp: ['other-agent-id'],
|
|
3244
4272
|
};
|
|
3245
4273
|
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
4274
|
+
const conferenceEnded = stampWrapUp(
|
|
4275
|
+
CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED,
|
|
4276
|
+
conflictingPayload
|
|
4277
|
+
);
|
|
4278
|
+
const conferenceTransferred = stampWrapUp(CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED, {
|
|
4279
|
+
...conflictingPayload,
|
|
4280
|
+
consultingAgentId: 'other-agent-id',
|
|
4281
|
+
});
|
|
4282
|
+
const remainingOnLeave = stampWrapUp(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, {
|
|
4283
|
+
...conflictingPayload,
|
|
4284
|
+
participantId: 'other-agent-id',
|
|
4285
|
+
interaction: {
|
|
4286
|
+
...taskDataMock.interaction,
|
|
4287
|
+
owner: 'test-agent-id',
|
|
4288
|
+
participants: {
|
|
4289
|
+
'test-agent-id': {id: 'test-agent-id', pType: 'Agent', hasLeft: false},
|
|
4290
|
+
},
|
|
4291
|
+
},
|
|
4292
|
+
});
|
|
3251
4293
|
|
|
3252
|
-
|
|
4294
|
+
expect(conferenceEnded.wrapUpRequired).toBe(false);
|
|
4295
|
+
expect(conferenceTransferred.wrapUpRequired).toBe(false);
|
|
4296
|
+
expect(remainingOnLeave.wrapUpRequired).toBe(false);
|
|
4297
|
+
});
|
|
3253
4298
|
|
|
3254
|
-
|
|
3255
|
-
const
|
|
3256
|
-
|
|
4299
|
+
it('stamps wrapUpRequired when pending includes this agent or pending is empty', () => {
|
|
4300
|
+
const pendingIncludesSelf = stampWrapUp(CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED, {
|
|
4301
|
+
...taskDataMock,
|
|
4302
|
+
wrapUpRequired: true,
|
|
4303
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
4304
|
+
});
|
|
4305
|
+
const emptyPendingFallback = stampWrapUp(CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED, {
|
|
4306
|
+
...taskDataMock,
|
|
4307
|
+
wrapUpRequired: true,
|
|
4308
|
+
agentsPendingWrapUp: [],
|
|
4309
|
+
});
|
|
4310
|
+
const missingPendingFallback = stampWrapUp(CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED, {
|
|
4311
|
+
...taskDataMock,
|
|
4312
|
+
wrapUpRequired: true,
|
|
4313
|
+
});
|
|
3257
4314
|
|
|
3258
|
-
|
|
3259
|
-
expect(
|
|
4315
|
+
expect(pendingIncludesSelf.wrapUpRequired).toBe(true);
|
|
4316
|
+
expect(emptyPendingFallback.wrapUpRequired).toBe(true);
|
|
4317
|
+
expect(missingPendingFallback.wrapUpRequired).toBe(true);
|
|
3260
4318
|
});
|
|
3261
4319
|
|
|
3262
|
-
it('
|
|
3263
|
-
const
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
interactionId: 'campaign-interaction-123',
|
|
3267
|
-
agentId: taskDataMock.agentId,
|
|
3268
|
-
orgId: taskDataMock.orgId,
|
|
3269
|
-
trackingId: 'campaign-tracking-456',
|
|
3270
|
-
interaction: {
|
|
3271
|
-
mediaType: 'telephony',
|
|
3272
|
-
callProcessingDetails: {
|
|
3273
|
-
campaignId: 'campaign-789',
|
|
3274
|
-
},
|
|
3275
|
-
},
|
|
3276
|
-
},
|
|
3277
|
-
};
|
|
3278
|
-
|
|
3279
|
-
// Send the first reservation to create the task
|
|
3280
|
-
webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
|
|
4320
|
+
it('sends mapped events to the task state machine service', () => {
|
|
4321
|
+
const payload = {...taskDataMock, type: CC_EVENTS.AGENT_CONTACT_ASSIGNED};
|
|
4322
|
+
const task = taskManager.getTask(taskId);
|
|
4323
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
3281
4324
|
|
|
3282
|
-
|
|
4325
|
+
webSocketManagerMock.emit('message', JSON.stringify({data: payload}));
|
|
3283
4326
|
|
|
3284
|
-
|
|
3285
|
-
|
|
4327
|
+
expect(sendStateMachineEventSpy).toHaveBeenCalledWith({
|
|
4328
|
+
type: TaskEvent.ASSIGN,
|
|
4329
|
+
taskData: payload,
|
|
4330
|
+
});
|
|
3286
4331
|
|
|
3287
|
-
|
|
3288
|
-
TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION,
|
|
3289
|
-
expect.objectContaining({
|
|
3290
|
-
data: expect.objectContaining({
|
|
3291
|
-
interactionId: 'campaign-interaction-123',
|
|
3292
|
-
}),
|
|
3293
|
-
})
|
|
3294
|
-
);
|
|
4332
|
+
sendStateMachineEventSpy.mockRestore();
|
|
3295
4333
|
});
|
|
4334
|
+
});
|
|
3296
4335
|
|
|
3297
|
-
|
|
3298
|
-
|
|
4336
|
+
describe('applyEnableWxBetterTogether', () => {
|
|
4337
|
+
it('updates config flags and propagates to active tasks', () => {
|
|
4338
|
+
const taskOne = {setEnableWxBetterTogether: jest.fn()};
|
|
4339
|
+
const taskTwo = {setEnableWxBetterTogether: jest.fn()};
|
|
3299
4340
|
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
interaction: {
|
|
3309
|
-
mediaType: 'telephony',
|
|
3310
|
-
callProcessingDetails: {
|
|
3311
|
-
campaignId: 'campaign-789',
|
|
3312
|
-
},
|
|
3313
|
-
},
|
|
3314
|
-
},
|
|
4341
|
+
taskManager.setConfigFlags({
|
|
4342
|
+
isEndTaskEnabled: true,
|
|
4343
|
+
isEndConsultEnabled: true,
|
|
4344
|
+
enableWxBetterTogether: true,
|
|
4345
|
+
});
|
|
4346
|
+
taskManager['taskCollection'] = {
|
|
4347
|
+
[taskId]: taskOne,
|
|
4348
|
+
'task-2': taskTwo,
|
|
3315
4349
|
};
|
|
3316
4350
|
|
|
3317
|
-
|
|
4351
|
+
taskManager.applyEnableWxBetterTogether(false);
|
|
3318
4352
|
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
expect(
|
|
4353
|
+
expect(taskManager['configFlags']?.enableWxBetterTogether).toBe(false);
|
|
4354
|
+
expect(taskOne.setEnableWxBetterTogether).toHaveBeenCalledWith(false);
|
|
4355
|
+
expect(taskTwo.setEnableWxBetterTogether).toHaveBeenCalledWith(false);
|
|
4356
|
+
});
|
|
4357
|
+
});
|
|
3322
4358
|
|
|
3323
|
-
|
|
4359
|
+
describe('applyWxAppMuteStateFromSync', () => {
|
|
4360
|
+
it('propagates mute state to all tasks', () => {
|
|
4361
|
+
const taskOne = {applyWxAppMuteStateFromSync: jest.fn()};
|
|
4362
|
+
const taskTwo = {applyWxAppMuteStateFromSync: jest.fn()};
|
|
3324
4363
|
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
type: CC_EVENTS.CAMPAIGN_CONTACT_UPDATED,
|
|
3329
|
-
interactionId: campaignInteractionId,
|
|
3330
|
-
agentId: taskDataMock.agentId,
|
|
3331
|
-
orgId: taskDataMock.orgId,
|
|
3332
|
-
interaction: {
|
|
3333
|
-
mediaType: 'telephony',
|
|
3334
|
-
state: 'new',
|
|
3335
|
-
callProcessingDetails: {
|
|
3336
|
-
campaignId: 'campaign-789',
|
|
3337
|
-
},
|
|
3338
|
-
},
|
|
3339
|
-
},
|
|
4364
|
+
taskManager['taskCollection'] = {
|
|
4365
|
+
[taskId]: taskOne,
|
|
4366
|
+
'task-2': taskTwo,
|
|
3340
4367
|
};
|
|
3341
4368
|
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
// Task should still exist in collection (not removed — non-terminal event)
|
|
3345
|
-
expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
|
|
4369
|
+
taskManager.applyWxAppMuteStateFromSync('call-1', true);
|
|
3346
4370
|
|
|
3347
|
-
|
|
3348
|
-
expect(
|
|
4371
|
+
expect(taskOne.applyWxAppMuteStateFromSync).toHaveBeenCalledWith('call-1', true);
|
|
4372
|
+
expect(taskTwo.applyWxAppMuteStateFromSync).toHaveBeenCalledWith('call-1', true);
|
|
3349
4373
|
});
|
|
3350
4374
|
});
|
|
3351
4375
|
});
|