fix(scheduler): reject 6-field cron in Scheduled Operations (#1435)

* fix(scheduler): reject 6-field cron in Scheduled Operations

Create and edit validation parsed cron with cron-parser, which accepts both
5- and 6-field expressions, while the form, presets, and docs all describe a
5-field cron. Because the scheduler ticks once per minute, a leading seconds
field can never improve precision, so a 6-field expression was silently
accepted but never honored on its stated schedule.

Add a field-count guard on both sides: the API rejects 6-field input at
create and edit with a clear message, and the form surfaces the same error
inline and disables save. Cron nicknames such as @daily still pass. Document
the five-field requirement in the cron reference.

* chore: merge main into scheduled cron validation

* fix: avoid logging policy bypass actor in debug output
This commit is contained in:
Anso
2026-06-24 23:00:56 -04:00
committed by GitHub
parent bc8c051962
commit db8bb70b7d
10 changed files with 715 additions and 472 deletions
@@ -177,6 +177,32 @@ describe('ScheduledOperationsView', () => {
});
});
it('disables Create when the cron expression has a seconds field', async () => {
render(<ScheduledOperationsView />);
await userEvent.click(await screen.findByRole('button', { name: /New Schedule/ }));
await userEvent.type(await screen.findByPlaceholderText('e.g. Nightly stack restart'), 'cleanup');
await userEvent.click(screen.getAllByRole('combobox')[0]);
await userEvent.click(await screen.findByRole('button', { name: 'System Prune' }));
await userEvent.click(screen.getAllByRole('combobox')[1]);
await userEvent.click(await screen.findByRole('button', { name: 'hub' }));
const createButton = screen.getByRole('button', { name: 'Create' });
expect(createButton).toBeEnabled();
const cronInput = screen.getByPlaceholderText('0 3 * * *');
await userEvent.clear(cronInput);
await userEvent.type(cronInput, '30 0 3 * * *');
expect(screen.getByText(/seconds field is not supported/i)).toBeInTheDocument();
expect(createButton).toBeDisabled();
await userEvent.clear(cronInput);
await userEvent.type(cronInput, '0 4 * * *');
expect(createButton).toBeEnabled();
});
it('keeps Create disabled for a prune until a node is selected', async () => {
render(<ScheduledOperationsView />);