create([ 'status' => ProvisioningRun::STATUS_PENDING, 'next_attempt_at' => null, ]); $stranded = ProvisioningRun::factory()->create([ 'status' => ProvisioningRun::STATUS_PENDING, 'next_attempt_at' => null, 'created_at' => now()->subHour(), ]); (new TickProvisioning)(); // The stranded one: a process died between creating the run and dispatching // its first job, and nothing else would ever look at it again. Queue::assertPushed(AdvanceRunJob::class, fn ($job) => $job->runUuid === $stranded->uuid); // The fresh one is left alone — its own job is simply still queued, and a // second one would run a duplicate chain beside it for the whole pipeline. Queue::assertNotPushed(AdvanceRunJob::class, fn ($job) => $job->runUuid === $fresh->uuid); }); it('dispatches advance jobs only for due running or waiting runs', function () { Queue::fake(); ProvisioningRun::factory()->create(['status' => 'waiting', 'next_attempt_at' => now()->subMinute()]); ProvisioningRun::factory()->create(['status' => 'running', 'next_attempt_at' => null]); ProvisioningRun::factory()->create(['status' => 'waiting', 'next_attempt_at' => now()->addHour()]); ProvisioningRun::factory()->create(['status' => 'completed']); ProvisioningRun::factory()->create(['status' => 'paused']); app(TickProvisioning::class)(); Queue::assertPushed(AdvanceRunJob::class, 2); });