@twentyhq/call-recorder 1.0.1 → 1.0.2

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 (237) hide show
  1. package/manifest.json +297 -0
  2. package/package.json +2 -2
  3. package/src/front-components/calendar-event-recording.front-component.mjs +269 -0
  4. package/src/front-components/calendar-event-recording.front-component.mjs.map +7 -0
  5. package/src/logic-functions/process-recall-webhook.mjs +1744 -0
  6. package/src/logic-functions/process-recall-webhook.mjs.map +7 -0
  7. package/src/logic-functions/recall-webhook.mjs +391 -0
  8. package/src/logic-functions/recall-webhook.mjs.map +7 -0
  9. package/src/logic-functions/reconcile-call-recorder-calendar-event.mjs +1602 -0
  10. package/src/logic-functions/reconcile-call-recorder-calendar-event.mjs.map +7 -0
  11. package/src/logic-functions/reconcile-stale-bot-state.mjs +2268 -0
  12. package/src/logic-functions/reconcile-stale-bot-state.mjs.map +7 -0
  13. package/.env.example +0 -5
  14. package/.nvmrc +0 -1
  15. package/.oxlintrc.json +0 -20
  16. package/AGENTS.md +0 -67
  17. package/CLAUDE.md +0 -67
  18. package/README.md +0 -24
  19. package/SETUP.md +0 -95
  20. package/src/__tests__/global-setup.ts +0 -100
  21. package/src/__tests__/schema.integration-test.ts +0 -104
  22. package/src/application-config.ts +0 -96
  23. package/src/constants/__tests__/call-recording-field-universal-identifiers.test.ts +0 -19
  24. package/src/constants/app-description.ts +0 -2
  25. package/src/constants/app-display-name.ts +0 -1
  26. package/src/constants/application-universal-identifier.ts +0 -2
  27. package/src/constants/calendar-event-reconciliation-logic-function-universal-identifier.ts +0 -2
  28. package/src/constants/calendar-event-record-page-layout-universal-identifier.ts +0 -2
  29. package/src/constants/calendar-event-recording-front-component-universal-identifier.ts +0 -2
  30. package/src/constants/calendar-event-recording-page-layout-tab-universal-identifier.ts +0 -2
  31. package/src/constants/calendar-event-recording-page-layout-widget-universal-identifier.ts +0 -2
  32. package/src/constants/call-recorder-everyone-left-timeout-seconds-app-variable-universal-identifier.ts +0 -2
  33. package/src/constants/call-recorder-failure-reason-on-call-recording-field-universal-identifier.ts +0 -2
  34. package/src/constants/call-recorder-join-early-minutes-app-variable-universal-identifier.ts +0 -2
  35. package/src/constants/call-recorder-name-app-variable-universal-identifier.ts +0 -2
  36. package/src/constants/call-recorder-noone-joined-timeout-seconds-app-variable-universal-identifier.ts +0 -2
  37. package/src/constants/call-recorder-preference-off-option-id.ts +0 -2
  38. package/src/constants/call-recorder-preference-on-calendar-event-field-universal-identifier.ts +0 -2
  39. package/src/constants/call-recorder-preference-on-calendar-event-view-field-universal-identifier.ts +0 -2
  40. package/src/constants/call-recorder-preference-on-option-id.ts +0 -2
  41. package/src/constants/call-recorder-preference.ts +0 -4
  42. package/src/constants/call-recorder-waiting-room-timeout-seconds-app-variable-universal-identifier.ts +0 -2
  43. package/src/constants/call-recording-audio-field-universal-identifier.ts +0 -2
  44. package/src/constants/call-recording-video-field-universal-identifier.ts +0 -2
  45. package/src/constants/default-role-universal-identifier.ts +0 -2
  46. package/src/constants/process-recall-webhook-logic-function-universal-identifier.ts +0 -2
  47. package/src/constants/recall-webhook-logic-function-universal-identifier.ts +0 -2
  48. package/src/constants/stale-bot-state-logic-function-universal-identifier.ts +0 -2
  49. package/src/default-role.ts +0 -69
  50. package/src/fields/call-recorder-failure-reason-on-call-recording.field.ts +0 -22
  51. package/src/fields/call-recorder-preference-on-calendar-event.field.ts +0 -41
  52. package/src/front-components/calendar-event-recording.front-component.tsx +0 -13
  53. package/src/front-components/components/CalendarEventRecording.tsx +0 -39
  54. package/src/front-components/components/CalendarEventRecordingBody.tsx +0 -96
  55. package/src/front-components/components/CalendarEventRecordingContent.tsx +0 -111
  56. package/src/front-components/components/RecordingTranscript.tsx +0 -92
  57. package/src/front-components/components/RecordingVideoPlayer.tsx +0 -52
  58. package/src/front-components/components/TranscriptEntryList.tsx +0 -61
  59. package/src/front-components/components/TranscriptEntryListItem.tsx +0 -115
  60. package/src/front-components/components/TranscriptErrorBox.tsx +0 -48
  61. package/src/front-components/components/TranscriptSpeakerAvatar.tsx +0 -141
  62. package/src/front-components/components/TranscriptSpeakerChip.tsx +0 -51
  63. package/src/front-components/constants/recording-theme-css-variables.ts +0 -40
  64. package/src/front-components/hooks/use-calendar-event-participants.ts +0 -172
  65. package/src/front-components/hooks/use-calendar-event-recording.ts +0 -155
  66. package/src/front-components/types/calendar-event-participant-by-speaker-name.type.ts +0 -6
  67. package/src/front-components/types/calendar-event-recording-participant.type.ts +0 -7
  68. package/src/front-components/types/transcript-entry.type.ts +0 -13
  69. package/src/front-components/utils/__tests__/find-active-transcript-entry-index.test.ts +0 -66
  70. package/src/front-components/utils/__tests__/format-transcript-timestamp.test.ts +0 -29
  71. package/src/front-components/utils/__tests__/get-speaker-name-match-keys.test.ts +0 -22
  72. package/src/front-components/utils/__tests__/parse-transcript-entries.test.ts +0 -162
  73. package/src/front-components/utils/build-calendar-event-participant-by-speaker-name.util.ts +0 -45
  74. package/src/front-components/utils/find-active-transcript-entry-index.util.ts +0 -77
  75. package/src/front-components/utils/format-transcript-timestamp.util.ts +0 -16
  76. package/src/front-components/utils/get-absolute-avatar-url.util.ts +0 -48
  77. package/src/front-components/utils/get-calendar-event-participant-for-speaker-name.util.ts +0 -24
  78. package/src/front-components/utils/get-speaker-name-match-keys.util.ts +0 -64
  79. package/src/front-components/utils/get-video-file-extension.util.ts +0 -23
  80. package/src/front-components/utils/parse-transcript-entries.util.ts +0 -85
  81. package/src/logic-functions/__tests__/process-recall-webhook.test.ts +0 -62
  82. package/src/logic-functions/__tests__/recall-webhook.test.ts +0 -180
  83. package/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds-env-var-name.ts +0 -2
  84. package/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds.ts +0 -1
  85. package/src/logic-functions/constants/call-recorder-join-early-minutes-env-var-name.ts +0 -2
  86. package/src/logic-functions/constants/call-recorder-name-env-var-name.ts +0 -1
  87. package/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds-env-var-name.ts +0 -2
  88. package/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds.ts +0 -1
  89. package/src/logic-functions/constants/call-recorder-recording-retention-hours-env-var-name.ts +0 -2
  90. package/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds-env-var-name.ts +0 -2
  91. package/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds.ts +0 -1
  92. package/src/logic-functions/constants/call-recording-micro-credits-per-hour.ts +0 -1
  93. package/src/logic-functions/constants/call-recording-request-status.ts +0 -5
  94. package/src/logic-functions/constants/call-recording-status.ts +0 -9
  95. package/src/logic-functions/constants/default-call-recorder-join-early-minutes.ts +0 -1
  96. package/src/logic-functions/constants/default-call-recorder-name.ts +0 -1
  97. package/src/logic-functions/constants/default-call-recorder-recording-retention-hours.ts +0 -2
  98. package/src/logic-functions/constants/default-recall-region.ts +0 -1
  99. package/src/logic-functions/constants/milliseconds-per-minute.ts +0 -1
  100. package/src/logic-functions/constants/non-terminal-call-recording-statuses.ts +0 -8
  101. package/src/logic-functions/constants/recall-api-key-env-var-name.ts +0 -1
  102. package/src/logic-functions/constants/recall-api-max-attempts.ts +0 -1
  103. package/src/logic-functions/constants/recall-api-retry-delay-ms.ts +0 -1
  104. package/src/logic-functions/constants/recall-bot-automatic-leave.ts +0 -74
  105. package/src/logic-functions/constants/recall-bot-everyone-left-min-activate-after-seconds.ts +0 -1
  106. package/src/logic-functions/constants/recall-bot-recording-config.ts +0 -34
  107. package/src/logic-functions/constants/recall-region-env-var-name.ts +0 -1
  108. package/src/logic-functions/constants/recall-webhook-secret-env-var-name.ts +0 -1
  109. package/src/logic-functions/constants/restricted-field-placeholder.ts +0 -3
  110. package/src/logic-functions/constants/stale-bot-state-cron-pattern.ts +0 -1
  111. package/src/logic-functions/constants/twenty-page-size.ts +0 -1
  112. package/src/logic-functions/data/__tests__/complete-call-recording-ingestion.test.ts +0 -55
  113. package/src/logic-functions/data/__tests__/fetch-all-nodes.test.ts +0 -43
  114. package/src/logic-functions/data/__tests__/get-current-workspace-id.test.ts +0 -38
  115. package/src/logic-functions/data/__tests__/strip-restricted-field-value.test.ts +0 -22
  116. package/src/logic-functions/data/complete-call-recording-ingestion.util.ts +0 -24
  117. package/src/logic-functions/data/create-call-recording.util.ts +0 -41
  118. package/src/logic-functions/data/fetch-all-nodes.util.ts +0 -44
  119. package/src/logic-functions/data/fetch-calendar-events-by-filter.util.ts +0 -80
  120. package/src/logic-functions/data/fetch-calendar-events-by-ids.util.ts +0 -20
  121. package/src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util.ts +0 -19
  122. package/src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util.ts +0 -17
  123. package/src/logic-functions/data/find-call-recordings-by-filter.util.ts +0 -102
  124. package/src/logic-functions/data/find-call-recordings-by-ids.util.ts +0 -17
  125. package/src/logic-functions/data/find-open-scheduled-call-recordings.util.ts +0 -14
  126. package/src/logic-functions/data/get-current-workspace-id.util.ts +0 -36
  127. package/src/logic-functions/data/strip-restricted-field-value.util.ts +0 -6
  128. package/src/logic-functions/data/update-call-recording.util.ts +0 -24
  129. package/src/logic-functions/domain/__tests__/build-call-recorder-policy-result.test.ts +0 -47
  130. package/src/logic-functions/domain/__tests__/compute-call-recording-charge.test.ts +0 -71
  131. package/src/logic-functions/domain/__tests__/compute-call-recording-id-for-meeting.test.ts +0 -37
  132. package/src/logic-functions/domain/__tests__/compute-real-meeting-key.test.ts +0 -88
  133. package/src/logic-functions/domain/__tests__/is-call-recording-ingestion-complete.test.ts +0 -59
  134. package/src/logic-functions/domain/__tests__/is-call-recording-status-downgrade.test.ts +0 -37
  135. package/src/logic-functions/domain/__tests__/resolve-call-recorder-policy-result.test.ts +0 -120
  136. package/src/logic-functions/domain/__tests__/should-complete-call-recording-ingestion.test.ts +0 -102
  137. package/src/logic-functions/domain/aggregate-call-recorder-policy-results-by-meeting.util.ts +0 -42
  138. package/src/logic-functions/domain/build-call-recorder-policy-result.util.ts +0 -53
  139. package/src/logic-functions/domain/build-failed-transcript-marker.util.ts +0 -13
  140. package/src/logic-functions/domain/build-pending-transcript-marker.util.ts +0 -13
  141. package/src/logic-functions/domain/build-recall-routing-metadata.util.ts +0 -12
  142. package/src/logic-functions/domain/build-transcript-failure-reason.util.ts +0 -7
  143. package/src/logic-functions/domain/compute-call-recording-charge.util.ts +0 -41
  144. package/src/logic-functions/domain/compute-call-recording-id-for-meeting.util.ts +0 -16
  145. package/src/logic-functions/domain/compute-real-meeting-key.util.ts +0 -48
  146. package/src/logic-functions/domain/compute-recall-bot-join-at.util.ts +0 -34
  147. package/src/logic-functions/domain/is-call-recording-ingestion-complete.util.ts +0 -19
  148. package/src/logic-functions/domain/is-call-recording-status-downgrade.util.ts +0 -37
  149. package/src/logic-functions/domain/is-recall-recording-done-signal.util.ts +0 -13
  150. package/src/logic-functions/domain/map-recall-status-code-to-call-recording-status.util.ts +0 -26
  151. package/src/logic-functions/domain/parse-transcript-marker.util.ts +0 -29
  152. package/src/logic-functions/domain/resolve-call-recorder-policy-result.util.ts +0 -72
  153. package/src/logic-functions/domain/should-complete-call-recording-ingestion.util.ts +0 -32
  154. package/src/logic-functions/flows/__tests__/charge-completed-call-recording.test.ts +0 -45
  155. package/src/logic-functions/flows/__tests__/complete-and-charge-call-recording.test.ts +0 -61
  156. package/src/logic-functions/flows/__tests__/converge-diverged-call-recordings.test.ts +0 -727
  157. package/src/logic-functions/flows/__tests__/download-transcript.test.ts +0 -74
  158. package/src/logic-functions/flows/__tests__/handle-recall-webhook.test.ts +0 -1301
  159. package/src/logic-functions/flows/__tests__/heal-call-recordings-missing-bot.test.ts +0 -225
  160. package/src/logic-functions/flows/__tests__/ingest-call-recording-media.test.ts +0 -153
  161. package/src/logic-functions/flows/__tests__/reap-orphaned-call-recorders.test.ts +0 -425
  162. package/src/logic-functions/flows/__tests__/reconcile-call-recorder.test.ts +0 -1007
  163. package/src/logic-functions/flows/cancel-call-recording-request.util.ts +0 -46
  164. package/src/logic-functions/flows/charge-completed-call-recording.util.ts +0 -31
  165. package/src/logic-functions/flows/complete-and-charge-call-recording.util.ts +0 -29
  166. package/src/logic-functions/flows/converge-diverged-call-recordings-result.type.ts +0 -8
  167. package/src/logic-functions/flows/converge-diverged-call-recordings.util.ts +0 -447
  168. package/src/logic-functions/flows/download-transcript.util.ts +0 -67
  169. package/src/logic-functions/flows/ensure-call-recorder.util.ts +0 -73
  170. package/src/logic-functions/flows/handle-recall-webhook.util.ts +0 -672
  171. package/src/logic-functions/flows/heal-call-recordings-missing-bot.util.ts +0 -82
  172. package/src/logic-functions/flows/ingest-call-recording-media.util.ts +0 -128
  173. package/src/logic-functions/flows/persist-call-recording-progress.util.ts +0 -58
  174. package/src/logic-functions/flows/reap-orphaned-call-recorders.util.ts +0 -183
  175. package/src/logic-functions/flows/reconcile-call-recorder.util.ts +0 -495
  176. package/src/logic-functions/flows/reconcile-call-recording-transcript-artifact-result.type.ts +0 -11
  177. package/src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util.ts +0 -182
  178. package/src/logic-functions/flows/reschedule-call-recording-bot.util.ts +0 -69
  179. package/src/logic-functions/process-recall-webhook.ts +0 -23
  180. package/src/logic-functions/recall-api/__tests__/extract-recall-bot-convergence.test.ts +0 -153
  181. package/src/logic-functions/recall-api/__tests__/extract-recall-media-urls.test.ts +0 -67
  182. package/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts +0 -744
  183. package/src/logic-functions/recall-api/__tests__/verify-recall-webhook-signature.test.ts +0 -122
  184. package/src/logic-functions/recall-api/cancel-recall-bot.util.ts +0 -28
  185. package/src/logic-functions/recall-api/create-async-recall-transcript.util.ts +0 -47
  186. package/src/logic-functions/recall-api/eject-recall-bot.util.ts +0 -28
  187. package/src/logic-functions/recall-api/extract-recall-bot-convergence.util.ts +0 -149
  188. package/src/logic-functions/recall-api/extract-recall-bot-id.util.ts +0 -10
  189. package/src/logic-functions/recall-api/extract-recall-media-urls.util.ts +0 -30
  190. package/src/logic-functions/recall-api/extract-twenty-workspace-id-from-recall-webhook.util.ts +0 -8
  191. package/src/logic-functions/recall-api/get-recall-api-config.util.ts +0 -59
  192. package/src/logic-functions/recall-api/get-recall-bot.util.ts +0 -42
  193. package/src/logic-functions/recall-api/get-recall-recording.util.ts +0 -31
  194. package/src/logic-functions/recall-api/get-recall-webhook-bot-metadata.util.ts +0 -18
  195. package/src/logic-functions/recall-api/list-recall-transcripts.util.ts +0 -141
  196. package/src/logic-functions/recall-api/list-scheduled-recall-bots.util.ts +0 -106
  197. package/src/logic-functions/recall-api/normalize-recall-timestamp.util.ts +0 -14
  198. package/src/logic-functions/recall-api/parse-recall-webhook-event.util.ts +0 -88
  199. package/src/logic-functions/recall-api/recall-bot-api-request.util.ts +0 -165
  200. package/src/logic-functions/recall-api/recall-transcript-summary.type.ts +0 -5
  201. package/src/logic-functions/recall-api/reschedule-recall-bot.util.ts +0 -56
  202. package/src/logic-functions/recall-api/retrieve-recall-transcript.util.ts +0 -71
  203. package/src/logic-functions/recall-api/schedule-recall-bot.util.ts +0 -68
  204. package/src/logic-functions/recall-api/verify-recall-webhook-signature.util.ts +0 -109
  205. package/src/logic-functions/recall-webhook.ts +0 -90
  206. package/src/logic-functions/reconcile-call-recorder-calendar-event.ts +0 -178
  207. package/src/logic-functions/reconcile-stale-bot-state.ts +0 -106
  208. package/src/logic-functions/types/calendar-event-record.type.ts +0 -5
  209. package/src/logic-functions/types/call-recorder-policy-calendar-event-input.type.ts +0 -10
  210. package/src/logic-functions/types/call-recorder-policy-input.type.ts +0 -9
  211. package/src/logic-functions/types/call-recorder-policy-not-required-reason.type.ts +0 -5
  212. package/src/logic-functions/types/call-recorder-policy-required-reason.type.ts +0 -1
  213. package/src/logic-functions/types/call-recorder-policy-result-for-calendar-event.type.ts +0 -9
  214. package/src/logic-functions/types/call-recorder-policy-result-for-meeting.type.ts +0 -6
  215. package/src/logic-functions/types/call-recorder-policy-result.type.ts +0 -12
  216. package/src/logic-functions/types/call-recorder-reconciliation-result.type.ts +0 -16
  217. package/src/logic-functions/types/call-recording-media-file.type.ts +0 -1
  218. package/src/logic-functions/types/call-recording-record.type.ts +0 -15
  219. package/src/logic-functions/types/call-recording-update-fields.type.ts +0 -20
  220. package/src/logic-functions/types/files-field-value.type.ts +0 -1
  221. package/src/logic-functions/types/meeting-recording.type.ts +0 -7
  222. package/src/logic-functions/types/recall-bot-operation-result.type.ts +0 -19
  223. package/src/logic-functions/types/recall-routing-metadata.type.ts +0 -4
  224. package/src/logic-functions/types/removed-call-recorder-occurrence.type.ts +0 -6
  225. package/src/logic-functions/types/transcript-marker.type.ts +0 -6
  226. package/src/logic-functions/utils/as-record.util.ts +0 -6
  227. package/src/logic-functions/utils/get-application-variable-value.util.ts +0 -3
  228. package/src/logic-functions/utils/get-record-at-path.util.ts +0 -10
  229. package/src/logic-functions/utils/get-string.util.ts +0 -4
  230. package/src/logic-functions/utils/get-unique-sorted-ids.util.ts +0 -8
  231. package/src/logic-functions/utils/is-non-empty-string.util.ts +0 -5
  232. package/src/page-layouts/calendar-event-recording-tab.ts +0 -33
  233. package/src/view-fields/call-recorder-preference-on-calendar-event.view-field.ts +0 -27
  234. package/tsconfig.json +0 -42
  235. package/tsconfig.spec.json +0 -9
  236. package/vitest.config.ts +0 -31
  237. package/vitest.unit.config.ts +0 -14
