428b7b0f01
Клиент во время сбора видит место в общей очереди (пока ждёт) и текущий этап (пока идёт). Очередь — длина общей 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>
104 lines
3.1 KiB
PHP
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, 'Изучаю фирму']]);
|
|
});
|