@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
@@ -14,9 +14,14 @@ import {
14
14
  isSecondaryEpDnAgent,
15
15
  getConsultMediaResourceId,
16
16
  getIsConsultInProgressForConferenceControls,
17
+ getIsConsultedAgentForControls,
18
+ getServerHoldStateForControls,
19
+ isCampaignPreviewTask,
20
+ isCampaignPreviewReservation,
17
21
  } from '../../../../../src/services/task/TaskUtils';
18
22
  import {ITask, Interaction, TaskData} from '../../../../../src/services/task/types';
19
23
  import {LoginOption} from '../../../../../src/types';
24
+ import {CC_EVENTS} from '../../../../../src/services/config/types';
20
25
 
21
26
  describe('TaskUtils', () => {
22
27
  let mockTask: ITask;
@@ -47,6 +52,73 @@ describe('TaskUtils', () => {
47
52
  } as any;
48
53
  });
49
54
 
55
+ describe('state-control helper branches', () => {
56
+ it('should derive consulted-agent and hold states from task context', () => {
57
+ expect(
58
+ getIsConsultedAgentForControls({isConsulted: true} as TaskData, {} as any, false)
59
+ ).toBe(true);
60
+ expect(
61
+ getIsConsultedAgentForControls(
62
+ {} as TaskData,
63
+ {consultInitiator: false} as any,
64
+ true
65
+ )
66
+ ).toBe(true);
67
+ expect(
68
+ getIsConsultedAgentForControls(
69
+ {} as TaskData,
70
+ {consultInitiator: true} as any,
71
+ true
72
+ )
73
+ ).toBe(false);
74
+
75
+ const taskData = {
76
+ interaction: {
77
+ media: {
78
+ main: {isHold: true},
79
+ fallback: {isHold: false},
80
+ },
81
+ },
82
+ mediaResourceId: 'fallback',
83
+ } as TaskData;
84
+
85
+ expect(getServerHoldStateForControls({taskData} as any, 'main')).toBe(true);
86
+ expect(getServerHoldStateForControls({taskData} as any)).toBe(false);
87
+ expect(getServerHoldStateForControls({} as any)).toBeUndefined();
88
+ });
89
+
90
+ it('should detect campaign preview tasks and reservations', () => {
91
+ expect(
92
+ isCampaignPreviewTask({
93
+ interaction: {
94
+ outboundType: 'STANDARD_PREVIEW_CAMPAIGN',
95
+ callProcessingDetails: {},
96
+ },
97
+ } as TaskData)
98
+ ).toBe(true);
99
+ expect(
100
+ isCampaignPreviewTask({
101
+ interaction: {
102
+ outboundType: 'OUTDIAL',
103
+ callProcessingDetails: {campaignType: 'preview_standard'},
104
+ },
105
+ } as unknown as TaskData)
106
+ ).toBe(true);
107
+ expect(isCampaignPreviewTask({interaction: {callProcessingDetails: {}}} as TaskData)).toBe(
108
+ false
109
+ );
110
+
111
+ expect(
112
+ isCampaignPreviewReservation({
113
+ data: {type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION},
114
+ } as ITask)
115
+ ).toBe(true);
116
+ expect(isCampaignPreviewReservation({data: {type: CC_EVENTS.AGENT_CONTACT}} as ITask)).toBe(
117
+ false
118
+ );
119
+ });
120
+ });
121
+
50
122
  describe('isPrimary', () => {
51
123
  it('should return true when agent is the owner', () => {
52
124
  expect(isPrimary(mockTask, mockAgentId)).toBe(true);
@@ -3,6 +3,7 @@ import LoggerProxy from '../../../../../../src/logger-proxy';
3
3
  import {
4
4
  TaskData,
5
5
  CONSULT_TRANSFER_DESTINATION_TYPE,
6
+ DESTINATION_TYPE,
6
7
  TASK_EVENTS,
7
8
  VOICE_VARIANT,
8
9
  } from '../../../../../../src/services/task/types';
@@ -11,6 +12,10 @@ import {TaskEvent, TaskState} from '../../../../../../src/services/task/state-ma
11
12
  import {computeUIControls} from '../../../../../../src/services/task/state-machine/uiControlsComputer';
12
13
  import * as Utils from '../../../../../../src/services/core/Utils';
13
14
  import {createTaskData} from '../taskTestUtils';
15
+ import {AIAssistantEventName} from '../../../../../../src/types';
16
+ import {AI_SUMMARY_ERROR_CODES} from '../../../../../../src/constants';
17
+ import {AI_SUMMARY_REQUEST_CANCELLED} from '../../../../../../src/services/task/constants';
18
+ import {createAISummaryError, flushMicrotasks} from '../../../../fixtures/aiSummaryTestUtils';
14
19
  import MetricsManager from '../../../../../../src/metrics/MetricsManager';
15
20
  import {METRIC_EVENT_NAMES} from '../../../../../../src/metrics/constants';
16
21
  import * as wxAppDiagnosticLogging from '../../../../../../src/services/wxAppDiagnosticLogging';
@@ -104,9 +109,7 @@ describe('Voice Task', () => {
104
109
  reason: 'CUSTOMER_BUSY',
105
110
  });
106
111
 
107
- const outdialFailedCall = emitSpy.mock.calls.find(
108
- (call) => call[0] === 'task:outdialFailed'
109
- );
112
+ const outdialFailedCall = emitSpy.mock.calls.find((call) => call[0] === 'task:outdialFailed');
110
113
  expect(outdialFailedCall).toBeDefined();
111
114
  expect(outdialFailedCall![1]).toBe('CUSTOMER_BUSY');
112
115
  });
@@ -127,9 +130,7 @@ describe('Voice Task', () => {
127
130
  reason: 'AGENT_ENDS',
128
131
  });
129
132
 
130
- const outdialFailedCall = emitSpy.mock.calls.find(
131
- (call) => call[0] === 'task:outdialFailed'
132
- );
133
+ const outdialFailedCall = emitSpy.mock.calls.find((call) => call[0] === 'task:outdialFailed');
133
134
  expect(outdialFailedCall).toBeUndefined();
134
135
  });
135
136
  });
