@webex/contact-center 3.12.0-next.147 → 3.12.0-next.149

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 (82) hide show
  1. package/.sdd/manifest.json +4 -2
  2. package/ai-docs/CONTRACTS.md +2 -0
  3. package/dist/cc.js +3 -1
  4. package/dist/cc.js.map +1 -1
  5. package/dist/constants.js +10 -1
  6. package/dist/constants.js.map +1 -1
  7. package/dist/index.js +9 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/metrics/constants.js +9 -0
  10. package/dist/metrics/constants.js.map +1 -1
  11. package/dist/services/ApiAiAssistant.js +87 -10
  12. package/dist/services/ApiAiAssistant.js.map +1 -1
  13. package/dist/services/config/types.js +10 -4
  14. package/dist/services/config/types.js.map +1 -1
  15. package/dist/services/core/types.js.map +1 -1
  16. package/dist/services/task/Task.js +202 -1
  17. package/dist/services/task/Task.js.map +1 -1
  18. package/dist/services/task/TaskFactory.js +4 -4
  19. package/dist/services/task/TaskFactory.js.map +1 -1
  20. package/dist/services/task/TaskManager.js +113 -26
  21. package/dist/services/task/TaskManager.js.map +1 -1
  22. package/dist/services/task/TaskUtils.js +11 -1
  23. package/dist/services/task/TaskUtils.js.map +1 -1
  24. package/dist/services/task/constants.js +11 -2
  25. package/dist/services/task/constants.js.map +1 -1
  26. package/dist/services/task/digital/Digital.js +2 -2
  27. package/dist/services/task/digital/Digital.js.map +1 -1
  28. package/dist/services/task/types.js +31 -0
  29. package/dist/services/task/types.js.map +1 -1
  30. package/dist/services/task/voice/Voice.js +2 -2
  31. package/dist/services/task/voice/Voice.js.map +1 -1
  32. package/dist/services/task/voice/WebRTC.js +2 -2
  33. package/dist/services/task/voice/WebRTC.js.map +1 -1
  34. package/dist/types/constants.d.ts +9 -0
  35. package/dist/types/index.d.ts +4 -5
  36. package/dist/types/metrics/constants.d.ts +8 -0
  37. package/dist/types/services/ApiAiAssistant.d.ts +4 -2
  38. package/dist/types/services/config/types.d.ts +17 -7
  39. package/dist/types/services/task/Task.d.ts +15 -4
  40. package/dist/types/services/task/TaskFactory.d.ts +3 -2
  41. package/dist/types/services/task/TaskUtils.d.ts +6 -0
  42. package/dist/types/services/task/constants.d.ts +9 -0
  43. package/dist/types/services/task/digital/Digital.d.ts +3 -2
  44. package/dist/types/services/task/types.d.ts +109 -1
  45. package/dist/types/services/task/voice/Voice.d.ts +3 -2
  46. package/dist/types/services/task/voice/WebRTC.d.ts +3 -2
  47. package/dist/types/types.d.ts +10 -4
  48. package/dist/types.js +8 -4
  49. package/dist/types.js.map +1 -1
  50. package/dist/webex.js +1 -1
  51. package/package.json +3 -3
  52. package/src/cc.ts +4 -0
  53. package/src/constants.ts +12 -0
  54. package/src/index.ts +10 -2
  55. package/src/metrics/ai-docs/metrics-spec.md +12 -4
  56. package/src/metrics/constants.ts +10 -0
  57. package/src/services/ApiAiAssistant.ts +120 -12
  58. package/src/services/config/types.ts +10 -4
  59. package/src/services/core/types.ts +31 -1
  60. package/src/services/task/Task.ts +308 -5
  61. package/src/services/task/TaskFactory.ts +26 -5
  62. package/src/services/task/TaskManager.ts +148 -29
  63. package/src/services/task/TaskUtils.ts +13 -0
  64. package/src/services/task/ai-docs/task-spec.md +30 -5
  65. package/src/services/task/constants.ts +10 -0
  66. package/src/services/task/digital/Digital.ts +8 -3
  67. package/src/services/task/types.ts +139 -0
  68. package/src/services/task/voice/Voice.ts +8 -3
  69. package/src/services/task/voice/WebRTC.ts +6 -3
  70. package/src/types.ts +10 -4
  71. package/test/unit/fixtures/aiSummaryTestUtils.ts +40 -0
  72. package/test/unit/spec/cc.ts +1314 -32
  73. package/test/unit/spec/index.ts +881 -0
  74. package/test/unit/spec/services/ApiAiAssistant.ts +122 -6
  75. package/test/unit/spec/services/core/WebexRequest.ts +5 -6
  76. package/test/unit/spec/services/task/Task.ts +1988 -10
  77. package/test/unit/spec/services/task/TaskFactory.ts +17 -0
  78. package/test/unit/spec/services/task/TaskManager.ts +2327 -180
  79. package/test/unit/spec/services/task/TaskUtils.ts +72 -0
  80. package/test/unit/spec/services/task/voice/Voice.ts +353 -18
  81. package/umd/contact-center.min.js +2 -2
  82. package/umd/contact-center.min.js.map +1 -1
