@@ -556,3 +556,118 @@ func TestJobPersistence(t *testing.T) {
556556 }
557557 })
558558}
559+
560+ // Additional coverage tests for validation errors, resume logic, cleanup, and persistence edge cases.
561+ func TestSchedulerEdgeCases (t * testing.T ) {
562+ module := NewModule ().(* SchedulerModule )
563+ app := newMockApp ()
564+ module .RegisterConfig (app )
565+ module .Init (app )
566+ ctx := context .Background ()
567+ require .NoError (t , module .Start (ctx ))
568+ defer module .Stop (ctx )
569+
570+ t .Run ("ScheduleJobMissingTiming" , func (t * testing.T ) {
571+ _ , err := module .ScheduleJob (Job {Name : "no-timing" })
572+ assert .ErrorIs (t , err , ErrJobInvalidSchedule )
573+ })
574+
575+ t .Run ("ScheduleRecurringMissingSchedule" , func (t * testing.T ) {
576+ _ , err := module .ScheduleJob (Job {Name : "rec-missing" , IsRecurring : true })
577+ // Current implementation returns ErrJobInvalidSchedule before specific recurring check
578+ assert .ErrorIs (t , err , ErrJobInvalidSchedule )
579+ })
580+
581+ t .Run ("ScheduleRecurringInvalidCron" , func (t * testing.T ) {
582+ _ , err := module .ScheduleJob (Job {Name : "rec-invalid" , IsRecurring : true , Schedule : "* * *" })
583+ assert .Error (t , err )
584+ })
585+
586+ t .Run ("ResumeJobMissingID" , func (t * testing.T ) {
587+ _ , err := module .scheduler .ResumeJob (Job {})
588+ assert .ErrorIs (t , err , ErrJobIDRequired )
589+ })
590+
591+ t .Run ("ResumeJobNoNextRunTime" , func (t * testing.T ) {
592+ // Past run time with no future next run forces ErrJobNoValidNextRunTime
593+ _ , err := module .scheduler .ResumeJob (Job {ID : "abc" , RunAt : time .Now ().Add (- 1 * time .Hour )})
594+ assert .ErrorIs (t , err , ErrJobNoValidNextRunTime )
595+ })
596+
597+ t .Run ("ResumeRecurringJobMissingID" , func (t * testing.T ) {
598+ _ , err := module .scheduler .ResumeRecurringJob (Job {IsRecurring : true , Schedule : "* * * * *" })
599+ assert .ErrorIs (t , err , ErrRecurringJobIDRequired )
600+ })
601+
602+ t .Run ("ResumeRecurringJobNotRecurring" , func (t * testing.T ) {
603+ _ , err := module .scheduler .ResumeRecurringJob (Job {ID : "id1" , IsRecurring : false })
604+ assert .ErrorIs (t , err , ErrJobMustBeRecurring )
605+ })
606+
607+ t .Run ("ResumeRecurringJobInvalidCron" , func (t * testing.T ) {
608+ _ , err := module .scheduler .ResumeRecurringJob (Job {ID : "id2" , IsRecurring : true , Schedule : "* * *" })
609+ assert .Error (t , err )
610+ })
611+
612+ // Success path: resume one-time job with future RunAt
613+ t .Run ("ResumeJobSuccess" , func (t * testing.T ) {
614+ future := time .Now ().Add (30 * time .Minute )
615+ job := Job {ID : "resume-one" , Name : "resume-one" , RunAt : future , Status : JobStatusCancelled }
616+ // Add job to store first
617+ require .NoError (t , module .scheduler .jobStore .AddJob (job ))
618+ _ , err := module .scheduler .ResumeJob (job )
619+ assert .NoError (t , err )
620+ stored , err := module .scheduler .GetJob ("resume-one" )
621+ require .NoError (t , err )
622+ assert .Equal (t , JobStatusPending , stored .Status )
623+ assert .NotNil (t , stored .NextRun )
624+ if stored .NextRun != nil {
625+ assert .WithinDuration (t , future , * stored .NextRun , time .Minute ) // allow minute boundary drift
626+ }
627+ })
628+
629+ // Success path: resume recurring job with valid cron schedule
630+ t .Run ("ResumeRecurringJobSuccess" , func (t * testing.T ) {
631+ job := Job {ID : "resume-rec" , Name : "resume-rec" , IsRecurring : true , Schedule : "* * * * *" , Status : JobStatusCancelled }
632+ require .NoError (t , module .scheduler .jobStore .AddJob (job ))
633+ _ , err := module .scheduler .ResumeRecurringJob (job )
634+ assert .NoError (t , err )
635+ stored , err := module .scheduler .GetJob ("resume-rec" )
636+ require .NoError (t , err )
637+ assert .Equal (t , JobStatusPending , stored .Status )
638+ assert .NotNil (t , stored .NextRun )
639+ })
640+ }
641+
642+ func TestMemoryJobStoreCleanupAndPersistenceEdges (t * testing.T ) {
643+ store := NewMemoryJobStore (24 * time .Hour )
644+
645+ // Add executions with different times
646+ oldExec := JobExecution {JobID : "job1" , StartTime : time .Now ().Add (- 48 * time .Hour ), Status : "completed" }
647+ recentExec := JobExecution {JobID : "job1" , StartTime : time .Now (), Status : "completed" }
648+ require .NoError (t , store .AddJobExecution (oldExec ))
649+ require .NoError (t , store .AddJobExecution (recentExec ))
650+
651+ // Cleanup older than 24h
652+ cutoff := time .Now ().Add (- 24 * time .Hour )
653+ require .NoError (t , store .CleanupOldExecutions (cutoff ))
654+ execs , err := store .GetJobExecutions ("job1" )
655+ require .NoError (t , err )
656+ assert .Len (t , execs , 1 )
657+ assert .Equal (t , recentExec .StartTime , execs [0 ].StartTime )
658+
659+ t .Run ("LoadFromFileNonexistent" , func (t * testing.T ) {
660+ jobs , err := store .LoadFromFile ("/tmp/nonexistent-file-should-not-exist.json" )
661+ require .NoError (t , err )
662+ assert .Len (t , jobs , 0 )
663+ })
664+
665+ t .Run ("SaveAndLoadEmptyJobs" , func (t * testing.T ) {
666+ tmp := fmt .Sprintf ("/tmp/scheduler-empty-%d.json" , time .Now ().UnixNano ())
667+ require .NoError (t , store .SaveToFile ([]Job {}, tmp ))
668+ jobs , err := store .LoadFromFile (tmp )
669+ require .NoError (t , err )
670+ assert .Len (t , jobs , 0 )
671+ _ = os .Remove (tmp )
672+ })
673+ }
0 commit comments