create()->id; } $tenantId = $GLOBALS['__sched_tenant_map'][$tenant]; $run = AutopodborRun::create([ 'tenant_id' => $tenantId, 'kind' => 'study', 'status' => $status, 'competitor_id' => null, 'region_code' => 1, 'params' => [], 'finished_at' => $finishedAt, ]); if ($createdAt !== null) { // created_at не в timestamps — задаём явно через query builder (мимо fillable), // чтобы порядок был детерминирован AutopodborRun::whereKey($run->id)->update(['created_at' => $createdAt]); $run->refresh(); } return $run; } it('дорожка занята: есть running → pickNext возвращает null', function () { studyRun(1, 'running'); studyRun(2, 'queued'); expect(app(AutopodborStudyScheduler::class)->pickNextRunId())->toBeNull(); }); it('чередует тенантов: наименее недавно обслуженный вперёд', function () { studyRun(1, 'done', finishedAt: now()->toDateTimeString()); studyRun(1, 'queued'); studyRun(2, 'done', finishedAt: now()->subHour()->toDateTimeString()); $t2q = studyRun(2, 'queued'); expect(app(AutopodborStudyScheduler::class)->pickNextRunId())->toBe($t2q->id); }); it('внутри тенанта берёт самый старый queued', function () { $old = studyRun(1, 'queued', createdAt: now()->subMinutes(10)->toDateTimeString()); studyRun(1, 'queued', createdAt: now()->toDateTimeString()); expect(app(AutopodborStudyScheduler::class)->pickNextRunId())->toBe($old->id); }); it('никогда не обслуженный тенант имеет приоритет', function () { studyRun(1, 'done', finishedAt: now()->subDay()->toDateTimeString()); studyRun(1, 'queued'); $t2q = studyRun(2, 'queued'); expect(app(AutopodborStudyScheduler::class)->pickNextRunId())->toBe($t2q->id); });