*/ public array $userPrompts = []; /** @param list $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'); });