definitions(); expect($definitions)->toHaveCount(23); expect(array_keys($definitions))->toContain( 'system.sync_logs', 'backups.process_jobs', 'backups.prune_retention', 'economic.transfer_queue', 'dynamicimages.pre_render', 'weatherapi.preload_department_responses', 'goals.progress_alerts', 'account.process_deletion_requests', 'selfserve.activate_opening_cleaner_relays' ); $transferQueue = $registry->get('EconomicTransferQueueCron'); expect($transferQueue)->not->toBeNull(); expect($transferQueue->id)->toBe('economic.transfer_queue'); expect($transferQueue->module)->toBe('economic'); expect($transferQueue->schedule)->toBe(['type' => 'interval', 'seconds' => 30]); }); it('keeps every discovered cron task in a module cron folder', function (): void { $files = glob(app_path('modules/*/cron/tasks.php')) ?: []; $modules = array_map( static fn(string $file): string => basename(dirname(dirname($file))), $files ); $registry = new cron_task_registry(app_path('modules')); foreach ($registry->definitions() as $definition) { expect($modules)->toContain($definition->module); } }); it('normalizes and advances interval schedules without tight loops', function (): void { expect(cron_schedule::normalize(['type' => 'interval', 'seconds' => 60])) ->toBe(['type' => 'interval', 'seconds' => 60]); $now = strtotime('2026-07-09 12:10:00'); $next = cron_schedule::nextRunAt( ['type' => 'interval', 'seconds' => 300], '2026-07-09 12:00:00', $now ); expect($next)->toBe('2026-07-09 12:15:00'); }); it('keeps minute schedules anchored to intended slots when execution finishes late', function (): void { $schedule = ['type' => 'interval', 'seconds' => 60]; $first = cron_schedule::nextRunAt( $schedule, '2026-07-27 12:00:00', strtotime('2026-07-27 12:00:47') ); $second = cron_schedule::nextRunAt( $schedule, $first, strtotime('2026-07-27 12:01:52') ); expect($first)->toBe('2026-07-27 12:01:00'); expect($second)->toBe('2026-07-27 12:02:00'); }); it('rejects unsafe cron intervals', function (): void { expect(fn() => cron_schedule::normalize(['type' => 'interval', 'seconds' => 5])) ->toThrow(InvalidArgumentException::class); });