Files
portal/app/tests/Unit/Autopodbor/ChannelB/ChannelBSearchTest.php
T

85 lines
4.3 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Services\Autopodbor\Agent\ChannelB\ChannelBSearch;
use App\Services\Autopodbor\Agent\ChannelB\ResearcherClient;
use App\Services\Autopodbor\Agent\ChannelB\ResearcherParser;
/**
* Канал В, ФИНАЛ владельца: ИИ — генератор ИМЁН федералов/онлайн. На вход ИИ даём СПИСОК ИЗ КАНАЛА А
* (уже найденные в справочниках фирмы) + примеры клиента как «уже известных — не повторять». ОДНА модель
* × 2 прохода, растущий стоп-лист. Выход — только новые ИМЕНА (+ тип). Якоря добывает потом Firecrawl.
* Чистая логика — модель за границей ResearcherClient.
*/
final class FakeResearcher implements ResearcherClient
{
/** @var list<string> */
public array $userPrompts = [];
/** @param list<string> $answers */
public function __construct(private array $answers) {}
public function research(string $system, string $user): string
{
$this->userPrompts[] = $user;
return $this->answers[count($this->userPrompts) - 1] ?? '[]';
}
}
it('2 прохода: копит новые имена без дублей, наращивает стоп-лист, исключает список из А', function () {
$pass1 = '[{"name":"CarMoney","type":"федеральная"},{"name":"Финансовый дом","type":"федеральная"}]';
$pass2 = '[{"name":"Webbankir","type":"федеральная"},{"name":"CarMoney","type":"федеральная"}]'; // дубль
$fake = new FakeResearcher([$pass1, $pass2]);
$search = new ChannelBSearch($fake, new ResearcherParser);
// knownFromA = имена, уже найденные каналом А (справочники)
$out = $search->harvest(
profile: 'займы под залог авто',
region: 'Красноярский край',
clientSite: 'lkomega.ru',
known: ['КрасЛомбард', 'Голд Авто Инвест', 'Ваш инвестор'],
passes: 2,
);
// 3 новых уникальных (CarMoney из прохода 2 — дубль, не добавлен)
expect($out)->toHaveCount(3)
->and(array_column($out, 'name'))->toContain('CarMoney', 'Финансовый дом', 'Webbankir');
expect($fake->userPrompts)->toHaveCount(2);
// Проход 1: стоп-лист несёт список из А
expect($fake->userPrompts[0])->toContain('КрасЛомбард')
->and($fake->userPrompts[0])->toContain('Голд Авто Инвест');
// Проход 2: стоп-лист вырос — несёт найденное в проходе 1
expect($fake->userPrompts[1])->toContain('CarMoney')
->and($fake->userPrompts[1])->toContain('Финансовый дом');
});
it('ИИ не должен повторять имена из канала А (дедуп с known)', function () {
// ИИ вернул фирму, уже найденную каналом А → не добавляем
$fake = new FakeResearcher(['[{"name":"КрасЛомбард","type":"региональная"},{"name":"Cashmotor","type":"федеральная"}]']);
$search = new ChannelBSearch($fake, new ResearcherParser);
$out = $search->harvest('займы', 'Красноярский край', 'lkomega.ru', ['КрасЛомбард'], 1);
expect($out)->toHaveCount(1)->and($out[0]['name'])->toBe('Cashmotor');
});
it('пустой ответ прохода не падает и не добавляет', function () {
$fake = new FakeResearcher(['[{"name":"Cashmotor","type":"федеральная"}]', '[]']);
$search = new ChannelBSearch($fake, new ResearcherParser);
$out = $search->harvest('займы', 'Красноярский край', 'lkomega.ru', [], 2);
expect($out)->toHaveCount(1)->and($out[0]['name'])->toBe('Cashmotor');
});
it('системный промт просит ТОЛЬКО названия (§11.3)', function () {
expect(ChannelBSearch::SYSTEM_PROMPT)->toContain('только НАЗВАНИЯ')
->and(ChannelBSearch::SYSTEM_PROMPT)->toContain('строго JSON');
});