fix: fire task alerts from the start when they are relative to it #1328

This commit is contained in:
Linus Rath
2026-10-24 17:57:56 +02:00
parent 0d4631c5a8
commit 35660690d2
2 changed files with 30 additions and 2 deletions
+25
View File
@@ -11,11 +11,13 @@ import {
alertsToRows,
rowsToAlerts,
newAlertRow,
computeTaskFireTime,
} from '../calendar-alerts';
import type {
CalendarEvent,
CalendarEventAlert,
Calendar,
CalendarTask,
} from '@/lib/jmap/types';
function makeEvent(overrides: Partial<CalendarEvent> = {}): CalendarEvent {
@@ -521,3 +523,26 @@ describe('getPendingAlerts', () => {
expect(result[0].calendarName).toBeNull();
});
});
describe('computeTaskFireTime (#1328)', () => {
const task = (start: string | null, due: string | null) => ({ start, due } as CalendarTask);
const offset = (relativeTo?: 'start' | 'end') => ({ '@type': 'OffsetTrigger' as const, offset: '-PT5M', ...(relativeTo ? { relativeTo } : {}) }) as Parameters<typeof computeTaskFireTime>[1];
const at = (s: string) => new Date(s).getTime();
it('counts a start-relative alert from the start', () => {
expect(computeTaskFireTime(task('2026-10-23T20:00:00Z', '2026-10-23T20:15:00Z'), offset('start'))).toBe(at('2026-10-23T19:55:00Z'));
});
it('treats a missing relativeTo as start, as Stalwart sends it', () => {
expect(computeTaskFireTime(task('2026-10-23T20:00:00Z', '2026-10-23T20:15:00Z'), offset())).toBe(at('2026-10-23T19:55:00Z'));
});
it('counts an end-relative alert from the due time', () => {
expect(computeTaskFireTime(task('2026-10-23T20:00:00Z', '2026-10-23T20:15:00Z'), offset('end'))).toBe(at('2026-10-23T20:10:00Z'));
});
it('falls back to the due time when the task has no start', () => {
expect(computeTaskFireTime(task(null, '2026-10-23T20:15:00Z'), offset('start'))).toBe(at('2026-10-23T20:10:00Z'));
expect(computeTaskFireTime(task(null, null), offset('start'))).toBeNull();
});
});
+5 -2
View File
@@ -275,8 +275,11 @@ export function computeTaskFireTime(
const offsetMs = parseAlertOffset(trigger.offset);
if (offsetMs === null) return null;
if (!task.due) return null;
const baseTime = new Date(task.due).getTime();
// RFC 8984 §4.5.2: relativeTo defaults to "start", which for a Task is its
// start; "end" is its due. A task without a start counts from its due (#1328).
const base = trigger.relativeTo !== 'end' && task.start ? task.start : task.due;
if (!base) return null;
const baseTime = new Date(base).getTime();
if (Number.isNaN(baseTime)) return null;
return baseTime + offsetMs;
}