@webex/contact-center 3.12.0-llmrefactor.8 → 3.12.0-llmrefactor.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.sdd/manifest.json +3 -2
  2. package/ai-docs/CONTRACTS.md +2 -1
  3. package/dist/cc.js +108 -6
  4. package/dist/cc.js.map +1 -1
  5. package/dist/metrics/MetricsManager.js +16 -0
  6. package/dist/metrics/MetricsManager.js.map +1 -1
  7. package/dist/metrics/behavioral-events.js +91 -0
  8. package/dist/metrics/behavioral-events.js.map +1 -1
  9. package/dist/metrics/constants.js +16 -0
  10. package/dist/metrics/constants.js.map +1 -1
  11. package/dist/services/AnswerCallOnWebexService.js +34 -6
  12. package/dist/services/AnswerCallOnWebexService.js.map +1 -1
  13. package/dist/services/WebexCrossClientService.js +54 -3
  14. package/dist/services/WebexCrossClientService.js.map +1 -1
  15. package/dist/services/WxAppTelephonyMercurySync.js +21 -6
  16. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -1
  17. package/dist/services/task/voice/Voice.js +81 -14
  18. package/dist/services/task/voice/Voice.js.map +1 -1
  19. package/dist/services/task/voice/wxAppVoiceMethods.js +154 -1
  20. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -1
  21. package/dist/services/wxAppDiagnosticLogging.js +122 -0
  22. package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
  23. package/dist/types/cc.d.ts +1 -0
  24. package/dist/types/metrics/MetricsManager.d.ts +6 -0
  25. package/dist/types/metrics/constants.d.ts +15 -0
  26. package/dist/types/services/AnswerCallOnWebexService.d.ts +1 -0
  27. package/dist/types/services/WebexCrossClientService.d.ts +4 -0
  28. package/dist/types/services/task/voice/Voice.d.ts +5 -1
  29. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +3 -0
  30. package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -0
  31. package/dist/webex.js +1 -1
  32. package/package.json +8 -8
  33. package/src/cc.ts +155 -7
  34. package/src/metrics/MetricsManager.ts +16 -0
  35. package/src/metrics/ai-docs/metrics-spec.md +71 -9
  36. package/src/metrics/behavioral-events.ts +92 -0
  37. package/src/metrics/constants.ts +18 -0
  38. package/src/services/AnswerCallOnWebexService.ts +41 -8
  39. package/src/services/WebexCrossClientService.ts +85 -4
  40. package/src/services/WxAppTelephonyMercurySync.ts +31 -4
  41. package/src/services/agent/ai-docs/agent-spec.md +1 -0
  42. package/src/services/task/ai-docs/task-spec.md +2 -0
  43. package/src/services/task/voice/Voice.ts +115 -26
  44. package/src/services/task/voice/wxAppVoiceMethods.ts +211 -2
  45. package/src/services/wxAppDiagnosticLogging.ts +174 -0
  46. package/test/unit/spec/cc.ts +251 -22
  47. package/test/unit/spec/metrics/MetricsManager.ts +25 -0
  48. package/test/unit/spec/metrics/behavioral-events.ts +42 -0
  49. package/test/unit/spec/services/AnswerCallOnWebexService.ts +41 -0
  50. package/test/unit/spec/services/WebexCrossClientService.ts +207 -1
  51. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +23 -0
  52. package/test/unit/spec/services/task/voice/Voice.ts +380 -0
  53. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +145 -1
  54. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
  55. package/umd/contact-center.min.js +2 -2
  56. package/umd/contact-center.min.js.map +1 -1
@@ -1,13 +1,30 @@
1
1
  import WebexCrossClientService from '../../../../src/services/WebexCrossClientService';
2
+ import MetricsManager from '../../../../src/metrics/MetricsManager';
3
+ import {METRIC_EVENT_NAMES} from '../../../../src/metrics/constants';
4
+
5
+ jest.mock('../../../../src/metrics/MetricsManager', () => ({
6
+ __esModule: true,
7
+ default: {
8
+ getInstance: jest.fn(),
9
+ },
10
+ }));
2
11
 
