Yeuoly b76e17b25d
feat: introduce trigger functionality (#27644)
Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: Stream <Stream_2@qq.com>
Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: zhsama <torvalds@linux.do>
Co-authored-by: Harry <xh001x@hotmail.com>
Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: yessenia <yessenia.contact@gmail.com>
Co-authored-by: hjlarry <hjlarry@163.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: WTW0313 <twwu@dify.ai>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-12 17:59:37 +08:00

350 lines
13 KiB
TypeScript

import { isValidCronExpression, parseCronExpression } from './cron-parser'
import { getNextExecutionTime, getNextExecutionTimes } from './execution-time-calculator'
import type { ScheduleTriggerNodeType } from '../types'
// Comprehensive integration tests for cron-parser and execution-time-calculator compatibility
describe('cron-parser + execution-time-calculator integration', () => {
beforeAll(() => {
jest.useFakeTimers()
jest.setSystemTime(new Date('2024-01-15T10:00:00Z'))
})
afterAll(() => {
jest.useRealTimers()
})
const createCronData = (overrides: Partial<ScheduleTriggerNodeType> = {}): ScheduleTriggerNodeType => ({
id: 'test-cron',
type: 'schedule-trigger',
mode: 'cron',
frequency: 'daily',
timezone: 'UTC',
...overrides,
})
describe('backward compatibility validation', () => {
it('maintains exact behavior for legacy cron expressions', () => {
const legacyExpressions = [
'15 10 1 * *', // Monthly 1st at 10:15
'0 0 * * 0', // Weekly Sunday midnight
'*/5 * * * *', // Every 5 minutes
'0 9-17 * * 1-5', // Business hours weekdays
'30 14 * * 1', // Monday 14:30
'0 0 1,15 * *', // 1st and 15th midnight
]
legacyExpressions.forEach((expression) => {
// Test direct cron-parser usage
const directResult = parseCronExpression(expression, 'UTC')
expect(directResult).toHaveLength(5)
expect(isValidCronExpression(expression)).toBe(true)
// Test through execution-time-calculator
const data = createCronData({ cron_expression: expression })
const calculatorResult = getNextExecutionTimes(data, 5)
expect(calculatorResult).toHaveLength(5)
// Results should be identical
directResult.forEach((directDate, index) => {
const calcDate = calculatorResult[index]
expect(calcDate.getTime()).toBe(directDate.getTime())
expect(calcDate.getHours()).toBe(directDate.getHours())
expect(calcDate.getMinutes()).toBe(directDate.getMinutes())
})
})
})
it('validates timezone handling consistency', () => {
const timezones = ['UTC', 'America/New_York', 'Asia/Tokyo', 'Europe/London']
const expression = '0 12 * * *' // Daily noon
timezones.forEach((timezone) => {
// Direct cron-parser call
const directResult = parseCronExpression(expression, timezone)
// Through execution-time-calculator
const data = createCronData({ cron_expression: expression, timezone })
const calculatorResult = getNextExecutionTimes(data, 5)
expect(directResult).toHaveLength(5)
expect(calculatorResult).toHaveLength(5)
// All results should show noon (12:00) in their respective timezone
directResult.forEach(date => expect(date.getHours()).toBe(12))
calculatorResult.forEach(date => expect(date.getHours()).toBe(12))
// Cross-validation: results should be identical
directResult.forEach((directDate, index) => {
expect(calculatorResult[index].getTime()).toBe(directDate.getTime())
})
})
})
it('error handling consistency', () => {
const invalidExpressions = [
'', // Empty string
' ', // Whitespace only
'60 10 1 * *', // Invalid minute
'15 25 1 * *', // Invalid hour
'15 10 32 * *', // Invalid day
'15 10 1 13 *', // Invalid month
'15 10 1', // Too few fields
'15 10 1 * * *', // Too many fields
'invalid expression', // Completely invalid
]
invalidExpressions.forEach((expression) => {
// Direct cron-parser calls
expect(isValidCronExpression(expression)).toBe(false)
expect(parseCronExpression(expression, 'UTC')).toEqual([])
// Through execution-time-calculator
const data = createCronData({ cron_expression: expression })
const result = getNextExecutionTimes(data, 5)
expect(result).toEqual([])
// getNextExecutionTime should return '--' for invalid cron
const timeString = getNextExecutionTime(data)
expect(timeString).toBe('--')
})
})
})
describe('enhanced features integration', () => {
it('month and day abbreviations work end-to-end', () => {
const enhancedExpressions = [
{ expr: '0 9 1 JAN *', month: 0, day: 1, hour: 9 }, // January 1st 9 AM
{ expr: '0 15 * * MON', weekday: 1, hour: 15 }, // Monday 3 PM
{ expr: '30 10 15 JUN,DEC *', month: [5, 11], day: 15, hour: 10, minute: 30 }, // Jun/Dec 15th
{ expr: '0 12 * JAN-MAR *', month: [0, 1, 2], hour: 12 }, // Q1 noon
]
enhancedExpressions.forEach(({ expr, month, day, weekday, hour, minute = 0 }) => {
// Validate through both paths
expect(isValidCronExpression(expr)).toBe(true)
const directResult = parseCronExpression(expr, 'UTC')
const data = createCronData({ cron_expression: expr })
const calculatorResult = getNextExecutionTimes(data, 3)
expect(directResult.length).toBeGreaterThan(0)
expect(calculatorResult.length).toBeGreaterThan(0)
// Validate expected properties
const validateDate = (date: Date) => {
expect(date.getHours()).toBe(hour)
expect(date.getMinutes()).toBe(minute)
if (month !== undefined) {
if (Array.isArray(month))
expect(month).toContain(date.getMonth())
else
expect(date.getMonth()).toBe(month)
}
if (day !== undefined)
expect(date.getDate()).toBe(day)
if (weekday !== undefined)
expect(date.getDay()).toBe(weekday)
}
directResult.forEach(validateDate)
calculatorResult.forEach(validateDate)
})
})
it('predefined expressions work through execution-time-calculator', () => {
const predefExpressions = [
{ expr: '@daily', hour: 0, minute: 0 },
{ expr: '@weekly', hour: 0, minute: 0, weekday: 0 }, // Sunday
{ expr: '@monthly', hour: 0, minute: 0, day: 1 }, // 1st of month
{ expr: '@yearly', hour: 0, minute: 0, month: 0, day: 1 }, // Jan 1st
]
predefExpressions.forEach(({ expr, hour, minute, weekday, day, month }) => {
expect(isValidCronExpression(expr)).toBe(true)
const data = createCronData({ cron_expression: expr })
const result = getNextExecutionTimes(data, 3)
expect(result.length).toBeGreaterThan(0)
result.forEach((date) => {
expect(date.getHours()).toBe(hour)
expect(date.getMinutes()).toBe(minute)
if (weekday !== undefined) expect(date.getDay()).toBe(weekday)
if (day !== undefined) expect(date.getDate()).toBe(day)
if (month !== undefined) expect(date.getMonth()).toBe(month)
})
})
})
it('special characters integration', () => {
const specialExpressions = [
'0 9 ? * 1', // ? wildcard for day
'0 12 * * 7', // Sunday as 7
'0 15 L * *', // Last day of month
]
specialExpressions.forEach((expr) => {
// Should validate and parse successfully
expect(isValidCronExpression(expr)).toBe(true)
const directResult = parseCronExpression(expr, 'UTC')
const data = createCronData({ cron_expression: expr })
const calculatorResult = getNextExecutionTimes(data, 2)
expect(directResult.length).toBeGreaterThan(0)
expect(calculatorResult.length).toBeGreaterThan(0)
// Results should be consistent
expect(calculatorResult[0].getHours()).toBe(directResult[0].getHours())
expect(calculatorResult[0].getMinutes()).toBe(directResult[0].getMinutes())
})
})
})
describe('DST and timezone edge cases', () => {
it('handles DST transitions consistently', () => {
// Test around DST spring forward (March 2024)
jest.setSystemTime(new Date('2024-03-08T10:00:00Z'))
const expression = '0 2 * * *' // 2 AM daily (problematic during DST)
const timezone = 'America/New_York'
const directResult = parseCronExpression(expression, timezone)
const data = createCronData({ cron_expression: expression, timezone })
const calculatorResult = getNextExecutionTimes(data, 5)
expect(directResult.length).toBeGreaterThan(0)
expect(calculatorResult.length).toBeGreaterThan(0)
// Both should handle DST gracefully
// During DST spring forward, 2 AM becomes 3 AM - this is correct behavior
directResult.forEach(date => expect([2, 3]).toContain(date.getHours()))
calculatorResult.forEach(date => expect([2, 3]).toContain(date.getHours()))
// Results should be identical
directResult.forEach((directDate, index) => {
expect(calculatorResult[index].getTime()).toBe(directDate.getTime())
})
})
it('complex timezone scenarios', () => {
const scenarios = [
{ tz: 'Asia/Kolkata', expr: '30 14 * * *', expectedHour: 14, expectedMinute: 30 }, // UTC+5:30
{ tz: 'Australia/Adelaide', expr: '0 8 * * *', expectedHour: 8, expectedMinute: 0 }, // UTC+9:30/+10:30
{ tz: 'Pacific/Kiritimati', expr: '0 12 * * *', expectedHour: 12, expectedMinute: 0 }, // UTC+14
]
scenarios.forEach(({ tz, expr, expectedHour, expectedMinute }) => {
const directResult = parseCronExpression(expr, tz)
const data = createCronData({ cron_expression: expr, timezone: tz })
const calculatorResult = getNextExecutionTimes(data, 2)
expect(directResult.length).toBeGreaterThan(0)
expect(calculatorResult.length).toBeGreaterThan(0)
// Validate expected time
directResult.forEach((date) => {
expect(date.getHours()).toBe(expectedHour)
expect(date.getMinutes()).toBe(expectedMinute)
})
calculatorResult.forEach((date) => {
expect(date.getHours()).toBe(expectedHour)
expect(date.getMinutes()).toBe(expectedMinute)
})
// Cross-validate consistency
expect(calculatorResult[0].getTime()).toBe(directResult[0].getTime())
})
})
})
describe('performance and reliability', () => {
it('handles high-frequency expressions efficiently', () => {
const highFreqExpressions = [
'*/1 * * * *', // Every minute
'*/5 * * * *', // Every 5 minutes
'0,15,30,45 * * * *', // Every 15 minutes
]
highFreqExpressions.forEach((expr) => {
const start = performance.now()
// Test both direct and through calculator
const directResult = parseCronExpression(expr, 'UTC')
const data = createCronData({ cron_expression: expr })
const calculatorResult = getNextExecutionTimes(data, 5)
const end = performance.now()
expect(directResult).toHaveLength(5)
expect(calculatorResult).toHaveLength(5)
expect(end - start).toBeLessThan(100) // Should be fast
// Results should be consistent
directResult.forEach((directDate, index) => {
expect(calculatorResult[index].getTime()).toBe(directDate.getTime())
})
})
})
it('stress test with complex expressions', () => {
const complexExpressions = [
'15,45 8-18 1,15 JAN-MAR MON-FRI', // Business hours, specific days, Q1, weekdays
'0 */2 ? * SUN#1,SUN#3', // First and third Sunday, every 2 hours
'30 9 L * *', // Last day of month, 9:30 AM
]
complexExpressions.forEach((expr) => {
if (isValidCronExpression(expr)) {
const directResult = parseCronExpression(expr, 'America/New_York')
const data = createCronData({
cron_expression: expr,
timezone: 'America/New_York',
})
const calculatorResult = getNextExecutionTimes(data, 3)
expect(directResult.length).toBeGreaterThan(0)
expect(calculatorResult.length).toBeGreaterThan(0)
// Validate consistency where results exist
const minLength = Math.min(directResult.length, calculatorResult.length)
for (let i = 0; i < minLength; i++)
expect(calculatorResult[i].getTime()).toBe(directResult[i].getTime())
}
})
})
})
describe('format compatibility', () => {
it('getNextExecutionTime formatting consistency', () => {
const testCases = [
{ expr: '0 9 * * *', timezone: 'UTC' },
{ expr: '30 14 * * 1-5', timezone: 'America/New_York' },
{ expr: '@daily', timezone: 'Asia/Tokyo' },
]
testCases.forEach(({ expr, timezone }) => {
const data = createCronData({ cron_expression: expr, timezone })
const timeString = getNextExecutionTime(data)
// Should return a formatted time string, not '--'
expect(timeString).not.toBe('--')
expect(typeof timeString).toBe('string')
expect(timeString.length).toBeGreaterThan(0)
// Should contain expected format elements
expect(timeString).toMatch(/\d+:\d+/) // Time format
expect(timeString).toMatch(/AM|PM/) // 12-hour format
expect(timeString).toMatch(/\d{4}/) // Year
})
})
})
})