mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 09:58:04 +08:00
feat: comprehensive trigger node system with Schedule Trigger implementation (#24039)
Co-authored-by: zhangxuhe1 <xuhezhang6@gmail.com>
This commit is contained in:
@ -0,0 +1,233 @@
|
||||
import { isValidCronExpression, parseCronExpression } from './cron-parser'
|
||||
|
||||
describe('cron-parser', () => {
|
||||
describe('isValidCronExpression', () => {
|
||||
test('validates correct cron expressions', () => {
|
||||
expect(isValidCronExpression('15 10 1 * *')).toBe(true)
|
||||
expect(isValidCronExpression('0 0 * * 0')).toBe(true)
|
||||
expect(isValidCronExpression('*/5 * * * *')).toBe(true)
|
||||
expect(isValidCronExpression('0 9-17 * * 1-5')).toBe(true)
|
||||
expect(isValidCronExpression('30 14 * * 1')).toBe(true)
|
||||
expect(isValidCronExpression('0 0 1,15 * *')).toBe(true)
|
||||
})
|
||||
|
||||
test('rejects invalid cron expressions', () => {
|
||||
expect(isValidCronExpression('')).toBe(false)
|
||||
expect(isValidCronExpression('15 10 1')).toBe(false) // Not enough fields
|
||||
expect(isValidCronExpression('15 10 1 * * *')).toBe(false) // Too many fields
|
||||
expect(isValidCronExpression('60 10 1 * *')).toBe(false) // Invalid minute
|
||||
expect(isValidCronExpression('15 25 1 * *')).toBe(false) // Invalid hour
|
||||
expect(isValidCronExpression('15 10 32 * *')).toBe(false) // Invalid day
|
||||
expect(isValidCronExpression('15 10 1 13 *')).toBe(false) // Invalid month
|
||||
expect(isValidCronExpression('15 10 1 * 7')).toBe(false) // Invalid day of week
|
||||
})
|
||||
|
||||
test('handles edge cases', () => {
|
||||
expect(isValidCronExpression(' 15 10 1 * * ')).toBe(true) // Whitespace
|
||||
expect(isValidCronExpression('0 0 29 2 *')).toBe(true) // Feb 29 (valid in leap years)
|
||||
expect(isValidCronExpression('59 23 31 12 6')).toBe(true) // Max values
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseCronExpression', () => {
|
||||
beforeEach(() => {
|
||||
// Mock current time to make tests deterministic
|
||||
jest.useFakeTimers()
|
||||
jest.setSystemTime(new Date('2024-01-15T10:00:00Z'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
test('parses monthly expressions correctly', () => {
|
||||
const result = parseCronExpression('15 10 1 * *') // 1st day of every month at 10:15
|
||||
|
||||
expect(result).toHaveLength(5)
|
||||
expect(result[0].getDate()).toBe(1) // February 1st
|
||||
expect(result[0].getHours()).toBe(10)
|
||||
expect(result[0].getMinutes()).toBe(15)
|
||||
expect(result[1].getDate()).toBe(1) // March 1st
|
||||
expect(result[2].getDate()).toBe(1) // April 1st
|
||||
})
|
||||
|
||||
test('parses weekly expressions correctly', () => {
|
||||
const result = parseCronExpression('30 14 * * 1') // Every Monday at 14:30
|
||||
|
||||
expect(result).toHaveLength(5)
|
||||
// Should find next 5 Mondays
|
||||
result.forEach((date) => {
|
||||
expect(date.getDay()).toBe(1) // Monday
|
||||
expect(date.getHours()).toBe(14)
|
||||
expect(date.getMinutes()).toBe(30)
|
||||
})
|
||||
})
|
||||
|
||||
test('parses daily expressions correctly', () => {
|
||||
const result = parseCronExpression('0 9 * * *') // Every day at 9:00
|
||||
|
||||
expect(result).toHaveLength(5)
|
||||
result.forEach((date) => {
|
||||
expect(date.getHours()).toBe(9)
|
||||
expect(date.getMinutes()).toBe(0)
|
||||
})
|
||||
|
||||
// Should be consecutive days (starting from tomorrow since current time is 10:00)
|
||||
for (let i = 1; i < result.length; i++) {
|
||||
const prevDate = new Date(result[i - 1])
|
||||
const currDate = new Date(result[i])
|
||||
const dayDiff = (currDate.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24)
|
||||
expect(dayDiff).toBe(1)
|
||||
}
|
||||
})
|
||||
|
||||
test('handles complex cron expressions with ranges', () => {
|
||||
const result = parseCronExpression('0 9-17 * * 1-5') // Weekdays, 9-17 hours
|
||||
|
||||
expect(result).toHaveLength(5)
|
||||
result.forEach((date) => {
|
||||
expect(date.getDay()).toBeGreaterThanOrEqual(1) // Monday
|
||||
expect(date.getDay()).toBeLessThanOrEqual(5) // Friday
|
||||
expect(date.getHours()).toBeGreaterThanOrEqual(9)
|
||||
expect(date.getHours()).toBeLessThanOrEqual(17)
|
||||
expect(date.getMinutes()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('handles step expressions', () => {
|
||||
const result = parseCronExpression('*/15 * * * *') // Every 15 minutes
|
||||
|
||||
expect(result).toHaveLength(5)
|
||||
result.forEach((date) => {
|
||||
expect(date.getMinutes() % 15).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('handles list expressions', () => {
|
||||
const result = parseCronExpression('0 0 1,15 * *') // 1st and 15th of each month
|
||||
|
||||
expect(result).toHaveLength(5)
|
||||
result.forEach((date) => {
|
||||
expect([1, 15]).toContain(date.getDate())
|
||||
expect(date.getHours()).toBe(0)
|
||||
expect(date.getMinutes()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('handles expressions that span multiple months', () => {
|
||||
// Test with an expression that might not have many matches in current month
|
||||
const result = parseCronExpression('0 12 29 * *') // 29th of each month at noon
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result.length).toBeLessThanOrEqual(5)
|
||||
result.forEach((date) => {
|
||||
expect(date.getDate()).toBe(29)
|
||||
expect(date.getHours()).toBe(12)
|
||||
expect(date.getMinutes()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('returns empty array for invalid expressions', () => {
|
||||
expect(parseCronExpression('')).toEqual([])
|
||||
expect(parseCronExpression('invalid')).toEqual([])
|
||||
expect(parseCronExpression('60 10 1 * *')).toEqual([])
|
||||
expect(parseCronExpression('15 25 1 * *')).toEqual([])
|
||||
})
|
||||
|
||||
test('handles edge case: February 29th in non-leap years', () => {
|
||||
// Set to a non-leap year
|
||||
jest.setSystemTime(new Date('2023-01-15T10:00:00Z'))
|
||||
|
||||
const result = parseCronExpression('0 12 29 2 *') // Feb 29th at noon
|
||||
|
||||
// Should return empty or skip 2023 and find 2024
|
||||
if (result.length > 0) {
|
||||
result.forEach((date) => {
|
||||
expect(date.getMonth()).toBe(1) // February
|
||||
expect(date.getDate()).toBe(29)
|
||||
// Should be in a leap year
|
||||
const year = date.getFullYear()
|
||||
expect(year % 4).toBe(0)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('sorts results chronologically', () => {
|
||||
const result = parseCronExpression('0 */6 * * *') // Every 6 hours
|
||||
|
||||
expect(result).toHaveLength(5)
|
||||
for (let i = 1; i < result.length; i++)
|
||||
expect(result[i].getTime()).toBeGreaterThan(result[i - 1].getTime())
|
||||
})
|
||||
|
||||
test('excludes past times', () => {
|
||||
// Set current time to 15:30
|
||||
jest.setSystemTime(new Date('2024-01-15T15:30:00Z'))
|
||||
|
||||
const result = parseCronExpression('0 10 * * *') // Daily at 10:00
|
||||
|
||||
expect(result).toHaveLength(5)
|
||||
result.forEach((date) => {
|
||||
expect(date.getTime()).toBeGreaterThan(Date.now())
|
||||
})
|
||||
|
||||
// First result should be tomorrow since today's 10:00 has passed
|
||||
expect(result[0].getDate()).toBe(16)
|
||||
})
|
||||
|
||||
test('handles midnight expressions correctly', () => {
|
||||
const result = parseCronExpression('0 0 * * *') // Daily at midnight
|
||||
|
||||
expect(result).toHaveLength(5)
|
||||
result.forEach((date) => {
|
||||
expect(date.getHours()).toBe(0)
|
||||
expect(date.getMinutes()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('handles year boundary correctly', () => {
|
||||
// Set to end of December
|
||||
jest.setSystemTime(new Date('2024-12-30T10:00:00Z'))
|
||||
|
||||
const result = parseCronExpression('0 12 1 * *') // 1st of every month at noon
|
||||
|
||||
expect(result).toHaveLength(5)
|
||||
// Should include January 1st of next year
|
||||
const nextYear = result.find(date => date.getFullYear() === 2025)
|
||||
expect(nextYear).toBeDefined()
|
||||
if (nextYear) {
|
||||
expect(nextYear.getMonth()).toBe(0) // January
|
||||
expect(nextYear.getDate()).toBe(1)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('performance tests', () => {
|
||||
test('performs well for complex expressions', () => {
|
||||
const start = performance.now()
|
||||
|
||||
// Test multiple complex expressions
|
||||
const expressions = [
|
||||
'*/5 9-17 * * 1-5', // Every 5 minutes, weekdays, business hours
|
||||
'0 */2 1,15 * *', // Every 2 hours on 1st and 15th
|
||||
'30 14 * * 1,3,5', // Mon, Wed, Fri at 14:30
|
||||
'15,45 8-18 * * 1-5', // 15 and 45 minutes past the hour, weekdays
|
||||
]
|
||||
|
||||
expressions.forEach((expr) => {
|
||||
const result = parseCronExpression(expr)
|
||||
expect(result).toHaveLength(5)
|
||||
})
|
||||
|
||||
// Test quarterly expression separately (may return fewer than 5 results)
|
||||
const quarterlyResult = parseCronExpression('0 0 1 */3 *') // First day of every 3rd month
|
||||
expect(quarterlyResult.length).toBeGreaterThan(0)
|
||||
expect(quarterlyResult.length).toBeLessThanOrEqual(5)
|
||||
|
||||
const end = performance.now()
|
||||
|
||||
// Should complete within reasonable time (less than 100ms for all expressions)
|
||||
expect(end - start).toBeLessThan(100)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user