Files
multi-simadmin/apps/api/src/application/automation/schedule-time.test.ts
T
chick f9186bd851 feat(api): absorb the Hub control plane into the local instance model
Add central notification channels, rules, queue and delivery logs, fleet
organization groups and tags, device discovery, the device action catalog,
instance module reads, the log centre, connection settings and system
maintenance as native /api/v1 routes backed by the existing secret store,
audit trail and pinned upstream transport.
2026-09-05 18:53:04 +08:00

119 lines
3.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
nextOccurrence,
nextOccurrenceFor,
previewCron,
previewTrigger,
reconcileOccurrence,
} from './schedule-time.js';
describe('schedule time', () => {
it('calculates future occurrences in Beijing time across the UTC day boundary', () => {
expect(previewCron('0 9 * * *', 2, new Date('2026-07-30T00:30:00.000Z'))).toEqual([
'2026-07-30T01:00:00.000Z',
'2026-07-31T01:00:00.000Z',
]);
expect(nextOccurrence('30 0 * * *', new Date('2026-07-30T15:59:00.000Z'))).toBe(
'2026-07-30T16:30:00.000Z',
);
});
it('rejects seconds fields and invalid cron grammar', () => {
expect(() => previewCron('* * * * * *', 1)).toThrow(/five-field/i);
expect(() => previewCron('99 99 * * *', 1)).toThrow(/cron/i);
});
it('reconciles a missed due time according to the per-task policy', () => {
const common = {
cronExpression: '*/10 * * * *',
nextDueAt: '2026-07-30T01:00:00.000Z',
};
expect(
reconcileOccurrence(
{ ...common, misfirePolicy: 'skip' },
new Date('2026-07-30T01:25:00.000Z'),
),
).toEqual({
action: 'skip',
dueAt: '2026-07-30T01:20:00.000Z',
nextDueAt: '2026-07-30T01:30:00.000Z',
});
expect(
reconcileOccurrence(
{ ...common, misfirePolicy: 'catch-up-once' },
new Date('2026-07-30T01:25:00.000Z'),
),
).toEqual({
action: 'run',
dueAt: '2026-07-30T01:20:00.000Z',
nextDueAt: '2026-07-30T01:30:00.000Z',
});
});
it('expands Hub fixed schedules over the Beijing weekday and time grid', () => {
const weekdays = { kind: 'fixed' as const, weekdays: [4, 5], times: ['09:00', '21:30'] };
// 2026-07-30 is a Thursday; 01:25Z is 09:25 in Beijing, so the same-day 21:30 slot is next.
expect(previewTrigger(weekdays, 2, new Date('2026-07-30T01:25:00.000Z'))).toEqual([
'2026-07-30T13:30:00.000Z',
'2026-07-31T01:00:00.000Z',
]);
expect(
nextOccurrenceFor(
{ kind: 'fixed', weekdays: [6], times: ['00:30'] },
new Date('2026-07-30T01:25:00.000Z'),
),
).toBe('2026-07-31T16:30:00.000Z');
});
it('keeps interval schedules anchored on the previous occurrence', () => {
const interval = { kind: 'interval' as const, value: 90, unit: 'mins' as const };
expect(nextOccurrenceFor(interval, new Date('2026-07-30T01:00:00.000Z'))).toBe(
'2026-07-30T02:30:00.000Z',
);
expect(
nextOccurrenceFor(
interval,
new Date('2026-07-30T05:00:00.000Z'),
new Date('2026-07-30T01:00:00.000Z'),
),
).toBe('2026-07-30T05:30:00.000Z');
expect(previewTrigger(interval, 3, new Date('2026-07-30T01:00:00.000Z'))).toEqual([
'2026-07-30T02:30:00.000Z',
'2026-07-30T04:00:00.000Z',
'2026-07-30T05:30:00.000Z',
]);
});
it('reconciles fixed and interval misfires against the stored due time', () => {
expect(
reconcileOccurrence(
{
trigger: { kind: 'interval', value: 90, unit: 'mins' },
nextDueAt: '2026-07-30T04:00:00.000Z',
misfirePolicy: 'catch-up-once',
},
new Date('2026-07-30T05:00:00.000Z'),
),
).toEqual({
action: 'run',
dueAt: '2026-07-30T04:00:00.000Z',
nextDueAt: '2026-07-30T05:30:00.000Z',
});
expect(
reconcileOccurrence(
{
trigger: { kind: 'fixed', weekdays: [4, 5], times: ['09:00', '21:30'] },
nextDueAt: '2026-07-30T01:00:00.000Z',
misfirePolicy: 'skip',
},
new Date('2026-07-30T01:25:00.000Z'),
),
).toEqual({
action: 'skip',
dueAt: '2026-07-30T01:00:00.000Z',
nextDueAt: '2026-07-30T13:30:00.000Z',
});
});
});