Files
portal/app/tests/Unit/Autopodbor/ChannelB/ExaSiteFinderTest.php
T
Дмитрий 5a65165114 feat(автоподбор): движок шага 1 пересобран под финал v4 (каналы А+В, EXA)
Замена вырожденного «одна фраза → одна страница» на §12/§11.3 финал:
- Шаг АНАЛИЗ (ChannelA\AitunnelQueryAnalyzer): описание → запросы-рубрики (мелкая модель).
- Канал А (ChannelA\CategoryScraper): скрейп категории 2ГИС с пагинацией → резолв карточек.
- Канал В (ChannelB\*): ОДНА модель sonar-reasoning-pro × 2 прохода → ТОЛЬКО имена
  федералов; стоп-лист = имена из А + примеры; сайт федерала через EXA (ExaSiteFinder),
  т.к. у федерала нет карточки в 2ГИС/Яндексе на регион.
- Оркестратор LiveFindCompetitors переписан: АНАЛИЗ→А→В→слияние→отсев→дедуп→похожесть→DTO.
- Провайдер перепрошит; config services.php +research_model/exa.
Похожесть — эмбеддер-модель (математически), резолвер/дедуп — без изменений.
Всё за тонкими границами, офлайн-тесты на фикстурах: модуль 130 unit + 74 feature зелёные.
Провайдер за флагом autopodbor.real_find; на проде не меняется.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 21:06:10 +03:00

69 lines
2.8 KiB
PHP

<?php
declare(strict_types=1);
use App\Services\Autopodbor\Agent\ChannelB\ExaSiteFinder;
use Illuminate\Http\Client\Factory as HttpFactory;
use Illuminate\Support\Facades\Config;
// Канал В, нормализация (ZAFIKSIROVANO / §11.5): у федерала НЕТ карточки в 2ГИС/Яндексе на регион,
// поэтому его САЙТ ищем через EXA по имени. Чёрный список каталогов/агрегаторов/иностранных TLD
// (§11.4) — не считать их сайтом фирмы. Сеть мокаем.
it('без ключа возвращает null', function () {
Config::set('services.exa.key', '');
$f = new ExaSiteFinder(app(HttpFactory::class));
expect($f->findSite('CarMoney', 'Красноярский край'))->toBeNull();
});
it('берёт домен первого результата (голый, без www/пути)', function () {
Config::set('services.exa', ['key' => 'k', 'base_url' => 'https://api.exa.ai', 'timeout_sec' => 30]);
$http = app(HttpFactory::class);
$http->fake([
'api.exa.ai/*' => $http->response([
'results' => [['url' => 'https://www.carmoney.ru/about?utm=1'], ['url' => 'https://carmoney.ru']],
], 200),
]);
$f = new ExaSiteFinder($http);
expect($f->findSite('CarMoney', 'Красноярский край'))->toBe('carmoney.ru');
});
it('пропускает агрегаторы/каталоги и иностранные TLD, берёт первый «настоящий»', function () {
Config::set('services.exa.key', 'k');
$http = app(HttpFactory::class);
$http->fake([
'api.exa.ai/*' => $http->response([
'results' => [
['url' => 'https://www.avito.ru/krasnoyarsk/zaim'],
['url' => 'https://banki.ru/products/cashmotor'],
['url' => 'https://cashmotor.kg/'], // иностранный TLD
['url' => 'https://cashmotor.ru/'], // настоящий
],
], 200),
]);
$f = new ExaSiteFinder($http);
expect($f->findSite('Cashmotor', 'Красноярский край'))->toBe('cashmotor.ru');
});
it('нет подходящих результатов или ошибка сети → null', function () {
Config::set('services.exa.key', 'k');
$http = app(HttpFactory::class);
$http->fake([
'api.exa.ai/*' => $http->response(['results' => [['url' => 'https://zoon.ru/x']]], 200),
]);
expect((new ExaSiteFinder($http))->findSite('X', 'Красноярский край'))->toBeNull();
$http2 = app(HttpFactory::class);
$http2->fake(['api.exa.ai/*' => $http2->response('boom', 500)]);
expect((new ExaSiteFinder($http2))->findSite('Y', 'Красноярский край'))->toBeNull();
});