diff --git a/web/app/components/workflow/nodes/trigger-schedule/utils/cron-parser.spec.ts b/web/app/components/workflow/nodes/trigger-schedule/utils/cron-parser.spec.ts deleted file mode 100644 index 6eb55c7666..0000000000 --- a/web/app/components/workflow/nodes/trigger-schedule/utils/cron-parser.spec.ts +++ /dev/null @@ -1,520 +0,0 @@ -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('validates enhanced dayOfWeek syntax', () => { - expect(isValidCronExpression('0 9 * * 7')).toBe(true) // Sunday as 7 - expect(isValidCronExpression('0 9 * * SUN')).toBe(true) // Sunday abbreviation - expect(isValidCronExpression('0 9 * * MON')).toBe(true) // Monday abbreviation - expect(isValidCronExpression('0 9 * * MON-FRI')).toBe(true) // Range with abbreviations - expect(isValidCronExpression('0 9 * * SUN,WED,FRI')).toBe(true) // List with abbreviations - }) - - test('validates enhanced month syntax', () => { - expect(isValidCronExpression('0 9 1 JAN *')).toBe(true) // January abbreviation - expect(isValidCronExpression('0 9 1 DEC *')).toBe(true) // December abbreviation - expect(isValidCronExpression('0 9 1 JAN-MAR *')).toBe(true) // Range with abbreviations - expect(isValidCronExpression('0 9 1 JAN,JUN,DEC *')).toBe(true) // List with abbreviations - }) - - test('validates special characters', () => { - expect(isValidCronExpression('0 9 ? * 1')).toBe(true) // ? wildcard - expect(isValidCronExpression('0 9 L * *')).toBe(true) // Last day of month - expect(isValidCronExpression('0 9 * * 1#1')).toBe(true) // First Monday of month - expect(isValidCronExpression('0 9 * * 1L')).toBe(true) // Last Monday of month - }) - - test('validates predefined expressions', () => { - expect(isValidCronExpression('@yearly')).toBe(true) - expect(isValidCronExpression('@monthly')).toBe(true) - expect(isValidCronExpression('@weekly')).toBe(true) - expect(isValidCronExpression('@daily')).toBe(true) - expect(isValidCronExpression('@hourly')).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 * 8')).toBe(false) // Invalid day of week - expect(isValidCronExpression('15 10 1 INVALID *')).toBe(false) // Invalid month abbreviation - expect(isValidCronExpression('15 10 1 * INVALID')).toBe(false) // Invalid day abbreviation - expect(isValidCronExpression('@invalid')).toBe(false) // Invalid predefined expression - }) - - 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 - expect(isValidCronExpression('0 0 29 FEB *')).toBe(true) // Feb 29 with month abbreviation - expect(isValidCronExpression('59 23 31 DEC SAT')).toBe(true) // Max values with abbreviations - }) - }) - - 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) => { - // Since we're using UTC timezone in this test, the returned dates should - // be in the future relative to the current time - // Note: our implementation returns dates in "user timezone representation" - // but for UTC, this should match the expected behavior - 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('enhanced syntax tests', () => { - test('handles month abbreviations correctly', () => { - const result = parseCronExpression('0 12 1 JAN *') // First day of January at noon - - expect(result).toHaveLength(5) - result.forEach((date) => { - expect(date.getMonth()).toBe(0) // January - expect(date.getDate()).toBe(1) - expect(date.getHours()).toBe(12) - expect(date.getMinutes()).toBe(0) - }) - }) - - test('handles day abbreviations correctly', () => { - const result = parseCronExpression('0 14 * * MON') // Every Monday at 14:00 - - expect(result).toHaveLength(5) - result.forEach((date) => { - expect(date.getDay()).toBe(1) // Monday - expect(date.getHours()).toBe(14) - expect(date.getMinutes()).toBe(0) - }) - }) - - test('handles Sunday as both 0 and 7', () => { - const result0 = parseCronExpression('0 10 * * 0') // Sunday as 0 - const result7 = parseCronExpression('0 10 * * 7') // Sunday as 7 - const resultSUN = parseCronExpression('0 10 * * SUN') // Sunday as SUN - - expect(result0).toHaveLength(5) - expect(result7).toHaveLength(5) - expect(resultSUN).toHaveLength(5) - - // All should return Sundays - result0.forEach(date => expect(date.getDay()).toBe(0)) - result7.forEach(date => expect(date.getDay()).toBe(0)) - resultSUN.forEach(date => expect(date.getDay()).toBe(0)) - }) - - test('handles question mark wildcard', () => { - const resultStar = parseCronExpression('0 9 * * 1') // Using * - const resultQuestion = parseCronExpression('0 9 ? * 1') // Using ? - - expect(resultStar).toHaveLength(5) - expect(resultQuestion).toHaveLength(5) - - // Both should return Mondays at 9:00 - resultStar.forEach((date) => { - expect(date.getDay()).toBe(1) - expect(date.getHours()).toBe(9) - }) - resultQuestion.forEach((date) => { - expect(date.getDay()).toBe(1) - expect(date.getHours()).toBe(9) - }) - }) - - test('handles predefined expressions', () => { - const daily = parseCronExpression('@daily') - const weekly = parseCronExpression('@weekly') - const monthly = parseCronExpression('@monthly') - - expect(daily).toHaveLength(5) - expect(weekly).toHaveLength(5) - expect(monthly).toHaveLength(5) - - // @daily should be at midnight - daily.forEach((date) => { - expect(date.getHours()).toBe(0) - expect(date.getMinutes()).toBe(0) - }) - - // @weekly should be on Sundays at midnight - weekly.forEach((date) => { - expect(date.getDay()).toBe(0) // Sunday - expect(date.getHours()).toBe(0) - expect(date.getMinutes()).toBe(0) - }) - - // @monthly should be on the 1st of each month at midnight - monthly.forEach((date) => { - expect(date.getDate()).toBe(1) - expect(date.getHours()).toBe(0) - expect(date.getMinutes()).toBe(0) - }) - }) - }) - - describe('edge cases and error handling', () => { - test('handles complex month/day combinations', () => { - // Test Feb 29 with month abbreviation - const result = parseCronExpression('0 12 29 FEB *') - if (result.length > 0) { - result.forEach((date) => { - expect(date.getMonth()).toBe(1) // February - expect(date.getDate()).toBe(29) - // Should only occur in leap years - const year = date.getFullYear() - expect(year % 4).toBe(0) - }) - } - }) - - test('handles mixed syntax correctly', () => { - // Mix of numbers and abbreviations (using only dayOfMonth OR dayOfWeek, not both) - // Test 1: Month abbreviations with specific day - const result1 = parseCronExpression('30 14 15 JAN,JUN,DEC *') - expect(result1.length).toBeGreaterThan(0) - result1.forEach((date) => { - expect(date.getDate()).toBe(15) // Should be 15th day - expect([0, 5, 11]).toContain(date.getMonth()) // Jan, Jun, Dec - expect(date.getHours()).toBe(14) - expect(date.getMinutes()).toBe(30) - }) - - // Test 2: Month abbreviations with weekdays - const result2 = parseCronExpression('0 9 * JAN-MAR MON-FRI') - expect(result2.length).toBeGreaterThan(0) - result2.forEach((date) => { - // Should be weekday OR in Q1 months - const isWeekday = date.getDay() >= 1 && date.getDay() <= 5 - const isQ1 = [0, 1, 2].includes(date.getMonth()) - expect(isWeekday || isQ1).toBe(true) - expect(date.getHours()).toBe(9) - expect(date.getMinutes()).toBe(0) - }) - }) - - test('handles timezone edge cases', () => { - // Test with different timezones - const utcResult = parseCronExpression('0 12 * * *', 'UTC') - const nyResult = parseCronExpression('0 12 * * *', 'America/New_York') - const tokyoResult = parseCronExpression('0 12 * * *', 'Asia/Tokyo') - - expect(utcResult).toHaveLength(5) - expect(nyResult).toHaveLength(5) - expect(tokyoResult).toHaveLength(5) - - // All should be at noon in their respective timezones - utcResult.forEach(date => expect(date.getHours()).toBe(12)) - nyResult.forEach(date => expect(date.getHours()).toBe(12)) - tokyoResult.forEach(date => expect(date.getHours()).toBe(12)) - }) - - test('timezone compatibility and DST handling', () => { - // Test DST boundary scenarios - jest.useFakeTimers() - - try { - // Test 1: DST spring forward (March 2024) - America/New_York - jest.setSystemTime(new Date('2024-03-08T10:00:00Z')) - const springDST = parseCronExpression('0 2 * * *', 'America/New_York') - expect(springDST).toHaveLength(5) - springDST.forEach(date => expect([2, 3]).toContain(date.getHours())) - - // Test 2: DST fall back (November 2024) - America/New_York - jest.setSystemTime(new Date('2024-11-01T10:00:00Z')) - const fallDST = parseCronExpression('0 1 * * *', 'America/New_York') - expect(fallDST).toHaveLength(5) - fallDST.forEach(date => expect(date.getHours()).toBe(1)) - - // Test 3: Cross-timezone consistency on same UTC moment - jest.setSystemTime(new Date('2024-06-15T12:00:00Z')) - const utcNoon = parseCronExpression('0 12 * * *', 'UTC') - const nycMorning = parseCronExpression('0 8 * * *', 'America/New_York') // 8 AM NYC = 12 PM UTC in summer - const tokyoEvening = parseCronExpression('0 21 * * *', 'Asia/Tokyo') // 9 PM Tokyo = 12 PM UTC - - expect(utcNoon).toHaveLength(5) - expect(nycMorning).toHaveLength(5) - expect(tokyoEvening).toHaveLength(5) - - // Verify timezone consistency - all should represent the same UTC moments - const utcTime = utcNoon[0] - const nycTime = nycMorning[0] - const tokyoTime = tokyoEvening[0] - - expect(utcTime.getHours()).toBe(12) - expect(nycTime.getHours()).toBe(8) - expect(tokyoTime.getHours()).toBe(21) - } - finally { - jest.useRealTimers() - } - }) - - test('backward compatibility with execution-time-calculator timezone logic', () => { - // Simulate the exact usage pattern from execution-time-calculator.ts:47 - const mockData = { - cron_expression: '30 14 * * 1-5', // 2:30 PM weekdays - timezone: 'America/New_York', - } - - // This is the exact call from execution-time-calculator.ts - const results = parseCronExpression(mockData.cron_expression, mockData.timezone) - expect(results).toHaveLength(5) - - results.forEach((date) => { - // Should be weekdays (1-5) - expect(date.getDay()).toBeGreaterThanOrEqual(1) - expect(date.getDay()).toBeLessThanOrEqual(5) - - // Should be 2:30 PM in the user's timezone representation - expect(date.getHours()).toBe(14) - expect(date.getMinutes()).toBe(30) - expect(date.getSeconds()).toBe(0) - - // Should be Date objects (not CronDate or other types) - expect(date).toBeInstanceOf(Date) - - // Should be in the future (relative to test time) - expect(date.getTime()).toBeGreaterThan(Date.now()) - }) - }) - - test('edge case timezone handling', () => { - // Test uncommon but valid timezones - const australiaResult = parseCronExpression('0 15 * * *', 'Australia/Sydney') - const indiaResult = parseCronExpression('0 9 * * *', 'Asia/Kolkata') // UTC+5:30 - const alaskaResult = parseCronExpression('0 6 * * *', 'America/Anchorage') - - expect(australiaResult).toHaveLength(5) - expect(indiaResult).toHaveLength(5) - expect(alaskaResult).toHaveLength(5) - - australiaResult.forEach(date => expect(date.getHours()).toBe(15)) - indiaResult.forEach(date => expect(date.getHours()).toBe(9)) - alaskaResult.forEach(date => expect(date.getHours()).toBe(6)) - - // Test invalid timezone graceful handling - const invalidTzResult = parseCronExpression('0 12 * * *', 'Invalid/Timezone') - // Should either return empty array or handle gracefully - expect(Array.isArray(invalidTzResult)).toBe(true) - }) - - test('gracefully handles invalid enhanced syntax', () => { - // Invalid but close to valid expressions - expect(parseCronExpression('0 12 * JANUARY *')).toEqual([]) // Full month name - expect(parseCronExpression('0 12 * * MONDAY')).toEqual([]) // Full day name - expect(parseCronExpression('0 12 32 JAN *')).toEqual([]) // Invalid day with valid month - expect(parseCronExpression('@invalid')).toEqual([]) // Invalid predefined - }) - }) - - describe('performance tests', () => { - test('performs well for complex expressions', () => { - const start = performance.now() - - // Test multiple complex expressions including new syntax - 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 - '0 9 * JAN-MAR MON-FRI', // Weekdays in Q1 at 9:00 - '0 12 ? * SUN', // Sundays at noon using ? - '@daily', // Predefined expression - '@weekly', // Predefined expression - ] - - expressions.forEach((expr) => { - const result = parseCronExpression(expr) - expect(result.length).toBeGreaterThan(0) - expect(result.length).toBeLessThanOrEqual(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 150ms for all expressions) - expect(end - start).toBeLessThan(150) - }) - }) -})