fix(FN-0000): stabilize automation cron scheduling and SSE coverage
This commit is contained in:
@@ -81,6 +81,12 @@ describe("AutomationStore", () => {
|
||||
expect(new Date(next).getUTCHours()).toBe(13);
|
||||
expect(new Date(next).getUTCMinutes()).toBe(0);
|
||||
});
|
||||
|
||||
it("computes monthly runs against UTC instead of local machine time", () => {
|
||||
const fromDate = new Date("2026-04-15T00:00:00Z");
|
||||
const next = store.computeNextRun("0 0 1 * *", fromDate);
|
||||
expect(next).toBe("2026-05-01T00:00:00.000Z");
|
||||
});
|
||||
});
|
||||
|
||||
// ── createSchedule ────────────────────────────────────────────────
|
||||
|
||||
@@ -12,6 +12,8 @@ import { AUTOMATION_PRESETS, MAX_RUN_HISTORY } from "./automation.js";
|
||||
import type { ScheduleType } from "./automation.js";
|
||||
import { Database, fromJson } from "./db.js";
|
||||
|
||||
const CRON_TIMEZONE = "UTC";
|
||||
|
||||
export interface AutomationStoreEvents {
|
||||
"schedule:created": [schedule: ScheduledTask];
|
||||
"schedule:updated": [schedule: ScheduledTask];
|
||||
@@ -170,6 +172,7 @@ export class AutomationStore extends EventEmitter<AutomationStoreEvents> {
|
||||
computeNextRun(cronExpression: string, fromDate?: Date): string {
|
||||
const interval = CronExpressionParser.parse(cronExpression, {
|
||||
currentDate: fromDate ?? new Date(),
|
||||
tz: CRON_TIMEZONE,
|
||||
});
|
||||
const next = interval.next();
|
||||
return next.toISOString() ?? new Date(next.getTime()).toISOString();
|
||||
|
||||
@@ -41,6 +41,7 @@ const allScheduleTypesRecord: Record<ScheduleType, true> = {
|
||||
};
|
||||
|
||||
const allScheduleTypes = Object.keys(allScheduleTypesRecord) as ScheduleType[];
|
||||
const CRON_TIMEZONE = "UTC";
|
||||
|
||||
function cronDateToDate(value: { toISOString(): string | null; getTime(): number }): Date {
|
||||
const iso = value.toISOString();
|
||||
@@ -50,6 +51,7 @@ function cronDateToDate(value: { toISOString(): string | null; getTime(): number
|
||||
function parseNextRun(cronExpression: string, currentDate?: Date): Date {
|
||||
const interval = CronExpressionParser.parse(cronExpression, {
|
||||
currentDate: currentDate ?? new Date(),
|
||||
tz: CRON_TIMEZONE,
|
||||
});
|
||||
return cronDateToDate(interval.next());
|
||||
}
|
||||
@@ -287,6 +289,7 @@ describe("Preset cron expression edge cases", () => {
|
||||
it("ensures weekdays preset never schedules Saturday or Sunday in the next 7 runs", () => {
|
||||
const interval = CronExpressionParser.parse(AUTOMATION_PRESETS.weekdays, {
|
||||
currentDate: new Date("2026-04-06T00:00:00.000Z"), // Monday
|
||||
tz: CRON_TIMEZONE,
|
||||
});
|
||||
|
||||
const days = Array.from({ length: 7 }, () => cronDateToDate(interval.next()).getUTCDay());
|
||||
@@ -301,6 +304,7 @@ describe("Preset cron expression edge cases", () => {
|
||||
it("ensures every15Minutes preset advances in 15 minute intervals", () => {
|
||||
const interval = CronExpressionParser.parse(AUTOMATION_PRESETS.every15Minutes, {
|
||||
currentDate: new Date("2026-01-01T00:00:00.000Z"),
|
||||
tz: CRON_TIMEZONE,
|
||||
});
|
||||
|
||||
const first = cronDateToDate(interval.next());
|
||||
@@ -314,6 +318,7 @@ describe("Preset cron expression edge cases", () => {
|
||||
it("ensures every2Hours preset advances in 2 hour intervals", () => {
|
||||
const interval = CronExpressionParser.parse(AUTOMATION_PRESETS.every2Hours, {
|
||||
currentDate: new Date("2026-01-01T00:00:00.000Z"),
|
||||
tz: CRON_TIMEZONE,
|
||||
});
|
||||
|
||||
const first = cronDateToDate(interval.next());
|
||||
|
||||
@@ -71,6 +71,12 @@ describe("RoutineStore", () => {
|
||||
expect(new Date(next).getUTCHours()).toBe(13);
|
||||
expect(new Date(next).getUTCMinutes()).toBe(0);
|
||||
});
|
||||
|
||||
it("computes monthly runs against UTC instead of local machine time", () => {
|
||||
const fromDate = new Date("2026-04-15T00:00:00Z");
|
||||
const next = store.computeNextRun("0 0 1 * *", fromDate);
|
||||
expect(next).toBe("2026-05-01T00:00:00.000Z");
|
||||
});
|
||||
});
|
||||
|
||||
// ── createRoutine ────────────────────────────────────────────────
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
MAX_ROUTINE_RUN_HISTORY,
|
||||
} from "./routine.js";
|
||||
|
||||
const CRON_TIMEZONE = "UTC";
|
||||
|
||||
export interface RoutineStoreEvents {
|
||||
"routine:created": [routine: Routine];
|
||||
"routine:updated": [routine: Routine];
|
||||
@@ -242,6 +244,7 @@ export class RoutineStore extends EventEmitter<RoutineStoreEvents> {
|
||||
computeNextRun(cronExpression: string, fromDate?: Date): string {
|
||||
const interval = CronExpressionParser.parse(cronExpression, {
|
||||
currentDate: fromDate ?? new Date(),
|
||||
tz: CRON_TIMEZONE,
|
||||
});
|
||||
const next = interval.next();
|
||||
return new Date(next.getTime()).toISOString();
|
||||
|
||||
Reference in New Issue
Block a user