@webex/contact-center 3.12.0-llmrefactor.2 → 3.12.0-llmrefactor.4
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 +5 -4
- package/ai-docs/ARCHITECTURE.md +1 -1
- package/ai-docs/CONTRACTS.md +6 -2
- package/ai-docs/SECURITY.md +1 -1
- package/ai-docs/contact-center-spec.md +22 -4
- package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
- package/dist/cc.js +440 -10
- package/dist/cc.js.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +10 -2
- package/dist/constants.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/metrics/behavioral-events.js +13 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +2 -0
- 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 +174 -0
- package/dist/services/AnswerCallOnWebexService.js.map +1 -0
- 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/WebexCrossClientService.js +171 -0
- package/dist/services/WebexCrossClientService.js.map +1 -0
- package/dist/services/WxAppTelephonyMercurySync.js +93 -0
- package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
- package/dist/services/config/Util.js +3 -0
- package/dist/services/config/Util.js.map +1 -1
- package/dist/services/config/constants.js +10 -6
- package/dist/services/config/constants.js.map +1 -1
- package/dist/services/config/types.js +4 -0
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/core/Err.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/task/Task.js +73 -7
- package/dist/services/task/Task.js.map +1 -1
- package/dist/services/task/TaskFactory.js +8 -4
- package/dist/services/task/TaskFactory.js.map +1 -1
- package/dist/services/task/TaskManager.js +353 -21
- package/dist/services/task/TaskManager.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 +4 -1
- 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/digital/Digital.js +3 -2
- package/dist/services/task/digital/Digital.js.map +1 -1
- package/dist/services/task/state-machine/TaskStateMachine.js +135 -37
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
- package/dist/services/task/state-machine/actions.js +6 -1
- package/dist/services/task/state-machine/actions.js.map +1 -1
- package/dist/services/task/state-machine/guards.js +38 -11
- package/dist/services/task/state-machine/guards.js.map +1 -1
- package/dist/services/task/state-machine/types.js.map +1 -1
- package/dist/services/task/state-machine/uiControlsComputer.js +118 -18
- package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
- package/dist/services/task/types.js +16 -1
- package/dist/services/task/types.js.map +1 -1
- package/dist/services/task/voice/Voice.js +350 -14
- package/dist/services/task/voice/Voice.js.map +1 -1
- package/dist/services/task/voice/WebRTC.js +4 -1
- package/dist/services/task/voice/WebRTC.js.map +1 -1
- package/dist/services/task/voice/wxAppVoiceMethods.js +201 -0
- package/dist/services/task/voice/wxAppVoiceMethods.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 +77 -4
- package/dist/types/config.d.ts +7 -0
- package/dist/types/constants.d.ts +8 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/metrics/constants.d.ts +2 -0
- package/dist/types/services/AddressBook.d.ts +2 -1
- package/dist/types/services/AnswerCallOnWebexService.d.ts +37 -0
- package/dist/types/services/EntryPoint.d.ts +7 -5
- package/dist/types/services/Queue.d.ts +5 -3
- package/dist/types/services/WebexCrossClientService.d.ts +28 -0
- package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
- package/dist/types/services/config/constants.d.ts +8 -5
- package/dist/types/services/config/types.d.ts +17 -4
- package/dist/types/services/core/Err.d.ts +2 -0
- package/dist/types/services/core/WebexRequest.d.ts +1 -0
- package/dist/types/services/core/types.d.ts +2 -0
- package/dist/types/services/task/Task.d.ts +20 -2
- package/dist/types/services/task/TaskFactory.d.ts +2 -1
- package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
- package/dist/types/services/task/constants.d.ts +3 -0
- package/dist/types/services/task/contact.d.ts +4 -0
- package/dist/types/services/task/digital/Digital.d.ts +2 -2
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +260 -56
- package/dist/types/services/task/state-machine/guards.d.ts +6 -5
- package/dist/types/services/task/state-machine/types.d.ts +9 -1
- package/dist/types/services/task/types.d.ts +81 -4
- package/dist/types/services/task/voice/Voice.d.ts +48 -9
- package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
- package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +52 -0
- package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
- package/dist/types/types.d.ts +14 -8
- package/dist/types/utils/PageCache.d.ts +20 -3
- package/dist/types.js +7 -2
- 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 +1 -1
- package/package.json +9 -9
- package/src/cc.ts +506 -9
- package/src/config.ts +7 -0
- package/src/constants.ts +8 -0
- package/src/index.ts +2 -0
- package/src/metrics/ai-docs/metrics-spec.md +9 -3
- package/src/metrics/behavioral-events.ts +14 -0
- package/src/metrics/constants.ts +2 -0
- package/src/services/AddressBook.ts +17 -6
- package/src/services/AnswerCallOnWebexService.ts +206 -0
- package/src/services/EntryPoint.ts +59 -60
- package/src/services/Queue.ts +29 -12
- package/src/services/WebexCrossClientService.ts +212 -0
- package/src/services/WxAppTelephonyMercurySync.ts +115 -0
- package/src/services/ai-docs/AGENTS.md +10 -10
- package/src/services/ai-docs/services-spec.md +6 -1
- package/src/services/config/Util.ts +3 -0
- package/src/services/config/ai-docs/AGENTS.md +1 -1
- package/src/services/config/ai-docs/ARCHITECTURE.md +2 -2
- package/src/services/config/ai-docs/config-spec.md +6 -0
- package/src/services/config/constants.ts +10 -6
- package/src/services/config/types.ts +16 -4
- package/src/services/core/Err.ts +1 -0
- package/src/services/core/WebexRequest.ts +3 -1
- package/src/services/core/ai-docs/core-spec.md +5 -1
- package/src/services/core/aqm-reqs.ts +30 -15
- package/src/services/core/types.ts +2 -0
- package/src/services/task/Task.ts +79 -8
- package/src/services/task/TaskFactory.ts +8 -3
- package/src/services/task/TaskManager.ts +522 -16
- package/src/services/task/WebexCallingUtils.ts +136 -0
- package/src/services/task/ai-docs/ARCHITECTURE.md +1 -0
- package/src/services/task/ai-docs/task-spec.md +152 -2
- package/src/services/task/constants.ts +3 -0
- package/src/services/task/contact.ts +30 -0
- package/src/services/task/digital/Digital.ts +4 -2
- package/src/services/task/state-machine/TaskStateMachine.ts +210 -63
- package/src/services/task/state-machine/actions.ts +5 -2
- package/src/services/task/state-machine/ai-docs/AGENTS.md +8 -4
- package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +17 -8
- package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +56 -24
- package/src/services/task/state-machine/guards.ts +64 -11
- package/src/services/task/state-machine/types.ts +16 -1
- package/src/services/task/state-machine/uiControlsComputer.ts +206 -32
- package/src/services/task/types.ts +114 -4
- package/src/services/task/voice/Voice.ts +462 -12
- package/src/services/task/voice/WebRTC.ts +5 -1
- package/src/services/task/voice/wxAppVoiceMethods.ts +307 -0
- package/src/services/wxAppTelephonyUtils.ts +14 -0
- package/src/types.ts +33 -9
- package/src/utils/AGENTS.md +21 -10
- package/src/utils/PageCache.ts +38 -5
- package/src/utils/ai-docs/utils-spec.md +21 -11
- package/test/unit/spec/cc.ts +1274 -0
- package/test/unit/spec/metrics/behavioral-events.ts +18 -0
- package/test/unit/spec/services/AddressBook.ts +37 -6
- package/test/unit/spec/services/AnswerCallOnWebexService.ts +223 -0
- package/test/unit/spec/services/EntryPoint.ts +87 -40
- package/test/unit/spec/services/Queue.ts +123 -12
- package/test/unit/spec/services/WebexCrossClientService.ts +261 -0
- package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +113 -0
- package/test/unit/spec/services/config/Util.ts +85 -0
- 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/task/Task.ts +200 -0
- package/test/unit/spec/services/task/TaskFactory.ts +39 -2
- package/test/unit/spec/services/task/TaskManager.ts +1294 -1
- 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/state-machine/TaskStateMachine.ts +854 -72
- package/test/unit/spec/services/task/state-machine/guards.ts +210 -8
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +445 -7
- package/test/unit/spec/services/task/voice/Voice.ts +874 -0
- package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +459 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -152,6 +152,24 @@ describe('metrics/behavioral-events', () => {
|
|
|
152
152
|
verb: 'fail',
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
+
expect(
|
|
156
|
+
getEventTaxonomy(METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_SUCCESS)
|
|
157
|
+
).toEqual({
|
|
158
|
+
product,
|
|
159
|
+
agent: 'user',
|
|
160
|
+
target: 'task_conference_participant_drop',
|
|
161
|
+
verb: 'complete',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(
|
|
165
|
+
getEventTaxonomy(METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_FAILED)
|
|
166
|
+
).toEqual({
|
|
167
|
+
product,
|
|
168
|
+
agent: 'user',
|
|
169
|
+
target: 'task_conference_participant_drop',
|
|
170
|
+
verb: 'fail',
|
|
171
|
+
});
|
|
172
|
+
|
|
155
173
|
expect(getEventTaxonomy(METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_SUCCESS)).toEqual({
|
|
156
174
|
product,
|
|
157
175
|
agent: 'user',
|
|
@@ -99,7 +99,8 @@ describe('AddressBook', () => {
|
|
|
99
99
|
|
|
100
100
|
expect(mockWebex.request).toHaveBeenCalledWith({
|
|
101
101
|
service: 'wcc-api-gateway',
|
|
102
|
-
resource:
|
|
102
|
+
resource:
|
|
103
|
+
'/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=100&sort=name%2CASC',
|
|
103
104
|
method: HTTP_METHODS.GET,
|
|
104
105
|
});
|
|
105
106
|
|
|
@@ -133,7 +134,8 @@ describe('AddressBook', () => {
|
|
|
133
134
|
module: 'AddressBook',
|
|
134
135
|
method: 'getEntries',
|
|
135
136
|
data: {
|
|
136
|
-
resource:
|
|
137
|
+
resource:
|
|
138
|
+
'/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=100&sort=name%2CASC',
|
|
137
139
|
service: 'wcc-api-gateway',
|
|
138
140
|
},
|
|
139
141
|
});
|
|
@@ -155,7 +157,8 @@ describe('AddressBook', () => {
|
|
|
155
157
|
|
|
156
158
|
expect(mockWebex.request).toHaveBeenCalledWith({
|
|
157
159
|
service: 'wcc-api-gateway',
|
|
158
|
-
resource:
|
|
160
|
+
resource:
|
|
161
|
+
'/organization/test-org-id/v2/address-book/custom-book-id/entry?page=1&pageSize=25&filter=name%3D%3D%22John+Doe%22&attributes=id%2Cname%2Cnumber&search=john&sort=name%2CASC',
|
|
159
162
|
method: HTTP_METHODS.GET,
|
|
160
163
|
});
|
|
161
164
|
|
|
@@ -175,6 +178,31 @@ describe('AddressBook', () => {
|
|
|
175
178
|
);
|
|
176
179
|
});
|
|
177
180
|
|
|
181
|
+
it('should allow callers to override the default backend sorting and bypass the default page cache', async () => {
|
|
182
|
+
(mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
|
|
183
|
+
|
|
184
|
+
await addressBookAPI.getEntries({
|
|
185
|
+
page: 0,
|
|
186
|
+
pageSize: 25,
|
|
187
|
+
sortBy: 'name',
|
|
188
|
+
sortOrder: 'desc',
|
|
189
|
+
});
|
|
190
|
+
await addressBookAPI.getEntries({
|
|
191
|
+
page: 0,
|
|
192
|
+
pageSize: 25,
|
|
193
|
+
sortBy: 'name',
|
|
194
|
+
sortOrder: 'desc',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(mockWebex.request).toHaveBeenCalledTimes(2);
|
|
198
|
+
expect(mockWebex.request).toHaveBeenLastCalledWith({
|
|
199
|
+
service: 'wcc-api-gateway',
|
|
200
|
+
resource:
|
|
201
|
+
'/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=25&sort=name%2CDESC',
|
|
202
|
+
method: HTTP_METHODS.GET,
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
178
206
|
it('should handle API errors and track metrics', async () => {
|
|
179
207
|
(mockWebex.request as jest.Mock).mockRejectedValue(new Error('Internal Server Error'));
|
|
180
208
|
|
|
@@ -182,7 +210,8 @@ describe('AddressBook', () => {
|
|
|
182
210
|
|
|
183
211
|
expect(mockWebex.request).toHaveBeenCalledWith({
|
|
184
212
|
service: 'wcc-api-gateway',
|
|
185
|
-
resource:
|
|
213
|
+
resource:
|
|
214
|
+
'/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=100&sort=name%2CASC',
|
|
186
215
|
method: HTTP_METHODS.GET,
|
|
187
216
|
});
|
|
188
217
|
|
|
@@ -316,14 +345,16 @@ describe('AddressBook', () => {
|
|
|
316
345
|
expect(result.meta.page).toBe(1);
|
|
317
346
|
expect(mockWebex.request).toHaveBeenCalledWith({
|
|
318
347
|
service: 'wcc-api-gateway',
|
|
319
|
-
resource:
|
|
348
|
+
resource:
|
|
349
|
+
'/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=1&pageSize=100&sort=name%2CASC',
|
|
320
350
|
method: HTTP_METHODS.GET,
|
|
321
351
|
});
|
|
322
352
|
expect(LoggerProxy.info).toHaveBeenCalledWith('Making API request to fetch address book entries', {
|
|
323
353
|
module: 'AddressBook',
|
|
324
354
|
method: 'getEntries',
|
|
325
355
|
data: {
|
|
326
|
-
resource:
|
|
356
|
+
resource:
|
|
357
|
+
'/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=1&pageSize=100&sort=name%2CASC',
|
|
327
358
|
service: 'wcc-api-gateway',
|
|
328
359
|
},
|
|
329
360
|
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import {expect} from '@jest/globals';
|
|
2
|
+
import AnswerCallOnWebexService from '../../../../src/services/AnswerCallOnWebexService';
|
|
3
|
+
import {WebexSDK} from '../../../../src/types';
|
|
4
|
+
import LoggerProxy from '../../../../src/logger-proxy';
|
|
5
|
+
|
|
6
|
+
jest.mock('../../../../src/logger-proxy');
|
|
7
|
+
jest.mock('../../../../src/services/core/WebexRequest', () => ({
|
|
8
|
+
__esModule: true,
|
|
9
|
+
default: {
|
|
10
|
+
getInstance: () => ({uploadLogs: jest.fn()}),
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const TELEPHONY_BASE = 'https://api.ciscospark.com';
|
|
15
|
+
const CALLS_URL = `${TELEPHONY_BASE}/telephony/calls`;
|
|
16
|
+
|
|
17
|
+
function makeMockWebex(overrides: Partial<{_serviceUrls: Record<string, string>}> = {}): WebexSDK {
|
|
18
|
+
return {
|
|
19
|
+
request: jest.fn().mockResolvedValue({statusCode: 200, body: {}}),
|
|
20
|
+
internal: {
|
|
21
|
+
services: {
|
|
22
|
+
_serviceUrls: {hydra: TELEPHONY_BASE, ...overrides._serviceUrls},
|
|
23
|
+
get: jest.fn(),
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
} as unknown as WebexSDK;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('AnswerCallOnWebexService', () => {
|
|
30
|
+
let webex: WebexSDK;
|
|
31
|
+
let service: AnswerCallOnWebexService;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.clearAllMocks();
|
|
35
|
+
webex = makeMockWebex();
|
|
36
|
+
service = new AnswerCallOnWebexService(webex);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('getTelephonyBaseUrl resolution', () => {
|
|
40
|
+
it('uses hydra URL from _serviceUrls when available', async () => {
|
|
41
|
+
await service.answerCall({callId: 'c1', endpointId: 'e1'});
|
|
42
|
+
|
|
43
|
+
expect(webex.request).toHaveBeenCalledWith(
|
|
44
|
+
expect.objectContaining({uri: `${CALLS_URL}/answer`})
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('falls back to services.get("hydra") when _serviceUrls.hydra is missing', async () => {
|
|
49
|
+
const hydraBase = 'https://api.ciscospark.com/v1';
|
|
50
|
+
const get = jest.fn().mockReturnValue(hydraBase);
|
|
51
|
+
webex = {
|
|
52
|
+
request: jest.fn().mockResolvedValue({statusCode: 200, body: {}}),
|
|
53
|
+
internal: {
|
|
54
|
+
services: {
|
|
55
|
+
_serviceUrls: {},
|
|
56
|
+
get,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
} as unknown as WebexSDK;
|
|
60
|
+
service = new AnswerCallOnWebexService(webex);
|
|
61
|
+
|
|
62
|
+
await service.answerCall({callId: 'c1', endpointId: 'e1'});
|
|
63
|
+
|
|
64
|
+
expect(get).toHaveBeenCalledWith('hydra');
|
|
65
|
+
expect(webex.request).toHaveBeenCalledWith(
|
|
66
|
+
expect.objectContaining({uri: `${hydraBase}/telephony/calls/answer`})
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('throws when hydra URL is unavailable', async () => {
|
|
71
|
+
webex = {
|
|
72
|
+
request: jest.fn(),
|
|
73
|
+
internal: {
|
|
74
|
+
services: {
|
|
75
|
+
_serviceUrls: {},
|
|
76
|
+
get: jest.fn().mockReturnValue(''),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
} as unknown as WebexSDK;
|
|
80
|
+
service = new AnswerCallOnWebexService(webex);
|
|
81
|
+
|
|
82
|
+
await expect(service.answerCall({callId: 'c1', endpointId: 'e1'})).rejects.toBeDefined();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('answerCall', () => {
|
|
87
|
+
it('POST /answer with callId and endpointId', async () => {
|
|
88
|
+
await service.answerCall({callId: 'call-123', endpointId: 'ep-456'});
|
|
89
|
+
|
|
90
|
+
expect(webex.request).toHaveBeenCalledWith({
|
|
91
|
+
uri: `${CALLS_URL}/answer`,
|
|
92
|
+
method: 'POST',
|
|
93
|
+
body: {callId: 'call-123', endpointId: 'ep-456'},
|
|
94
|
+
addAuthHeader: true,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('includes lineOwnerId when provided', async () => {
|
|
99
|
+
await service.answerCall({callId: 'c1', endpointId: 'e1', lineOwnerId: 'lo-1'});
|
|
100
|
+
|
|
101
|
+
expect(webex.request).toHaveBeenCalledWith(
|
|
102
|
+
expect.objectContaining({body: {callId: 'c1', endpointId: 'e1', lineOwnerId: 'lo-1'}})
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('omits lineOwnerId when not provided', async () => {
|
|
107
|
+
await service.answerCall({callId: 'c1', endpointId: 'e1'});
|
|
108
|
+
|
|
109
|
+
const call = (webex.request as jest.Mock).mock.calls[0][0];
|
|
110
|
+
expect(call.body).not.toHaveProperty('lineOwnerId');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('rejects and logs on request failure', async () => {
|
|
114
|
+
const err = {
|
|
115
|
+
statusCode: 400,
|
|
116
|
+
details: {
|
|
117
|
+
trackingId: 'track-wxapp-1',
|
|
118
|
+
data: {reason: 'TELEPHONY_ERROR'},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
(webex.request as jest.Mock).mockRejectedValue(err);
|
|
122
|
+
|
|
123
|
+
await expect(service.answerCall({callId: 'c1', endpointId: 'e1'})).rejects.toMatchObject({
|
|
124
|
+
isWxAppTelephonyError: true,
|
|
125
|
+
message: 'TELEPHONY_ERROR',
|
|
126
|
+
trackingId: 'track-wxapp-1',
|
|
127
|
+
status: 400,
|
|
128
|
+
});
|
|
129
|
+
expect(LoggerProxy.error).toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('rejectCall', () => {
|
|
134
|
+
it('POST /reject with callId', async () => {
|
|
135
|
+
await service.rejectCall({callId: 'call-123'});
|
|
136
|
+
|
|
137
|
+
expect(webex.request).toHaveBeenCalledWith({
|
|
138
|
+
uri: `${CALLS_URL}/reject`,
|
|
139
|
+
method: 'POST',
|
|
140
|
+
body: {callId: 'call-123'},
|
|
141
|
+
addAuthHeader: true,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('muteCall', () => {
|
|
147
|
+
it('POST /mute with callId', async () => {
|
|
148
|
+
await service.muteCall({callId: 'call-123'});
|
|
149
|
+
|
|
150
|
+
expect(webex.request).toHaveBeenCalledWith(
|
|
151
|
+
expect.objectContaining({uri: `${CALLS_URL}/mute`, body: {callId: 'call-123'}})
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('unmuteCall', () => {
|
|
157
|
+
it('POST /unmute with callId', async () => {
|
|
158
|
+
await service.unmuteCall({callId: 'call-123'});
|
|
159
|
+
|
|
160
|
+
expect(webex.request).toHaveBeenCalledWith(
|
|
161
|
+
expect.objectContaining({uri: `${CALLS_URL}/unmute`, body: {callId: 'call-123'}})
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('transmitDtmf', () => {
|
|
167
|
+
it('POST /transmitDtmf with callId and dtmf', async () => {
|
|
168
|
+
await service.transmitDtmf({callId: 'call-123', dtmf: '5'});
|
|
169
|
+
|
|
170
|
+
expect(webex.request).toHaveBeenCalledWith({
|
|
171
|
+
uri: `${CALLS_URL}/transmitDtmf`,
|
|
172
|
+
method: 'POST',
|
|
173
|
+
body: {callId: 'call-123', dtmf: '5'},
|
|
174
|
+
addAuthHeader: true,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('includes lineOwnerId in DTMF request when provided', async () => {
|
|
179
|
+
await service.transmitDtmf({callId: 'c1', dtmf: '9', lineOwnerId: 'lo-1'});
|
|
180
|
+
|
|
181
|
+
expect(webex.request).toHaveBeenCalledWith(
|
|
182
|
+
expect.objectContaining({body: {callId: 'c1', dtmf: '9', lineOwnerId: 'lo-1'}})
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('getCallDetails', () => {
|
|
188
|
+
it('GET /telephony/calls/{callId}', async () => {
|
|
189
|
+
(webex.request as jest.Mock).mockResolvedValue({body: {muted: true}});
|
|
190
|
+
|
|
191
|
+
const result = await service.getCallDetails({callId: 'call-123'});
|
|
192
|
+
|
|
193
|
+
expect(webex.request).toHaveBeenCalledWith({
|
|
194
|
+
uri: `${CALLS_URL}/call-123`,
|
|
195
|
+
method: 'GET',
|
|
196
|
+
addAuthHeader: true,
|
|
197
|
+
});
|
|
198
|
+
expect(result).toEqual({muted: true});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('includes lineOwnerId query param when provided', async () => {
|
|
202
|
+
await service.getCallDetails({callId: 'call-123', lineOwnerId: 'lo-1'});
|
|
203
|
+
|
|
204
|
+
expect(webex.request).toHaveBeenCalledWith(
|
|
205
|
+
expect.objectContaining({uri: `${CALLS_URL}/call-123?lineOwnerId=lo-1`})
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('rejects and logs on request failure', async () => {
|
|
210
|
+
(webex.request as jest.Mock).mockRejectedValue(new Error('network fail'));
|
|
211
|
+
|
|
212
|
+
await expect(service.getCallDetails({callId: 'c1'})).rejects.toBeDefined();
|
|
213
|
+
expect(LoggerProxy.error).toHaveBeenCalled();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('rejects without error log for expected call-not-found during mute backfill race', async () => {
|
|
217
|
+
(webex.request as jest.Mock).mockRejectedValue({status: 400, message: 'Call not found'});
|
|
218
|
+
|
|
219
|
+
await expect(service.getCallDetails({callId: 'c1'})).rejects.toBeDefined();
|
|
220
|
+
expect(LoggerProxy.error).not.toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import EntryPoint from '../../../../src/services/EntryPoint';
|
|
2
|
-
import {HTTP_METHODS, WebexSDK, IHttpResponse} from '../../../../src/types';
|
|
2
|
+
import {HTTP_METHODS, WebexSDK, IHttpResponse, EntryPointRecord} from '../../../../src/types';
|
|
3
3
|
import {METRIC_EVENT_NAMES} from '../../../../src/metrics/constants';
|
|
4
4
|
import WebexRequest from '../../../../src/services/core/WebexRequest';
|
|
5
5
|
import MetricsManager from '../../../../src/metrics/MetricsManager';
|
|
@@ -50,30 +50,38 @@ describe('EntryPoint', () => {
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
describe('getEntryPoints', () => {
|
|
53
|
-
const
|
|
53
|
+
const mockDialNumberMappings = [
|
|
54
54
|
{
|
|
55
|
-
id: '
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
orgId: 'test-org-id',
|
|
55
|
+
id: 'dial-number-1',
|
|
56
|
+
dialledNumber: '+1-555-0101',
|
|
57
|
+
entryPointId: 'entry1',
|
|
58
|
+
entryPointName: 'Test Entry Point 1',
|
|
60
59
|
},
|
|
61
60
|
{
|
|
62
|
-
id: '
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
orgId: 'test-org-id',
|
|
61
|
+
id: 'dial-number-2',
|
|
62
|
+
dialledNumber: '+1-555-0102',
|
|
63
|
+
entryPointId: 'entry2',
|
|
64
|
+
entryPointName: 'Test Entry Point 2',
|
|
67
65
|
},
|
|
68
66
|
];
|
|
67
|
+
const expectedEntryPoints: EntryPointRecord[] = [
|
|
68
|
+
{id: 'entry1', name: 'Test Entry Point 1', number: '+1-555-0101'},
|
|
69
|
+
{id: 'entry2', name: 'Test Entry Point 2', number: '+1-555-0102'},
|
|
70
|
+
];
|
|
71
|
+
const defaultResource =
|
|
72
|
+
'/organization/test-org-id/v3/dial-number?page=0&pageSize=100&attributes=id%2CdialledNumber%2CentryPointId%2CentryPointName&sort=entryPointName%2CASC&desktopProfileFilter=true&includeEntryPointName=true';
|
|
73
|
+
const requestHeaders = {
|
|
74
|
+
'X-ORGANIZATION-ID': 'test-org-id',
|
|
75
|
+
'x-ignore-internal-data': 'false',
|
|
76
|
+
};
|
|
69
77
|
|
|
70
78
|
const mockResponse: IHttpResponse = {
|
|
71
79
|
statusCode: 200,
|
|
72
80
|
method: 'GET',
|
|
73
|
-
url: '/organization/test-org-id/
|
|
81
|
+
url: '/organization/test-org-id/v3/dial-number',
|
|
74
82
|
headers: {} as any,
|
|
75
83
|
body: {
|
|
76
|
-
data:
|
|
84
|
+
data: mockDialNumberMappings,
|
|
77
85
|
meta: {
|
|
78
86
|
page: 0,
|
|
79
87
|
pageSize: 100,
|
|
@@ -99,11 +107,14 @@ describe('EntryPoint', () => {
|
|
|
99
107
|
|
|
100
108
|
expect(mockWebex.request).toHaveBeenCalledWith({
|
|
101
109
|
service: 'wcc-api-gateway',
|
|
102
|
-
resource:
|
|
110
|
+
resource: defaultResource,
|
|
103
111
|
method: HTTP_METHODS.GET,
|
|
112
|
+
body: undefined,
|
|
113
|
+
headers: requestHeaders,
|
|
104
114
|
});
|
|
105
115
|
|
|
106
|
-
expect(result).toEqual(mockResponse.body);
|
|
116
|
+
expect(result).toEqual({...mockResponse.body, data: expectedEntryPoints});
|
|
117
|
+
expect(result.data[0].number).toBe('+1-555-0101');
|
|
107
118
|
expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith(METRIC_EVENT_NAMES.ENTRYPOINT_FETCH_SUCCESS);
|
|
108
119
|
expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
|
|
109
120
|
METRIC_EVENT_NAMES.ENTRYPOINT_FETCH_SUCCESS,
|
|
@@ -122,7 +133,7 @@ describe('EntryPoint', () => {
|
|
|
122
133
|
{module: 'EntryPoint', method: 'getEntryPoints'}
|
|
123
134
|
);
|
|
124
135
|
expect(LoggerProxy.log).toHaveBeenCalledWith(
|
|
125
|
-
`Making API request to fetch entry points - resource:
|
|
136
|
+
`Making API request to fetch entry points - resource: ${defaultResource}, service: wcc-api-gateway`,
|
|
126
137
|
{module: 'EntryPoint', method: 'getEntryPoints'}
|
|
127
138
|
);
|
|
128
139
|
});
|
|
@@ -134,7 +145,7 @@ describe('EntryPoint', () => {
|
|
|
134
145
|
page: 1,
|
|
135
146
|
pageSize: 25,
|
|
136
147
|
search: 'test',
|
|
137
|
-
filter: '
|
|
148
|
+
filter: 'entryPointId=="entry1"',
|
|
138
149
|
attributes: 'id,name',
|
|
139
150
|
sortBy: 'name',
|
|
140
151
|
sortOrder: 'desc' as const,
|
|
@@ -144,8 +155,11 @@ describe('EntryPoint', () => {
|
|
|
144
155
|
|
|
145
156
|
expect(mockWebex.request).toHaveBeenCalledWith({
|
|
146
157
|
service: 'wcc-api-gateway',
|
|
147
|
-
resource:
|
|
158
|
+
resource:
|
|
159
|
+
'/organization/test-org-id/v3/dial-number?page=1&pageSize=25&attributes=id%2CdialledNumber%2CentryPointId%2CentryPointName%2Cname&search=test&filter=entryPointId%3D%3D%22entry1%22&sort=entryPointName%2CDESC&desktopProfileFilter=true&includeEntryPointName=true',
|
|
148
160
|
method: HTTP_METHODS.GET,
|
|
161
|
+
body: undefined,
|
|
162
|
+
headers: requestHeaders,
|
|
149
163
|
});
|
|
150
164
|
|
|
151
165
|
expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
|
|
@@ -162,6 +176,41 @@ describe('EntryPoint', () => {
|
|
|
162
176
|
);
|
|
163
177
|
});
|
|
164
178
|
|
|
179
|
+
it('should allow existing parameters to override the default entry-point mapping policy', async () => {
|
|
180
|
+
(mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
|
|
181
|
+
|
|
182
|
+
const result = await entryPointAPI.getEntryPoints({
|
|
183
|
+
page: 1,
|
|
184
|
+
pageSize: 25,
|
|
185
|
+
search: 'sales',
|
|
186
|
+
filter: 'entryPointId==entry1',
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(mockWebex.request).toHaveBeenCalledWith({
|
|
190
|
+
service: 'wcc-api-gateway',
|
|
191
|
+
resource:
|
|
192
|
+
'/organization/test-org-id/v3/dial-number?page=1&pageSize=25&attributes=id%2CdialledNumber%2CentryPointId%2CentryPointName&search=sales&filter=entryPointId%3D%3Dentry1&sort=entryPointName%2CASC&desktopProfileFilter=true&includeEntryPointName=true',
|
|
193
|
+
method: HTTP_METHODS.GET,
|
|
194
|
+
body: undefined,
|
|
195
|
+
headers: requestHeaders,
|
|
196
|
+
});
|
|
197
|
+
expect(result).toEqual({...mockResponse.body, data: expectedEntryPoints});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should issue every profile-scoped request directly', async () => {
|
|
201
|
+
(mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
|
|
202
|
+
|
|
203
|
+
await entryPointAPI.getEntryPoints();
|
|
204
|
+
await entryPointAPI.getEntryPoints();
|
|
205
|
+
|
|
206
|
+
expect(mockWebex.request).toHaveBeenCalledTimes(2);
|
|
207
|
+
expect(mockWebex.request).toHaveBeenLastCalledWith(
|
|
208
|
+
expect.objectContaining({
|
|
209
|
+
resource: expect.stringContaining('desktopProfileFilter=true'),
|
|
210
|
+
})
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
165
214
|
it('should handle API errors and track metrics', async () => {
|
|
166
215
|
(mockWebex.request as jest.Mock).mockRejectedValue(new Error('Internal Server Error'));
|
|
167
216
|
|
|
@@ -169,8 +218,10 @@ describe('EntryPoint', () => {
|
|
|
169
218
|
|
|
170
219
|
expect(mockWebex.request).toHaveBeenCalledWith({
|
|
171
220
|
service: 'wcc-api-gateway',
|
|
172
|
-
resource:
|
|
221
|
+
resource: defaultResource,
|
|
173
222
|
method: HTTP_METHODS.GET,
|
|
223
|
+
body: undefined,
|
|
224
|
+
headers: requestHeaders,
|
|
174
225
|
});
|
|
175
226
|
|
|
176
227
|
expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
|
|
@@ -187,22 +238,24 @@ describe('EntryPoint', () => {
|
|
|
187
238
|
expect(LoggerProxy.error).toHaveBeenCalled();
|
|
188
239
|
});
|
|
189
240
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
it('should not track metrics for subsequent pages in simple pagination', async () => {
|
|
241
|
+
it('should not track success metrics for subsequent pages under the default mapping policy', async () => {
|
|
193
242
|
(mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
|
|
194
243
|
|
|
195
244
|
const result = await entryPointAPI.getEntryPoints({page: 2});
|
|
196
|
-
expect(result).toEqual(mockResponse.body);
|
|
245
|
+
expect(result).toEqual({...mockResponse.body, data: expectedEntryPoints});
|
|
197
246
|
|
|
198
|
-
expect(mockMetricsManager.trackEvent).not.
|
|
247
|
+
expect(mockMetricsManager.trackEvent).not.toHaveBeenCalledWith(
|
|
248
|
+
METRIC_EVENT_NAMES.ENTRYPOINT_FETCH_SUCCESS,
|
|
249
|
+
expect.anything(),
|
|
250
|
+
expect.anything()
|
|
251
|
+
);
|
|
199
252
|
});
|
|
200
253
|
|
|
201
254
|
it('should track metrics for search requests on any page', async () => {
|
|
202
255
|
(mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
|
|
203
256
|
|
|
204
257
|
const result2 = await entryPointAPI.getEntryPoints({page: 2, search: 'test'});
|
|
205
|
-
expect(result2).toEqual(mockResponse.body);
|
|
258
|
+
expect(result2).toEqual({...mockResponse.body, data: expectedEntryPoints});
|
|
206
259
|
|
|
207
260
|
expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
|
|
208
261
|
METRIC_EVENT_NAMES.ENTRYPOINT_FETCH_SUCCESS,
|
|
@@ -218,15 +271,11 @@ describe('EntryPoint', () => {
|
|
|
218
271
|
);
|
|
219
272
|
});
|
|
220
273
|
|
|
221
|
-
it('should
|
|
222
|
-
(mockWebex.request as jest.Mock).
|
|
223
|
-
|
|
224
|
-
await entryPointAPI.getEntryPoints({page: 0});
|
|
225
|
-
|
|
226
|
-
(mockWebex.request as jest.Mock).mockResolvedValueOnce({
|
|
274
|
+
it('should request the specified page', async () => {
|
|
275
|
+
(mockWebex.request as jest.Mock).mockResolvedValue({
|
|
227
276
|
...mockResponse,
|
|
228
277
|
body: {
|
|
229
|
-
data:
|
|
278
|
+
data: mockDialNumberMappings,
|
|
230
279
|
meta: {
|
|
231
280
|
page: 1,
|
|
232
281
|
pageSize: 100,
|
|
@@ -237,21 +286,19 @@ describe('EntryPoint', () => {
|
|
|
237
286
|
},
|
|
238
287
|
});
|
|
239
288
|
|
|
240
|
-
const callsBefore = (mockWebex.request as jest.Mock).mock.calls.length;
|
|
241
|
-
(LoggerProxy.log as jest.Mock).mockClear();
|
|
242
|
-
|
|
243
289
|
const result = await entryPointAPI.getEntryPoints({page: 1});
|
|
244
|
-
const callsAfter = (mockWebex.request as jest.Mock).mock.calls.length;
|
|
245
290
|
|
|
246
|
-
expect(callsAfter).toBe(callsBefore + 1);
|
|
247
291
|
expect(result.meta.page).toBe(1);
|
|
248
292
|
expect(mockWebex.request).toHaveBeenCalledWith({
|
|
249
293
|
service: 'wcc-api-gateway',
|
|
250
|
-
resource:
|
|
294
|
+
resource:
|
|
295
|
+
'/organization/test-org-id/v3/dial-number?page=1&pageSize=100&attributes=id%2CdialledNumber%2CentryPointId%2CentryPointName&sort=entryPointName%2CASC&desktopProfileFilter=true&includeEntryPointName=true',
|
|
251
296
|
method: HTTP_METHODS.GET,
|
|
297
|
+
body: undefined,
|
|
298
|
+
headers: requestHeaders,
|
|
252
299
|
});
|
|
253
300
|
expect(LoggerProxy.log).toHaveBeenCalledWith(
|
|
254
|
-
`Making API request to fetch entry points - resource: /organization/test-org-id/
|
|
301
|
+
`Making API request to fetch entry points - resource: /organization/test-org-id/v3/dial-number?page=1&pageSize=100&attributes=id%2CdialledNumber%2CentryPointId%2CentryPointName&sort=entryPointName%2CASC&desktopProfileFilter=true&includeEntryPointName=true, service: wcc-api-gateway`,
|
|
255
302
|
{module: 'EntryPoint', method: 'getEntryPoints'}
|
|
256
303
|
);
|
|
257
304
|
});
|