@@ -211,7 +212,10 @@ describe('Voice Task', () => {
211
212
  }) as any;
212
213
  const voice = new Voice(dummyContact, heldConferenceData, {});
213
214
  primeConnectedState(voice, heldConferenceData);
214
- voice.stateMachineService?.send({type: TaskEvent.CONFERENCE_START, taskData: heldConferenceData});
215
+ voice.stateMachineService?.send({
216
+ type: TaskEvent.CONFERENCE_START,
217
+ taskData: heldConferenceData,
218
+ });
215
219
  expect(voice.stateMachineService?.getSnapshot().value).toBe(TaskState.CONFERENCING);
216
220
 
217
221
  await voice.holdResume();
@@ -598,6 +602,341 @@ describe('Voice Task', () => {
598
602
  });
599
603
  });
600
604
 
605
+ describe('AI summary noninterference for call controls', () => {
606
+ const summaryAgentId = 'summary-agent-1';
607
+ const wrapupPayload = {wrapUpReason: 'Customer resolved', auxCodeId: 'wrapup-code-1'} as const;
608
+ const consultPayload = {
609
+ to: 'consult-agent-1',
610
+ destinationType: DESTINATION_TYPE.AGENT,
611
+ holdParticipants: true,
612
+ } as const;
613
+ const transferPayload = {
614
+ to: 'transfer-agent-1',
615
+ destinationType: DESTINATION_TYPE.AGENT,
616
+ } as const;
617
+ const callControlMethods = [
618
+ 'wrapup',
619
+ 'consult',
620
+ 'blindTransfer',
621
+ 'vteamTransfer',
622
+ 'consultTransfer',
623
+ ] as const;
624
+ type CallControlMethod = (typeof callControlMethods)[number];
625
+ type CallControlCase = {
626
+ label: string;
627
+ createOperationResult: () => unknown;
628
+ expectedContactCalls: Record<CallControlMethod, unknown[][]>;
629
+ prime: (voice: Voice, taskData: TaskData) => void;
630
+ invoke: (voice: Voice) => Promise<unknown>;
631
+ requestSummary: (voice: Voice) => Promise<unknown>;
632
+ summaryEventName: AIAssistantEventName;
633
+ };
634
+
635
+ const createContactMock = (operationResult: any) =>
636
+ ({
637
+ hold: jest.fn().mockResolvedValue('held'),
638
+ unHold: jest.fn().mockResolvedValue('resumed'),
639
+ pauseRecording: jest.fn().mockResolvedValue('paused'),
640
+ resumeRecording: jest.fn().mockResolvedValue('resumedRecording'),
641
+ wrapup: jest.fn().mockResolvedValue(operationResult),
642
+ consult: jest.fn().mockResolvedValue(operationResult),
643
+ blindTransfer: jest.fn().mockResolvedValue(operationResult),
644
+ vteamTransfer: jest.fn().mockResolvedValue(operationResult),
645
+ consultTransfer: jest.fn().mockResolvedValue(operationResult),
646
+ } as any);
647
+
648
+ const createSummaryAdapter = () => {
649
+ let rejectPending: ((error: Error) => void) | undefined;
650
+ const adapter: any = {
651
+ sendEvent: jest.fn().mockResolvedValue(undefined),
652
+ requestAndWaitForRtd: jest.fn(async (options: any) => {
653
+ const result = new Promise((_resolve, reject) => {
654
+ rejectPending = reject;
655
+ });
656
+ await adapter.sendEvent(
657
+ options.agentId,
658
+ options.interactionId,
659
+ options.eventType,
660
+ options.eventName,
661
+ options.eventMetaData,
662
+ undefined,
663
+ undefined,
664
+ Date.now(),
665
+ options.timeout
666
+ );
667
+ return result;
668
+ }),
669
+ clear: jest.fn(() =>
670
+ rejectPending?.(createAISummaryError(AI_SUMMARY_REQUEST_CANCELLED))
671
+ ),
672
+ };
673
+
674
+ return adapter;
675
+ };
676
+
677
+ const injectAISummaryDependencies = (
678
+ voice: Voice,
679
+ adapter = createSummaryAdapter(),
680
+ featureEnablement = {
681
+ interactionId: 'int1',
682
+ postCallEnabled: true,
683
+ midCallEnabled: true,
684
+ }
685
+ ) => {
686
+ adapter.aiFeature = {
687
+ id: 'summary-flags',
688
+ generatedSummaries: {
689
+ wrapUpSummariesEnabled: true,
690
+ consultTransferSummariesEnabled: true,
691
+ },
692
+ };
693
+ (voice as any).apiAIAssistant = adapter;
694
+ voice.setFeatureEnablement(featureEnablement);
695
+
696
+ return {adapter, coordinator: adapter};
697
+ };
698
+
699
+ const normalizeEmitCalls = (emitSpy: jest.SpyInstance, voice: Voice) =>
700
+ emitSpy.mock.calls.map(([eventName, ...args]) => [
701
+ eventName,
702
+ ...args.map((arg) => (arg === voice ? 'VOICE_SELF' : arg)),
703
+ ]);
704
+
705
+ const getContactCalls = (contact: any) =>
706
+ callControlMethods.reduce((calls, method) => {
707
+ calls[method] = contact[method].mock.calls;
708
+
709
+ return calls;
710
+ }, {} as Record<CallControlMethod, unknown[][]>);
711
+
712
+ const createVoiceHarness = (testCase: CallControlCase) => {
713
+ const taskData = createBaseData();
714
+ const operationResult = testCase.createOperationResult();
715
+ const contact = createContactMock(operationResult);
716
+ const voice = new Voice(
717
+ contact,
718
+ taskData,
719
+ {isEndTaskEnabled: true, isEndConsultEnabled: true},
720
+ undefined,
721
+ summaryAgentId
722
+ );
723
+
724
+ testCase.prime(voice, taskData);
725
+ const emitSpy = jest.spyOn(voice, 'emit');
726
+
727
+ return {contact, emitSpy, operationResult, taskData, voice};
728
+ };
729
+
730
+ const captureCallControlOutcome = (
731
+ harness: ReturnType<typeof createVoiceHarness>,
732
+ result: unknown
733
+ ) => {
734
+ const snapshot = harness.voice.stateMachineService?.getSnapshot();
735
+
736
+ return {
737
+ contactCalls: getContactCalls(harness.contact),
738
+ emitCalls: normalizeEmitCalls(harness.emitSpy, harness.voice),
739
+ result,
740
+ state: snapshot?.value,
741
+ context: snapshot?.context,
742
+ uiControls: harness.voice.uiControls,
743
+ };
744
+ };
745
+
746
+ const runCallControl = async (testCase: CallControlCase) => {
747
+ const harness = createVoiceHarness(testCase);
748
+ const result = await testCase.invoke(harness.voice);
749
+
750
+ expect(result).toBe(harness.operationResult);
751
+
752
+ return captureCallControlOutcome(harness, result);
753
+ };
754
+
755
+ const callControlCases: CallControlCase[] = [
756
+ {
757
+ label: 'wrapup',
758
+ createOperationResult: () => ({
759
+ trackingId: 'wrapup-tracking-1',
760
+ data: {wrappedUp: true},
761
+ }),
762
+ expectedContactCalls: {
763
+ wrapup: [[{interactionId: 'int1', data: wrapupPayload}]],
764
+ consult: [],
765
+ blindTransfer: [],
766
+ vteamTransfer: [],
767
+ consultTransfer: [],
768
+ },
769
+ prime: primeConnectedState,
770
+ invoke: (voice: Voice) => voice.wrapup({...wrapupPayload}),
771
+ requestSummary: (voice: Voice) => voice.requestPostCallSummary(),
772
+ summaryEventName: AIAssistantEventName.GET_POST_CALL_SUMMARY,
773
+ },
774
+ {
775
+ label: 'consult',
776
+ createOperationResult: () => ({
777
+ trackingId: 'consult-tracking-1',
778
+ data: createBaseData({
779
+ destinationType: DESTINATION_TYPE.AGENT,
780
+ destAgentId: 'consult-agent-1',
781
+ interaction: {state: 'consulting'} as any,
782
+ }),
783
+ }),
784
+ expectedContactCalls: {
785
+ wrapup: [],
786
+ consult: [[{interactionId: 'int1', data: consultPayload}]],
787
+ blindTransfer: [],
788
+ vteamTransfer: [],
789
+ consultTransfer: [],
790
+ },
791
+ prime: primeConnectedState,
792
+ invoke: (voice: Voice) => voice.consult({...consultPayload}),
793
+ requestSummary: (voice: Voice) => voice.requestMidCallSummary('CONSULT'),
794
+ summaryEventName: AIAssistantEventName.GET_MID_CALL_CONSULT_SUMMARY,
795
+ },
796
+ {
797
+ label: 'transfer',
798
+ createOperationResult: () => ({
799
+ trackingId: 'transfer-tracking-1',
800
+ data: {transferred: true},
801
+ }),
802
+ expectedContactCalls: {
803
+ wrapup: [],
804
+ consult: [],
805
+ blindTransfer: [[{interactionId: 'int1', data: transferPayload}]],
806
+ vteamTransfer: [],
807
+ consultTransfer: [],
808
+ },
809
+ prime: primeConnectedState,
810
+ invoke: (voice: Voice) => voice.transfer({...transferPayload}),
811
+ requestSummary: (voice: Voice) => voice.requestMidCallSummary('TRANSFER'),
812
+ summaryEventName: AIAssistantEventName.GET_MID_CALL_TRANSFER_SUMMARY,
813
+ },
814
+ ];
815
+
816
+ const expectSummaryGetRequest = (
817
+ adapter: ReturnType<typeof createSummaryAdapter>,
818
+ summaryEventName: AIAssistantEventName
819
+ ) => {
820
+ expect(adapter.sendEvent).toHaveBeenCalledTimes(1);
821
+ expect(adapter.sendEvent).toHaveBeenCalledWith(
822
+ summaryAgentId,
823
+ 'int1',
824
+ 'CTI_EVENT',
825
+ summaryEventName,
826
+ expect.objectContaining({
827
+ conversationId: 'int1',
828
+ clientType: 'WxCC',
829
+ }),
830
+ undefined,
831
+ undefined,
832
+ expect.any(Number),
833
+ 15000
834
+ );
835
+ };
836
+
837
+ const expectCancelledSummary = async (summaryRequest: Promise<unknown>) => {
838
+ await expect(summaryRequest).rejects.toMatchObject({
839
+ message: AI_SUMMARY_REQUEST_CANCELLED,
840
+ data: {errorCode: AI_SUMMARY_REQUEST_CANCELLED},
841
+ });
842
+ };
843
+
844
+ const summaryActivityCases = [
845
+ {
846
+ label: 'while the matching AI summary request is pending',
847
+ prepare: async (harness: VoiceHarnessWithCase) => {
848
+ const {adapter, coordinator} = injectAISummaryDependencies(harness.voice);
849
+ const summaryRequest = harness.testCase.requestSummary(harness.voice);
850
+ const summaryObserver = jest.fn();
851
+
852
+ summaryRequest.then(summaryObserver, summaryObserver);
853
+ await flushMicrotasks();
854
+
855
+ expectSummaryGetRequest(adapter, harness.testCase.summaryEventName);
856
+ expect(summaryObserver).not.toHaveBeenCalled();
857
+
858
+ return async () => {
859
+ expect(summaryObserver).not.toHaveBeenCalled();
860
+ coordinator.clear('int1', 'int1');
861
+ await expectCancelledSummary(summaryRequest);
862
+ };
863
+ },
864
+ },
865
+ {
866
+ label: 'after coordinator owner cancellation',
867
+ prepare: async (harness: VoiceHarnessWithCase) => {
868
+ const {adapter, coordinator} = injectAISummaryDependencies(harness.voice);
869
+ const summaryRequest = harness.testCase.requestSummary(harness.voice);
870
+
871
+ await flushMicrotasks();
872
+ expectSummaryGetRequest(adapter, harness.testCase.summaryEventName);
873
+
874
+ coordinator.clear('int1', 'int1');
875
+ await expectCancelledSummary(summaryRequest);
876
+
877
+ return async () => undefined;
878
+ },
879
+ },
880
+ {
881
+ label: 'after adapter rejection',
882
+ prepare: async (harness: VoiceHarnessWithCase) => {
883
+ const adapter = createSummaryAdapter();
884
+ const adapterError = createAISummaryError(
885
+ AI_SUMMARY_ERROR_CODES.AI_ASSISTANT_BASE_URL_NOT_AVAILABLE
886
+ );
887
+
888
+ adapter.sendEvent.mockRejectedValueOnce(adapterError);
889
+ injectAISummaryDependencies(harness.voice, adapter);
890
+
891
+ await expect(harness.testCase.requestSummary(harness.voice)).rejects.toBe(adapterError);
892
+ expectSummaryGetRequest(adapter, harness.testCase.summaryEventName);
893
+
894
+ return async () => undefined;
895
+ },
896
+ },
897
+ ];
898
+
899
+ type VoiceHarnessWithCase = ReturnType<typeof createVoiceHarness> & {
900
+ testCase: CallControlCase;
901
+ };
902
+
903
+ const createVoiceHarnessWithCase = (testCase: CallControlCase): VoiceHarnessWithCase => ({
904
+ ...createVoiceHarness(testCase),
905
+ testCase,
906
+ });
907
+
908
+ beforeEach(() => {
909
+ jest.useFakeTimers();
910
+ });
911
+
912
+ afterEach(() => {
913
+ jest.clearAllTimers();
914
+ jest.useRealTimers();
915
+ });
916
+
917
+ describe.each(summaryActivityCases)('$label', (summaryActivity) => {
918
+ it.each(callControlCases)(
919
+ '$label preserves contact arguments, resolved value, and task events',
920
+ async (testCase) => {
921
+ const baseline = await runCallControl(testCase);
922
+ const harness = createVoiceHarnessWithCase(testCase);
923
+ const finalizeSummaryActivity = await summaryActivity.prepare(harness);
924
+
925
+ try {
926
+ const result = await testCase.invoke(harness.voice);
927
+ const observed = captureCallControlOutcome(harness, result);
928
+
929
+ expect(result).toBe(harness.operationResult);
930
+ expect(observed.contactCalls).toEqual(testCase.expectedContactCalls);
931
+ expect(observed).toEqual(baseline);
932
+ } finally {
933
+ await finalizeSummaryActivity();
934
+ }
935
+ }
936
+ );
937
+ });
938
+ });
939
+
601
940
  describe('exitConference()', () => {
602
941
  const conferenceTaskData = () =>
603
942
  createBaseData({
@@ -951,13 +1290,7 @@ describe('Voice Task', () => {
951
1290
  ['number', 503],
952
1291
  ])('records failure telemetry and preserves a %s rejection', async (_label, failure) => {
953
1292
  dummyContact.dropConferenceParticipant.mockRejectedValueOnce(failure);
954
- const voice = new Voice(
955
- dummyContact,
956
- createBaseData(),
957
- {},
958
- undefined,
959
- 'current-agent-id'
960
- );
1293
+ const voice = new Voice(dummyContact, createBaseData(), {}, undefined, 'current-agent-id');
961
1294
  const metricsManager = MetricsManager.getInstance();
962
1295
  const failureFieldsSpy = jest.spyOn(
963
1296
  MetricsManager,
@@ -967,9 +1300,9 @@ describe('Voice Task', () => {
967
1300
  const trackEventSpy = jest.spyOn(metricsManager, 'trackEvent').mockImplementation();
968
1301
  const errorSpy = jest.spyOn(LoggerProxy, 'error').mockImplementation();
969
1302
 
970
- await expect(
971
- voice.dropConferenceParticipant({participantId: 'participant-id'})
972
- ).rejects.toBe(failure);
1303
+ await expect(voice.dropConferenceParticipant({participantId: 'participant-id'})).rejects.toBe(
1304
+ failure
1305
+ );
973
1306
 
974
1307
  expect(failureFieldsSpy).toHaveBeenCalledWith({});
975
1308
  expect(trackEventSpy).toHaveBeenCalledWith(
@@ -1057,7 +1390,9 @@ describe('Voice Task', () => {
1057
1390
 
1058
1391
  voice.applyWxAppMuteStateFromSync('prefix:call-id-1', true);
1059
1392
 
1060
- expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {muted: true});
1393
+ expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {
1394
+ muted: true,
1395
+ });
1061
1396
  });
1062
1397
 
1063
1398
  it('no-ops when callId does not match active call', () => {