@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,727 +0,0 @@
1
- import { type CoreApiClient } from 'twenty-client-sdk/core';
2
- import { beforeEach, describe, expect, it, vi } from 'vitest';
3
-
4
- import { convergeDivergedCallRecordings } from 'src/logic-functions/flows/converge-diverged-call-recordings.util';
5
-
6
- const getRecallBotMock = vi.hoisted(() => vi.fn());
7
- const listRecallTranscriptsMock = vi.hoisted(() => vi.fn());
8
- const createAsyncRecallTranscriptMock = vi.hoisted(() => vi.fn());
9
- const downloadTranscriptMock = vi.hoisted(() => vi.fn());
10
- const ingestCallRecordingMediaMock = vi.hoisted(() => vi.fn());
11
- const chargeCompletedCallRecordingMock = vi.hoisted(() => vi.fn());
12
-
13
- vi.mock('src/logic-functions/recall-api/get-recall-bot.util', () => ({
14
- getRecallBot: getRecallBotMock,
15
- }));
16
-
17
- vi.mock('src/logic-functions/recall-api/list-recall-transcripts.util', () => ({
18
- listRecallTranscripts: listRecallTranscriptsMock,
19
- }));
20
-
21
- vi.mock(
22
- 'src/logic-functions/recall-api/create-async-recall-transcript.util',
23
- () => ({
24
- createAsyncRecallTranscript: createAsyncRecallTranscriptMock,
25
- }),
26
- );
27
-
28
- vi.mock('src/logic-functions/flows/download-transcript.util', () => ({
29
- downloadTranscript: downloadTranscriptMock,
30
- }));
31
-
32
- vi.mock('src/logic-functions/flows/ingest-call-recording-media.util', () => ({
33
- ingestCallRecordingMedia: ingestCallRecordingMediaMock,
34
- }));
35
-
36
- vi.mock(
37
- 'src/logic-functions/flows/charge-completed-call-recording.util',
38
- () => ({
39
- chargeCompletedCallRecording: chargeCompletedCallRecordingMock,
40
- }),
41
- );
42
-
43
- const NOW = new Date('2026-06-10T12:00:00.000Z');
44
-
45
- type CallRecordingNode = Record<string, unknown>;
46
-
47
- class FakeCoreApiClient {
48
- mutations: Array<{ id: string; data: Record<string, unknown> }> = [];
49
-
50
- constructor(private callRecordingNodes: CallRecordingNode[]) {}
51
-
52
- async query(_query: any): Promise<any> {
53
- return {
54
- callRecordings: {
55
- pageInfo: { hasNextPage: false, endCursor: undefined },
56
- edges: this.callRecordingNodes.map((node) => ({ node })),
57
- },
58
- };
59
- }
60
-
61
- async mutation(mutation: any): Promise<any> {
62
- if (mutation.updateCallRecordings !== undefined) {
63
- const { filter, data } = mutation.updateCallRecordings.__args;
64
- const id = filter.id.eq;
65
-
66
- this.mutations.push({ id, data });
67
-
68
- return { updateCallRecordings: [{ id }] };
69
- }
70
-
71
- const { id, data } = mutation.updateCallRecording.__args;
72
-
73
- this.mutations.push({ id, data });
74
-
75
- return { updateCallRecording: { id } };
76
- }
77
- }
78
-
79
- const buildClient = (callRecordingNodes: CallRecordingNode[]) =>
80
- new FakeCoreApiClient(callRecordingNodes);
81
-
82
- const buildStuckRecordingNode = (
83
- overrides: CallRecordingNode = {},
84
- ): CallRecordingNode => ({
85
- id: 'call-recording-1',
86
- status: 'RECORDING',
87
- startedAt: null,
88
- endedAt: null,
89
- externalBotId: 'recall-bot-1',
90
- externalRecordingId: null,
91
- transcript: null,
92
- audio: null,
93
- video: null,
94
- createdAt: '2026-06-09T12:00:00.000Z',
95
- calendarEvent: {
96
- startsAt: '2026-06-09T12:00:00.000Z',
97
- endsAt: '2026-06-09T13:00:00.000Z',
98
- },
99
- ...overrides,
100
- });
101
-
102
- describe('convergeDivergedCallRecordings', () => {
103
- beforeEach(() => {
104
- vi.spyOn(console, 'warn').mockImplementation(() => {});
105
- getRecallBotMock.mockReset();
106
- listRecallTranscriptsMock.mockReset();
107
- listRecallTranscriptsMock.mockResolvedValue({
108
- ok: true,
109
- transcripts: [],
110
- });
111
- createAsyncRecallTranscriptMock.mockReset();
112
- createAsyncRecallTranscriptMock.mockResolvedValue({
113
- ok: true,
114
- transcriptId: 'recall-transcript-1',
115
- });
116
- downloadTranscriptMock.mockReset();
117
- downloadTranscriptMock.mockResolvedValue({ outcome: 'pending' });
118
- ingestCallRecordingMediaMock.mockReset();
119
- ingestCallRecordingMediaMock.mockResolvedValue({});
120
- chargeCompletedCallRecordingMock.mockReset();
121
- chargeCompletedCallRecordingMock.mockResolvedValue(undefined);
122
- });
123
-
124
- it('heals a stuck RECORDING record from the Recall bot state', async () => {
125
- getRecallBotMock.mockResolvedValue({
126
- ok: true,
127
- bot: {
128
- status_changes: [
129
- { code: 'in_call_recording', created_at: '2026-06-09T13:02:30.000Z' },
130
- { code: 'call_ended', created_at: '2026-06-09T14:00:30.000Z' },
131
- { code: 'done', created_at: '2026-06-09T14:05:00.000Z' },
132
- ],
133
- recordings: [
134
- {
135
- id: 'recall-recording-1',
136
- started_at: '2026-06-09T13:02:00.000Z',
137
- completed_at: '2026-06-09T14:00:00.000Z',
138
- },
139
- ],
140
- },
141
- });
142
- const client = buildClient([buildStuckRecordingNode()]);
143
-
144
- const result = await convergeDivergedCallRecordings({
145
- client: client as unknown as CoreApiClient,
146
- now: NOW,
147
- });
148
-
149
- expect(getRecallBotMock).toHaveBeenCalledWith({
150
- externalBotId: 'recall-bot-1',
151
- });
152
- expect(ingestCallRecordingMediaMock).toHaveBeenCalledWith({
153
- callRecordingId: 'call-recording-1',
154
- externalRecordingId: 'recall-recording-1',
155
- hasAudio: false,
156
- hasVideo: false,
157
- });
158
- expect(listRecallTranscriptsMock).toHaveBeenCalledWith({
159
- externalRecordingId: 'recall-recording-1',
160
- });
161
- expect(createAsyncRecallTranscriptMock).toHaveBeenCalledWith({
162
- externalRecordingId: 'recall-recording-1',
163
- });
164
- expect(client.mutations).toEqual([
165
- expect.objectContaining({
166
- id: 'call-recording-1',
167
- data: expect.objectContaining({
168
- status: 'PROCESSING',
169
- startedAt: '2026-06-09T13:02:00.000Z',
170
- endedAt: '2026-06-09T14:00:00.000Z',
171
- externalRecordingId: 'recall-recording-1',
172
- }),
173
- }),
174
- ]);
175
- expect(chargeCompletedCallRecordingMock).not.toHaveBeenCalled();
176
- expect(result).toEqual({
177
- candidateCount: 1,
178
- updatedCallRecordingIds: ['call-recording-1'],
179
- markedFailedCallRecordingIds: [],
180
- requestedTranscriptCallRecordingIds: ['call-recording-1'],
181
- unconvergeableCallRecordingIds: [],
182
- skippedNotStartedCallRecordingIds: [],
183
- });
184
- });
185
-
186
- it('marks FAILED when Recall is done but has no recording artifact path', async () => {
187
- getRecallBotMock.mockResolvedValue({
188
- ok: true,
189
- bot: {
190
- status_changes: [
191
- { code: 'done', created_at: '2026-06-09T14:05:00.000Z' },
192
- ],
193
- recordings: [],
194
- },
195
- });
196
- const client = buildClient([buildStuckRecordingNode()]);
197
-
198
- const result = await convergeDivergedCallRecordings({
199
- client: client as unknown as CoreApiClient,
200
- now: NOW,
201
- });
202
-
203
- expect(listRecallTranscriptsMock).not.toHaveBeenCalled();
204
- expect(ingestCallRecordingMediaMock).not.toHaveBeenCalled();
205
- expect(client.mutations).toEqual([
206
- {
207
- id: 'call-recording-1',
208
- data: {
209
- status: 'FAILED',
210
- callRecorderFailureReason: 'recording_artifacts_unavailable',
211
- },
212
- },
213
- ]);
214
- expect(result.updatedCallRecordingIds).toEqual(['call-recording-1']);
215
- });
216
-
217
- it('completes and charges when convergence lands the last artifact', async () => {
218
- getRecallBotMock.mockResolvedValue({
219
- ok: true,
220
- bot: {
221
- status_changes: [
222
- { code: 'done', created_at: '2026-06-09T14:05:00.000Z' },
223
- ],
224
- recordings: [
225
- {
226
- id: 'recall-recording-1',
227
- started_at: '2026-06-09T13:02:00.000Z',
228
- completed_at: '2026-06-09T14:00:00.000Z',
229
- },
230
- ],
231
- },
232
- });
233
- ingestCallRecordingMediaMock.mockResolvedValue({
234
- audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }],
235
- video: [{ fileId: 'file-video-1', label: 'video.mp4' }],
236
- });
237
- const client = buildClient([
238
- buildStuckRecordingNode({
239
- status: 'PROCESSING',
240
- startedAt: '2026-06-09T13:02:00.000Z',
241
- endedAt: '2026-06-09T14:00:00.000Z',
242
- externalRecordingId: 'recall-recording-1',
243
- transcript: [{ participant: { id: 1 }, words: [] }],
244
- }),
245
- ]);
246
-
247
- const result = await convergeDivergedCallRecordings({
248
- client: client as unknown as CoreApiClient,
249
- now: NOW,
250
- });
251
-
252
- expect(createAsyncRecallTranscriptMock).not.toHaveBeenCalled();
253
- expect(listRecallTranscriptsMock).not.toHaveBeenCalled();
254
- expect(client.mutations).toEqual([
255
- {
256
- id: 'call-recording-1',
257
- data: {
258
- audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }],
259
- video: [{ fileId: 'file-video-1', label: 'video.mp4' }],
260
- },
261
- },
262
- {
263
- id: 'call-recording-1',
264
- data: { status: 'COMPLETED' },
265
- },
266
- ]);
267
- expect(chargeCompletedCallRecordingMock).toHaveBeenCalledWith({
268
- callRecordingId: 'call-recording-1',
269
- startedAt: '2026-06-09T13:02:00.000Z',
270
- endedAt: '2026-06-09T14:00:00.000Z',
271
- });
272
- expect(result).toEqual({
273
- candidateCount: 1,
274
- updatedCallRecordingIds: ['call-recording-1'],
275
- markedFailedCallRecordingIds: [],
276
- requestedTranscriptCallRecordingIds: [],
277
- unconvergeableCallRecordingIds: [],
278
- skippedNotStartedCallRecordingIds: [],
279
- });
280
- });
281
-
282
- it('skips records whose meeting has not started yet', async () => {
283
- const client = buildClient([
284
- buildStuckRecordingNode({
285
- calendarEvent: {
286
- startsAt: '2026-06-10T12:30:00.000Z',
287
- endsAt: '2026-06-10T13:30:00.000Z',
288
- },
289
- }),
290
- ]);
291
-
292
- const result = await convergeDivergedCallRecordings({
293
- client: client as unknown as CoreApiClient,
294
- now: NOW,
295
- });
296
-
297
- expect(getRecallBotMock).not.toHaveBeenCalled();
298
- expect(client.mutations).toEqual([]);
299
- expect(result.skippedNotStartedCallRecordingIds).toEqual([
300
- 'call-recording-1',
301
- ]);
302
- });
303
-
304
- it('converges a meeting that ended early while its scheduled end is still in the future', async () => {
305
- getRecallBotMock.mockResolvedValue({
306
- ok: true,
307
- bot: {
308
- status_changes: [
309
- { code: 'done', created_at: '2026-06-10T11:30:00.000Z' },
310
- ],
311
- recordings: [
312
- {
313
- id: 'recall-recording-1',
314
- started_at: '2026-06-10T11:05:00.000Z',
315
- completed_at: '2026-06-10T11:25:00.000Z',
316
- },
317
- ],
318
- },
319
- });
320
- const client = buildClient([
321
- buildStuckRecordingNode({
322
- calendarEvent: {
323
- startsAt: '2026-06-10T11:00:00.000Z',
324
- endsAt: '2026-06-10T13:00:00.000Z',
325
- },
326
- }),
327
- ]);
328
-
329
- const result = await convergeDivergedCallRecordings({
330
- client: client as unknown as CoreApiClient,
331
- now: NOW,
332
- });
333
-
334
- expect(getRecallBotMock).toHaveBeenCalledWith({
335
- externalBotId: 'recall-bot-1',
336
- });
337
- expect(result.updatedCallRecordingIds).toEqual(['call-recording-1']);
338
- expect(result.skippedNotStartedCallRecordingIds).toEqual([]);
339
- });
340
-
341
- it('marks FAILED without clearing the bot id when Recall returns 404', async () => {
342
- getRecallBotMock.mockResolvedValue({
343
- ok: false,
344
- status: 404,
345
- errorMessage: 'Recall API responded with HTTP 404',
346
- });
347
- const client = buildClient([buildStuckRecordingNode()]);
348
-
349
- const result = await convergeDivergedCallRecordings({
350
- client: client as unknown as CoreApiClient,
351
- now: NOW,
352
- });
353
-
354
- expect(client.mutations).toEqual([
355
- {
356
- id: 'call-recording-1',
357
- data: {
358
- status: 'FAILED',
359
- callRecorderFailureReason: 'recall_bot_not_found',
360
- },
361
- },
362
- ]);
363
- expect(result.markedFailedCallRecordingIds).toEqual(['call-recording-1']);
364
- expect(console.warn).toHaveBeenCalled();
365
- });
366
-
367
- it('does not downgrade a COMPLETED record when its bot 404s', async () => {
368
- getRecallBotMock.mockResolvedValue({
369
- ok: false,
370
- status: 404,
371
- errorMessage: 'Recall API responded with HTTP 404',
372
- });
373
- const client = buildClient([
374
- buildStuckRecordingNode({
375
- status: 'COMPLETED',
376
- startedAt: '2026-06-09T13:02:00.000Z',
377
- transcript: [{ participant: { id: 1 }, words: [] }],
378
- }),
379
- ]);
380
-
381
- const result = await convergeDivergedCallRecordings({
382
- client: client as unknown as CoreApiClient,
383
- now: NOW,
384
- });
385
-
386
- expect(client.mutations).toEqual([]);
387
- expect(result.unconvergeableCallRecordingIds).toEqual(['call-recording-1']);
388
- });
389
-
390
- it('logs candidates whose meeting ended before the lookback bound instead of converging them', async () => {
391
- const client = buildClient([
392
- buildStuckRecordingNode({
393
- calendarEvent: { endsAt: '2026-06-01T13:00:00.000Z' },
394
- }),
395
- ]);
396
-
397
- const result = await convergeDivergedCallRecordings({
398
- client: client as unknown as CoreApiClient,
399
- now: NOW,
400
- });
401
-
402
- expect(getRecallBotMock).not.toHaveBeenCalled();
403
- expect(client.mutations).toEqual([]);
404
- expect(result.unconvergeableCallRecordingIds).toEqual(['call-recording-1']);
405
- expect(console.warn).toHaveBeenCalled();
406
- });
407
-
408
- it('converges candidates created long before a recently ended meeting', async () => {
409
- getRecallBotMock.mockResolvedValue({
410
- ok: true,
411
- bot: {
412
- status_changes: [
413
- { code: 'in_call_recording', created_at: '2026-06-09T13:02:00.000Z' },
414
- ],
415
- },
416
- });
417
- const client = buildClient([
418
- buildStuckRecordingNode({
419
- createdAt: '2026-06-01T12:00:00.000Z',
420
- startedAt: '2026-06-09T13:02:00.000Z',
421
- }),
422
- ]);
423
-
424
- const result = await convergeDivergedCallRecordings({
425
- client: client as unknown as CoreApiClient,
426
- now: NOW,
427
- });
428
-
429
- expect(getRecallBotMock).toHaveBeenCalledWith({
430
- externalBotId: 'recall-bot-1',
431
- });
432
- expect(result.unconvergeableCallRecordingIds).toEqual([]);
433
- });
434
-
435
- it('applies the downgrade guard to pulled statuses while still filling timestamps', async () => {
436
- getRecallBotMock.mockResolvedValue({
437
- ok: true,
438
- bot: {
439
- status_changes: [
440
- { code: 'in_call_recording', created_at: '2026-06-09T13:02:00.000Z' },
441
- ],
442
- recordings: [
443
- { id: 'recall-recording-1', started_at: '2026-06-09T13:02:00.000Z' },
444
- ],
445
- },
446
- });
447
- const client = buildClient([
448
- buildStuckRecordingNode({ status: 'PROCESSING' }),
449
- ]);
450
-
451
- await convergeDivergedCallRecordings({
452
- client: client as unknown as CoreApiClient,
453
- now: NOW,
454
- });
455
-
456
- expect(client.mutations).toEqual([
457
- {
458
- id: 'call-recording-1',
459
- data: {
460
- startedAt: '2026-06-09T13:02:00.000Z',
461
- externalRecordingId: 'recall-recording-1',
462
- },
463
- },
464
- ]);
465
- });
466
-
467
- it('requests a transcript for a COMPLETED candidate that has none', async () => {
468
- getRecallBotMock.mockResolvedValue({
469
- ok: true,
470
- bot: {
471
- status_changes: [
472
- { code: 'done', created_at: '2026-06-09T14:05:00.000Z' },
473
- ],
474
- recordings: [
475
- {
476
- id: 'recall-recording-1',
477
- started_at: '2026-06-09T13:02:00.000Z',
478
- completed_at: '2026-06-09T14:00:00.000Z',
479
- },
480
- ],
481
- },
482
- });
483
- const client = buildClient([
484
- buildStuckRecordingNode({
485
- status: 'COMPLETED',
486
- startedAt: '2026-06-09T13:02:00.000Z',
487
- externalRecordingId: 'recall-recording-1',
488
- }),
489
- ]);
490
-
491
- const result = await convergeDivergedCallRecordings({
492
- client: client as unknown as CoreApiClient,
493
- now: NOW,
494
- });
495
-
496
- expect(createAsyncRecallTranscriptMock).toHaveBeenCalledTimes(1);
497
- expect(createAsyncRecallTranscriptMock).toHaveBeenCalledWith({
498
- externalRecordingId: 'recall-recording-1',
499
- });
500
- expect(client.mutations).toEqual([
501
- {
502
- id: 'call-recording-1',
503
- data: {
504
- endedAt: '2026-06-09T14:00:00.000Z',
505
- transcript: {
506
- recallTranscriptId: 'recall-transcript-1',
507
- status: 'PENDING',
508
- requestedAt: NOW.toISOString(),
509
- },
510
- },
511
- },
512
- ]);
513
- expect(result.requestedTranscriptCallRecordingIds).toEqual([
514
- 'call-recording-1',
515
- ]);
516
- });
517
-
518
- it('does not create a duplicate transcript when Recall already has one processing', async () => {
519
- getRecallBotMock.mockResolvedValue({
520
- ok: true,
521
- bot: {
522
- status_changes: [
523
- { code: 'done', created_at: '2026-06-09T14:05:00.000Z' },
524
- ],
525
- recordings: [
526
- {
527
- id: 'recall-recording-1',
528
- started_at: '2026-06-09T13:02:00.000Z',
529
- completed_at: '2026-06-09T14:00:00.000Z',
530
- },
531
- ],
532
- },
533
- });
534
- listRecallTranscriptsMock.mockResolvedValue({
535
- ok: true,
536
- transcripts: [
537
- {
538
- id: 'recall-transcript-1',
539
- statusCode: 'processing',
540
- statusSubCode: undefined,
541
- },
542
- ],
543
- });
544
- const client = buildClient([buildStuckRecordingNode()]);
545
-
546
- const result = await convergeDivergedCallRecordings({
547
- client: client as unknown as CoreApiClient,
548
- now: NOW,
549
- });
550
-
551
- expect(createAsyncRecallTranscriptMock).not.toHaveBeenCalled();
552
- expect(downloadTranscriptMock).not.toHaveBeenCalled();
553
- expect(client.mutations).toEqual([
554
- {
555
- id: 'call-recording-1',
556
- data: {
557
- status: 'PROCESSING',
558
- startedAt: '2026-06-09T13:02:00.000Z',
559
- endedAt: '2026-06-09T14:00:00.000Z',
560
- externalRecordingId: 'recall-recording-1',
561
- },
562
- },
563
- ]);
564
- expect(result.requestedTranscriptCallRecordingIds).toEqual([]);
565
- });
566
-
567
- it('fills a completed Recall transcript artifact during convergence', async () => {
568
- const transcriptContent = [
569
- {
570
- participant: { id: 1, name: 'Ada' },
571
- words: [{ text: 'hello', start_timestamp: 1, end_timestamp: 2 }],
572
- },
573
- ];
574
-
575
- getRecallBotMock.mockResolvedValue({
576
- ok: true,
577
- bot: {
578
- status_changes: [
579
- { code: 'done', created_at: '2026-06-09T14:05:00.000Z' },
580
- ],
581
- recordings: [
582
- {
583
- id: 'recall-recording-1',
584
- started_at: '2026-06-09T13:02:00.000Z',
585
- completed_at: '2026-06-09T14:00:00.000Z',
586
- },
587
- ],
588
- },
589
- });
590
- listRecallTranscriptsMock.mockResolvedValue({
591
- ok: true,
592
- transcripts: [
593
- {
594
- id: 'recall-transcript-1',
595
- statusCode: 'done',
596
- statusSubCode: undefined,
597
- },
598
- ],
599
- });
600
- downloadTranscriptMock.mockResolvedValue({
601
- outcome: 'filled',
602
- content: transcriptContent,
603
- });
604
- const client = buildClient([
605
- buildStuckRecordingNode({
606
- status: 'PROCESSING',
607
- startedAt: '2026-06-09T13:02:00.000Z',
608
- endedAt: '2026-06-09T14:00:00.000Z',
609
- externalRecordingId: 'recall-recording-1',
610
- transcript: {
611
- recallTranscriptId: 'legacy-pending-transcript',
612
- status: 'PENDING',
613
- requestedAt: '2026-06-09T14:05:30.000Z',
614
- },
615
- audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }],
616
- video: [{ fileId: 'file-video-1', label: 'video.mp4' }],
617
- }),
618
- ]);
619
-
620
- const result = await convergeDivergedCallRecordings({
621
- client: client as unknown as CoreApiClient,
622
- now: NOW,
623
- });
624
-
625
- expect(createAsyncRecallTranscriptMock).not.toHaveBeenCalled();
626
- expect(downloadTranscriptMock).toHaveBeenCalledWith({
627
- transcriptId: 'recall-transcript-1',
628
- });
629
- expect(client.mutations).toEqual([
630
- {
631
- id: 'call-recording-1',
632
- data: { transcript: transcriptContent },
633
- },
634
- {
635
- id: 'call-recording-1',
636
- data: { status: 'COMPLETED' },
637
- },
638
- ]);
639
- expect(chargeCompletedCallRecordingMock).toHaveBeenCalledWith({
640
- callRecordingId: 'call-recording-1',
641
- startedAt: '2026-06-09T13:02:00.000Z',
642
- endedAt: '2026-06-09T14:00:00.000Z',
643
- });
644
- expect(result.requestedTranscriptCallRecordingIds).toEqual([]);
645
- });
646
-
647
- it('marks the call recording failed when Recall has a failed transcript artifact', async () => {
648
- getRecallBotMock.mockResolvedValue({
649
- ok: true,
650
- bot: {
651
- status_changes: [
652
- { code: 'done', created_at: '2026-06-09T14:05:00.000Z' },
653
- ],
654
- recordings: [
655
- {
656
- id: 'recall-recording-1',
657
- started_at: '2026-06-09T13:02:00.000Z',
658
- completed_at: '2026-06-09T14:00:00.000Z',
659
- },
660
- ],
661
- },
662
- });
663
- listRecallTranscriptsMock.mockResolvedValue({
664
- ok: true,
665
- transcripts: [
666
- {
667
- id: 'recall-transcript-1',
668
- statusCode: 'failed',
669
- statusSubCode: 'audio_missing',
670
- },
671
- ],
672
- });
673
- const client = buildClient([
674
- buildStuckRecordingNode({
675
- status: 'PROCESSING',
676
- startedAt: '2026-06-09T13:02:00.000Z',
677
- endedAt: '2026-06-09T14:00:00.000Z',
678
- externalRecordingId: 'recall-recording-1',
679
- }),
680
- ]);
681
-
682
- const result = await convergeDivergedCallRecordings({
683
- client: client as unknown as CoreApiClient,
684
- now: NOW,
685
- });
686
-
687
- expect(createAsyncRecallTranscriptMock).not.toHaveBeenCalled();
688
- expect(downloadTranscriptMock).not.toHaveBeenCalled();
689
- expect(client.mutations).toEqual([
690
- {
691
- id: 'call-recording-1',
692
- data: {
693
- status: 'FAILED',
694
- transcript: {
695
- recallTranscriptId: 'recall-transcript-1',
696
- status: 'FAILED',
697
- subCode: 'audio_missing',
698
- },
699
- callRecorderFailureReason: 'transcript_failed:audio_missing',
700
- },
701
- },
702
- ]);
703
- expect(result.requestedTranscriptCallRecordingIds).toEqual([]);
704
- });
705
-
706
- it('does not mutate a record the bot state agrees with', async () => {
707
- getRecallBotMock.mockResolvedValue({
708
- ok: true,
709
- bot: {
710
- status_changes: [
711
- { code: 'in_call_recording', created_at: '2026-06-09T13:02:00.000Z' },
712
- ],
713
- },
714
- });
715
- const client = buildClient([
716
- buildStuckRecordingNode({ startedAt: '2026-06-09T13:02:00.000Z' }),
717
- ]);
718
-
719
- const result = await convergeDivergedCallRecordings({
720
- client: client as unknown as CoreApiClient,
721
- now: NOW,
722
- });
723
-
724
- expect(client.mutations).toEqual([]);
725
- expect(result.updatedCallRecordingIds).toEqual([]);
726
- });
727
- });