@@ -1,1301 +0,0 @@
1
- import { type CoreApiClient } from 'twenty-client-sdk/core';
2
- import { beforeEach, describe, expect, it, vi } from 'vitest';
3
-
4
- import { handleRecallWebhook } from 'src/logic-functions/flows/handle-recall-webhook.util';
5
-
6
- const WORKSPACE_ID = '123e4567-e89b-12d3-a456-426614174000';
7
-
8
- const buildRecordingDoneWebhookBody = () => ({
9
- event: 'recording.done',
10
- data: {
11
- bot: {
12
- id: 'recall-bot-1',
13
- metadata: {
14
- twentyWorkspaceId: WORKSPACE_ID,
15
- twentyCallRecordingId: 'call-recording-1',
16
- },
17
- },
18
- recording: {
19
- id: 'recall-recording-1',
20
- },
21
- },
22
- });
23
-
24
- const getRecallBotMock = vi.hoisted(() => vi.fn());
25
- const listRecallTranscriptsMock = vi.hoisted(() => vi.fn());
26
- const createAsyncRecallTranscriptMock = vi.hoisted(() => vi.fn());
27
- const retrieveRecallTranscriptMock = vi.hoisted(() => vi.fn());
28
- const ingestCallRecordingMediaMock = vi.hoisted(() => vi.fn());
29
- const chargeCompletedCallRecordingMock = vi.hoisted(() => vi.fn());
30
-
31
- vi.mock('src/logic-functions/recall-api/get-recall-bot.util', () => ({
32
- getRecallBot: getRecallBotMock,
33
- }));
34
-
35
- vi.mock('src/logic-functions/recall-api/list-recall-transcripts.util', () => ({
36
- listRecallTranscripts: listRecallTranscriptsMock,
37
- }));
38
-
39
- vi.mock(
40
- 'src/logic-functions/recall-api/create-async-recall-transcript.util',
41
- () => ({
42
- createAsyncRecallTranscript: createAsyncRecallTranscriptMock,
43
- }),
44
- );
45
-
46
- vi.mock(
47
- 'src/logic-functions/recall-api/retrieve-recall-transcript.util',
48
- () => ({
49
- retrieveRecallTranscript: retrieveRecallTranscriptMock,
50
- }),
51
- );
52
-
53
- vi.mock('src/logic-functions/flows/ingest-call-recording-media.util', () => ({
54
- ingestCallRecordingMedia: ingestCallRecordingMediaMock,
55
- }));
56
-
57
- vi.mock(
58
- 'src/logic-functions/flows/charge-completed-call-recording.util',
59
- () => ({
60
- chargeCompletedCallRecording: chargeCompletedCallRecordingMock,
61
- }),
62
- );
63
-
64
- type CallRecordingNode = {
65
- id: string;
66
- status?: string | null;
67
- externalBotId?: string | null;
68
- externalRecordingId?: string | null;
69
- startedAt?: string | null;
70
- endedAt?: string | null;
71
- transcript?: unknown;
72
- audio?: unknown;
73
- video?: unknown;
74
- };
75
-
76
- class FakeCoreApiClient {
77
- callRecordings: CallRecordingNode[];
78
- mutations: Array<{ id: string; data: Record<string, unknown> }> = [];
79
-
80
- constructor(callRecordings: CallRecordingNode[]) {
81
- this.callRecordings = callRecordings;
82
- }
83
-
84
- async query(query: any): Promise<any> {
85
- if (query.callRecordings !== undefined) {
86
- const filter = query.callRecordings.__args.filter;
87
-
88
- return {
89
- callRecordings: {
90
- edges: this.filterCallRecordings(filter).map((callRecording) => ({
91
- node: callRecording,
92
- })),
93
- },
94
- };
95
- }
96
-
97
- throw new Error(`Unhandled query: ${JSON.stringify(query)}`);
98
- }
99
-
100
- async mutation(mutation: any): Promise<any> {
101
- if (mutation.updateCallRecordings !== undefined) {
102
- const { filter, data } = mutation.updateCallRecordings.__args;
103
- const id = filter.id.eq;
104
-
105
- this.mutations.push({ id, data });
106
-
107
- return { updateCallRecordings: [{ id }] };
108
- }
109
-
110
- if (mutation.updateCallRecording !== undefined) {
111
- const { id, data } = mutation.updateCallRecording.__args;
112
-
113
- this.mutations.push({ id, data });
114
-
115
- return {
116
- updateCallRecording: {
117
- id,
118
- },
119
- };
120
- }
121
-
122
- throw new Error(`Unhandled mutation: ${JSON.stringify(mutation)}`);
123
- }
124
-
125
- private filterCallRecordings(filter: any): CallRecordingNode[] {
126
- if (filter.id?.eq !== undefined) {
127
- return this.callRecordings.filter(
128
- (callRecording) => callRecording.id === filter.id.eq,
129
- );
130
- }
131
-
132
- if (filter.externalBotId?.eq !== undefined) {
133
- return this.callRecordings.filter(
134
- (callRecording) =>
135
- callRecording.externalBotId === filter.externalBotId.eq,
136
- );
137
- }
138
-
139
- throw new Error(
140
- `Unhandled call recording filter: ${JSON.stringify(filter)}`,
141
- );
142
- }
143
- }
144
-
145
- describe('handleRecallWebhook', () => {
146
- beforeEach(() => {
147
- vi.spyOn(console, 'warn').mockImplementation(() => {});
148
- getRecallBotMock.mockReset();
149
- getRecallBotMock.mockResolvedValue({
150
- ok: false,
151
- status: null,
152
- errorMessage: 'bot fetch disabled in test',
153
- });
154
- listRecallTranscriptsMock.mockReset();
155
- listRecallTranscriptsMock.mockResolvedValue({
156
- ok: true,
157
- transcripts: [],
158
- });
159
- createAsyncRecallTranscriptMock.mockReset();
160
- createAsyncRecallTranscriptMock.mockResolvedValue({
161
- ok: false,
162
- status: null,
163
- errorMessage: 'transcript request disabled in test',
164
- });
165
- retrieveRecallTranscriptMock.mockReset();
166
- retrieveRecallTranscriptMock.mockResolvedValue({
167
- ok: false,
168
- status: null,
169
- errorMessage: 'transcript retrieval disabled in test',
170
- });
171
- ingestCallRecordingMediaMock.mockReset();
172
- ingestCallRecordingMediaMock.mockResolvedValue({});
173
- chargeCompletedCallRecordingMock.mockReset();
174
- chargeCompletedCallRecordingMock.mockResolvedValue(undefined);
175
- });
176
-
177
- it('updates a call recording from bot metadata on status change events', async () => {
178
- const client = new FakeCoreApiClient([
179
- {
180
- id: 'call-recording-1',
181
- status: 'JOINING',
182
- externalBotId: 'recall-bot-1',
183
- },
184
- ]);
185
-
186
- const result = await handleRecallWebhook({
187
- client: client as unknown as CoreApiClient,
188
- body: {
189
- event: 'bot.status_change',
190
- data: {
191
- bot: {
192
- id: 'recall-bot-1',
193
- metadata: {
194
- twentyWorkspaceId: WORKSPACE_ID,
195
- twentyCallRecordingId: 'call-recording-1',
196
- },
197
- },
198
- status: {
199
- code: 'in_call_recording',
200
- },
201
- },
202
- },
203
- });
204
-
205
- expect(result).toEqual({
206
- status: 'updated',
207
- event: 'bot.status_change',
208
- callRecordingId: 'call-recording-1',
209
- callRecordingStatus: 'RECORDING',
210
- });
211
- expect(client.mutations).toEqual([
212
- {
213
- id: 'call-recording-1',
214
- data: {
215
- status: 'RECORDING',
216
- externalBotId: 'recall-bot-1',
217
- },
218
- },
219
- ]);
220
- });
221
-
222
- it('reads bot metadata nested under data when a top-level bot has none', async () => {
223
- const client = new FakeCoreApiClient([
224
- {
225
- id: 'call-recording-1',
226
- status: 'JOINING',
227
- externalBotId: 'recall-bot-1',
228
- },
229
- ]);
230
-
231
- const result = await handleRecallWebhook({
232
- client: client as unknown as CoreApiClient,
233
- body: {
234
- event: 'bot.status_change',
235
- bot: {
236
- id: 'recall-bot-1',
237
- },
238
- data: {
239
- bot: {
240
- id: 'recall-bot-1',
241
- metadata: {
242
- twentyWorkspaceId: WORKSPACE_ID,
243
- twentyCallRecordingId: 'call-recording-1',
244
- },
245
- },
246
- status: {
247
- code: 'in_call_recording',
248
- },
249
- },
250
- },
251
- });
252
-
253
- expect(result).toEqual({
254
- status: 'updated',
255
- event: 'bot.status_change',
256
- callRecordingId: 'call-recording-1',
257
- callRecordingStatus: 'RECORDING',
258
- });
259
- });
260
-
261
- it('matches by metadata id when the recording carries no external bot id', async () => {
262
- const client = new FakeCoreApiClient([
263
- {
264
- id: 'call-recording-1',
265
- status: 'SCHEDULED',
266
- externalBotId: null,
267
- },
268
- ]);
269
-
270
- const result = await handleRecallWebhook({
271
- client: client as unknown as CoreApiClient,
272
- body: {
273
- event: 'bot.status_change',
274
- data: {
275
- bot: {
276
- id: 'recall-bot-1',
277
- metadata: {
278
- twentyWorkspaceId: WORKSPACE_ID,
279
- twentyCallRecordingId: 'call-recording-1',
280
- },
281
- },
282
- status: {
283
- code: 'in_call_recording',
284
- },
285
- },
286
- },
287
- });
288
-
289
- expect(result).toEqual({
290
- status: 'updated',
291
- event: 'bot.status_change',
292
- callRecordingId: 'call-recording-1',
293
- callRecordingStatus: 'RECORDING',
294
- });
295
- expect(client.mutations).toEqual([
296
- {
297
- id: 'call-recording-1',
298
- data: {
299
- status: 'RECORDING',
300
- externalBotId: 'recall-bot-1',
301
- },
302
- },
303
- ]);
304
- });
305
-
306
- it('prefers the metadata id over a different recording carrying the bot id', async () => {
307
- const client = new FakeCoreApiClient([
308
- {
309
- id: 'call-recording-stale',
310
- status: 'SCHEDULED',
311
- externalBotId: 'recall-bot-1',
312
- },
313
- {
314
- id: 'call-recording-current',
315
- status: 'SCHEDULED',
316
- externalBotId: null,
317
- },
318
- ]);
319
-
320
- const result = await handleRecallWebhook({
321
- client: client as unknown as CoreApiClient,
322
- body: {
323
- event: 'bot.status_change',
324
- data: {
325
- bot: {
326
- id: 'recall-bot-1',
327
- metadata: {
328
- twentyWorkspaceId: WORKSPACE_ID,
329
- twentyCallRecordingId: 'call-recording-current',
330
- },
331
- },
332
- status: {
333
- code: 'in_call_recording',
334
- },
335
- },
336
- },
337
- });
338
-
339
- expect(result).toEqual({
340
- status: 'updated',
341
- event: 'bot.status_change',
342
- callRecordingId: 'call-recording-current',
343
- callRecordingStatus: 'RECORDING',
344
- });
345
- expect(client.mutations).toEqual([
346
- {
347
- id: 'call-recording-current',
348
- data: {
349
- status: 'RECORDING',
350
- externalBotId: 'recall-bot-1',
351
- },
352
- },
353
- ]);
354
- });
355
-
356
- it('falls back to external bot id matching when call recording metadata is absent', async () => {
357
- const client = new FakeCoreApiClient([
358
- {
359
- id: 'call-recording-1',
360
- status: 'PROCESSING',
361
- externalBotId: 'recall-bot-1',
362
- },
363
- ]);
364
-
365
- const result = await handleRecallWebhook({
366
- client: client as unknown as CoreApiClient,
367
- body: {
368
- event: 'recording.done',
369
- data: {
370
- bot: {
371
- id: 'recall-bot-1',
372
- metadata: {
373
- twentyWorkspaceId: WORKSPACE_ID,
374
- },
375
- },
376
- recording: {
377
- id: 'recall-recording-1',
378
- },
379
- },
380
- },
381
- });
382
-
383
- expect(result).toEqual({
384
- status: 'updated',
385
- event: 'recording.done',
386
- callRecordingId: 'call-recording-1',
387
- callRecordingStatus: 'PROCESSING',
388
- });
389
- expect(client.mutations).toEqual([
390
- {
391
- id: 'call-recording-1',
392
- data: {
393
- status: 'PROCESSING',
394
- externalBotId: 'recall-bot-1',
395
- externalRecordingId: 'recall-recording-1',
396
- },
397
- },
398
- ]);
399
- });
400
-
401
- it('fills startedAt from the status timestamp when the bot starts recording', async () => {
402
- const client = new FakeCoreApiClient([
403
- {
404
- id: 'call-recording-1',
405
- status: 'JOINING',
406
- externalBotId: 'recall-bot-1',
407
- },
408
- ]);
409
-
410
- const result = await handleRecallWebhook({
411
- client: client as unknown as CoreApiClient,
412
- body: {
413
- event: 'bot.status_change',
414
- data: {
415
- bot: {
416
- id: 'recall-bot-1',
417
- metadata: {
418
- twentyWorkspaceId: WORKSPACE_ID,
419
- twentyCallRecordingId: 'call-recording-1',
420
- },
421
- },
422
- status: {
423
- code: 'in_call_recording',
424
- created_at: '2026-01-01T13:02:00.000Z',
425
- },
426
- },
427
- },
428
- });
429
-
430
- expect(result).toEqual({
431
- status: 'updated',
432
- event: 'bot.status_change',
433
- callRecordingId: 'call-recording-1',
434
- callRecordingStatus: 'RECORDING',
435
- });
436
- expect(client.mutations).toEqual([
437
- {
438
- id: 'call-recording-1',
439
- data: {
440
- status: 'RECORDING',
441
- externalBotId: 'recall-bot-1',
442
- startedAt: '2026-01-01T13:02:00.000Z',
443
- },
444
- },
445
- ]);
446
- });
447
-
448
- it('fills endedAt from the status timestamp when the recording is done', async () => {
449
- const client = new FakeCoreApiClient([
450
- {
451
- id: 'call-recording-1',
452
- status: 'PROCESSING',
453
- externalBotId: 'recall-bot-1',
454
- startedAt: '2026-01-01T13:02:00.000Z',
455
- },
456
- ]);
457
-
458
- const result = await handleRecallWebhook({
459
- client: client as unknown as CoreApiClient,
460
- body: {
461
- event: 'bot.status_change',
462
- data: {
463
- bot: {
464
- id: 'recall-bot-1',
465
- metadata: {
466
- twentyWorkspaceId: WORKSPACE_ID,
467
- twentyCallRecordingId: 'call-recording-1',
468
- },
469
- },
470
- status: {
471
- code: 'done',
472
- created_at: '2026-01-01T14:05:00.000Z',
473
- },
474
- },
475
- },
476
- });
477
-
478
- expect(result).toEqual({
479
- status: 'updated',
480
- event: 'bot.status_change',
481
- callRecordingId: 'call-recording-1',
482
- callRecordingStatus: 'PROCESSING',
483
- });
484
- expect(client.mutations).toEqual([
485
- {
486
- id: 'call-recording-1',
487
- data: {
488
- status: 'PROCESSING',
489
- externalBotId: 'recall-bot-1',
490
- endedAt: '2026-01-01T14:05:00.000Z',
491
- },
492
- },
493
- ]);
494
- });
495
-
496
- it('normalizes microsecond-precision Recall timestamps before writing them', async () => {
497
- const client = new FakeCoreApiClient([
498
- {
499
- id: 'call-recording-1',
500
- status: 'PROCESSING',
501
- externalBotId: 'recall-bot-1',
502
- startedAt: '2026-06-10T11:02:00.000Z',
503
- },
504
- ]);
505
-
506
- await handleRecallWebhook({
507
- client: client as unknown as CoreApiClient,
508
- body: {
509
- event: 'bot.status_change',
510
- data: {
511
- bot: {
512
- id: 'recall-bot-1',
513
- metadata: {
514
- twentyWorkspaceId: WORKSPACE_ID,
515
- twentyCallRecordingId: 'call-recording-1',
516
- },
517
- },
518
- status: {
519
- code: 'done',
520
- created_at: '2026-06-10T12:17:28.281597+00:00',
521
- },
522
- },
523
- },
524
- });
525
-
526
- expect(client.mutations).toEqual([
527
- {
528
- id: 'call-recording-1',
529
- data: {
530
- status: 'PROCESSING',
531
- externalBotId: 'recall-bot-1',
532
- endedAt: '2026-06-10T12:17:28.281Z',
533
- },
534
- },
535
- ]);
536
- });
537
-
538
- it('does not overwrite an already-set startedAt on a redelivered recording event', async () => {
539
- const client = new FakeCoreApiClient([
540
- {
541
- id: 'call-recording-1',
542
- status: 'RECORDING',
543
- externalBotId: 'recall-bot-1',
544
- startedAt: '2026-01-01T13:02:00.000Z',
545
- },
546
- ]);
547
-
548
- await handleRecallWebhook({
549
- client: client as unknown as CoreApiClient,
550
- body: {
551
- event: 'bot.status_change',
552
- data: {
553
- bot: {
554
- id: 'recall-bot-1',
555
- metadata: {
556
- twentyWorkspaceId: WORKSPACE_ID,
557
- twentyCallRecordingId: 'call-recording-1',
558
- },
559
- },
560
- status: {
561
- code: 'in_call_recording',
562
- created_at: '2026-01-01T13:09:00.000Z',
563
- },
564
- },
565
- },
566
- });
567
-
568
- expect(client.mutations).toEqual([
569
- {
570
- id: 'call-recording-1',
571
- data: {
572
- status: 'RECORDING',
573
- externalBotId: 'recall-bot-1',
574
- },
575
- },
576
- ]);
577
- });
578
-
579
- it('does not overwrite an already-set endedAt on a redelivered done event', async () => {
580
- const client = new FakeCoreApiClient([
581
- {
582
- id: 'call-recording-1',
583
- status: 'PROCESSING',
584
- externalBotId: 'recall-bot-1',
585
- startedAt: '2026-01-01T13:02:00.000Z',
586
- endedAt: '2026-01-01T14:05:00.000Z',
587
- transcript: {
588
- recallTranscriptId: 'recall-transcript-1',
589
- status: 'PENDING',
590
- requestedAt: '2026-01-01T14:06:00.000Z',
591
- },
592
- },
593
- ]);
594
-
595
- await handleRecallWebhook({
596
- client: client as unknown as CoreApiClient,
597
- body: {
598
- event: 'bot.status_change',
599
- data: {
600
- bot: {
601
- id: 'recall-bot-1',
602
- metadata: {
603
- twentyWorkspaceId: WORKSPACE_ID,
604
- twentyCallRecordingId: 'call-recording-1',
605
- },
606
- },
607
- status: {
608
- code: 'done',
609
- created_at: '2026-01-01T14:11:00.000Z',
610
- },
611
- },
612
- },
613
- });
614
-
615
- expect(client.mutations).toEqual([
616
- {
617
- id: 'call-recording-1',
618
- data: {
619
- status: 'PROCESSING',
620
- externalBotId: 'recall-bot-1',
621
- },
622
- },
623
- ]);
624
- });
625
-
626
- it('skips a late done event once the recording is COMPLETED', async () => {
627
- const client = new FakeCoreApiClient([
628
- {
629
- id: 'call-recording-1',
630
- status: 'COMPLETED',
631
- externalBotId: 'recall-bot-1',
632
- startedAt: '2026-01-01T13:02:00.000Z',
633
- endedAt: '2026-01-01T14:05:00.000Z',
634
- },
635
- ]);
636
-
637
- const result = await handleRecallWebhook({
638
- client: client as unknown as CoreApiClient,
639
- body: {
640
- event: 'bot.status_change',
641
- data: {
642
- bot: {
643
- id: 'recall-bot-1',
644
- metadata: {
645
- twentyWorkspaceId: WORKSPACE_ID,
646
- twentyCallRecordingId: 'call-recording-1',
647
- },
648
- },
649
- status: {
650
- code: 'done',
651
- created_at: '2026-01-01T14:11:00.000Z',
652
- },
653
- },
654
- },
655
- });
656
-
657
- expect(result).toEqual({
658
- status: 'skipped',
659
- event: 'bot.status_change',
660
- reason: 'stale status event (COMPLETED -> PROCESSING)',
661
- });
662
- expect(client.mutations).toEqual([]);
663
- });
664
-
665
- it('skips out-of-order events that would move the status backwards', async () => {
666
- const client = new FakeCoreApiClient([
667
- {
668
- id: 'call-recording-1',
669
- status: 'COMPLETED',
670
- externalBotId: 'recall-bot-1',
671
- },
672
- ]);
673
-
674
- const result = await handleRecallWebhook({
675
- client: client as unknown as CoreApiClient,
676
- body: {
677
- event: 'bot.status_change',
678
- data: {
679
- bot: {
680
- id: 'recall-bot-1',
681
- metadata: {
682
- twentyWorkspaceId: WORKSPACE_ID,
683
- twentyCallRecordingId: 'call-recording-1',
684
- },
685
- },
686
- status: {
687
- code: 'in_call_recording',
688
- },
689
- },
690
- },
691
- });
692
-
693
- expect(result).toEqual({
694
- status: 'skipped',
695
- event: 'bot.status_change',
696
- reason: 'stale status event (COMPLETED -> RECORDING)',
697
- });
698
- expect(client.mutations).toEqual([]);
699
- });
700
-
701
- it('skips events whose metadata points at a missing call recording', async () => {
702
- const client = new FakeCoreApiClient([]);
703
-
704
- const result = await handleRecallWebhook({
705
- client: client as unknown as CoreApiClient,
706
- body: {
707
- event: 'bot.status_change',
708
- data: {
709
- bot: {
710
- metadata: {
711
- twentyWorkspaceId: WORKSPACE_ID,
712
- twentyCallRecordingId: 'call-recording-deleted',
713
- },
714
- },
715
- status: {
716
- code: 'in_call_recording',
717
- },
718
- },
719
- },
720
- });
721
-
722
- expect(result).toEqual({
723
- status: 'skipped',
724
- event: 'bot.status_change',
725
- reason: 'no matching call recording',
726
- });
727
- expect(client.mutations).toEqual([]);
728
- });
729
-
730
- it('skips unsupported events', async () => {
731
- const client = new FakeCoreApiClient([]);
732
-
733
- const result = await handleRecallWebhook({
734
- client: client as unknown as CoreApiClient,
735
- body: {
736
- event: 'participant_events.done',
737
- data: {},
738
- },
739
- });
740
-
741
- expect(result).toEqual({
742
- status: 'skipped',
743
- event: 'participant_events.done',
744
- reason: 'unsupported Recall event status participant_events.done',
745
- });
746
- expect(client.mutations).toEqual([]);
747
- });
748
-
749
- it('requests a transcript once when the recording first completes', async () => {
750
- createAsyncRecallTranscriptMock.mockResolvedValue({
751
- ok: true,
752
- transcriptId: 'recall-transcript-1',
753
- });
754
- const client = new FakeCoreApiClient([
755
- {
756
- id: 'call-recording-1',
757
- status: 'PROCESSING',
758
- externalBotId: 'recall-bot-1',
759
- transcript: null,
760
- },
761
- ]);
762
-
763
- await handleRecallWebhook({
764
- client: client as unknown as CoreApiClient,
765
- body: buildRecordingDoneWebhookBody(),
766
- });
767
-
768
- expect(createAsyncRecallTranscriptMock).toHaveBeenCalledTimes(1);
769
- expect(createAsyncRecallTranscriptMock).toHaveBeenCalledWith({
770
- externalRecordingId: 'recall-recording-1',
771
- });
772
- expect(client.mutations).toEqual([
773
- {
774
- id: 'call-recording-1',
775
- data: {
776
- status: 'PROCESSING',
777
- externalBotId: 'recall-bot-1',
778
- externalRecordingId: 'recall-recording-1',
779
- transcript: {
780
- recallTranscriptId: 'recall-transcript-1',
781
- status: 'PENDING',
782
- requestedAt: expect.any(String),
783
- },
784
- },
785
- },
786
- ]);
787
- });
788
-
789
- it('does not re-request a transcript on a redelivered done event while Recall list is stale', async () => {
790
- const client = new FakeCoreApiClient([
791
- {
792
- id: 'call-recording-1',
793
- status: 'PROCESSING',
794
- externalBotId: 'recall-bot-1',
795
- externalRecordingId: 'recall-recording-1',
796
- transcript: {
797
- recallTranscriptId: 'recall-transcript-1',
798
- status: 'PENDING',
799
- requestedAt: '2026-01-01T14:06:00.000Z',
800
- },
801
- },
802
- ]);
803
-
804
- await handleRecallWebhook({
805
- client: client as unknown as CoreApiClient,
806
- body: buildRecordingDoneWebhookBody(),
807
- });
808
-
809
- expect(createAsyncRecallTranscriptMock).not.toHaveBeenCalled();
810
- expect(listRecallTranscriptsMock).toHaveBeenCalledWith({
811
- externalRecordingId: 'recall-recording-1',
812
- });
813
- expect(retrieveRecallTranscriptMock).toHaveBeenCalledWith({
814
- transcriptId: 'recall-transcript-1',
815
- });
816
- expect(client.mutations).toEqual([
817
- {
818
- id: 'call-recording-1',
819
- data: {
820
- status: 'PROCESSING',
821
- externalBotId: 'recall-bot-1',
822
- externalRecordingId: 'recall-recording-1',
823
- },
824
- },
825
- ]);
826
- });
827
-
828
- it('resolves the recording id from the bot when the payload and record lack one', async () => {
829
- getRecallBotMock.mockResolvedValue({
830
- ok: true,
831
- bot: {
832
- recordings: [{ id: 'recall-recording-9' }],
833
- },
834
- });
835
- createAsyncRecallTranscriptMock.mockResolvedValue({
836
- ok: true,
837
- transcriptId: 'recall-transcript-9',
838
- });
839
- const client = new FakeCoreApiClient([
840
- {
841
- id: 'call-recording-1',
842
- status: 'PROCESSING',
843
- externalBotId: 'recall-bot-1',
844
- transcript: null,
845
- },
846
- ]);
847
-
848
- await handleRecallWebhook({
849
- client: client as unknown as CoreApiClient,
850
- body: {
851
- event: 'bot.status_change',
852
- data: {
853
- bot: {
854
- id: 'recall-bot-1',
855
- metadata: {
856
- twentyWorkspaceId: WORKSPACE_ID,
857
- twentyCallRecordingId: 'call-recording-1',
858
- },
859
- },
860
- status: {
861
- code: 'done',
862
- },
863
- },
864
- },
865
- });
866
-
867
- expect(getRecallBotMock).toHaveBeenCalledWith({
868
- externalBotId: 'recall-bot-1',
869
- });
870
- expect(createAsyncRecallTranscriptMock).toHaveBeenCalledWith({
871
- externalRecordingId: 'recall-recording-9',
872
- });
873
- expect(client.mutations).toEqual([
874
- expect.objectContaining({
875
- id: 'call-recording-1',
876
- data: expect.objectContaining({
877
- status: 'PROCESSING',
878
- externalBotId: 'recall-bot-1',
879
- externalRecordingId: 'recall-recording-9',
880
- }),
881
- }),
882
- ]);
883
- });
884
-
885
- it('ingests media on recording.done and completes once all artifacts are present', async () => {
886
- getRecallBotMock.mockResolvedValue({
887
- ok: true,
888
- bot: { id: 'recall-bot-1' },
889
- });
890
- ingestCallRecordingMediaMock.mockResolvedValue({
891
- audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }],
892
- video: [{ fileId: 'file-video-1', label: 'video.mp4' }],
893
- });
894
- const client = new FakeCoreApiClient([
895
- {
896
- id: 'call-recording-1',
897
- status: 'PROCESSING',
898
- externalBotId: 'recall-bot-1',
899
- externalRecordingId: 'recall-recording-1',
900
- startedAt: '2026-01-01T13:02:00.000Z',
901
- endedAt: '2026-01-01T14:05:00.000Z',
902
- transcript: [{ participant: { id: 1 }, words: [] }],
903
- },
904
- ]);
905
-
906
- await handleRecallWebhook({
907
- client: client as unknown as CoreApiClient,
908
- body: buildRecordingDoneWebhookBody(),
909
- });
910
-
911
- expect(ingestCallRecordingMediaMock).toHaveBeenCalledWith({
912
- callRecordingId: 'call-recording-1',
913
- externalRecordingId: 'recall-recording-1',
914
- hasAudio: false,
915
- hasVideo: false,
916
- });
917
- expect(client.mutations).toEqual([
918
- {
919
- id: 'call-recording-1',
920
- data: {
921
- externalBotId: 'recall-bot-1',
922
- externalRecordingId: 'recall-recording-1',
923
- audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }],
924
- video: [{ fileId: 'file-video-1', label: 'video.mp4' }],
925
- },
926
- },
927
- {
928
- id: 'call-recording-1',
929
- data: { status: 'COMPLETED' },
930
- },
931
- ]);
932
- expect(chargeCompletedCallRecordingMock).toHaveBeenCalledWith({
933
- callRecordingId: 'call-recording-1',
934
- startedAt: '2026-01-01T13:02:00.000Z',
935
- endedAt: '2026-01-01T14:05:00.000Z',
936
- });
937
- });
938
-
939
- it('stays PROCESSING on recording.done while artifacts are missing', async () => {
940
- getRecallBotMock.mockResolvedValue({
941
- ok: true,
942
- bot: { id: 'recall-bot-1' },
943
- });
944
- ingestCallRecordingMediaMock.mockResolvedValue({
945
- audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }],
946
- });
947
- createAsyncRecallTranscriptMock.mockResolvedValue({
948
- ok: true,
949
- transcriptId: 'recall-transcript-1',
950
- });
951
- const client = new FakeCoreApiClient([
952
- {
953
- id: 'call-recording-1',
954
- status: 'PROCESSING',
955
- externalBotId: 'recall-bot-1',
956
- startedAt: '2026-01-01T13:02:00.000Z',
957
- endedAt: '2026-01-01T14:05:00.000Z',
958
- transcript: null,
959
- },
960
- ]);
961
-
962
- await handleRecallWebhook({
963
- client: client as unknown as CoreApiClient,
964
- body: buildRecordingDoneWebhookBody(),
965
- });
966
-
967
- expect(createAsyncRecallTranscriptMock).toHaveBeenCalledWith({
968
- externalRecordingId: 'recall-recording-1',
969
- });
970
- expect(client.mutations).toEqual([
971
- expect.objectContaining({
972
- id: 'call-recording-1',
973
- data: expect.objectContaining({
974
- status: 'PROCESSING',
975
- externalBotId: 'recall-bot-1',
976
- externalRecordingId: 'recall-recording-1',
977
- audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }],
978
- }),
979
- }),
980
- ]);
981
- expect(chargeCompletedCallRecordingMock).not.toHaveBeenCalled();
982
- });
983
-
984
- it('marks FAILED on recording.done when no recording artifact path exists', async () => {
985
- getRecallBotMock.mockResolvedValue({
986
- ok: true,
987
- bot: { id: 'recall-bot-1', recordings: [] },
988
- });
989
- const client = new FakeCoreApiClient([
990
- {
991
- id: 'call-recording-1',
992
- status: 'PROCESSING',
993
- externalBotId: 'recall-bot-1',
994
- startedAt: '2026-01-01T13:02:00.000Z',
995
- endedAt: '2026-01-01T14:05:00.000Z',
996
- transcript: null,
997
- },
998
- ]);
999
-
1000
- const result = await handleRecallWebhook({
1001
- client: client as unknown as CoreApiClient,
1002
- body: {
1003
- event: 'recording.done',
1004
- data: {
1005
- bot: {
1006
- id: 'recall-bot-1',
1007
- metadata: {
1008
- twentyWorkspaceId: WORKSPACE_ID,
1009
- },
1010
- },
1011
- },
1012
- },
1013
- });
1014
-
1015
- expect(result).toEqual({
1016
- status: 'updated',
1017
- event: 'recording.done',
1018
- callRecordingId: 'call-recording-1',
1019
- callRecordingStatus: 'FAILED',
1020
- });
1021
- expect(client.mutations).toEqual([
1022
- {
1023
- id: 'call-recording-1',
1024
- data: {
1025
- status: 'FAILED',
1026
- externalBotId: 'recall-bot-1',
1027
- callRecorderFailureReason: 'recording_artifacts_unavailable',
1028
- },
1029
- },
1030
- ]);
1031
- expect(chargeCompletedCallRecordingMock).not.toHaveBeenCalled();
1032
- });
1033
-
1034
- it('completes and charges on transcript.done when media is already ingested', async () => {
1035
- const transcriptContent = [
1036
- {
1037
- participant: { id: 1, name: 'Alice' },
1038
- words: [{ text: 'hello', start_timestamp: { relative: 0.5 } }],
1039
- },
1040
- ];
1041
-
1042
- retrieveRecallTranscriptMock.mockResolvedValue({
1043
- ok: true,
1044
- transcript: {
1045
- downloadUrl: 'https://recall-transcripts.example.com/transcript-1',
1046
- statusCode: 'done',
1047
- statusSubCode: null,
1048
- },
1049
- });
1050
- vi.stubGlobal(
1051
- 'fetch',
1052
- vi.fn().mockResolvedValue({
1053
- ok: true,
1054
- json: async () => transcriptContent,
1055
- }),
1056
- );
1057
-
1058
- const client = new FakeCoreApiClient([
1059
- {
1060
- id: 'call-recording-1',
1061
- status: 'PROCESSING',
1062
- externalBotId: 'recall-bot-1',
1063
- externalRecordingId: 'recall-recording-1',
1064
- startedAt: '2026-01-01T13:02:00.000Z',
1065
- endedAt: '2026-01-01T14:05:00.000Z',
1066
- transcript: {
1067
- recallTranscriptId: 'recall-transcript-1',
1068
- status: 'PENDING',
1069
- requestedAt: '2026-01-01T14:06:00.000Z',
1070
- },
1071
- audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }],
1072
- video: [{ fileId: 'file-video-1', label: 'video.mp4' }],
1073
- },
1074
- ]);
1075
-
1076
- const result = await handleRecallWebhook({
1077
- client: client as unknown as CoreApiClient,
1078
- body: {
1079
- event: 'transcript.done',
1080
- data: {
1081
- bot: {
1082
- id: 'recall-bot-1',
1083
- metadata: {
1084
- twentyWorkspaceId: WORKSPACE_ID,
1085
- twentyCallRecordingId: 'call-recording-1',
1086
- },
1087
- },
1088
- transcript: {
1089
- id: 'recall-transcript-1',
1090
- },
1091
- },
1092
- },
1093
- });
1094
-
1095
- expect(result).toEqual({
1096
- status: 'updated',
1097
- event: 'transcript.done',
1098
- callRecordingId: 'call-recording-1',
1099
- transcriptOutcome: 'FILLED',
1100
- });
1101
- expect(client.mutations).toEqual([
1102
- {
1103
- id: 'call-recording-1',
1104
- data: { transcript: transcriptContent },
1105
- },
1106
- {
1107
- id: 'call-recording-1',
1108
- data: { status: 'COMPLETED' },
1109
- },
1110
- ]);
1111
- expect(chargeCompletedCallRecordingMock).toHaveBeenCalledWith({
1112
- callRecordingId: 'call-recording-1',
1113
- startedAt: '2026-01-01T13:02:00.000Z',
1114
- endedAt: '2026-01-01T14:05:00.000Z',
1115
- });
1116
-
1117
- vi.unstubAllGlobals();
1118
- });
1119
-
1120
- it('fills the transcript from the download URL on transcript.done', async () => {
1121
- const transcriptContent = [
1122
- {
1123
- participant: { id: 1, name: 'Alice' },
1124
- words: [{ text: 'hello', start_timestamp: { relative: 0.5 } }],
1125
- },
1126
- ];
1127
-
1128
- retrieveRecallTranscriptMock.mockResolvedValue({
1129
- ok: true,
1130
- transcript: {
1131
- downloadUrl: 'https://recall-transcripts.example.com/transcript-1',
1132
- statusCode: 'done',
1133
- statusSubCode: null,
1134
- },
1135
- });
1136
- vi.stubGlobal(
1137
- 'fetch',
1138
- vi.fn().mockResolvedValue({
1139
- ok: true,
1140
- json: async () => transcriptContent,
1141
- }),
1142
- );
1143
-
1144
- const client = new FakeCoreApiClient([
1145
- {
1146
- id: 'call-recording-1',
1147
- status: 'COMPLETED',
1148
- externalBotId: 'recall-bot-1',
1149
- transcript: {
1150
- recallTranscriptId: 'recall-transcript-1',
1151
- status: 'PENDING',
1152
- requestedAt: '2026-01-01T14:06:00.000Z',
1153
- },
1154
- },
1155
- ]);
1156
-
1157
- const result = await handleRecallWebhook({
1158
- client: client as unknown as CoreApiClient,
1159
- body: {
1160
- event: 'transcript.done',
1161
- data: {
1162
- bot: {
1163
- id: 'recall-bot-1',
1164
- metadata: {
1165
- twentyWorkspaceId: WORKSPACE_ID,
1166
- twentyCallRecordingId: 'call-recording-1',
1167
- },
1168
- },
1169
- transcript: {
1170
- id: 'recall-transcript-1',
1171
- },
1172
- recording: {
1173
- id: 'recall-recording-1',
1174
- },
1175
- },
1176
- },
1177
- });
1178
-
1179
- expect(result).toEqual({
1180
- status: 'updated',
1181
- event: 'transcript.done',
1182
- callRecordingId: 'call-recording-1',
1183
- transcriptOutcome: 'FILLED',
1184
- });
1185
- expect(retrieveRecallTranscriptMock).toHaveBeenCalledWith({
1186
- transcriptId: 'recall-transcript-1',
1187
- });
1188
- expect(client.mutations).toEqual([
1189
- {
1190
- id: 'call-recording-1',
1191
- data: {
1192
- transcript: transcriptContent,
1193
- externalRecordingId: 'recall-recording-1',
1194
- },
1195
- },
1196
- ]);
1197
- expect(chargeCompletedCallRecordingMock).not.toHaveBeenCalled();
1198
-
1199
- vi.unstubAllGlobals();
1200
- });
1201
-
1202
- it('writes a FAILED marker on transcript.failed', async () => {
1203
- const client = new FakeCoreApiClient([
1204
- {
1205
- id: 'call-recording-1',
1206
- status: 'PROCESSING',
1207
- externalBotId: 'recall-bot-1',
1208
- externalRecordingId: 'recall-recording-1',
1209
- transcript: {
1210
- recallTranscriptId: 'recall-transcript-1',
1211
- status: 'PENDING',
1212
- requestedAt: '2026-01-01T14:06:00.000Z',
1213
- },
1214
- },
1215
- ]);
1216
-
1217
- const result = await handleRecallWebhook({
1218
- client: client as unknown as CoreApiClient,
1219
- body: {
1220
- event: 'transcript.failed',
1221
- data: {
1222
- bot: {
1223
- id: 'recall-bot-1',
1224
- metadata: {
1225
- twentyWorkspaceId: WORKSPACE_ID,
1226
- twentyCallRecordingId: 'call-recording-1',
1227
- },
1228
- },
1229
- transcript: {
1230
- id: 'recall-transcript-1',
1231
- },
1232
- status: {
1233
- sub_code: 'transcription_failed',
1234
- },
1235
- },
1236
- },
1237
- });
1238
-
1239
- expect(result).toEqual({
1240
- status: 'updated',
1241
- event: 'transcript.failed',
1242
- callRecordingId: 'call-recording-1',
1243
- transcriptOutcome: 'FAILED',
1244
- });
1245
- expect(client.mutations).toEqual([
1246
- {
1247
- id: 'call-recording-1',
1248
- data: {
1249
- transcript: {
1250
- recallTranscriptId: 'recall-transcript-1',
1251
- status: 'FAILED',
1252
- subCode: 'transcription_failed',
1253
- },
1254
- callRecorderFailureReason: 'transcript_failed:transcription_failed',
1255
- status: 'FAILED',
1256
- },
1257
- },
1258
- ]);
1259
- expect(console.warn).toHaveBeenCalled();
1260
- });
1261
-
1262
- it('does not clobber a downloaded transcript with a late transcript.failed', async () => {
1263
- const client = new FakeCoreApiClient([
1264
- {
1265
- id: 'call-recording-1',
1266
- status: 'COMPLETED',
1267
- externalBotId: 'recall-bot-1',
1268
- transcript: [{ participant: { id: 1 }, words: [] }],
1269
- },
1270
- ]);
1271
-
1272
- const result = await handleRecallWebhook({
1273
- client: client as unknown as CoreApiClient,
1274
- body: {
1275
- event: 'transcript.failed',
1276
- data: {
1277
- bot: {
1278
- id: 'recall-bot-1',
1279
- metadata: {
1280
- twentyWorkspaceId: WORKSPACE_ID,
1281
- twentyCallRecordingId: 'call-recording-1',
1282
- },
1283
- },
1284
- transcript: {
1285
- id: 'recall-transcript-1',
1286
- },
1287
- status: {
1288
- sub_code: 'transcription_failed',
1289
- },
1290
- },
1291
- },
1292
- });
1293
-
1294
- expect(result).toEqual({
1295
- status: 'skipped',
1296
- event: 'transcript.failed',
1297
- reason: 'transcript already filled',
1298
- });
1299
- expect(client.mutations).toEqual([]);
1300
- });
1301
- });