feat(scheduler): flag one-shot tasks in the Scheduled Operations table (#1433)

A one-shot task (delete after successful run) self-deletes only on a
successful run; a failed one-shot is kept so it can be retried or
debugged. The table gave no signal for this, so a lingering failed
one-shot was indistinguishable from a stuck recurring task. Add a
"One-shot" chip to the status cell for every delete-after-run task,
with a tooltip that explains the keep-on-failure behavior when the
task has failed.
This commit is contained in:
Anso
2026-06-24 19:04:42 -04:00
committed by GitHub
parent 5143350771
commit 2c70e11485
2 changed files with 53 additions and 7 deletions
@@ -79,6 +79,37 @@ describe('ScheduledOperationsView', () => {
expect(await screen.findByText('nightly-prune')).toBeInTheDocument();
});
it('marks a one-shot task with a chip and leaves recurring tasks unmarked', async () => {
tasksFixture = [
makeTask({ id: 1, name: 'recurring-task' }),
makeTask({ id: 2, name: 'one-shot-task', delete_after_run: 1 }),
];
render(<ScheduledOperationsView />);
await userEvent.click(await screen.findByRole('button', { name: /All tasks/ }));
await screen.findByText('recurring-task');
// A single One-shot chip, scoped to the one-shot row.
const chips = screen.getAllByText('One-shot');
expect(chips).toHaveLength(1);
// A pending one-shot carries the plain lifecycle tooltip, not the failure copy.
expect(chips[0]).toHaveAttribute('title', expect.stringContaining('Deletes itself after a successful run'));
expect(chips[0]).toHaveAttribute('title', expect.not.stringContaining('kept after a failed run'));
const oneShotRow = screen.getByText('one-shot-task').closest('tr');
expect(oneShotRow).toContainElement(chips[0]);
const recurringRow = screen.getByText('recurring-task').closest('tr');
expect(recurringRow?.textContent).not.toContain('One-shot');
});
it('explains a failed one-shot was kept to debug', async () => {
tasksFixture = [makeTask({ id: 1, name: 'failed-one-shot', delete_after_run: 1, last_status: 'failure' })];
render(<ScheduledOperationsView />);
await userEvent.click(await screen.findByRole('button', { name: /All tasks/ }));
const chip = await screen.findByText('One-shot');
expect(chip).toHaveAttribute('title', expect.stringContaining('kept after a failed run'));
});
it('opens the create modal from a prefill and consumes it once', async () => {
const onPrefillConsumed = vi.fn();
render(