@@ -169,3 +169,277 @@ describe("calculateNextScheduledTimestamp", () => {
169169 expect ( nextRun . getTime ( ) ) . toBeGreaterThan ( Date . now ( ) ) ;
170170 } ) ;
171171} ) ;
172+
173+ describe ( "calculateNextScheduledTimestamp - Fuzzy Testing" , ( ) => {
174+ beforeEach ( ( ) => {
175+ vi . useFakeTimers ( ) ;
176+ vi . setSystemTime ( new Date ( "2024-01-15T12:30:00.000Z" ) ) ; // Monday, mid-day
177+ } ) ;
178+
179+ afterEach ( ( ) => {
180+ vi . useRealTimers ( ) ;
181+ } ) ;
182+
183+ // Helper function to generate random cron expressions
184+ function generateRandomCronExpression ( ) : string {
185+ const patterns = [
186+ // Minutes
187+ "*/1 * * * *" , // Every minute
188+ "*/5 * * * *" , // Every 5 minutes
189+ "*/15 * * * *" , // Every 15 minutes
190+ "30 * * * *" , // Every hour at 30 minutes
191+
192+ // Hours
193+ "0 * * * *" , // Every hour
194+ "0 */2 * * *" , // Every 2 hours
195+ "0 */6 * * *" , // Every 6 hours
196+ "0 9 * * *" , // Daily at 9 AM
197+ "0 14 * * *" , // Daily at 2 PM
198+
199+ // Days
200+ "0 9 * * 1" , // Every Monday at 9 AM
201+ "0 14 * * 5" , // Every Friday at 2 PM
202+ "0 10 * * 1-5" , // Weekdays at 10 AM
203+ "0 0 * * 0" , // Every Sunday at midnight
204+
205+ // Weekly/Monthly
206+ "0 9 * * MON" , // Every Monday at 9 AM
207+ "0 12 1 * *" , // First of every month at noon
208+ "0 15 15 * *" , // 15th of every month at 3 PM
209+
210+ // Complex patterns
211+ "0 9,17 * * 1-5" , // 9 AM and 5 PM on weekdays
212+ "30 8-18/2 * * *" , // Every 2 hours from 8:30 AM to 6:30 PM
213+ ] ;
214+
215+ return patterns [ Math . floor ( Math . random ( ) * patterns . length ) ] ;
216+ }
217+
218+ // Helper function to generate random timestamps
219+ function generateRandomTimestamp ( ) : Date {
220+ const now = Date . now ( ) ;
221+ const possibilities = [
222+ // Recent timestamps (within last few hours)
223+ new Date ( now - Math . random ( ) * 4 * 60 * 60 * 1000 ) ,
224+
225+ // Old timestamps (days ago)
226+ new Date ( now - Math . random ( ) * 30 * 24 * 60 * 60 * 1000 ) ,
227+
228+ // Very old timestamps (months/years ago)
229+ new Date ( now - Math . random ( ) * 365 * 24 * 60 * 60 * 1000 ) ,
230+
231+ // Extremely old timestamps
232+ new Date ( now - Math . random ( ) * 10 * 365 * 24 * 60 * 60 * 1000 ) ,
233+
234+ // Edge case: exactly now
235+ new Date ( now ) ,
236+
237+ // Edge case: 1ms ago
238+ new Date ( now - 1 ) ,
239+
240+ // Edge case: future timestamp (should be handled gracefully)
241+ new Date ( now + Math . random ( ) * 24 * 60 * 60 * 1000 ) ,
242+ ] ;
243+
244+ return possibilities [ Math . floor ( Math . random ( ) * possibilities . length ) ] ;
245+ }
246+
247+ test ( "fuzzy test: invariants should hold for random scenarios" , ( ) => {
248+ const numTests = 50 ;
249+
250+ for ( let i = 0 ; i < numTests ; i ++ ) {
251+ const schedule = generateRandomCronExpression ( ) ;
252+ const lastTimestamp = generateRandomTimestamp ( ) ;
253+ const timezone = Math . random ( ) > 0.7 ? "America/New_York" : null ;
254+
255+ try {
256+ const startTime = performance . now ( ) ;
257+ const nextRun = calculateNextScheduledTimestamp ( schedule , timezone , lastTimestamp ) ;
258+ const duration = performance . now ( ) - startTime ;
259+
260+ // Invariant 1: Result should always be a valid Date
261+ expect ( nextRun ) . toBeInstanceOf ( Date ) ;
262+ expect ( nextRun . getTime ( ) ) . not . toBeNaN ( ) ;
263+
264+ // Invariant 2: Result should be in the future (or equal to now if lastTimestamp was in future)
265+ if ( lastTimestamp . getTime ( ) <= Date . now ( ) ) {
266+ expect ( nextRun . getTime ( ) ) . toBeGreaterThan ( Date . now ( ) ) ;
267+ }
268+
269+ // Invariant 3: Performance should be reasonable (no event loop lag)
270+ expect ( duration ) . toBeLessThan ( 100 ) ; // Should complete within 100ms
271+
272+ // Invariant 4: Function should be deterministic
273+ const nextRun2 = calculateNextScheduledTimestamp ( schedule , timezone , lastTimestamp ) ;
274+ expect ( nextRun . getTime ( ) ) . toBe ( nextRun2 . getTime ( ) ) ;
275+ } catch ( error ) {
276+ // If there's an error, log the inputs for debugging
277+ console . error (
278+ `Failed with schedule: ${ schedule } , lastTimestamp: ${ lastTimestamp . toISOString ( ) } , timezone: ${ timezone } `
279+ ) ;
280+ throw error ;
281+ }
282+ }
283+ } ) ;
284+
285+ test ( "fuzzy test: performance under stress with frequent schedules" , ( ) => {
286+ const frequentSchedules = [ "* * * * *" , "*/2 * * * *" , "*/5 * * * *" ] ;
287+
288+ for ( let i = 0 ; i < 20 ; i ++ ) {
289+ const schedule = frequentSchedules [ Math . floor ( Math . random ( ) * frequentSchedules . length ) ] ;
290+
291+ // Generate very old timestamps that would cause many iterations without optimization
292+ const veryOldTimestamp = new Date ( Date . now ( ) - Math . random ( ) * 5 * 365 * 24 * 60 * 60 * 1000 ) ;
293+
294+ const startTime = performance . now ( ) ;
295+ const nextRun = calculateNextScheduledTimestamp ( schedule , null , veryOldTimestamp ) ;
296+ const duration = performance . now ( ) - startTime ;
297+
298+ // Should complete quickly even with very old timestamps
299+ expect ( duration ) . toBeLessThan ( 20 ) ;
300+ expect ( nextRun . getTime ( ) ) . toBeGreaterThan ( Date . now ( ) ) ;
301+ }
302+ } ) ;
303+
304+ test ( "fuzzy test: edge cases around daylight saving time" , ( ) => {
305+ // Test around DST transition dates (spring forward, fall back)
306+ const dstTestDates = [
307+ "2024-03-10T06:00:00.000Z" , // Around US spring DST
308+ "2024-11-03T06:00:00.000Z" , // Around US fall DST
309+ "2024-03-31T01:00:00.000Z" , // Around EU spring DST
310+ "2024-10-27T01:00:00.000Z" , // Around EU fall DST
311+ ] ;
312+
313+ const timezones = [ "America/New_York" , "Europe/London" , "America/Los_Angeles" ] ;
314+
315+ for ( let i = 0 ; i < 15 ; i ++ ) {
316+ const schedule = generateRandomCronExpression ( ) ;
317+ const testDate = dstTestDates [ Math . floor ( Math . random ( ) * dstTestDates . length ) ] ;
318+ const timezone = timezones [ Math . floor ( Math . random ( ) * timezones . length ) ] ;
319+
320+ vi . setSystemTime ( new Date ( testDate ) ) ;
321+
322+ const lastTimestamp = new Date ( Date . now ( ) - Math . random ( ) * 7 * 24 * 60 * 60 * 1000 ) ;
323+
324+ const nextRun = calculateNextScheduledTimestamp ( schedule , timezone , lastTimestamp ) ;
325+
326+ // Should handle DST transitions gracefully
327+ expect ( nextRun ) . toBeInstanceOf ( Date ) ;
328+ expect ( nextRun . getTime ( ) ) . not . toBeNaN ( ) ;
329+ expect ( nextRun . getTime ( ) ) . toBeGreaterThan ( Date . now ( ) ) ;
330+ }
331+ } ) ;
332+
333+ test ( "fuzzy test: boundary conditions" , ( ) => {
334+ const boundaryTests = [
335+ // End of month transitions
336+ { time : "2024-02-29T23:59:59.000Z" , schedule : "0 0 1 * *" } , // Leap year to March 1st
337+ { time : "2024-04-30T23:59:59.000Z" , schedule : "0 0 31 * *" } , // April 30th to May 31st
338+
339+ // End of year
340+ { time : "2024-12-31T23:59:59.000Z" , schedule : "0 0 1 1 *" } , // New Year
341+
342+ // Weekday transitions
343+ { time : "2024-01-15T06:59:59.000Z" , schedule : "0 7 * * MON" } , // Monday morning
344+
345+ // Hour boundaries
346+ { time : "2024-01-15T11:59:59.000Z" , schedule : "0 12 * * *" } , // Noon
347+ { time : "2024-01-15T23:59:59.000Z" , schedule : "0 0 * * *" } , // Midnight
348+ ] ;
349+
350+ for ( const test of boundaryTests ) {
351+ vi . setSystemTime ( new Date ( test . time ) ) ;
352+
353+ // Test with timestamps both before and after the boundary
354+ const beforeBoundary = new Date ( Date . now ( ) - 1000 ) ;
355+ const afterBoundary = new Date ( Date . now ( ) + 1000 ) ;
356+
357+ const nextRun1 = calculateNextScheduledTimestamp ( test . schedule , null , beforeBoundary ) ;
358+ const nextRun2 = calculateNextScheduledTimestamp ( test . schedule , null , afterBoundary ) ;
359+
360+ expect ( nextRun1 . getTime ( ) ) . toBeGreaterThan ( Date . now ( ) ) ;
361+ expect ( nextRun2 . getTime ( ) ) . toBeGreaterThan ( Date . now ( ) ) ;
362+ }
363+ } ) ;
364+
365+ test ( "fuzzy test: complex cron expressions" , ( ) => {
366+ const complexSchedules = [
367+ "0 9,17 * * 1-5" , // 9 AM and 5 PM on weekdays
368+ "30 8-18/2 * * *" , // Every 2 hours from 8:30 AM to 6:30 PM
369+ "0 0 1,15 * *" , // 1st and 15th of every month
370+ "0 12 * * MON#2" , // Second Monday of every month (if supported)
371+ "0 0 L * *" , // Last day of month (if supported)
372+ "15,45 */2 * * *" , // 15 and 45 minutes past every 2nd hour
373+ ] ;
374+
375+ for ( let i = 0 ; i < 30 ; i ++ ) {
376+ const schedule = complexSchedules [ Math . floor ( Math . random ( ) * complexSchedules . length ) ] ;
377+ const lastTimestamp = generateRandomTimestamp ( ) ;
378+
379+ try {
380+ const startTime = performance . now ( ) ;
381+ const nextRun = calculateNextScheduledTimestamp ( schedule , null , lastTimestamp ) ;
382+ const duration = performance . now ( ) - startTime ;
383+
384+ expect ( nextRun ) . toBeInstanceOf ( Date ) ;
385+ expect ( duration ) . toBeLessThan ( 100 ) ;
386+
387+ if ( lastTimestamp . getTime ( ) <= Date . now ( ) ) {
388+ expect ( nextRun . getTime ( ) ) . toBeGreaterThan ( Date . now ( ) ) ;
389+ }
390+ } catch ( error ) {
391+ // Some complex expressions might not be supported, that's okay
392+ if ( ! error . message . includes ( "not supported" ) && ! error . message . includes ( "Invalid" ) ) {
393+ console . error ( `Unexpected error with schedule: ${ schedule } ` ) ;
394+ throw error ;
395+ }
396+ }
397+ }
398+ } ) ;
399+
400+ test ( "fuzzy test: consistency across multiple calls" , ( ) => {
401+ // Test that the function is consistent when called multiple times with same inputs
402+ for ( let i = 0 ; i < 20 ; i ++ ) {
403+ const schedule = generateRandomCronExpression ( ) ;
404+ const lastTimestamp = generateRandomTimestamp ( ) ;
405+ const timezone = Math . random ( ) > 0.5 ? "UTC" : "America/New_York" ;
406+
407+ const results : Date [ ] = [ ] ;
408+ for ( let j = 0 ; j < 5 ; j ++ ) {
409+ results . push ( calculateNextScheduledTimestamp ( schedule , timezone , lastTimestamp ) ) ;
410+ }
411+
412+ // All results should be identical
413+ for ( let j = 1 ; j < results . length ; j ++ ) {
414+ expect ( results [ j ] . getTime ( ) ) . toBe ( results [ 0 ] . getTime ( ) ) ;
415+ }
416+ }
417+ } ) ;
418+
419+ test ( "fuzzy test: optimization threshold boundary (around 10 steps)" , ( ) => {
420+ // Test cases specifically around the 10-step optimization threshold
421+ const testCases = [
422+ { schedule : "*/5 * * * *" , minutesAgo : 50 } , // Exactly 10 steps
423+ { schedule : "*/5 * * * *" , minutesAgo : 55 } , // 11 steps (should optimize)
424+ { schedule : "*/5 * * * *" , minutesAgo : 45 } , // 9 steps (should not optimize)
425+ { schedule : "*/10 * * * *" , minutesAgo : 100 } , // Exactly 10 steps
426+ { schedule : "*/10 * * * *" , minutesAgo : 110 } , // 11 steps (should optimize)
427+ { schedule : "*/15 * * * *" , minutesAgo : 150 } , // Exactly 10 steps
428+ { schedule : "*/1 * * * *" , minutesAgo : 10 } , // Exactly 10 steps
429+ { schedule : "*/1 * * * *" , minutesAgo : 11 } , // 11 steps (should optimize)
430+ ] ;
431+
432+ for ( const testCase of testCases ) {
433+ const lastTimestamp = new Date ( Date . now ( ) - testCase . minutesAgo * 60 * 1000 ) ;
434+
435+ const startTime = performance . now ( ) ;
436+ const nextRun = calculateNextScheduledTimestamp ( testCase . schedule , null , lastTimestamp ) ;
437+ const duration = performance . now ( ) - startTime ;
438+
439+ // All cases should complete quickly and return valid results
440+ expect ( duration ) . toBeLessThan ( 50 ) ;
441+ expect ( nextRun . getTime ( ) ) . toBeGreaterThan ( Date . now ( ) ) ;
442+ expect ( nextRun ) . toBeInstanceOf ( Date ) ;
443+ }
444+ } ) ;
445+ } ) ;
0 commit comments