set('autopodbor.phone_reverse', true); config()->set('services.exa', ['key' => 't', 'base_url' => 'https://api.exa.ai']); Http::fake([ 'api.exa.ai/*' => Http::response(['results' => [['url' => 'https://gk-dsk.ru/']]]), '*' => Http::response(['response_body_base64' => '']), ]); $siteFetcher = new class implements Fetcher { public function site(string $url): FetchedSite { return new FetchedSite($url, 'gk-dsk.ru'); } public function directory(string $url): array { return []; } }; $reverse = new class implements PhoneReverse { public function expand(string $gisSlug, array $seedPhones, array $seedDomains, string $yandexSlug = ''): PhoneExpansionResult { return new PhoneExpansionResult( sites: [['domain' => 'zvezda-dsk.ru', 'site_url' => 'https://zvezda-dsk.ru', 'card_url' => 'https://2gis.ru/krasnoyarsk/firm/111']], phones: [['digits' => '73912002288', 'url' => 'https://2gis.ru/krasnoyarsk/firm/111', 'source' => '2ГИС']], ); } }; $collector = new DeepStudyCollector( xfetch: new XfetchClient(null), opener: new SiteOpener($siteFetcher), reverse: $reverse, ); $fp = new Fingerprint('ДСК', 'gk-dsk.ru', 'Красноярск', '391', 'krasnoyarsk', false); $rows = $collector->collect($fp); $siteDomains = array_map(fn ($r) => $r['identifier'], array_filter($rows, fn ($r) => $r['signal_type'] === 'site')); $callDigits = array_map(fn ($r) => $r['identifier'], array_filter($rows, fn ($r) => $r['signal_type'] === 'call')); expect($siteDomains)->toContain('zvezda-dsk.ru') ->and($callDigits)->toContain('73912002288'); }); it('без флага phone_reverse разворот не зовётся (находок разворота нет)', function () { config()->set('autopodbor.phone_reverse', false); config()->set('services.exa', ['key' => 't', 'base_url' => 'https://api.exa.ai']); Http::fake([ 'api.exa.ai/*' => Http::response(['results' => []]), '*' => Http::response(['response_body_base64' => '']), ]); $siteFetcher = new class implements Fetcher { public function site(string $url): FetchedSite { return new FetchedSite($url, ''); } public function directory(string $url): array { return []; } }; $reverse = new class implements PhoneReverse { public function expand(string $gisSlug, array $seedPhones, array $seedDomains, string $yandexSlug = ''): PhoneExpansionResult { return new PhoneExpansionResult( sites: [['domain' => 'nemojet.ru', 'site_url' => 'https://nemojet.ru', 'card_url' => 'https://2gis.ru/x/firm/9']], ); } }; $collector = new DeepStudyCollector( xfetch: new XfetchClient(null), opener: new SiteOpener($siteFetcher), reverse: $reverse, ); $fp = new Fingerprint('ДСК', 'gk-dsk.ru', 'Красноярск', '391', 'krasnoyarsk', false); $rows = $collector->collect($fp); $siteDomains = array_map(fn ($r) => $r['identifier'], array_filter($rows, fn ($r) => $r['signal_type'] === 'site')); expect($siteDomains)->not->toContain('nemojet.ru'); }); it('без сайта-якоря (seedDomain пуст) разворот НЕ зовётся — защита от мусора', function () { config()->set('autopodbor.phone_reverse', true); config()->set('services.exa', ['key' => 't', 'base_url' => 'https://api.exa.ai']); Http::fake([ 'api.exa.ai/*' => Http::response(['results' => []]), '*' => Http::response(['response_body_base64' => '']), ]); $siteFetcher = new class implements Fetcher { public function site(string $url): FetchedSite { return new FetchedSite($url, ''); } public function directory(string $url): array { return []; } }; $called = (object) ['n' => 0]; $reverse = new class($called) implements PhoneReverse { public function __construct(private object $c) {} public function expand(string $gisSlug, array $seedPhones, array $seedDomains, string $yandexSlug = ''): PhoneExpansionResult { $this->c->n++; return new PhoneExpansionResult( sites: [['domain' => 'junk.ru', 'site_url' => 'https://junk.ru', 'card_url' => 'x']], ); } }; $collector = new DeepStudyCollector( xfetch: new XfetchClient(null), opener: new SiteOpener($siteFetcher), reverse: $reverse, ); // seedDomain пуст (у элемента нет своего сайта — размытый конкурент как «Южный берег») $fp = new Fingerprint('Южный берег', '', 'Красноярск', '391', 'krasnoyarsk', false, '62/krasnoyarsk'); $rows = $collector->collect($fp); $siteDomains = array_map(fn ($r) => $r['identifier'], array_filter($rows, fn ($r) => $r['signal_type'] === 'site')); expect($called->n)->toBe(0) // разворот не звался ->and($siteDomains)->not->toContain('junk.ru'); // мусор не приполз }); it('в разворот передаётся yandexSlug из отпечатка (для Яндекс-разворота)', function () { config()->set('autopodbor.phone_reverse', true); config()->set('services.exa', ['key' => 't', 'base_url' => 'https://api.exa.ai']); \Illuminate\Support\Facades\Http::fake([ 'api.exa.ai/*' => \Illuminate\Support\Facades\Http::response(['results' => [['url' => 'https://gk-dsk.ru/']]]), '*' => \Illuminate\Support\Facades\Http::response(['response_body_base64' => '']), ]); $siteFetcher = new class implements \App\Services\Autopodbor\Agent\Fetch\Fetcher { public function site(string $url): \App\Services\Autopodbor\Agent\Fetch\FetchedSite { return new \App\Services\Autopodbor\Agent\Fetch\FetchedSite($url, 'x'); } public function directory(string $url): array { return []; } }; $captured = (object) ['yandexSlug' => null]; $reverse = new class($captured) implements \App\Services\Autopodbor\Agent\Study\PhoneReverse { public function __construct(private object $cap) {} public function expand(string $gisSlug, array $seedPhones, array $seedDomains, string $yandexSlug = ''): \App\Services\Autopodbor\Agent\Study\PhoneExpansionResult { $this->cap->yandexSlug = $yandexSlug; return new \App\Services\Autopodbor\Agent\Study\PhoneExpansionResult; } }; $collector = new \App\Services\Autopodbor\Agent\Study\DeepStudyCollector( xfetch: new \App\Services\Autopodbor\Agent\Fetch\XfetchClient(null), opener: new \App\Services\Autopodbor\Agent\Study\SiteOpener($siteFetcher), reverse: $reverse, ); $fp = new \App\Services\Autopodbor\Agent\Study\Fingerprint('ДСК', 'gk-dsk.ru', 'Красноярск', '391', 'krasnoyarsk', false, '62/krasnoyarsk'); $collector->collect($fp); expect($captured->yandexSlug)->toBe('62/krasnoyarsk'); });