d228a501dc
Задача 5: RunAutopodborStudyJob::handle() оборачивает существующее тело в try/finally — после ЛЮБОГО завершения прогона (done/empty/failed, включая проброшенное исключение) распорядитель AutopodborStudyScheduler::tick() берёт следующего кандидата с освободившейся дорожки шага 2. Задача 6: artisan autopodbor:study-tick — крон-страховка раз в минуту на случай, если дорожка освободилась не через завершение джобы. Без withoutOverlapping() — требует таблицу cache_locks, которой нет в схеме (см. комментарий в routes/console.php); overlap уже защищён внутренним Cache::lock в AutopodborStudyScheduler::tick().
62 lines
3.1 KiB
PHP
62 lines
3.1 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use App\Jobs\Autopodbor\RunAutopodborStudyJob;
|
||
use App\Models\AutopodborCompetitor;
|
||
use App\Models\AutopodborRun;
|
||
use App\Models\SystemSetting;
|
||
use App\Models\Tenant;
|
||
use App\Services\Autopodbor\Agent\CompetitorAgent;
|
||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Queue;
|
||
use Illuminate\Support\Str;
|
||
use Tests\Concerns\SharesSupplierPdo;
|
||
use Tests\Doubles\EmptyCompetitorAgent;
|
||
|
||
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
|
||
|
||
it('после завершения прогона джоба сама зовёт распорядителя, и тот диспатчит следующий queued', function () {
|
||
Queue::fake();
|
||
// Детерминированная заглушка агента (без реальной сети) — прогон уйдёт в `empty` быстро.
|
||
app()->bind(CompetitorAgent::class, EmptyCompetitorAgent::class);
|
||
|
||
$tenant = Tenant::factory()->create(['balance_rub' => '100000.00']);
|
||
DB::statement('SET app.current_tenant_id = '.$tenant->id);
|
||
SystemSetting::updateOrCreate(['key' => 'autopodbor_price_study_rub'], ['value' => '300', 'type' => 'decimal']);
|
||
|
||
$comp1 = AutopodborCompetitor::create([
|
||
'tenant_id' => $tenant->id, 'name' => 'Окна А', 'origin' => 'manual',
|
||
'dedup_key' => Str::slug('Окна А').'-'.Str::random(6), 'box' => 'field',
|
||
]);
|
||
$comp2 = AutopodborCompetitor::create([
|
||
'tenant_id' => $tenant->id, 'name' => 'Окна Б', 'origin' => 'manual',
|
||
'dedup_key' => Str::slug('Окна Б').'-'.Str::random(6), 'box' => 'field',
|
||
]);
|
||
|
||
$run1 = AutopodborRun::create([
|
||
'tenant_id' => $tenant->id, 'kind' => 'study', 'status' => 'queued',
|
||
'competitor_id' => $comp1->id, 'region_code' => 1, 'params' => [],
|
||
]);
|
||
AutopodborRun::whereKey($run1->id)->update(['created_at' => now()->subMinutes(5)]);
|
||
|
||
$run2 = AutopodborRun::create([
|
||
'tenant_id' => $tenant->id, 'kind' => 'study', 'status' => 'queued',
|
||
'competitor_id' => $comp2->id, 'region_code' => 1, 'params' => [],
|
||
]);
|
||
AutopodborRun::whereKey($run2->id)->update(['created_at' => now()]);
|
||
|
||
// Класс должен инстанцироваться с новой зависимостью в handle() без ошибок конструктора/резолва.
|
||
expect(new RunAutopodborStudyJob($run1->id, $tenant->id))->toBeInstanceOf(RunAutopodborStudyJob::class);
|
||
|
||
// Прогоняем ПЕРВЫЙ прогон РЕАЛЬНО (через handle(), не через Queue) — Queue::fake() гарантирует,
|
||
// что единственный dispatch RunAutopodborStudyJob, который мы увидим, придёт ИЗНУТРИ handle()
|
||
// (от распорядителя), а не от нашего собственного вызова.
|
||
app()->call([new RunAutopodborStudyJob($run1->id, $tenant->id), 'handle']);
|
||
|
||
expect($run1->fresh()->status)->toBe('empty');
|
||
|
||
Queue::assertPushed(RunAutopodborStudyJob::class, fn ($job) => $job->runId === $run2->id);
|
||
});
|