Skip to content

Commit 298fd55

Browse files
committed
Converge Recall transcript artifacts
1 parent 0eca0b9 commit 298fd55

14 files changed

Lines changed: 852 additions & 741 deletions

packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/domain/build-pending-transcript-marker.util.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/__tests__/converge-diverged-call-recordings.test.ts

Lines changed: 219 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,31 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
44
import { convergeDivergedCallRecordings } from 'src/logic-functions/flows/converge-diverged-call-recordings.util';
55

66
const getRecallBotMock = vi.hoisted(() => vi.fn());
7+
const listRecallTranscriptsMock = vi.hoisted(() => vi.fn());
78
const createAsyncRecallTranscriptMock = vi.hoisted(() => vi.fn());
9+
const downloadTranscriptMock = vi.hoisted(() => vi.fn());
810
const ingestCallRecordingMediaMock = vi.hoisted(() => vi.fn());
911
const chargeCompletedCallRecordingMock = vi.hoisted(() => vi.fn());
1012

1113
vi.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+
1521
vi.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+
2232
vi.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

Comments
 (0)