File size: 7,796 Bytes
eb47743 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | import { describe, it, expect } from 'vitest'
import { parseNaturalSchedule, isCronDue } from '../schedule-parser'
describe('parseNaturalSchedule', () => {
it('returns null for empty input', () => {
expect(parseNaturalSchedule('')).toBeNull()
expect(parseNaturalSchedule(' ')).toBeNull()
})
it('passes through valid cron expressions', () => {
const result = parseNaturalSchedule('0 9 * * *')
expect(result).not.toBeNull()
expect(result!.cronExpr).toBe('0 9 * * *')
expect(result!.humanReadable).toContain('0 9 * * *')
})
it('passes through step cron expressions when formatted as pure cron', () => {
// The CRON_REGEX requires each field to be * or digits/commas/ranges.
// "*/5 * * * *" has a mixed field so it falls through as null (natural language fallback)
// Instead test a valid 5-field numeric cron:
const result = parseNaturalSchedule('5 * * * *')
expect(result!.cronExpr).toBe('5 * * * *')
})
it('parses "hourly"', () => {
const result = parseNaturalSchedule('hourly')
expect(result!.cronExpr).toBe('0 * * * *')
expect(result!.humanReadable).toMatch(/every hour/i)
})
it('parses "daily"', () => {
const result = parseNaturalSchedule('daily')
expect(result!.cronExpr).toBe('0 9 * * *')
})
it('parses "every day"', () => {
const result = parseNaturalSchedule('every day')
expect(result!.cronExpr).toBe('0 9 * * *')
})
it('parses "weekly"', () => {
const result = parseNaturalSchedule('weekly')
expect(result!.cronExpr).toBe('0 9 * * 1')
expect(result!.humanReadable).toMatch(/monday/i)
})
it('parses "every N minutes"', () => {
expect(parseNaturalSchedule('every 5 minutes')!.cronExpr).toBe('*/5 * * * *')
expect(parseNaturalSchedule('every 1 minute')!.cronExpr).toBe('*/1 * * * *')
expect(parseNaturalSchedule('every 30 minutes')!.cronExpr).toBe('*/30 * * * *')
})
it('returns null for invalid minute intervals', () => {
expect(parseNaturalSchedule('every 0 minutes')).toBeNull()
expect(parseNaturalSchedule('every 60 minutes')).toBeNull()
})
it('parses "every N hours"', () => {
expect(parseNaturalSchedule('every 2 hours')!.cronExpr).toBe('0 */2 * * *')
expect(parseNaturalSchedule('every 1 hour')!.cronExpr).toBe('0 */1 * * *')
})
it('returns null for invalid hour intervals', () => {
expect(parseNaturalSchedule('every 0 hours')).toBeNull()
expect(parseNaturalSchedule('every 24 hours')).toBeNull()
})
it('parses "daily at TIME"', () => {
const result = parseNaturalSchedule('daily at 9am')
expect(result!.cronExpr).toBe('0 9 * * *')
expect(result!.humanReadable).toMatch(/9.*AM/i)
})
it('parses "every morning at TIME"', () => {
const result = parseNaturalSchedule('every morning at 8am')
expect(result!.cronExpr).toBe('0 8 * * *')
})
it('parses "every evening at TIME"', () => {
const result = parseNaturalSchedule('every evening at 6pm')
expect(result!.cronExpr).toBe('0 18 * * *')
})
it('parses time with minutes', () => {
const result = parseNaturalSchedule('daily at 9:30am')
expect(result!.cronExpr).toBe('30 9 * * *')
expect(result!.humanReadable).toMatch(/9:30/i)
})
it('parses "at TIME every day"', () => {
const result = parseNaturalSchedule('at 10am every day')
expect(result!.cronExpr).toBe('0 10 * * *')
})
it('parses "weekly on DAYNAME"', () => {
expect(parseNaturalSchedule('weekly on monday')!.cronExpr).toBe('0 9 * * 1')
expect(parseNaturalSchedule('weekly on friday')!.cronExpr).toBe('0 9 * * 5')
expect(parseNaturalSchedule('weekly on sunday')!.cronExpr).toBe('0 9 * * 0')
})
it('parses "every DAYNAME"', () => {
expect(parseNaturalSchedule('every monday')!.cronExpr).toBe('0 9 * * 1')
expect(parseNaturalSchedule('every saturday')!.cronExpr).toBe('0 9 * * 6')
})
it('parses "every DAYNAME at TIME"', () => {
const result = parseNaturalSchedule('every tuesday at 3pm')
expect(result!.cronExpr).toBe('0 15 * * 2')
expect(result!.humanReadable).toMatch(/tuesday/i)
expect(result!.humanReadable).toMatch(/3.*PM/i)
})
it('returns null for unrecognized input', () => {
expect(parseNaturalSchedule('some random text')).toBeNull()
expect(parseNaturalSchedule('every foo bar')).toBeNull()
})
it('handles abbreviated day names', () => {
expect(parseNaturalSchedule('every mon')!.cronExpr).toBe('0 9 * * 1')
expect(parseNaturalSchedule('every fri')!.cronExpr).toBe('0 9 * * 5')
})
it('parses pm time correctly (12pm = noon)', () => {
const result = parseNaturalSchedule('daily at 12pm')
expect(result!.cronExpr).toBe('0 12 * * *')
})
it('parses 12am as midnight', () => {
const result = parseNaturalSchedule('daily at 12am')
expect(result!.cronExpr).toBe('0 0 * * *')
})
})
describe('isCronDue', () => {
// Build a local-time date for Monday at a specific hour/minute
// isCronDue uses .getHours()/.getMinutes()/.getDay() which are local time methods
function makeLocalTime(dayOfWeek: number, hour: number, minute: number, second = 0): number {
// Find a date that has the right local day of week
const d = new Date()
d.setSeconds(second)
d.setMilliseconds(0)
d.setMinutes(minute)
d.setHours(hour)
// Move to the desired day of week
const diff = dayOfWeek - d.getDay()
d.setDate(d.getDate() + diff)
return d.getTime()
}
it('returns true when cron matches and not recently spawned', () => {
const t = makeLocalTime(1, 9, 0) // Monday 09:00 local
expect(isCronDue('0 9 * * 1', t, 0)).toBe(true)
})
it('returns true for * in all fields', () => {
const t = makeLocalTime(1, 9, 0)
expect(isCronDue('* * * * *', t, 0)).toBe(true)
})
it('returns false when minute does not match', () => {
const t = makeLocalTime(1, 9, 5) // Monday 09:05
expect(isCronDue('0 9 * * 1', t, 0)).toBe(false)
})
it('returns false when hour does not match', () => {
const t = makeLocalTime(1, 10, 0) // Monday 10:00
expect(isCronDue('0 9 * * 1', t, 0)).toBe(false)
})
it('returns false when day of week does not match', () => {
const t = makeLocalTime(2, 9, 0) // Tuesday 09:00
expect(isCronDue('0 9 * * 1', t, 0)).toBe(false) // Monday only
})
it('returns false if already spawned in same minute', () => {
const t = makeLocalTime(1, 9, 0, 45) // Monday 09:00:45
const spawnedJustNow = t - 30000 // 30s ago = 09:00:15, same minute
expect(isCronDue('0 9 * * 1', t, spawnedJustNow)).toBe(false)
})
it('returns true if spawned in a previous minute', () => {
const t = makeLocalTime(1, 9, 0)
const spawnedPrevMinute = t - 120000 // 2 min ago, different minute
expect(isCronDue('0 9 * * 1', t, spawnedPrevMinute)).toBe(true)
})
it('handles step expressions', () => {
const t30 = makeLocalTime(1, 9, 30)
expect(isCronDue('*/30 * * * *', t30, 0)).toBe(true)
expect(isCronDue('*/15 * * * *', t30, 0)).toBe(true)
expect(isCronDue('*/7 * * * *', t30, 0)).toBe(false) // 30 % 7 != 0
})
it('returns false for invalid cron expression', () => {
const t = makeLocalTime(1, 9, 0)
expect(isCronDue('invalid', t, 0)).toBe(false)
expect(isCronDue('0 9 * *', t, 0)).toBe(false) // only 4 parts
})
it('handles comma-separated values', () => {
const t9 = makeLocalTime(1, 9, 0)
const t10 = makeLocalTime(1, 10, 0)
expect(isCronDue('0 9,10 * * *', t9, 0)).toBe(true)
expect(isCronDue('0 9,10 * * *', t10, 0)).toBe(true)
})
it('handles range expressions', () => {
const t9 = makeLocalTime(1, 9, 0)
const t18 = makeLocalTime(1, 18, 0)
expect(isCronDue('0 9-17 * * *', t9, 0)).toBe(true)
expect(isCronDue('0 9-17 * * *', t18, 0)).toBe(false)
})
})
|