@@ -4,21 +4,31 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
44import { convergeDivergedCallRecordings } from 'src/logic-functions/flows/converge-diverged-call-recordings.util' ;
55
66const getRecallBotMock = vi . hoisted ( ( ) => vi . fn ( ) ) ;
7+ const listRecallTranscriptsMock = vi . hoisted ( ( ) => vi . fn ( ) ) ;
78const createAsyncRecallTranscriptMock = vi . hoisted ( ( ) => vi . fn ( ) ) ;
9+ const downloadTranscriptMock = vi . hoisted ( ( ) => vi . fn ( ) ) ;
810const ingestCallRecordingMediaMock = vi . hoisted ( ( ) => vi . fn ( ) ) ;
911const chargeCompletedCallRecordingMock = vi . hoisted ( ( ) => vi . fn ( ) ) ;
1012
1113vi . mock ( 'src/logic-functions/recall-api/get-recall-bot.util' , ( ) => ( {
1214 getRecallBot : getRecallBotMock ,
1315} ) ) ;
1416
17+ vi . mock ( 'src/logic-functions/recall-api/list-recall-transcripts.util' , ( ) => ( {
18+ listRecallTranscripts : listRecallTranscriptsMock ,
19+ } ) ) ;
20+
1521vi . mock (
1622 'src/logic-functions/recall-api/create-async-recall-transcript.util' ,
1723 ( ) => ( {
1824 createAsyncRecallTranscript : createAsyncRecallTranscriptMock ,
1925 } ) ,
2026) ;
2127
28+ vi . mock ( 'src/logic-functions/flows/download-transcript.util' , ( ) => ( {
29+ downloadTranscript : downloadTranscriptMock ,
30+ } ) ) ;
31+
2232vi . mock ( 'src/logic-functions/flows/ingest-call-recording-media.util' , ( ) => ( {
2333 ingestCallRecordingMedia : ingestCallRecordingMediaMock ,
2434} ) ) ;
@@ -93,11 +103,18 @@ describe('convergeDivergedCallRecordings', () => {
93103 beforeEach ( ( ) => {
94104 vi . spyOn ( console , 'warn' ) . mockImplementation ( ( ) => { } ) ;
95105 getRecallBotMock . mockReset ( ) ;
106+ listRecallTranscriptsMock . mockReset ( ) ;
107+ listRecallTranscriptsMock . mockResolvedValue ( {
108+ ok : true ,
109+ transcripts : [ ] ,
110+ } ) ;
96111 createAsyncRecallTranscriptMock . mockReset ( ) ;
97112 createAsyncRecallTranscriptMock . mockResolvedValue ( {
98113 ok : true ,
99114 transcriptId : 'recall-transcript-1' ,
100115 } ) ;
116+ downloadTranscriptMock . mockReset ( ) ;
117+ downloadTranscriptMock . mockResolvedValue ( { outcome : 'pending' } ) ;
101118 ingestCallRecordingMediaMock . mockReset ( ) ;
102119 ingestCallRecordingMediaMock . mockResolvedValue ( { } ) ;
103120 chargeCompletedCallRecordingMock . mockReset ( ) ;
@@ -138,30 +155,21 @@ describe('convergeDivergedCallRecordings', () => {
138155 hasAudio : false ,
139156 hasVideo : false ,
140157 } ) ;
158+ expect ( listRecallTranscriptsMock ) . toHaveBeenCalledWith ( {
159+ externalRecordingId : 'recall-recording-1' ,
160+ } ) ;
161+ expect ( createAsyncRecallTranscriptMock ) . toHaveBeenCalledWith ( {
162+ externalRecordingId : 'recall-recording-1' ,
163+ callRecordingId : 'call-recording-1' ,
164+ } ) ;
141165 expect ( client . mutations ) . toEqual ( [
142- {
143- id : 'call-recording-1' ,
144- data : {
145- transcript : {
146- recallTranscriptId : 'recall-transcript-1' ,
147- status : 'PENDING' ,
148- requestedAt : NOW . toISOString ( ) ,
149- } ,
150- externalRecordingId : 'recall-recording-1' ,
151- } ,
152- } ,
153166 {
154167 id : 'call-recording-1' ,
155168 data : {
156169 status : 'PROCESSING' ,
157170 startedAt : '2026-06-09T13:02:00.000Z' ,
158171 endedAt : '2026-06-09T14:00:00.000Z' ,
159172 externalRecordingId : 'recall-recording-1' ,
160- transcript : {
161- recallTranscriptId : 'recall-transcript-1' ,
162- status : 'PENDING' ,
163- requestedAt : NOW . toISOString ( ) ,
164- } ,
165173 } ,
166174 } ,
167175 ] ) ;
@@ -170,6 +178,7 @@ describe('convergeDivergedCallRecordings', () => {
170178 candidateCount : 1 ,
171179 updatedCallRecordingIds : [ 'call-recording-1' ] ,
172180 markedFailedCallRecordingIds : [ ] ,
181+ requestedTranscriptCallRecordingIds : [ 'call-recording-1' ] ,
173182 unconvergeableCallRecordingIds : [ ] ,
174183 skippedNotStartedCallRecordingIds : [ ] ,
175184 } ) ;
@@ -211,6 +220,7 @@ describe('convergeDivergedCallRecordings', () => {
211220 } ) ;
212221
213222 expect ( createAsyncRecallTranscriptMock ) . not . toHaveBeenCalled ( ) ;
223+ expect ( listRecallTranscriptsMock ) . not . toHaveBeenCalled ( ) ;
214224 expect ( client . mutations ) . toEqual ( [
215225 {
216226 id : 'call-recording-1' ,
@@ -233,6 +243,7 @@ describe('convergeDivergedCallRecordings', () => {
233243 candidateCount : 1 ,
234244 updatedCallRecordingIds : [ 'call-recording-1' ] ,
235245 markedFailedCallRecordingIds : [ ] ,
246+ requestedTranscriptCallRecordingIds : [ ] ,
236247 unconvergeableCallRecordingIds : [ ] ,
237248 skippedNotStartedCallRecordingIds : [ ] ,
238249 } ) ;
@@ -444,40 +455,223 @@ describe('convergeDivergedCallRecordings', () => {
444455 } ) ,
445456 ] ) ;
446457
447- await convergeDivergedCallRecordings ( {
458+ const result = await convergeDivergedCallRecordings ( {
448459 client : client as unknown as CoreApiClient ,
449460 now : NOW ,
450461 } ) ;
451462
452463 expect ( createAsyncRecallTranscriptMock ) . toHaveBeenCalledTimes ( 1 ) ;
453464 expect ( createAsyncRecallTranscriptMock ) . toHaveBeenCalledWith ( {
454465 externalRecordingId : 'recall-recording-1' ,
466+ callRecordingId : 'call-recording-1' ,
455467 } ) ;
456468 expect ( client . mutations ) . toEqual ( [
457469 {
458470 id : 'call-recording-1' ,
459471 data : {
460- transcript : {
461- recallTranscriptId : 'recall-transcript-1' ,
462- status : 'PENDING' ,
463- requestedAt : NOW . toISOString ( ) ,
464- } ,
465- externalRecordingId : 'recall-recording-1' ,
472+ endedAt : '2026-06-09T14:00:00.000Z' ,
466473 } ,
467474 } ,
475+ ] ) ;
476+ expect ( result . requestedTranscriptCallRecordingIds ) . toEqual ( [
477+ 'call-recording-1' ,
478+ ] ) ;
479+ } ) ;
480+
481+ it ( 'does not create a duplicate transcript when Recall already has one processing' , async ( ) => {
482+ getRecallBotMock . mockResolvedValue ( {
483+ ok : true ,
484+ bot : {
485+ status_changes : [
486+ { code : 'done' , created_at : '2026-06-09T14:05:00.000Z' } ,
487+ ] ,
488+ recordings : [
489+ {
490+ id : 'recall-recording-1' ,
491+ started_at : '2026-06-09T13:02:00.000Z' ,
492+ completed_at : '2026-06-09T14:00:00.000Z' ,
493+ } ,
494+ ] ,
495+ } ,
496+ } ) ;
497+ listRecallTranscriptsMock . mockResolvedValue ( {
498+ ok : true ,
499+ transcripts : [
500+ {
501+ id : 'recall-transcript-1' ,
502+ createdAt : '2026-06-09T14:06:00.000Z' ,
503+ downloadUrl : undefined ,
504+ provider : 'recallai_async' ,
505+ statusCode : 'processing' ,
506+ statusSubCode : undefined ,
507+ } ,
508+ ] ,
509+ } ) ;
510+ const client = buildClient ( [ buildStuckRecordingNode ( ) ] ) ;
511+
512+ const result = await convergeDivergedCallRecordings ( {
513+ client : client as unknown as CoreApiClient ,
514+ now : NOW ,
515+ } ) ;
516+
517+ expect ( createAsyncRecallTranscriptMock ) . not . toHaveBeenCalled ( ) ;
518+ expect ( downloadTranscriptMock ) . not . toHaveBeenCalled ( ) ;
519+ expect ( client . mutations ) . toEqual ( [
468520 {
469521 id : 'call-recording-1' ,
470522 data : {
523+ status : 'PROCESSING' ,
524+ startedAt : '2026-06-09T13:02:00.000Z' ,
471525 endedAt : '2026-06-09T14:00:00.000Z' ,
472526 externalRecordingId : 'recall-recording-1' ,
527+ } ,
528+ } ,
529+ ] ) ;
530+ expect ( result . requestedTranscriptCallRecordingIds ) . toEqual ( [ ] ) ;
531+ } ) ;
532+
533+ it ( 'fills a completed Recall transcript artifact during convergence' , async ( ) => {
534+ const transcriptContent = [
535+ {
536+ participant : { id : 1 , name : 'Ada' } ,
537+ words : [ { text : 'hello' , start_timestamp : 1 , end_timestamp : 2 } ] ,
538+ } ,
539+ ] ;
540+
541+ getRecallBotMock . mockResolvedValue ( {
542+ ok : true ,
543+ bot : {
544+ status_changes : [
545+ { code : 'done' , created_at : '2026-06-09T14:05:00.000Z' } ,
546+ ] ,
547+ recordings : [
548+ {
549+ id : 'recall-recording-1' ,
550+ started_at : '2026-06-09T13:02:00.000Z' ,
551+ completed_at : '2026-06-09T14:00:00.000Z' ,
552+ } ,
553+ ] ,
554+ } ,
555+ } ) ;
556+ listRecallTranscriptsMock . mockResolvedValue ( {
557+ ok : true ,
558+ transcripts : [
559+ {
560+ id : 'recall-transcript-1' ,
561+ createdAt : '2026-06-09T14:06:00.000Z' ,
562+ downloadUrl : 'https://recall-transcripts.example.com/transcript' ,
563+ provider : 'recallai_async' ,
564+ statusCode : 'done' ,
565+ statusSubCode : undefined ,
566+ } ,
567+ ] ,
568+ } ) ;
569+ downloadTranscriptMock . mockResolvedValue ( {
570+ outcome : 'filled' ,
571+ content : transcriptContent ,
572+ } ) ;
573+ const client = buildClient ( [
574+ buildStuckRecordingNode ( {
575+ status : 'PROCESSING' ,
576+ startedAt : '2026-06-09T13:02:00.000Z' ,
577+ endedAt : '2026-06-09T14:00:00.000Z' ,
578+ externalRecordingId : 'recall-recording-1' ,
579+ transcript : {
580+ recallTranscriptId : 'legacy-pending-transcript' ,
581+ status : 'PENDING' ,
582+ requestedAt : '2026-06-09T14:05:30.000Z' ,
583+ } ,
584+ audio : [ { fileId : 'file-audio-1' , label : 'audio.mp3' } ] ,
585+ video : [ { fileId : 'file-video-1' , label : 'video.mp4' } ] ,
586+ } ) ,
587+ ] ) ;
588+
589+ const result = await convergeDivergedCallRecordings ( {
590+ client : client as unknown as CoreApiClient ,
591+ now : NOW ,
592+ } ) ;
593+
594+ expect ( createAsyncRecallTranscriptMock ) . not . toHaveBeenCalled ( ) ;
595+ expect ( downloadTranscriptMock ) . toHaveBeenCalledWith ( {
596+ transcriptId : 'recall-transcript-1' ,
597+ } ) ;
598+ expect ( client . mutations ) . toEqual ( [
599+ {
600+ id : 'call-recording-1' ,
601+ data : { transcript : transcriptContent } ,
602+ } ,
603+ {
604+ id : 'call-recording-1' ,
605+ data : { status : 'COMPLETED' } ,
606+ } ,
607+ ] ) ;
608+ expect ( chargeCompletedCallRecordingMock ) . toHaveBeenCalledWith ( {
609+ callRecordingId : 'call-recording-1' ,
610+ startedAt : '2026-06-09T13:02:00.000Z' ,
611+ endedAt : '2026-06-09T14:00:00.000Z' ,
612+ } ) ;
613+ expect ( result . requestedTranscriptCallRecordingIds ) . toEqual ( [ ] ) ;
614+ } ) ;
615+
616+ it ( 'marks the call recording failed when Recall has a failed transcript artifact' , async ( ) => {
617+ getRecallBotMock . mockResolvedValue ( {
618+ ok : true ,
619+ bot : {
620+ status_changes : [
621+ { code : 'done' , created_at : '2026-06-09T14:05:00.000Z' } ,
622+ ] ,
623+ recordings : [
624+ {
625+ id : 'recall-recording-1' ,
626+ started_at : '2026-06-09T13:02:00.000Z' ,
627+ completed_at : '2026-06-09T14:00:00.000Z' ,
628+ } ,
629+ ] ,
630+ } ,
631+ } ) ;
632+ listRecallTranscriptsMock . mockResolvedValue ( {
633+ ok : true ,
634+ transcripts : [
635+ {
636+ id : 'recall-transcript-1' ,
637+ createdAt : '2026-06-09T14:06:00.000Z' ,
638+ downloadUrl : undefined ,
639+ provider : 'recallai_async' ,
640+ statusCode : 'failed' ,
641+ statusSubCode : 'audio_missing' ,
642+ } ,
643+ ] ,
644+ } ) ;
645+ const client = buildClient ( [
646+ buildStuckRecordingNode ( {
647+ status : 'PROCESSING' ,
648+ startedAt : '2026-06-09T13:02:00.000Z' ,
649+ endedAt : '2026-06-09T14:00:00.000Z' ,
650+ externalRecordingId : 'recall-recording-1' ,
651+ } ) ,
652+ ] ) ;
653+
654+ const result = await convergeDivergedCallRecordings ( {
655+ client : client as unknown as CoreApiClient ,
656+ now : NOW ,
657+ } ) ;
658+
659+ expect ( createAsyncRecallTranscriptMock ) . not . toHaveBeenCalled ( ) ;
660+ expect ( downloadTranscriptMock ) . not . toHaveBeenCalled ( ) ;
661+ expect ( client . mutations ) . toEqual ( [
662+ {
663+ id : 'call-recording-1' ,
664+ data : {
665+ status : 'FAILED_UNKNOWN' ,
473666 transcript : {
474667 recallTranscriptId : 'recall-transcript-1' ,
475- status : 'PENDING ' ,
476- requestedAt : NOW . toISOString ( ) ,
668+ status : 'FAILED ' ,
669+ subCode : 'audio_missing' ,
477670 } ,
478671 } ,
479672 } ,
480673 ] ) ;
674+ expect ( result . requestedTranscriptCallRecordingIds ) . toEqual ( [ ] ) ;
481675 } ) ;
482676
483677 it ( 'does not mutate a record the bot state agrees with' , async ( ) => {
0 commit comments