refactor(schedule): delegate safe-year cron search

This commit is contained in:
pku-xht
2026-08-08 03:37:20 +08:00
committed by Tianyi Cui
parent 31c0c6a9f6
commit 3b16c5ca00
2 changed files with 71 additions and 131 deletions

View File

@@ -170,6 +170,17 @@ describe('Croner calendar adapter', () => {
occurrenceAt: '0100-01-01T00:00:00.000Z',
nextScheduledAt: '0100-01-02T00:00:00.000Z',
})
const yearOne = createCronScheduleRecord(
ScheduleId('schedule-reverse-1'),
'reverse year one',
'0 0 * * *',
'UTC',
Date.parse('0001-01-01T00:00:00.000Z'),
)
expect(resolveCronOccurrence(yearOne, Date.parse(yearOne.scheduledAt))).toEqual({
occurrenceAt: yearOne.scheduledAt,
nextScheduledAt: '0001-01-03T00:00:00.000Z',
})
})
it('skips a DST gap and chooses the first instant in an overlap', () => {
@@ -209,6 +220,13 @@ describe('Croner calendar adapter', () => {
occurrenceAt: '2026-11-01T05:30:00.000Z',
nextScheduledAt: '2026-11-02T06:30:00.000Z',
})
expect(createCronScheduleRecord(
ScheduleId('schedule-overlap-after-first'),
'after first overlap instant',
'30 1 * * *',
'America/New_York',
Date.parse('2026-11-01T05:45:00.000Z'),
).scheduledAt).toBe('2026-11-02T06:30:00.000Z')
})
it('selects the latest current match after a persisted baseline', () => {
@@ -246,7 +264,7 @@ describe('Croner calendar adapter', () => {
), 'no_future_occurrence')
})
it('contains dependency cursor failures and preserves the baseline when current search has no match', () => {
it('contains invalid dependency results without replacing safe-year calendar search', () => {
const record = createCronScheduleRecord(
ScheduleId('schedule-dependency'),
'dependency',
@@ -261,63 +279,6 @@ describe('Croner calendar adapter', () => {
})
noPrevious.mockRestore()
const repeatedPrevious = vi.spyOn(Cron.prototype, 'previousRuns')
.mockImplementationOnce((_count, reference) => [new Date(reference ?? record.scheduledAt)])
.mockReturnValue([new Date('2026-11-01T05:30:00.000Z')])
expect(resolveCronOccurrence(record, Date.parse('2026-11-01T07:00:00.000Z'))).toMatchObject({
occurrenceAt: '2026-11-01T05:30:00.000Z',
})
repeatedPrevious.mockRestore()
const laterOverlap = vi.spyOn(Cron.prototype, 'previousRuns')
.mockReturnValue([new Date('2026-11-01T06:30:00.000Z')])
expect(resolveCronOccurrence(record, Date.parse('2026-11-01T07:00:00.000Z'))).toMatchObject({
occurrenceAt: '2026-11-01T05:30:00.000Z',
})
laterOverlap.mockRestore()
const gapThenNone = vi.spyOn(Cron.prototype, 'previousRuns')
.mockReturnValueOnce([new Date('2026-03-08T07:30:00.000Z')])
.mockReturnValueOnce([])
const gapBaseline = {
...record,
cron: '30 2 * * *',
scheduledAt: '2026-03-07T07:30:00.000Z',
}
expect(resolveCronOccurrence(gapBaseline, Date.parse('2026-03-08T08:00:00.000Z'))).toMatchObject({
occurrenceAt: gapBaseline.scheduledAt,
})
gapThenNone.mockRestore()
const boundaryThenEnd = vi.spyOn(Cron.prototype, 'previousRuns')
.mockReturnValue([new Date('0001-01-01T00:00:00.000Z')])
const boundaryBaseline = {
...record,
cron: '1 0 * * *',
timeZone: 'UTC',
scheduledAt: '0001-01-01T00:01:00.000Z',
}
expect(resolveCronOccurrence(boundaryBaseline, Date.parse('2026-01-01T00:00:00.000Z'))).toMatchObject({
occurrenceAt: '2025-12-31T00:01:00.000Z',
})
boundaryThenEnd.mockRestore()
const repeatedNext = vi.spyOn(Cron.prototype, 'nextRun')
.mockImplementationOnce(reference =>
reference instanceof Date ? new Date(reference) : new Date('2026-01-01T00:00:00.000Z'))
.mockReturnValue(new Date('2026-01-02T00:00:00.000Z'))
expect(createCronScheduleRecord(
ScheduleId('stuck-next'), 'x', '0 0 * * *', 'UTC', Date.parse('2026-01-01T00:00:00Z'),
).scheduledAt).toBe('2026-01-02T00:00:00.000Z')
repeatedNext.mockRestore()
const neverAdvancingNext = vi.spyOn(Cron.prototype, 'nextRun').mockImplementation(reference =>
reference instanceof Date ? new Date(reference) : new Date('2026-01-01T00:00:00.000Z'))
expect(createCronScheduleRecord(
ScheduleId('fallback-next'), 'x', '0 0 * * *', 'UTC', Date.parse('2026-01-01T00:00:00Z'),
).scheduledAt).toBe('2026-01-02T00:00:00.000Z')
neverAdvancingNext.mockRestore()
const invalidNext = vi.spyOn(Cron.prototype, 'nextRun').mockReturnValue(new Date(Number.NaN))
expectInputCode(() => createCronScheduleRecord(
ScheduleId('invalid-next'), 'x', '0 0 * * *', 'UTC', Date.parse('2026-01-01T00:00:00Z'),
@@ -337,42 +298,6 @@ describe('Croner calendar adapter', () => {
.toThrow(/cron evaluation failed: The cron evaluator did not retreat/)
invalidPrevious.mockRestore()
const repeatedAtBaseline = vi.spyOn(Cron.prototype, 'previousRuns')
.mockImplementation((_count, reference) => [new Date(reference ?? record.scheduledAt)])
expect(resolveCronOccurrence(record, Date.parse(record.scheduledAt))).toMatchObject({
occurrenceAt: record.scheduledAt,
})
repeatedAtBaseline.mockRestore()
const neverRetreating = vi.spyOn(Cron.prototype, 'previousRuns')
.mockImplementation((_count, reference) => [new Date(reference ?? record.scheduledAt)])
expect(resolveCronOccurrence({
...record,
scheduledAt: '2026-10-31T05:30:00.000Z',
}, Date.parse('2026-11-01T07:00:00.000Z'))).toMatchObject({
occurrenceAt: '2026-11-01T05:30:00.000Z',
})
neverRetreating.mockRestore()
const nonMinutePrevious = vi.spyOn(Cron.prototype, 'previousRuns')
.mockReturnValue([new Date('2026-11-01T05:30:30.000Z')])
expect(resolveCronOccurrence(record, Date.parse('2026-11-01T07:00:00.000Z'))).toMatchObject({
occurrenceAt: '2026-11-01T05:30:00.000Z',
})
nonMinutePrevious.mockRestore()
const gapWithOwnedMatch = vi.spyOn(Cron.prototype, 'previousRuns')
.mockReturnValue([new Date('2026-03-08T07:30:00.000Z')])
const gapWithNextDay = {
...record,
cron: '30 2 * * *',
scheduledAt: '2026-03-07T07:30:00.000Z',
}
expect(resolveCronOccurrence(gapWithNextDay, Date.parse('2026-03-09T08:00:00.000Z'))).toMatchObject({
occurrenceAt: '2026-03-09T06:30:00.000Z',
})
gapWithOwnedMatch.mockRestore()
const thrownNext = vi.spyOn(Cron.prototype, 'nextRun').mockImplementation(() => {
throw new Error('dependency failed')
})