Fix join at in the past (#23000)

as title, we floor the bot join at date 1second in the future

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23000?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
martmull
2026-07-17 16:59:09 +02:00
committed by GitHub
parent aafee63706
commit 4e2f9e3416
8 changed files with 81 additions and 3 deletions
@@ -1,6 +1,6 @@
{
"name": "@twentyhq/call-recorder",
"version": "1.1.0",
"version": "1.2.0",
"license": "MIT",
"engines": {
"node": "^24.5.0",
@@ -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);
@@ -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');
@@ -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);
});
});
@@ -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 () => {
@@ -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();
};
@@ -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)
? {}
@@ -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)
? {}