3
12
  describe('WebexCrossClientService', () => {
4
13
  const userId = 'user-123';
5
14
  const deviceUrl = 'https://wdm.example.com/devices/device-abc';
15
+ const trackEvent = jest.fn();
16
+ const timeEvent = jest.fn();
17
+ const cancelTimedEvent = jest.fn();
6
18
 
7
19
  let webex: {request: jest.Mock; internal: {device: {userId: string; url: string}}};
8
20
  let service: WebexCrossClientService;
9
21
 
10
22
  beforeEach(() => {
23
+ trackEvent.mockClear();
24
+ timeEvent.mockClear();
25
+ cancelTimedEvent.mockClear();
26
+ (MetricsManager.getInstance as jest.Mock).mockReturnValue({trackEvent, timeEvent, cancelTimedEvent});
27
+
11
28
  webex = {
12
29
  request: jest.fn().mockResolvedValue({body: {}}),
13
30
  internal: {
@@ -82,6 +99,26 @@ describe('WebexCrossClientService', () => {
82
99
  'User ID is unavailable for cross-client publish'
83
100
  );
84
101
  expect(webex.request).not.toHaveBeenCalled();
102
+ expect(trackEvent).not.toHaveBeenCalled();
103
+ });
104
+
105
+ it('emits usersub publish failed when user ID is unavailable and trackPublishMetrics is true', async () => {
106
+ webex.internal.device.userId = undefined as unknown as string;
107
+
108
+ await expect(
109
+ service.setManageWebexCallingInWxcc(true, {trackPublishMetrics: true})
110
+ ).rejects.toThrow('User ID is unavailable for cross-client publish');
111
+
112
+ expect(trackEvent).toHaveBeenCalledWith(
113
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
114
+ expect.objectContaining({
115
+ enableWxBetterTogether: true,
116
+ skipReason: 'user_id_unavailable',
117
+ }),
118
+ ['operational', 'behavioral']
119
+ );
120
+ expect(timeEvent).not.toHaveBeenCalled();
121
+ expect(webex.request).not.toHaveBeenCalled();
85
122
  });
86
123
 
87
124
  it('throws when device URL is unavailable', async () => {
@@ -91,13 +128,182 @@ describe('WebexCrossClientService', () => {
91
128
  'Device URL is unavailable for cross-client publish'
92
129
  );
93
130
  expect(webex.request).not.toHaveBeenCalled();
131
+ expect(trackEvent).not.toHaveBeenCalled();
132
+ });
133
+
134
+ it('emits usersub publish failed when device URL is unavailable and trackPublishMetrics is true', async () => {
135
+ webex.internal.device.url = undefined as unknown as string;
136
+
137
+ await expect(
138
+ service.setManageWebexCallingInWxcc(true, {trackPublishMetrics: true})
139
+ ).rejects.toThrow('Device URL is unavailable for cross-client publish');
140
+
141
+ expect(trackEvent).toHaveBeenCalledWith(
142
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
143
+ expect.objectContaining({
144
+ enableWxBetterTogether: true,
145
+ skipReason: 'device_url_unavailable',
146
+ }),
147
+ ['operational', 'behavioral']
148
+ );
149
+ expect(timeEvent).not.toHaveBeenCalled();
150
+ expect(webex.request).not.toHaveBeenCalled();
94
151
  });
95
152
 
96
153
  it('rejects when usersub publish fails', async () => {
97
154
  const err = new Error('usersub failed');
98
155
  webex.request = jest.fn().mockRejectedValue(err);
99
156
 
100
- await expect(service.setManageWebexCallingInWxcc(true)).rejects.toThrow('usersub failed');
157
+ await expect(
158
+ service.setManageWebexCallingInWxcc(true, {trackPublishMetrics: true})
159
+ ).rejects.toThrow('usersub failed');
160
+
161
+ expect(trackEvent).toHaveBeenCalledWith(
162
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
163
+ expect.objectContaining({enableWxBetterTogether: true}),
164
+ ['operational', 'behavioral']
165
+ );
166
+ });
167
+
168
+ it('tracks usersub publish success when trackPublishMetrics is enabled', async () => {
169
+ await service.setManageWebexCallingInWxcc(true, {trackPublishMetrics: true});
170
+
171
+ expect(timeEvent).toHaveBeenCalledWith([
172
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_SUCCESS,
173
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
174
+ ]);
175
+ expect(trackEvent).toHaveBeenCalledWith(
176
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_SUCCESS,
177
+ {enableWxBetterTogether: true},
178
+ ['operational', 'behavioral']
179
+ );
180
+ });
181
+
182
+ it('cancels usersub publish timer when publish completes after teardown bumps generation', async () => {
183
+ let resolvePublish: (value: {body: Record<string, never>}) => void;
184
+ const publishPromise = new Promise<{body: Record<string, never>}>((resolve) => {
185
+ resolvePublish = resolve;
186
+ });
187
+ webex.request = jest.fn().mockReturnValue(publishPromise);
188
+
189
+ const publishCall = service.setManageWebexCallingInWxcc(true, {trackPublishMetrics: true});
190
+ service.teardown();
191
+ resolvePublish!({body: {}});
192
+ await publishCall;
193
+
194
+ expect(cancelTimedEvent).toHaveBeenCalledWith([
195
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_SUCCESS,
196
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
197
+ ]);
198
+ expect(trackEvent).not.toHaveBeenCalled();
199
+ });
200
+
201
+ it('cancels usersub publish timer when publish fails after teardown bumps generation', async () => {
202
+ let rejectPublish: (error: Error) => void;
203
+ const publishPromise = new Promise((_, reject) => {
204
+ rejectPublish = reject;
205
+ });
206
+ webex.request = jest.fn().mockReturnValue(publishPromise);
207
+
208
+ const publishCall = service.setManageWebexCallingInWxcc(true, {trackPublishMetrics: true});
209
+ service.teardown();
210
+ rejectPublish!(new Error('usersub failed'));
211
+ await expect(publishCall).rejects.toThrow('usersub failed');
212
+
213
+ expect(cancelTimedEvent).toHaveBeenCalledWith([
214
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_SUCCESS,
215
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
216
+ ]);
217
+ expect(trackEvent).not.toHaveBeenCalled();
218
+ });
219
+
220
+ it('does not cancel newer publish timer when stale publish completes', async () => {
221
+ let resolveFirstPublish: (value: {body: Record<string, never>}) => void;
222
+ const firstPublishPromise = new Promise<{body: Record<string, never>}>((resolve) => {
223
+ resolveFirstPublish = resolve;
224
+ });
225
+ webex.request = jest
226
+ .fn()
227
+ .mockReturnValueOnce(firstPublishPromise)
228
+ .mockResolvedValueOnce({body: {}});
229
+
230
+ const firstPublishCall = service.setManageWebexCallingInWxcc(true, {
231
+ trackPublishMetrics: true,
232
+ });
233
+ const secondPublishCall = service.setManageWebexCallingInWxcc(false, {
234
+ trackPublishMetrics: true,
235
+ });
236
+
237
+ await secondPublishCall;
238
+
239
+ service.teardown();
240
+ resolveFirstPublish!({body: {}});
241
+ await firstPublishCall;
242
+
243
+ expect(cancelTimedEvent).not.toHaveBeenCalled();
244
+ expect(trackEvent).toHaveBeenCalledWith(
245
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_SUCCESS,
246
+ {enableWxBetterTogether: false},
247
+ ['operational', 'behavioral']
248
+ );
249
+ });
250
+
251
+ it('preserves duration_ms on newer publish when stale publish completes', async () => {
252
+ const {default: RealMetricsManager} = jest.requireActual(
253
+ '../../../../src/metrics/MetricsManager'
254
+ );
255
+ RealMetricsManager.resetInstance();
256
+ const realManager = RealMetricsManager.getInstance();
257
+ jest.spyOn(realManager as any, 'isMetricsDisabled').mockReturnValue(false);
258
+ const trackEventSpy = jest.spyOn(realManager, 'trackEvent');
259
+ (MetricsManager.getInstance as jest.Mock).mockReturnValue(realManager);
260
+
261
+ let resolveFirstPublish: (value: {body: Record<string, never>}) => void;
262
+ const firstPublishPromise = new Promise<{body: Record<string, never>}>((resolve) => {
263
+ resolveFirstPublish = resolve;
264
+ });
265
+ webex.request = jest
266
+ .fn()
267
+ .mockReturnValueOnce(firstPublishPromise)
268
+ .mockResolvedValueOnce({body: {}});
269
+
270
+ const firstPublishCall = service.setManageWebexCallingInWxcc(true, {
271
+ trackPublishMetrics: true,
272
+ });
273
+ const secondPublishCall = service.setManageWebexCallingInWxcc(false, {
274
+ trackPublishMetrics: true,
275
+ });
276
+
277
+ await secondPublishCall;
278
+ service.teardown();
279
+ resolveFirstPublish!({body: {}});
280
+ await firstPublishCall;
281
+
282
+ const successCall = trackEventSpy.mock.calls.find(
283
+ ([eventName, payload]) =>
284
+ eventName === METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_SUCCESS &&
285
+ (payload as {enableWxBetterTogether?: boolean}).enableWxBetterTogether === false
286
+ );
287
+ expect(successCall?.[1]).toEqual(
288
+ expect.objectContaining({
289
+ enableWxBetterTogether: false,
290
+ duration_ms: expect.any(Number),
291
+ })
292
+ );
293
+
294
+ RealMetricsManager.resetInstance();
295
+ (MetricsManager.getInstance as jest.Mock).mockReturnValue({
296
+ trackEvent,
297
+ timeEvent,
298
+ cancelTimedEvent,
299
+ });
300
+ });
301
+
302
+ it('does not track usersub metrics by default', async () => {
303
+ await service.setManageWebexCallingInWxcc(true);
304
+
305
+ expect(trackEvent).not.toHaveBeenCalled();
306
+ expect(timeEvent).not.toHaveBeenCalled();
101
307
  });
102
308
 
103
309
  it('uses custom ttl from options', async () => {
@@ -2,15 +2,28 @@ import WxAppTelephonyMercurySync, {
2
2
  TELEPHONY_CALL_MUTED,
3
3
  TELEPHONY_CALL_UNMUTED,
4
4
  } from '../../../../src/services/WxAppTelephonyMercurySync';
5
+ import MetricsManager from '../../../../src/metrics/MetricsManager';
6
+ import {METRIC_EVENT_NAMES} from '../../../../src/metrics/constants';
7
+
8
+ jest.mock('../../../../src/metrics/MetricsManager', () => ({
9
+ __esModule: true,
10
+ default: {
11
+ getInstance: jest.fn(),
12
+ },
13
+ }));
5
14
 
6
15
  describe('WxAppTelephonyMercurySync', () => {
7
16
  const agentId = 'agent-123';
17
+ const trackEvent = jest.fn();
8
18
  let mercury: {on: jest.Mock; off: jest.Mock};
9
19
  let webex: {internal: {mercury?: typeof mercury}};
10
20
  let sync: WxAppTelephonyMercurySync;
11
21
  let onMuteChange: jest.Mock;
12
22
 
13
23
  beforeEach(() => {
24
+ trackEvent.mockClear();
25
+ (MetricsManager.getInstance as jest.Mock).mockReturnValue({trackEvent});
26
+
14
27
  mercury = {on: jest.fn(), off: jest.fn()};
15
28
  webex = {internal: {mercury}};
16
29
  sync = new WxAppTelephonyMercurySync(webex as never);
@@ -27,6 +40,11 @@ describe('WxAppTelephonyMercurySync', () => {
27
40
  expect(mercury.on).toHaveBeenCalledWith(TELEPHONY_CALL_MUTED, expect.any(Function));
28
41
  expect(mercury.on).toHaveBeenCalledWith(TELEPHONY_CALL_UNMUTED, expect.any(Function));
29
42
  expect(sync.isSubscribed()).toBe(true);
43
+ expect(trackEvent).toHaveBeenCalledWith(
44
+ METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_SUCCESS,
45
+ {},
46
+ ['operational', 'behavioral']
47
+ );
30
48
  });
31
49
 
32
50
  it('invokes callback with decoded callId and muted when actorId matches agent', () => {
@@ -77,6 +95,11 @@ describe('WxAppTelephonyMercurySync', () => {
77
95
 
78
96
  expect(sync.isSubscribed()).toBe(false);
79
97
  expect(onMuteChange).not.toHaveBeenCalled();
98
+ expect(trackEvent).toHaveBeenCalledWith(
99
+ METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_FAILED,
100
+ expect.objectContaining({error: expect.stringContaining('Mercury is unavailable')}),
101
+ ['operational', 'behavioral']
102
+ );
80
103
  });
81
104
 
82
105
  it('invokes callback with muted false on unmuted Mercury event', () => {