'success', 'count' => self::$executionCount]; } } #[Schedule(at: new Every(hours: 1))] final class TestHourlyJob { public static int $executionCount = 0; public function __invoke(): string { self::$executionCount++; return 'hourly job executed'; } } describe('ScheduleDiscoveryService Integration', function () { beforeEach(function () { // Reset execution counters TestFiveMinuteJob::$executionCount = 0; TestHourlyJob::$executionCount = 0; // Create minimal logger mock $this->logger = Mockery::mock(Logger::class); $this->logger->shouldReceive('debug')->andReturn(null); $this->logger->shouldReceive('info')->andReturn(null); $this->logger->shouldReceive('warning')->andReturn(null); $this->logger->shouldReceive('error')->andReturn(null); $this->schedulerService = new SchedulerService( $this->logger ); // Create minimal DiscoveryRegistry mock $this->discoveryRegistry = Mockery::mock(DiscoveryRegistry::class); $this->scheduleDiscovery = new ScheduleDiscoveryService( $this->discoveryRegistry, $this->schedulerService ); }); afterEach(function () { Mockery::close(); }); it('discovers and registers scheduled jobs from attribute registry', function () { // Mock discovery to return our test jobs $this->discoveryRegistry ->shouldReceive('getClassesWithAttribute') ->with(Schedule::class) ->once() ->andReturn([ TestFiveMinuteJob::class, TestHourlyJob::class ]); $registered = $this->scheduleDiscovery->discoverAndRegister(); expect($registered)->toBe(2); // Verify tasks were registered with scheduler $scheduledTasks = $this->schedulerService->getScheduledTasks(); expect($scheduledTasks)->toHaveCount(2); }); it('generates correct task IDs from class names', function () { $this->discoveryRegistry ->shouldReceive('getClassesWithAttribute') ->with(Schedule::class) ->once() ->andReturn([ TestFiveMinuteJob::class, TestHourlyJob::class ]); $this->scheduleDiscovery->discoverAndRegister(); $scheduledTasks = $this->schedulerService->getScheduledTasks(); $taskIds = array_map(fn($task) => $task->taskId, $scheduledTasks); expect($taskIds)->toContain('test-five-minute-job'); expect($taskIds)->toContain('test-hourly-job'); }); it('executes scheduled jobs correctly', function () { $this->discoveryRegistry ->shouldReceive('getClassesWithAttribute') ->with(Schedule::class) ->once() ->andReturn([TestFiveMinuteJob::class]); $this->scheduleDiscovery->discoverAndRegister(); // Get the scheduled task $scheduledTasks = $this->schedulerService->getScheduledTasks(); expect($scheduledTasks)->toHaveCount(1); $task = $scheduledTasks[0]; // Execute the task $result = $this->schedulerService->executeTask($task); expect($result->success)->toBeTrue(); expect($result->result)->toBeArray(); expect($result->result['status'])->toBe('success'); expect($result->result['count'])->toBe(1); expect(TestFiveMinuteJob::$executionCount)->toBe(1); }); it('executes callable jobs correctly', function () { $this->discoveryRegistry ->shouldReceive('getClassesWithAttribute') ->with(Schedule::class) ->once() ->andReturn([TestHourlyJob::class]); $this->scheduleDiscovery->discoverAndRegister(); $scheduledTasks = $this->schedulerService->getScheduledTasks(); expect($scheduledTasks)->toHaveCount(1); $task = $scheduledTasks[0]; // Execute the task $result = $this->schedulerService->executeTask($task); expect($result->success)->toBeTrue(); expect($result->result)->toBe('hourly job executed'); expect(TestHourlyJob::$executionCount)->toBe(1); }); it('uses correct intervals from Every value object', function () { $this->discoveryRegistry ->shouldReceive('getClassesWithAttribute') ->with(Schedule::class) ->once() ->andReturn([ TestFiveMinuteJob::class, // 5 minutes = 300 seconds TestHourlyJob::class // 1 hour = 3600 seconds ]); $this->scheduleDiscovery->discoverAndRegister(); $scheduledTasks = $this->schedulerService->getScheduledTasks(); // Find the 5-minute job $fiveMinuteTask = array_values(array_filter( $scheduledTasks, fn($task) => $task->taskId === 'test-five-minute-job' ))[0] ?? null; expect($fiveMinuteTask)->not->toBeNull(); // Execute task $result = $this->schedulerService->executeTask($fiveMinuteTask); expect($result->success)->toBeTrue(); // Get updated task $scheduledTasks = $this->schedulerService->getScheduledTasks(); $updatedTask = array_values(array_filter( $scheduledTasks, fn($task) => $task->taskId === 'test-five-minute-job' ))[0] ?? null; // Next execution should be set (schedule updated) expect($updatedTask->nextExecution)->not->toBeNull(); }); it('handles jobs without handle() or __invoke() gracefully', function () { // Create a job class without handle() or __invoke() $invalidJobClass = new class { // No handle() or __invoke() }; $this->discoveryRegistry ->shouldReceive('getClassesWithAttribute') ->with(Schedule::class) ->once() ->andReturn([$invalidJobClass::class]); $this->scheduleDiscovery->discoverAndRegister(); $scheduledTasks = $this->schedulerService->getScheduledTasks(); expect($scheduledTasks)->toHaveCount(1); $task = $scheduledTasks[0]; // Executing should throw RuntimeException $result = $this->schedulerService->executeTask($task); expect($result->success)->toBeFalse(); expect($result->error)->toContain('must have handle() method or be callable'); }); it('returns 0 when no scheduled jobs found', function () { $this->discoveryRegistry ->shouldReceive('getClassesWithAttribute') ->with(Schedule::class) ->once() ->andReturn([]); $registered = $this->scheduleDiscovery->discoverAndRegister(); expect($registered)->toBe(0); $scheduledTasks = $this->schedulerService->getScheduledTasks(); expect($scheduledTasks)->toHaveCount(0); }); it('can retrieve scheduled tasks via getScheduledTasks()', function () { $this->discoveryRegistry ->shouldReceive('getClassesWithAttribute') ->with(Schedule::class) ->once() ->andReturn([ TestFiveMinuteJob::class, TestHourlyJob::class ]); $this->scheduleDiscovery->discoverAndRegister(); $tasks = $this->scheduleDiscovery->getScheduledTasks(); expect($tasks)->toHaveCount(2); expect($tasks[0])->toHaveProperty('taskId'); expect($tasks[0])->toHaveProperty('nextExecution'); }); it('executes multiple jobs independently', function () { $this->discoveryRegistry ->shouldReceive('getClassesWithAttribute') ->with(Schedule::class) ->once() ->andReturn([ TestFiveMinuteJob::class, TestHourlyJob::class ]); $this->scheduleDiscovery->discoverAndRegister(); $scheduledTasks = $this->schedulerService->getScheduledTasks(); // Execute both jobs foreach ($scheduledTasks as $task) { $result = $this->schedulerService->executeTask($task); expect($result->success)->toBeTrue(); } // Both counters should have incremented expect(TestFiveMinuteJob::$executionCount)->toBe(1); expect(TestHourlyJob::$executionCount)->toBe(1); }); });