@@ -0,0 +1,881 @@
1
+ import path from 'path';
2
+ import ts from 'typescript';
3
+
4
+ const CONTACT_CENTER_ROOT = path.resolve(__dirname, '../../..');
5
+ const SRC_ROOT = path.join(CONTACT_CENTER_ROOT, 'src');
6
+ const INDEX_PATH = path.join(SRC_ROOT, 'index.ts');
7
+ const TYPES_PATH = path.join(SRC_ROOT, 'types.ts');
8
+ const CONFIG_TYPES_PATH = path.join(SRC_ROOT, 'services/config/types.ts');
9
+
10
+ const EXPECTED_ROOT_EXPORTS = Object.freeze([
11
+ 'AGENT_EVENTS',
12
+ 'AISummaryAction',
13
+ 'AISummary',
14
+ 'AISummaryFeedback',
15
+ 'AISummaryResponse',
16
+ 'AISummarySections',
17
+ 'AISummaryState',
18
+ 'AI_SUMMARY_ERROR_CODES',
19
+ 'AddressBook',
20
+ 'AddressBookEntriesResponse',
21
+ 'AddressBookEntry',
22
+ 'AddressBookEntrySearchParams',
23
+ 'AgentContact',
24
+ 'AgentEvents',
25
+ 'AgentLogin',
26
+ 'AgentProfileUpdate',
27
+ 'AgentResponse',
28
+ 'AgentState',
29
+ 'ApiAIAssistant',
30
+ 'AuxCode',
31
+ 'AuxCodeType',
32
+ 'BuddyAgents',
33
+ 'BuddyAgentsResponse',
34
+ 'BuddyAgentsSuccess',
35
+ 'BuddyDetails',
36
+ 'CCPluginConfig',
37
+ 'CC_AGENT_EVENTS',
38
+ 'CC_TASK_EVENTS',
39
+ 'CC_EVENTS',
40
+ 'ConsultEndPayload',
41
+ 'ConsultPayload',
42
+ 'ConsultTransferPayLoad',
43
+ 'ContactCenter',
44
+ 'ContactCenterEvents',
45
+ 'ContactServiceQueue',
46
+ 'ContactServiceQueueSearchParams',
47
+ 'ContactServiceQueuesResponse',
48
+ 'CreateUserPreferenceRequest',
49
+ 'DesktopProfileResponse',
50
+ 'DeviceType',
51
+ 'DeviceTypeUpdateSuccess',
52
+ 'DialPlan',
53
+ 'DialPlanEntity',
54
+ 'DialerPayload',
55
+ 'DropConferenceParticipantPayload',
56
+ 'Entity',
57
+ 'EntryPointListResponse',
58
+ 'EntryPointRecord',
59
+ 'EntryPointSearchParams',
60
+ 'AISummaryFeatureEnablement',
61
+ 'GenericError',
62
+ 'GetUserPreferenceParams',
63
+ 'IContactCenter',
64
+ 'IDLE_CODE',
65
+ 'IEventEmitter',
66
+ 'ITask',
67
+ 'Interaction',
68
+ 'InteractionUIControls',
69
+ 'ListAuxCodesResponse',
70
+ 'ListTeamsResponse',
71
+ 'LoginOption',
72
+ 'Logout',
73
+ 'LogoutSuccess',
74
+ 'MultimediaProfileResponse',
75
+ 'OrgInfo',
76
+ 'OrgSettings',
77
+ 'Profile',
78
+ 'ReloginSuccess',
79
+ 'RespondToWellnessBreakParams',
80
+ 'ResumeRecordingPayload',
81
+ 'SetStateResponse',
82
+ 'SiteInfo',
83
+ 'StateChange',
84
+ 'StateChangeSuccess',
85
+ 'StationLoginResponse',
86
+ 'StationLoginSuccess',
87
+ 'StationLoginSuccessResponse',
88
+ 'StationLogoutResponse',
89
+ 'SubscribeRequest',
90
+ 'TASK_EVENTS',
91
+ 'Task',
92
+ 'TaskData',
93
+ 'TaskEvents',
94
+ 'TaskResponse',
95
+ 'TaskUIControlState',
96
+ 'TaskUIControls',
97
+ 'TaskUILeg',
98
+ 'TeamList',
99
+ 'TenantData',
100
+ 'TransferPayLoad',
101
+ 'URLMapping',
102
+ 'UpdateDeviceTypeResponse',
103
+ 'UpdateUserPreferenceRequest',
104
+ 'UploadLogsResponse',
105
+ 'UserPreference',
106
+ 'UserPreferenceData',
107
+ 'UserStationLogin',
108
+ 'WELLNESS_BREAK_NOTIFICATION_ACTIONS',
109
+ 'WELLNESS_BREAK_USER_ACTIONS',
110
+ 'WRAP_UP_CODE',
111
+ 'WebSocketEvent',
112
+ 'WebexSDK',
113
+ 'WellnessBreakEvent',
114
+ 'WellnessBreakNotificationAction',
115
+ 'WellnessBreakUserAction',
116
+ 'WrapUpReason',
117
+ 'WrapupData',
118
+ 'WrapupPayLoad',
119
+ 'default',
120
+ 'getDefaultUIControls',
121
+ 'routingAgent',
122
+ ]);
123
+
124
+ const INTERNAL_ROOT_EXPORTS = Object.freeze([
125
+ 'AIAssistantEventName',
126
+ 'AISummaryTimeoutCodeByInboundType',
127
+ 'AI_SUMMARY_DURATION_MS',
128
+ 'AI_SUMMARY_REQUEST_CANCELLED',
129
+ 'AI_ASSISTANT_CLIENT_TYPE',
130
+ ]);
131
+
132
+ const EXPECTED_AI_SUMMARY_ERROR_CODES = Object.freeze([
133
+ ['POST_CALL_SUMMARY_DISABLED', 'POST_CALL_SUMMARY_DISABLED'],
134
+ ['MID_CALL_SUMMARY_DISABLED', 'MID_CALL_SUMMARY_DISABLED'],
135
+ ['AI_ASSISTANT_BASE_URL_NOT_AVAILABLE', 'AI_ASSISTANT_BASE_URL_NOT_AVAILABLE'],
136
+ ['POST_CALL_SUMMARY_TIMEOUT', 'POST_CALL_SUMMARY_TIMEOUT'],
137
+ ['MID_CALL_SUMMARY_TIMEOUT', 'MID_CALL_SUMMARY_TIMEOUT'],
138
+ ]);
139
+
140
+ const EXPECTED_AI_SUMMARY_WIRE_LITERALS = Object.freeze({
141
+ AIAssistantEventName: Object.freeze({
142
+ GET_POST_CALL_SUMMARY: 'GET_POST_CALL_SUMMARY',
143
+ GET_MID_CALL_CONSULT_SUMMARY: 'GET_MID_CALL_CONSULT_SUMMARY',
144
+ GET_MID_CALL_TRANSFER_SUMMARY: 'GET_MID_CALL_TRANSFER_SUMMARY',
145
+ POST_CALL_SUMMARY_RESPONSE: 'POST_CALL_SUMMARY_RESPONSE',
146
+ MID_CALL_CONSULT_SUMMARY_RESPONSE: 'MID_CALL_CONSULT_SUMMARY_RESPONSE',
147
+ MID_CALL_TRANSFER_SUMMARY_RESPONSE: 'MID_CALL_TRANSFER_SUMMARY_RESPONSE',
148
+ }),
149
+ CC_TASK_EVENTS: Object.freeze({
150
+ REAL_TIME_TRANSCRIPTION: 'REAL_TIME_TRANSCRIPTION',
151
+ SUGGESTED_RESPONSE: 'SUGGESTED_RESPONSE',
152
+ SUGGESTED_RESPONSE_ACKNOWLEDGE: 'SUGGESTED_RESPONSE_ACKNOWLEDGE',
153
+ POST_CALL_SUMMARY: 'POST_CALL_SUMMARY',
154
+ MID_CALL_SUMMARY: 'MID_CALL_SUMMARY',
155
+ FEATURE_ENABLEMENT: 'FEATURE_ENABLEMENT',
156
+ MID_CALL_SUMMARY_RESPONSE_SUBSEQUENT_AGENT: 'MID_CALL_SUMMARY_RESPONSE_SUBSEQUENT_AGENT',
157
+ }),
158
+ });
159
+
160
+ const MISSING_EXPORT_DIAGNOSTIC_CODES = Object.freeze([2305, 2614, 2724]);
161
+
162
+ const VALID_PUBLIC_CONTRACT_FIXTURE = `
163
+ import {
164
+ AI_SUMMARY_ERROR_CODES,
165
+ CC_TASK_EVENTS,
166
+ TASK_EVENTS,
167
+ } from '../../../src';
168
+ import type {
169
+ AISummaryAction,
170
+ AISummaryFeedback,
171
+ AISummaryResponse,
172
+ AISummaryFeatureEnablement,
173
+ ITask,
174
+ } from '../../../src';
175
+
176
+ const action: AISummaryAction = 'CONSULT';
177
+ const transferAction: AISummaryAction = 'TRANSFER';
178
+ const feedback: AISummaryFeedback = 'thumbs_down';
179
+ const postCallEnabledTrue: AISummaryFeatureEnablement = {
180
+ interactionId: 'post-call-true',
181
+ postCallEnabled: true,
182
+ };
183
+ const postCallEnabledFalse: AISummaryFeatureEnablement = {
184
+ interactionId: 'post-call-false',
185
+ postCallEnabled: false,
186
+ };
187
+ const postCallEnabledOmitted: AISummaryFeatureEnablement = {
188
+ interactionId: 'post-call-omitted',
189
+ midCallEnabled: true,
190
+ };
191
+ const midCallEnabledTrue: AISummaryFeatureEnablement = {
192
+ interactionId: 'mid-call-true',
193
+ midCallEnabled: true,
194
+ };
195
+ const midCallEnabledFalse: AISummaryFeatureEnablement = {
196
+ interactionId: 'mid-call-false',
197
+ midCallEnabled: false,
198
+ };
199
+ const midCallEnabledOmitted: AISummaryFeatureEnablement = {
200
+ interactionId: 'mid-call-omitted',
201
+ postCallEnabled: true,
202
+ };
203
+
204
+ const structuredPostCall: AISummaryResponse = {
205
+ summary: {
206
+ initialContactReason: 'billing',
207
+ additionalContactReasons: 'renewal',
208
+ additionalContext: 'caller asked for an email',
209
+ keyActionsTaken: 'verified account',
210
+ nextSteps: 'send receipt',
211
+ },
212
+ feedback,
213
+ state: 'DEFAULT',
214
+ wrapUpCode: 'resolved',
215
+ numberOfTimesViewed: 1,
216
+ numberOfTimesEdited: 0,
217
+ numberOfTimesCopied: 0,
218
+ };
219
+ const textPostCall: AISummaryResponse = {
220
+ summary: 'plain post call summary',
221
+ feedback: 'thumbs_up',
222
+ state: 'IGNORED',
223
+ wrapUpCode: 'ignored',
224
+ numberOfTimesViewed: 2,
225
+ numberOfTimesEdited: 1,
226
+ numberOfTimesCopied: 1,
227
+ };
228
+ const noPostCallSummary: AISummaryResponse = {
229
+ summary: '',
230
+ feedback: 'none',
231
+ state: 'NOT_RECEIVED',
232
+ wrapUpCode: 'not-received',
233
+ numberOfTimesViewed: 0,
234
+ numberOfTimesEdited: 0,
235
+ numberOfTimesCopied: 0,
236
+ };
237
+
238
+ const structuredMidCall: AISummaryResponse = {
239
+ summaryReceived: true,
240
+ summary: {
241
+ reasonForTransferOrConsult: 'expert needed',
242
+ additionalContext: 'customer has prior ticket',
243
+ keyActionsTaken: 'authenticated caller',
244
+ },
245
+ feedback: 'none',
246
+ state: 'EXCLUDED',
247
+ numberOfTimesViewed: 1,
248
+ numberOfTimesEdited: 0,
249
+ numberOfTimesCopied: 0,
250
+ };
251
+ const textMidCall: AISummaryResponse = {
252
+ summaryReceived: true,
253
+ summary: 'plain mid call summary',
254
+ feedback: 'thumbs_up',
255
+ state: 'MID_CALL_CANCELLED',
256
+ numberOfTimesViewed: 0,
257
+ numberOfTimesEdited: 0,
258
+ numberOfTimesCopied: 0,
259
+ };
260
+ const unavailableMidCall: AISummaryResponse = {
261
+ summaryReceived: false,
262
+ summary: '',
263
+ feedback: 'thumbs_down',
264
+ state: 'NOT_RECEIVED',
265
+ numberOfTimesViewed: 0,
266
+ numberOfTimesEdited: 0,
267
+ numberOfTimesCopied: 0,
268
+ };
269
+ const cancelledMidCall: AISummaryResponse = {
270
+ summaryReceived: false,
271
+ summary: '',
272
+ feedback: 'none',
273
+ state: 'MID_CALL_CANCELLED',
274
+ numberOfTimesViewed: 0,
275
+ numberOfTimesEdited: 0,
276
+ numberOfTimesCopied: 0,
277
+ };
278
+
279
+ const taskSummaryMethods: Pick<
280
+ ITask,
281
+ | 'requestPostCallSummary'
282
+ | 'sendPostCallSummaryResponse'
283
+ | 'requestMidCallSummary'
284
+ | 'sendMidCallSummaryResponse'
285
+ > = {
286
+ requestPostCallSummary: async () => ({conversationId: 'conversation-1'}),
287
+ sendPostCallSummaryResponse: async (payload) => {
288
+ const responseState = payload.state;
289
+ void responseState;
290
+ },
291
+ requestMidCallSummary: async (requestedAction) => {
292
+ const selectedAction: AISummaryAction = requestedAction;
293
+ void selectedAction;
294
+ return {conversationId: 'conversation-1'};
295
+ },
296
+ sendMidCallSummaryResponse: async (payload, requestedAction) => {
297
+ const selectedAction: AISummaryAction = requestedAction;
298
+ const responseState = payload.state;
299
+ void selectedAction;
300
+ void responseState;
301
+ },
302
+ };
303
+
304
+ const inboundPost = CC_TASK_EVENTS.POST_CALL_SUMMARY;
305
+ const receivingEvent = TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED;
306
+ const disabledCode = AI_SUMMARY_ERROR_CODES.POST_CALL_SUMMARY_DISABLED;
307
+ void action;
308
+ void transferAction;
309
+ void postCallEnabledTrue;
310
+ void postCallEnabledFalse;
311
+ void postCallEnabledOmitted;
312
+ void midCallEnabledTrue;
313
+ void midCallEnabledFalse;
314
+ void midCallEnabledOmitted;
315
+ void structuredPostCall;
316
+ void textPostCall;
317
+ void noPostCallSummary;
318
+ void structuredMidCall;
319
+ void textMidCall;
320
+ void unavailableMidCall;
321
+ void cancelledMidCall;
322
+ void taskSummaryMethods;
323
+ void inboundPost;
324
+ void receivingEvent;
325
+ void disabledCode;
326
+ `;
327
+
328
+ const createInternalRootImportFixture = () => `
329
+ import {
330
+ ${INTERNAL_ROOT_EXPORTS.map((name) => ` ${name},`).join('\n')}
331
+ } from '../../../src';
332
+
333
+ void [
334
+ ${INTERNAL_ROOT_EXPORTS.map((name) => ` ${name},`).join('\n')}
335
+ ];
336
+ `;
337
+
338
+ const INVALID_PUBLIC_CONTRACT_FIXTURE = `
339
+ import {
340
+ type AISummaryAction,
341
+ type AISummaryResponse,
342
+ type AISummaryFeatureEnablement,
343
+ } from '../../../src';
344
+
345
+ let invalidAction: AISummaryAction;
346
+ // @ts-expect-error invalid summary action type must not widen
347
+ invalidAction = 'WRAP_UP';
348
+ void invalidAction;
349
+
350
+ // @ts-expect-error postCallEnabled must stay boolean; string values must not satisfy the public flag contract
351
+ const invalidPostCallEnabledString: AISummaryFeatureEnablement = {interactionId: 'interaction-1', postCallEnabled: 'true'};
352
+ void invalidPostCallEnabledString;
353
+
354
+ // @ts-expect-error postCallEnabled must stay boolean; number values must not satisfy the public flag contract
355
+ const invalidPostCallEnabledNumber: AISummaryFeatureEnablement = {interactionId: 'interaction-2', postCallEnabled: 1};
356
+ void invalidPostCallEnabledNumber;
357
+
358
+ // @ts-expect-error midCallEnabled must stay boolean; string values must not satisfy the public flag contract
359
+ const invalidMidCallEnabledString: AISummaryFeatureEnablement = {interactionId: 'interaction-3', midCallEnabled: 'false'};
360
+ void invalidMidCallEnabledString;
361
+
362
+ // @ts-expect-error midCallEnabled must stay boolean; number values must not satisfy the public flag contract
363
+ const invalidMidCallEnabledNumber: AISummaryFeatureEnablement = {interactionId: 'interaction-4', midCallEnabled: 0};
364
+ void invalidMidCallEnabledNumber;
365
+
366
+ // @ts-expect-error invalid summary feedback must not be accepted
367
+ const invalidFeedback: AISummaryResponse = {summary: 'x', feedback: 'ok', state: 'DEFAULT', wrapUpCode: 'wrap', numberOfTimesViewed: 1, numberOfTimesEdited: 0, numberOfTimesCopied: 0};
368
+ void invalidFeedback;
369
+
370
+ // @ts-expect-error summaryReceived must stay boolean when supplied
371
+ const invalidDiscriminator: AISummaryResponse = {summaryReceived: 'true', summary: 'x', feedback: 'none', state: 'DEFAULT', numberOfTimesViewed: 1, numberOfTimesEdited: 0, numberOfTimesCopied: 0};
372
+ void invalidDiscriminator;
373
+
374
+ // @ts-expect-error unknown summary states must not be accepted
375
+ const invalidSummaryState: AISummaryResponse = {summaryReceived: true, summary: 'x', feedback: 'none', state: 'POST_CALL_ONLY', numberOfTimesViewed: 1, numberOfTimesEdited: 0, numberOfTimesCopied: 0};
376
+ void invalidSummaryState;
377
+
378
+ // @ts-expect-error actionTimeStamp is transport metadata generated by the SDK
379
+ const invalidActionTimestamp: AISummaryResponse = {summary: 'x', feedback: 'none', state: 'DEFAULT', numberOfTimesViewed: 1, numberOfTimesEdited: 0, numberOfTimesCopied: 0, actionTimeStamp: 100};
380
+ void invalidActionTimestamp;
381
+
382
+ // @ts-expect-error publishTimestamp is transport metadata generated by the SDK
383
+ const invalidPublishTimestamp: AISummaryResponse = {summary: 'x', feedback: 'none', state: 'DEFAULT', numberOfTimesViewed: 1, numberOfTimesEdited: 0, numberOfTimesCopied: 0, publishTimestamp: 100};
384
+ void invalidPublishTimestamp;
385
+ `;
386
+
387
+ const LEGACY_TASK_CALL_CONTROL_CONTRACT_FIXTURE = `
388
+ import type {
389
+ ITask,
390
+ TaskResponse,
391
+ } from '../../../src';
392
+
393
+ type IsExact<Actual, Expected> =
394
+ (<Value>() => Value extends Actual ? 1 : 2) extends
395
+ (<Value>() => Value extends Expected ? 1 : 2)
396
+ ? (<Value>() => Value extends Expected ? 1 : 2) extends
397
+ (<Value>() => Value extends Actual ? 1 : 2)
398
+ ? true
399
+ : false
400
+ : false;
401
+ type AssertExact<Actual extends true> = Actual;
402
+
403
+ type ExpectedWrapupParameters = [
404
+ wrapupPayload: {
405
+ wrapUpReason: string;
406
+ auxCodeId: string;
407
+ },
408
+ ];
409
+ type ExpectedConsultParameters = [
410
+ consultPayload: {
411
+ to: string | undefined;
412
+ destinationType: string;
413
+ holdParticipants?: boolean;
414
+ },
415
+ ];
416
+ type ExpectedTransferParameters = [
417
+ transferPayload: {
418
+ to: string;
419
+ destinationType: string;
420
+ },
421
+ options?: {
422
+ [key: string]: unknown;
423
+ },
424
+ ];
425
+
426
+ type WrapupParametersAreRetained = AssertExact<
427
+ IsExact<Parameters<ITask['wrapup']>, ExpectedWrapupParameters>
428
+ >;
429
+ type WrapupReturnIsRetained = AssertExact<
430
+ IsExact<ReturnType<ITask['wrapup']>, Promise<TaskResponse>>
431
+ >;
432
+ type ConsultParametersAreRetained = AssertExact<
433
+ IsExact<Parameters<ITask['consult']>, ExpectedConsultParameters>
434
+ >;
435
+ type ConsultReturnIsRetained = AssertExact<
436
+ IsExact<ReturnType<ITask['consult']>, Promise<TaskResponse>>
437
+ >;
438
+ type TransferParametersAreRetained = AssertExact<
439
+ IsExact<Parameters<ITask['transfer']>, ExpectedTransferParameters>
440
+ >;
441
+ type TransferReturnIsRetained = AssertExact<
442
+ IsExact<ReturnType<ITask['transfer']>, Promise<TaskResponse>>
443
+ >;
444
+
445
+ declare const task: ITask;
446
+
447
+ const wrapupResult: Promise<TaskResponse> = task.wrapup({
448
+ wrapUpReason: 'Customer issue resolved',
449
+ auxCodeId: 'RESOLVED',
450
+ });
451
+ const consultResult: Promise<TaskResponse> = task.consult({
452
+ to: 'agent-1',
453
+ destinationType: 'agent',
454
+ });
455
+ const consultWithOptionalHoldResult: Promise<TaskResponse> = task.consult({
456
+ to: undefined,
457
+ destinationType: 'queue',
458
+ holdParticipants: true,
459
+ });
460
+ const transferResult: Promise<TaskResponse> = task.transfer({
461
+ to: 'queue-1',
462
+ destinationType: 'queue',
463
+ });
464
+ const transferWithOptionsResult: Promise<TaskResponse> = task.transfer(
465
+ {
466
+ to: 'agent-2',
467
+ destinationType: 'agent',
468
+ },
469
+ {trackingId: 'transfer-1'}
470
+ );
471
+
472
+ // @ts-expect-error wrapup must keep requiring its retained payload tuple
473
+ task.wrapup();
474
+ // @ts-expect-error wrapup payload must keep auxCodeId as a required field
475
+ task.wrapup({wrapUpReason: 'Customer issue resolved'});
476
+ // @ts-expect-error wrapup payload must not accept AI summary response fields threaded into the call control API
477
+ task.wrapup({wrapUpReason: 'Customer issue resolved', auxCodeId: 'RESOLVED', actionTimeStamp: 100});
478
+ // @ts-expect-error wrapup payload must not accept post-call summary state as a control argument
479
+ task.wrapup({wrapUpReason: 'Customer issue resolved', auxCodeId: 'RESOLVED', state: 'DEFAULT'});
480
+
481
+ // @ts-expect-error consult must keep requiring its retained payload tuple
482
+ task.consult();
483
+ // @ts-expect-error consult payload must keep destinationType as a string
484
+ task.consult({to: 'agent-1', destinationType: 1});
485
+ // @ts-expect-error consult payload must not accept mid-call summary action fields threaded into the payload
486
+ task.consult({to: 'agent-1', destinationType: 'agent', actionType: 'CONSULT'});
487
+ // @ts-expect-error consult must not accept a feature action as a second argument
488
+ task.consult({to: 'agent-1', destinationType: 'agent'}, 'CONSULT');
489
+
490
+ // @ts-expect-error transfer must keep requiring its retained payload tuple
491
+ task.transfer();
492
+ // @ts-expect-error transfer payload must keep to as a required string field
493
+ task.transfer({destinationType: 'queue'});
494
+ // @ts-expect-error transfer payload must keep destinationType as a string
495
+ task.transfer({to: 'queue-1', destinationType: 1});
496
+ // @ts-expect-error transfer payload must not accept mid-call summary action fields threaded into the payload
497
+ task.transfer({to: 'agent-2', destinationType: 'agent', actionType: 'TRANSFER'});
498
+ // @ts-expect-error transfer options must not be replaced by a feature action argument
499
+ task.transfer({to: 'agent-2', destinationType: 'agent'}, 'TRANSFER');
500
+ // @ts-expect-error transfer must not accept a third feature action argument
501
+ task.transfer({to: 'agent-2', destinationType: 'agent'}, {trackingId: 'transfer-2'}, 'TRANSFER');
502
+
503
+ void wrapupResult;
504
+ void consultResult;
505
+ void consultWithOptionalHoldResult;
506
+ void transferResult;
507
+ void transferWithOptionsResult;
508
+ `;
509
+
510
+ const createProgram = (virtualSources: Record<string, string> = {}) => {
511
+ const configPath = path.join(CONTACT_CENTER_ROOT, 'tsconfig.json');
512
+ const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
513
+ const parsed = ts.parseJsonConfigFileContent(
514
+ configFile.config,
515
+ ts.sys,
516
+ CONTACT_CENTER_ROOT,
517
+ {emitDeclarationOnly: false, noEmit: true},
518
+ configPath
519
+ );
520
+ const virtualFileNames = Object.keys(virtualSources);
521
+ const host = ts.createCompilerHost(parsed.options, true);
522
+ const originalGetSourceFile = host.getSourceFile.bind(host);
523
+ const originalFileExists = host.fileExists.bind(host);
524
+ const originalReadFile = host.readFile.bind(host);
525
+
526
+ host.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => {
527
+ const normalizedFileName = path.normalize(fileName);
528
+ if (Object.prototype.hasOwnProperty.call(virtualSources, normalizedFileName)) {
529
+ return ts.createSourceFile(
530
+ normalizedFileName,
531
+ virtualSources[normalizedFileName],
532
+ languageVersion,
533
+ true
534
+ );
535
+ }
536
+
537
+ return originalGetSourceFile(
538
+ fileName,
539
+ languageVersion,
540
+ onError,
541
+ shouldCreateNewSourceFile
542
+ );
543
+ };
544
+
545
+ host.fileExists = (fileName) => {
546
+ const normalizedFileName = path.normalize(fileName);
547
+ return Object.prototype.hasOwnProperty.call(virtualSources, normalizedFileName)
548
+ ? true
549
+ : originalFileExists(fileName);
550
+ };
551
+
552
+ host.readFile = (fileName) => {
553
+ const normalizedFileName = path.normalize(fileName);
554
+ return Object.prototype.hasOwnProperty.call(virtualSources, normalizedFileName)
555
+ ? virtualSources[normalizedFileName]
556
+ : originalReadFile(fileName);
557
+ };
558
+
559
+ return ts.createProgram([...parsed.fileNames, ...virtualFileNames], parsed.options, host);
560
+ };
561
+
562
+ const getRootModuleSymbol = (program: ts.Program) => {
563
+ const checker = program.getTypeChecker();
564
+ const indexSource = program.getSourceFile(INDEX_PATH);
565
+ if (!indexSource) {
566
+ throw new Error(`Missing source file ${INDEX_PATH}`);
567
+ }
568
+ const moduleSymbol = checker.getSymbolAtLocation(indexSource);
569
+ if (!moduleSymbol) {
570
+ throw new Error('Unable to resolve contact-center root module symbol');
571
+ }
572
+
573
+ return moduleSymbol;
574
+ };
575
+
576
+ const getRootExportNames = (program: ts.Program) => {
577
+ const checker = program.getTypeChecker();
578
+
579
+ return checker
580
+ .getExportsOfModule(getRootModuleSymbol(program))
581
+ .map((symbol) => symbol.getName())
582
+ .sort();
583
+ };
584
+
585
+ const getRootExportSymbol = (program: ts.Program, exportName: string) => {
586
+ return getModuleExportSymbol(program, INDEX_PATH, exportName);
587
+ };
588
+
589
+ const getModuleExportSymbol = (
590
+ program: ts.Program,
591
+ modulePath: string,
592
+ exportName: string
593
+ ) => {
594
+ const checker = program.getTypeChecker();
595
+ const sourceFile = program.getSourceFile(modulePath);
596
+ if (!sourceFile) {
597
+ throw new Error(`Missing source file ${modulePath}`);
598
+ }
599
+
600
+ const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
601
+ if (!moduleSymbol) {
602
+ throw new Error(`Unable to resolve module symbol ${modulePath}`);
603
+ }
604
+
605
+ const exportSymbol = checker
606
+ .getExportsOfModule(moduleSymbol)
607
+ .find((symbol) => symbol.getName() === exportName);
608
+
609
+ if (!exportSymbol) {
610
+ throw new Error(`Missing root export ${exportName}`);
611
+ }
612
+
613
+ return resolveSymbol(checker, exportSymbol);
614
+ };
615
+
616
+ const getDiagnosticsForFiles = (program: ts.Program, fileNames: string[]) =>
617
+ ts.getPreEmitDiagnostics(program).filter((diagnostic) => {
618
+ const fileName = diagnostic.file?.fileName;
619
+
620
+ return !!fileName && fileNames.includes(fileName);
621
+ });
622
+
623
+ const getMissingExportNameFromDiagnostic = (diagnostic: ts.Diagnostic) => {
624
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
625
+ const match = message.match(/has no exported member(?: named)? '([^']+)'/);
626
+
627
+ if (!match) {
628
+ throw new Error(`Unexpected TS${diagnostic.code} diagnostic: ${message}`);
629
+ }
630
+
631
+ return match[1];
632
+ };
633
+
634
+ const getCanonicalMissingExportDiagnosticCode = (diagnostic: ts.Diagnostic) => {
635
+ if (
636
+ MISSING_EXPORT_DIAGNOSTIC_CODES.includes(diagnostic.code) &&
637
+ getMissingExportNameFromDiagnostic(diagnostic)
638
+ ) {
639
+ return 2305;
640
+ }
641
+
642
+ return diagnostic.code;
643
+ };
644
+
645
+ const getObjectLiteralInitializer = (symbol: ts.Symbol | undefined, symbolName: string) => {
646
+ const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0];
647
+
648
+ if (!declaration || !ts.isVariableDeclaration(declaration)) {
649
+ throw new Error(`Root export ${symbolName} does not resolve to a variable declaration`);
650
+ }
651
+
652
+ let initializer = declaration.initializer;
653
+ if (!initializer) {
654
+ throw new Error(`Root export ${symbolName} is missing an initializer`);
655
+ }
656
+
657
+ while (ts.isAsExpression(initializer)) {
658
+ initializer = initializer.expression;
659
+ }
660
+
661
+ if (!ts.isObjectLiteralExpression(initializer)) {
662
+ throw new Error(`Root export ${symbolName} does not resolve to an object literal`);
663
+ }
664
+
665
+ return initializer;
666
+ };
667
+
668
+ const getStringLiteralObjectEntries = (objectLiteral: ts.ObjectLiteralExpression) =>
669
+ objectLiteral.properties.map((property) => {
670
+ if (
671
+ !ts.isPropertyAssignment(property) ||
672
+ !ts.isIdentifier(property.name) ||
673
+ !ts.isStringLiteral(property.initializer)
674
+ ) {
675
+ throw new Error('Expected string-literal object property assignment');
676
+ }
677
+
678
+ return [property.name.text, property.initializer.text];
679
+ });
680
+
681
+ const getNamedStringLiteralObjectEntries = (
682
+ objectLiteral: ts.ObjectLiteralExpression,
683
+ propertyNames: string[],
684
+ symbolName: string
685
+ ) =>
686
+ propertyNames.map((propertyName) => {
687
+ const property = objectLiteral.properties.find(
688
+ (candidate): candidate is ts.PropertyAssignment =>
689
+ ts.isPropertyAssignment(candidate) &&
690
+ ts.isIdentifier(candidate.name) &&
691
+ candidate.name.text === propertyName
692
+ );
693
+
694
+ if (!property) {
695
+ throw new Error(`Missing ${symbolName}.${propertyName}`);
696
+ }
697
+
698
+ if (!ts.isStringLiteral(property.initializer)) {
699
+ throw new Error(`Expected ${symbolName}.${propertyName} to be a string literal`);
700
+ }
701
+
702
+ return [propertyName, property.initializer.text];
703
+ });
704
+
705
+ const getObjectLiteralVariableInitializer = (
706
+ sourceFile: ts.SourceFile,
707
+ variableName: string
708
+ ) => {
709
+ let objectLiteral: ts.ObjectLiteralExpression | undefined;
710
+
711
+ const visit = (node: ts.Node) => {
712
+ if (
713
+ ts.isVariableDeclaration(node) &&
714
+ ts.isIdentifier(node.name) &&
715
+ node.name.text === variableName &&
716
+ node.initializer
717
+ ) {
718
+ const initializer = ts.isAsExpression(node.initializer)
719
+ ? node.initializer.expression
720
+ : node.initializer;
721
+
722
+ if (!ts.isObjectLiteralExpression(initializer)) {
723
+ throw new Error(`${variableName} does not resolve to an object literal`);
724
+ }
725
+
726
+ objectLiteral = initializer;
727
+ return;
728
+ }
729
+
730
+ ts.forEachChild(node, visit);
731
+ };
732
+
733
+ visit(sourceFile);
734
+
735
+ if (!objectLiteral) {
736
+ throw new Error(`Missing ${variableName} object literal`);
737
+ }
738
+
739
+ return objectLiteral;
740
+ };
741
+
742
+ const getObjectLiteralPropertyAssignment = (
743
+ objectLiteral: ts.ObjectLiteralExpression,
744
+ propertyName: string,
745
+ symbolName: string
746
+ ) => {
747
+ const property = objectLiteral.properties.find(
748
+ (candidate): candidate is ts.PropertyAssignment =>
749
+ ts.isPropertyAssignment(candidate) &&
750
+ ts.isIdentifier(candidate.name) &&
751
+ candidate.name.text === propertyName
752
+ );
753
+
754
+ if (!property) {
755
+ throw new Error(`Missing ${symbolName}.${propertyName}`);
756
+ }
757
+
758
+ return property;
759
+ };
760
+
761
+ const entriesToObject = (entries: string[][]) =>
762
+ entries.reduce<Record<string, string>>((accumulator, [name, value]) => {
763
+ accumulator[name] = value;
764
+
765
+ return accumulator;
766
+ }, {});
767
+
768
+ const resolveSymbol = (checker: ts.TypeChecker, symbol: ts.Symbol | undefined) => {
769
+ if (!symbol) return undefined;
770
+
771
+ return symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
772
+ };
773
+
774
+ describe('contact-center root public contract', () => {
775
+ it('preserves the literal root barrel export list and omits package internals', () => {
776
+ const program = createProgram();
777
+ const exportNames = getRootExportNames(program);
778
+
779
+ expect(exportNames).toEqual([...EXPECTED_ROOT_EXPORTS].sort());
780
+ INTERNAL_ROOT_EXPORTS.forEach((name) => {
781
+ expect(exportNames).not.toContain(name);
782
+ });
783
+ });
784
+
785
+ it('rejects root imports of package internals with TS2305 diagnostics', () => {
786
+ const internalFixture = path.join(
787
+ CONTACT_CENTER_ROOT,
788
+ 'test/unit/spec/__root_internals.ts'
789
+ );
790
+ const program = createProgram({
791
+ [internalFixture]: createInternalRootImportFixture(),
792
+ });
793
+ const diagnostics = getDiagnosticsForFiles(program, [internalFixture]);
794
+
795
+ expect(diagnostics.map(getCanonicalMissingExportDiagnosticCode)).toEqual(
796
+ INTERNAL_ROOT_EXPORTS.map(() => 2305)
797
+ );
798
+ expect(diagnostics.map(getMissingExportNameFromDiagnostic).sort()).toEqual(
799
+ [...INTERNAL_ROOT_EXPORTS].sort()
800
+ );
801
+ });
802
+
803
+ it('re-exports the exact AI summary error code object from the root symbol', () => {
804
+ const program = createProgram();
805
+ const errorCodeEntries = getStringLiteralObjectEntries(
806
+ getObjectLiteralInitializer(
807
+ getRootExportSymbol(program, 'AI_SUMMARY_ERROR_CODES'),
808
+ 'AI_SUMMARY_ERROR_CODES'
809
+ )
810
+ );
811
+
812
+ expect(errorCodeEntries).toEqual(EXPECTED_AI_SUMMARY_ERROR_CODES);
813
+ });
814
+
815
+ it('pins AI summary wire event literals to the backend contract', () => {
816
+ const program = createProgram();
817
+ const aiAssistantEventNameLiterals = entriesToObject(
818
+ getNamedStringLiteralObjectEntries(
819
+ getObjectLiteralInitializer(
820
+ getModuleExportSymbol(program, TYPES_PATH, 'AIAssistantEventName'),
821
+ 'AIAssistantEventName'
822
+ ),
823
+ Object.keys(EXPECTED_AI_SUMMARY_WIRE_LITERALS.AIAssistantEventName),
824
+ 'AIAssistantEventName'
825
+ )
826
+ );
827
+ const ccAISummaryEventLiterals = entriesToObject(
828
+ getNamedStringLiteralObjectEntries(
829
+ getObjectLiteralInitializer(
830
+ getRootExportSymbol(program, 'CC_TASK_EVENTS'),
831
+ 'CC_TASK_EVENTS'
832
+ ),
833
+ Object.keys(EXPECTED_AI_SUMMARY_WIRE_LITERALS.CC_TASK_EVENTS),
834
+ 'CC_TASK_EVENTS'
835
+ )
836
+ );
837
+
838
+ expect({
839
+ AIAssistantEventName: aiAssistantEventNameLiterals,
840
+ CC_TASK_EVENTS: ccAISummaryEventLiterals,
841
+ }).toEqual(EXPECTED_AI_SUMMARY_WIRE_LITERALS);
842
+ });
843
+
844
+ it('type-checks public AI summary assignments and keeps invalid branches narrow', () => {
845
+ const validFixture = path.join(CONTACT_CENTER_ROOT, 'test/unit/spec/__ai_summary_valid.ts');
846
+ const invalidFixture = path.join(
847
+ CONTACT_CENTER_ROOT,
848
+ 'test/unit/spec/__ai_summary_invalid.ts'
849
+ );
850
+ const program = createProgram({
851
+ [validFixture]: VALID_PUBLIC_CONTRACT_FIXTURE,
852
+ [invalidFixture]: INVALID_PUBLIC_CONTRACT_FIXTURE,
853
+ });
854
+ const fixtureDiagnostics = getDiagnosticsForFiles(program, [validFixture, invalidFixture]);
855
+
856
+ expect(
857
+ fixtureDiagnostics.map((diagnostic) => ({
858
+ code: diagnostic.code,
859
+ message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
860
+ }))
861
+ ).toEqual([]);
862
+ });
863
+
864
+ it('pins retained ITask wrapup, consult, and transfer signatures', () => {
865
+ const legacyTaskContractFixture = path.join(
866
+ CONTACT_CENTER_ROOT,
867
+ 'test/unit/spec/__legacy_task_call_control_contract.ts'
868
+ );
869
+ const program = createProgram({
870
+ [legacyTaskContractFixture]: LEGACY_TASK_CALL_CONTROL_CONTRACT_FIXTURE,
871
+ });
872
+ const fixtureDiagnostics = getDiagnosticsForFiles(program, [legacyTaskContractFixture]);
873
+
874
+ expect(
875
+ fixtureDiagnostics.map((diagnostic) => ({
876
+ code: diagnostic.code,
877
+ message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
878
+ }))
879
+ ).toEqual([]);
880
+ });
881
+ });