create(); $campaign = AdCampaign::create([ 'tenant_id' => $tenant->id, 'name' => 'C', 'mode' => AdCampaign::MODE_MANUAL, 'audience_days' => 10, 'client_cpm_rub' => '120.00', ]); return AdCreativeJob::create(array_merge([ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'status' => AdCreativeJob::STATUS_TAKEN, 'attempts' => 1, 'snapshot_before' => [], 'taken_at' => now()->subMinutes(40), ], $attributes)); } it('returns a job abandoned in flight back to the queue', function () { $job = makeStuckJob(); $this->artisan('creative-jobs:reap')->assertExitCode(0); expect($job->fresh()->status)->toBe(AdCreativeJob::STATUS_QUEUED) ->and($job->fresh()->taken_at)->toBeNull(); }); it('leaves a job that was taken just now alone', function () { // Робот работает минутами: качает файлы, поднимает браузер, грузит 15 креативов. // Отобрать у него задание на середине — значит получить два робота на одной кампании. $job = makeStuckJob(['taken_at' => now()->subMinutes(3)]); $this->artisan('creative-jobs:reap')->assertExitCode(0); expect($job->fresh()->status)->toBe(AdCreativeJob::STATUS_TAKEN); }); it('closes the job as failed once the attempts are spent instead of looping forever', function () { $job = makeStuckJob(['attempts' => 3]); $this->artisan('creative-jobs:reap')->assertExitCode(0); expect($job->fresh()->status)->toBe(AdCreativeJob::STATUS_FAILED) ->and($job->fresh()->failure_reason)->toContain('не отчитался') ->and($job->fresh()->finished_at)->not->toBeNull(); }); it('does not touch jobs that are not in flight', function () { $job = makeStuckJob(['status' => AdCreativeJob::STATUS_DONE, 'taken_at' => now()->subDays(3)]); $this->artisan('creative-jobs:reap')->assertExitCode(0); expect($job->fresh()->status)->toBe(AdCreativeJob::STATUS_DONE); });