Files
portal/app/tests/Feature/Autopodbor/StudyProgressTest.php
T
Дмитрий 428b7b0f01 feat(autopodbor): прогресс прогона — очередь и этапы (бэкенд)
Клиент во время сбора видит место в общей очереди (пока ждёт) и текущий
этап (пока идёт). Очередь — длина общей Redis-очереди (честное ожидание,
без RLS, одинаково dev/прод). Этапы пишет сам движок через RunProgressChannel:
поиск (анализ→справочники→федералы→сбор→отбор), изучение — по каждому элементу.

- миграция progress (jsonb) на autopodbor_runs + cast
- RunResource отдаёт queue_position (AutopodborQueue) и progress
- RunProgressChannel (синглтон), джобы привязывают его к своему прогону
- SearchStages + инструментовка LiveFindCompetitors и RealCompetitorAgent
- 16 тестов (Feature+Unit), все зелёные

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 04:31:01 +03:00

104 lines
3.1 KiB
PHP

<?php
declare(strict_types=1);
use App\Services\Autopodbor\Agent\Dto\StudyCompetitorRequest;
use App\Services\Autopodbor\Agent\FakeCompetitorAgent;
use App\Services\Autopodbor\Agent\Fetch\FetchedSite;
use App\Services\Autopodbor\Agent\Fetch\Fetcher;
use App\Services\Autopodbor\Agent\RealCompetitorAgent;
use App\Services\Autopodbor\Agent\Study\Fingerprint;
use App\Services\Autopodbor\Agent\Study\SourceCollector;
use App\Services\Autopodbor\RunProgressChannel;
function studySpyChannel(): RunProgressChannel
{
return new class extends RunProgressChannel
{
/** @var list<array{int,int,string}> */
public array $calls = [];
public function stage(int $stage, int $total, string $label): void
{
$this->calls[] = [$stage, $total, $label];
}
};
}
function unusedFetcher(): Fetcher
{
return new class implements Fetcher
{
public function site(string $url): FetchedSite
{
throw new RuntimeException('в deep-пути не используется');
}
public function directory(string $url): array
{
return [];
}
};
}
/** Заглушка глубокого собирателя — ничего не находит, но даёт агенту пройти цикл по элементам. */
function emptyDeepCollector(): SourceCollector
{
return new class implements SourceCollector
{
public function collect(Fingerprint $fp): array
{
return [];
}
};
}
it('изучение отмечает этап на КАЖДЫЙ элемент конкурента (холдинг)', function () {
config(['autopodbor.deep_study' => true]);
$spy = studySpyChannel();
$agent = new RealCompetitorAgent(
unusedFetcher(),
new FakeCompetitorAgent,
deep: emptyDeepCollector(),
progress: $spy,
);
$agent->studyCompetitor(new StudyCompetitorRequest(
competitor: [
'name' => 'Холдинг',
'elements' => [
['name' => 'Первый', 'sites' => ['a.ru'], 'cards' => []],
['name' => 'Второй', 'sites' => [], 'cards' => []],
['name' => 'Третий', 'sites' => [], 'cards' => []],
],
],
regionCode: 16,
));
expect($spy->calls)->toBe([
[1, 3, 'Изучаю: Первый'],
[2, 3, 'Изучаю: Второй'],
[3, 3, 'Изучаю: Третий'],
]);
});
it('изучение конкурента без элементов — один этап «Изучаю фирму»', function () {
config(['autopodbor.deep_study' => true]);
$spy = studySpyChannel();
$agent = new RealCompetitorAgent(
unusedFetcher(),
new FakeCompetitorAgent,
deep: emptyDeepCollector(),
progress: $spy,
);
$agent->studyCompetitor(new StudyCompetitorRequest(
competitor: ['name' => 'Одна Фирма', 'site_url' => 'x.ru', 'elements' => []],
regionCode: 16,
));
expect($spy->calls)->toBe([[1, 1, 'Изучаю фирму']]);
});