From 37870fce9ec7d4e354937baa31d0a7196eebc8fa Mon Sep 17 00:00:00 2001 From: "@daniel-lxs" <57051444+daniel-lxs@users.noreply.github.com> Date: Thu, 10 Sep 2026 05:40:48 +0000 Subject: [PATCH] fix: show custom automation cadence in deployment timezone --- ...AutomationsSettings.render.client.test.tsx | 87 ++++++++++++++++++- .../automations/CustomAutomationsSection.tsx | 17 ++-- .../custom-automation-schedule.test.ts | 6 +- 3 files changed, 100 insertions(+), 10 deletions(-) diff --git a/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx b/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx index 4047e6fe90..56007093b0 100644 --- a/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx +++ b/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx @@ -16,6 +16,7 @@ const state = vi.hoisted(() => ({ queriedKeys: [] as unknown[], customAutomationsPending: false, customAutomationRunPendingId: null as string | null, + customAutomationTimeZone: 'UTC' as string | undefined, customAutomations: [] as Array<{ id: string; name: string; @@ -334,7 +335,7 @@ vi.mock('@tanstack/react-query', () => ({ state.settingsQuery.data.settings.managerSlackChannelId, managerDiscordChannelId: state.settingsQuery.data.settings.managerDiscordChannelId, - effectiveTimeZone: 'UTC', + effectiveTimeZone: state.customAutomationTimeZone, }, }; } @@ -668,6 +669,7 @@ describe('AutomationsSettings', () => { state.settingsQuery.data.reviewer.relayReviewResultsToTask = false; state.settingsQuery.data.reviewer.relayUsers = []; state.customAutomations = []; + state.customAutomationTimeZone = 'UTC'; state.customAutomationsPending = false; state.settingsQuery.isPending = false; state.environments = []; @@ -1435,7 +1437,7 @@ describe('AutomationsSettings', () => { expect( await screen.findByText( - 'At 09:00 AM, Monday through Friday, in Production →', + 'At 09:00 AM, Monday through Friday (UTC), in Production →', ), ).toBeInTheDocument(); expect(screen.getByText(/Created by Ada/)).toHaveTextContent( @@ -1444,6 +1446,87 @@ describe('AutomationsSettings', () => { expect(screen.queryByText('0 9 * * 1-5')).not.toBeInTheDocument(); }); + it('shows saved cron cadence in the deployment timezone', async () => { + state.customAutomationTimeZone = 'America/New_York'; + state.environments = [{ id: 'env-1', name: 'Production' }]; + state.customAutomations = [ + { + id: 'automation-1', + name: 'Daily scan', + prompt: 'Find flaky tests.', + enabled: true, + scheduleMode: 'cron', + cronExpression: '0 9 * * *', + model: null, + environmentId: 'env-1', + target: { provider: 'slack', externalRef: 'C123MANAGER' }, + lastRunAt: null, + lastSucceededAt: null, + lastFailedAt: null, + lastError: null, + lastLaunchedTaskId: null, + createdByName: 'Ada', + createdAt: new Date('2026-01-01T00:00:00Z'), + updatedAt: new Date('2026-01-01T00:00:00Z'), + }, + ]; + + render(); + + expect( + await screen.findByText( + 'Daily at 09:00 AM (America/New York), in Production →', + ), + ).toBeInTheDocument(); + fireEvent.click( + screen.getByRole('button', { name: 'Configure Daily scan' }), + ); + expect( + screen.getByText('Daily at 09:00 AM (America/New York)'), + ).toBeInTheDocument(); + }); + + it.each([ + { + reason: 'the deployment timezone is unavailable', + timeZone: undefined, + cronExpression: '0 9 * * *', + }, + { + reason: 'the saved cron is invalid', + timeZone: 'UTC', + cronExpression: '99 99 * * *', + }, + ])('falls back when $reason', async ({ timeZone, cronExpression }) => { + state.customAutomationTimeZone = timeZone; + state.customAutomations = [ + { + id: 'automation-1', + name: 'Daily scan', + prompt: 'Find flaky tests.', + enabled: true, + scheduleMode: 'cron', + cronExpression, + model: null, + executionMode: 'fast', + environmentId: '__fast__', + target: {}, + lastRunAt: null, + lastSucceededAt: null, + lastFailedAt: null, + lastError: null, + lastLaunchedTaskId: null, + createdByName: 'Ada', + createdAt: new Date('2026-01-01T00:00:00Z'), + updatedAt: new Date('2026-01-01T00:00:00Z'), + }, + ]; + + render(); + + expect(await screen.findByText('Custom schedule →')).toBeInTheDocument(); + }); + it('shows Slack DM me as a custom automation destination', async () => { state.environments = [{ id: 'env-1', name: 'Production' }]; state.customAutomations = [ diff --git a/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx b/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx index deecc9e4e7..150961dc15 100644 --- a/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx +++ b/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx @@ -124,14 +124,21 @@ function scheduleLabel(mode: CustomAutomationScheduleMode): string { ); } -function cadenceLabel(row: CustomAutomationListItem): string { +function cadenceLabel( + row: CustomAutomationListItem, + timeZone: string | undefined, +): string { if (row.scheduleMode !== 'cron') { return scheduleLabel(row.scheduleMode); } - return row.cronExpression - ? (tryParseCronSchedule(row.cronExpression, 'UTC')?.summary ?? - 'Custom schedule') + if (!row.cronExpression || !timeZone) { + return 'Custom schedule'; + } + + const parsed = tryParseCronSchedule(row.cronExpression, timeZone); + return parsed + ? scheduleSummaryLine(parsed.summary, timeZone) : 'Custom schedule'; } @@ -1027,7 +1034,7 @@ export function CustomAutomationsSection() {

{row.name}

- {cadenceLabel(row)} + {cadenceLabel(row, schedulingTimeZone)} {environmentName ? `, in ${environmentName}` : ''} → diff --git a/packages/sdk/src/server/automations/__tests__/custom-automation-schedule.test.ts b/packages/sdk/src/server/automations/__tests__/custom-automation-schedule.test.ts index c9c17f269e..c7c76b11d5 100644 --- a/packages/sdk/src/server/automations/__tests__/custom-automation-schedule.test.ts +++ b/packages/sdk/src/server/automations/__tests__/custom-automation-schedule.test.ts @@ -19,11 +19,11 @@ describe('custom automation schedule helpers', () => { it('uses the configured timezone for the next occurrence', () => { const next = getCronOccurrence( '0 9 * * *', - 'America/Los_Angeles', + 'America/New_York', 'next', - new Date('2026-08-02T12:00:00Z'), + new Date('2026-08-01T00:00:00Z'), ); - expect(next.toISOString()).toBe('2026-08-02T16:00:00.000Z'); + expect(next.toISOString()).toBe('2026-08-01T13:00:00.000Z'); }); it('launches once when the latest occurrence is newer than the baseline', () => {