From 4e2f9e3416af99a772907aef974b869d93c23e10 Mon Sep 17 00:00:00 2001 From: martmull Date: Fri, 17 Jul 2026 16:59:09 +0200 Subject: [PATCH] Fix join at in the past (#23000) as title, we floor the bot join at date 1second in the future Review in cubic --- .../public/call-recorder/package.json | 2 +- .../__tests__/reconcile-call-recorder.test.ts | 2 + ...l-bots-for-pending-call-recordings.test.ts | 2 + .../compute-maximum-join-at.utils.test.ts | 60 +++++++++++++++++++ .../__tests__/recall-bot-api.test.ts | 4 ++ .../compute-maximum-join-at.utils.ts | 8 +++ .../recall-api/reschedule-recall-bot.util.ts | 3 +- .../recall-api/schedule-recall-bot.util.ts | 3 +- 8 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/compute-maximum-join-at.utils.test.ts create mode 100644 packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/compute-maximum-join-at.utils.ts diff --git a/packages/twenty-apps/public/call-recorder/package.json b/packages/twenty-apps/public/call-recorder/package.json index 646484fd36..166f74d451 100644 --- a/packages/twenty-apps/public/call-recorder/package.json +++ b/packages/twenty-apps/public/call-recorder/package.json @@ -1,6 +1,6 @@ { "name": "@twentyhq/call-recorder", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "engines": { "node": "^24.5.0", diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/reconcile-call-recorder.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/reconcile-call-recorder.test.ts index e35273f770..ecdd91004a 100644 --- a/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/reconcile-call-recorder.test.ts +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/reconcile-call-recorder.test.ts @@ -219,6 +219,8 @@ const buildFakeCoreApiClient = ( describe('reconcileCallRecorderForCalendarEventIds', () => { beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(NOW); vi.spyOn(console, 'warn').mockImplementation(() => {}); vi.spyOn(console, 'error').mockImplementation(() => {}); vi.stubGlobal('fetch', fetchMock); diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/schedule-recall-bots-for-pending-call-recordings.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/schedule-recall-bots-for-pending-call-recordings.test.ts index 0d04bd72df..a21626bbdb 100644 --- a/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/schedule-recall-bots-for-pending-call-recordings.test.ts +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/schedule-recall-bots-for-pending-call-recordings.test.ts @@ -181,6 +181,8 @@ const createBotCalls = () => describe('scheduleRecallBotsForPendingCallRecordings', () => { beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(NOW); vi.spyOn(console, 'warn').mockImplementation(() => {}); vi.stubGlobal('fetch', fetchMock); vi.stubEnv('RECALL_API_KEY', 'recall-api-key'); diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/compute-maximum-join-at.utils.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/compute-maximum-join-at.utils.test.ts new file mode 100644 index 0000000000..0ad1a219c6 --- /dev/null +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/compute-maximum-join-at.utils.test.ts @@ -0,0 +1,60 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { computeMaximumJoinAt } from 'src/logic-functions/recall-api/compute-maximum-join-at.utils'; + +const NOW = new Date('2026-07-17T10:00:00.000Z'); + +describe('computeMaximumJoinAt', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(NOW); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('returns joinAt when it is later than now plus one second', () => { + const joinAt = '2026-07-17T10:00:10.000Z'; + + const result = computeMaximumJoinAt(joinAt); + + expect(result).toBe(joinAt); + }); + + it('returns now plus one second when joinAt is in the past', () => { + const joinAt = '2026-07-17T09:59:00.000Z'; + + const result = computeMaximumJoinAt(joinAt); + + expect(result).toBe('2026-07-17T10:00:01.000Z'); + }); + + it('returns now plus one second when joinAt is equal to now', () => { + const joinAt = '2026-07-17T10:00:00.000Z'; + + const result = computeMaximumJoinAt(joinAt); + + expect(result).toBe('2026-07-17T10:00:01.000Z'); + }); + + it('returns joinAt when it is equal to now plus one second', () => { + const joinAt = '2026-07-17T10:00:01.000Z'; + + const result = computeMaximumJoinAt(joinAt); + + expect(result).toBe('2026-07-17T10:00:01.000Z'); + }); + + it('normalizes an ISO 8601 value with a timezone offset to UTC', () => { + const joinAt = '2026-07-17T12:00:10.000+02:00'; + + const result = computeMaximumJoinAt(joinAt); + + expect(result).toBe('2026-07-17T10:00:10.000Z'); + }); + + it('throws when joinAt is not a valid date', () => { + expect(() => computeMaximumJoinAt('not-a-date')).toThrow(RangeError); + }); +}); diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts index e3432ec352..4a26938158 100644 --- a/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts @@ -15,6 +15,7 @@ import { CALL_RECORDER_RECORDING_RETENTION_HOURS_ENV_VAR_NAME } from 'src/logic- import { RECALL_API_KEY_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-api-key-env-var-name'; import { RECALL_REGION_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-region-env-var-name'; +const NOW = new Date('2026-01-01T12:00:00.000Z'); const WORKSPACE_ID = '123e4567-e89b-12d3-a456-426614174000'; const RECALL_ROUTING_METADATA = { twentyWorkspaceId: WORKSPACE_ID, @@ -34,6 +35,8 @@ describe('recall bot api', () => { const fetchMock = vi.fn(); beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(NOW); delete process.env[CALL_RECORDER_RECORDING_RETENTION_HOURS_ENV_VAR_NAME]; process.env[RECALL_API_KEY_ENV_VAR_NAME] = 'recall-api-key'; process.env[RECALL_REGION_ENV_VAR_NAME] = 'ap-northeast-1'; @@ -56,6 +59,7 @@ describe('recall bot api', () => { } }); vi.unstubAllGlobals(); + vi.useRealTimers(); }); it('creates Recall bot requests with the Token authorization scheme', async () => { diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/compute-maximum-join-at.utils.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/compute-maximum-join-at.utils.ts new file mode 100644 index 0000000000..ca967f57c5 --- /dev/null +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/compute-maximum-join-at.utils.ts @@ -0,0 +1,8 @@ +export const computeMaximumJoinAt = (joinAt: string): string => { + const joinAtDate = new Date(joinAt); + const nowPlusOneSecond = new Date(Date.now() + 1_000); + + return new Date( + Math.max(joinAtDate.getTime(), nowPlusOneSecond.getTime()), + ).toISOString(); +}; diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/reschedule-recall-bot.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/reschedule-recall-bot.util.ts index a45d27ea3b..6d35eb0936 100644 --- a/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/reschedule-recall-bot.util.ts +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/reschedule-recall-bot.util.ts @@ -10,6 +10,7 @@ import { import { getRecallApiConfig } from 'src/logic-functions/recall-api/get-recall-api-config.util'; import { recallBotApiRequest } from 'src/logic-functions/recall-api/recall-bot-api-request.util'; import { type ScheduleRecallBotArgs } from 'src/logic-functions/recall-api/schedule-recall-bot.util'; +import { computeMaximumJoinAt } from 'src/logic-functions/recall-api/compute-maximum-join-at.utils'; type RescheduleRecallBotArgs = ScheduleRecallBotArgs & { externalBotId: string; @@ -35,7 +36,7 @@ export const rescheduleRecallBot = async ({ method: 'PATCH', body: { meeting_url: meetingUrl, - join_at: joinAt, + join_at: computeMaximumJoinAt(joinAt), // We can't join in the past, so we floor this date 1s in the future bot_name: configResult.config.botName, ...(isUndefined(automaticLeave) ? {} diff --git a/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/schedule-recall-bot.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/schedule-recall-bot.util.ts index f2f71f42e4..71dfe546c8 100644 --- a/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/schedule-recall-bot.util.ts +++ b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/schedule-recall-bot.util.ts @@ -13,6 +13,7 @@ import { } from 'src/logic-functions/recall-api/extract-recall-bot-id.util'; import { getRecallApiConfig } from 'src/logic-functions/recall-api/get-recall-api-config.util'; import { recallBotApiRequest } from 'src/logic-functions/recall-api/recall-bot-api-request.util'; +import { computeMaximumJoinAt } from 'src/logic-functions/recall-api/compute-maximum-join-at.utils'; export type ScheduleRecallBotArgs = { meetingUrl: string; @@ -47,7 +48,7 @@ export const scheduleRecallBot = async ({ idempotencyKey, body: { meeting_url: meetingUrl, - join_at: joinAt, + join_at: computeMaximumJoinAt(joinAt), // We can't join in the past, so we floor this date 1s in the future bot_name: configResult.config.botName, ...(isUndefined(automaticLeave) ? {}