*/ public array $calls = []; public function stage(int $stage, int $total, string $label): void { $this->calls[] = [$stage, $total, $label]; } }; } /** * Живой движок с заглушками-интерфейсами (анализ отдаёт пустые запросы → живые каналы не дёргаются) и * дешёвыми экземплярами конкретных зависимостей (они не вызываются при пустом наборе). Так проверяем * ТОЛЬКО последовательность этапов, не поднимая сеть. */ function liveFindWithProgress(RunProgressChannel $progress, bool $withEnricher = false): LiveFindCompetitors { $http = app(HttpFactory::class); $xfetch = new XfetchClient(apiKey: '', endpoint: 'https://xf4.ru/fetch', concurrency: 1); $analyzer = new class implements QueryAnalyzer { public function analyze(string $description, string $region, string $selfSite = '', array $competitorExamples = []): array { return []; } }; $yandex = new class implements YandexDirectory { public function collect(string $city, array $queries): array { return []; } }; $assembler = new FindCompetitorsAssembler( new AggregatorFilter(app(AitunnelAggregatorClassifier::class)), new AutopodborDedup(new AutopodborNormalizer), new EmbeddingRelevance(app(AitunnelEmbedder::class)), ); return new LiveFindCompetitors( $analyzer, new CategoryScraper(new LivePageFetcher($xfetch), new CategoryListingParser, 4), $yandex, new ChannelBSearch(new AitunnelResearcher($http), new ResearcherParser), new ExaSiteFinder($http), $assembler, progress: $progress, ); } it('поиск отмечает этапы в канал по порядку (базовый: 3 этапа)', function () { $spy = spyProgressChannel(); $live = liveFindWithProgress($spy); $live->find(new FindCompetitorsRequest( regionCode: 16, examples: [], aboutSelf: [], includeFederal: false, maxCompetitors: 15, )); expect($spy->calls)->toBe([ [1, 3, 'Разбираем ваш запрос'], [2, 3, 'Ищем фирмы в справочниках'], [3, 3, 'Отбираем самых похожих'], ]); });