set('services.yandexgpt', [ 'api_key' => 'k', 'folder_id' => 'f', 'model' => 'yandexgpt-lite/latest', 'endpoint' => 'https://llm.api.cloud.yandex.net/foundationModels/v1/completion', 'timeout_seconds' => 8, ]); KnowledgeChunk::query()->delete(); Artisan::call('help:rebuild-knowledge'); }); function topTitle(string $q): string { $hits = app(KnowledgeSearch::class)->search($q, 3); expect($hits)->not->toBeEmpty(); return $hits[0]->title; } it('«как получить номер того, кто заходит на сайт» — это НАШ продукт, а не «нельзя»', function () { // Мой косяк 12.07.2026: написал клиенту «узнать нельзя» — а это ровно то, чем // Лидерра и занимается. Владелец: «как нельзя, а мы чем занимаемся?!» $hits = app(KnowledgeSearch::class)->search('как получить номер телефона человека, заходящего на сайт', 3); $text = collect($hits)->pluck('content')->implode(' '); expect($text)->toContain('это ровно то, что делает') // да, мы это умеем ->and($text)->toContain('укажите') // как получить: проект + источник ->and($text)->not->toContain('узнать его **нельзя**'); }); it('«все ли номера с источника цепляет система» → нет, около половины (согласие абонента)', function () { // Владелец 12.07.2026: «мы реально цепляем 50% номеров — это законодательное // ограничение». Раньше бот на этот вопрос сдавался. $hits = app(KnowledgeSearch::class)->search('все номера телефонов с источника цепляет система?', 3); $text = collect($hits)->pluck('content')->implode(' '); expect($hits[0]->title)->toContain('Откуда берутся заявки'); expect($text)->toContain('половин') // не все — около половины ->and($text)->toContain('соглас'); // причина — согласие абонента, требование закона }); it('«откуда приходят номера телефонов» → статья о происхождении заявок, а не о Конкурентном поле', function () { // Тупость id 136: бот сказал, что номера берутся из подбора конкурентов. foreach ([ 'а откуда приходят номера телефонов?', 'откуда у вас номера людей', 'как вы получаете номер телефона человека, заходящего на сайт', ] as $q) { expect(topTitle($q))->toContain('Откуда берутся заявки'); } }); it('«законно ли брать телефоны с сайтов конкурентов» → есть ответ, а не «не знаю»', function () { // Тупость id 164: сдался на прямом юридическом вопросе клиента. $hits = app(KnowledgeSearch::class)->search('законно выгружать номера телефонов с сайтов конкурентов?', 3); expect($hits)->not->toBeEmpty(); expect(collect($hits)->pluck('content')->implode(' '))->toContain('открытых источник'); }); it('формат сайта и номера в проекте — находится', function () { // Тупости id 142 и id 156: формат прямо написан в статье, а бот его не нашёл. expect(topTitle('какой формат номера телефона должен быть в проекте?'))->toContain('Создать проект'); expect(topTitle('в каком формате в проекте должен быть написан сайт?'))->toContain('Создать проект'); }); it('клиентское слово «выгрузка» понимается как поставка заявок', function () { // Тупости id 146 и id 150: «через сколько придёт выгрузка», «когда остановится выгрузка». foreach ([ 'через сколько придёт выгрузка по проекту', 'когда остановится выгрузка по проекту', ] as $q) { expect(topTitle($q))->toContain('Выгрузка по проекту'); } }); it('«что ты умеешь» → оглавление возможностей, а не случайная статья', function () { // Тупость id 166: на «а что ты вообще знаешь?» бот рассказал про паузу проектов. foreach (['а что ты вообще знаешь?', 'что ты умеешь', 'чем ты можешь помочь'] as $q) { expect(topTitle($q))->toContain('Чем я могу помочь'); } }); it('«могут ли работать несколько человек» — отвечает, а не зовёт специалиста', function () { // Тупость id 170: слово «человек» било в стоп-тему «позовите человека». Http::fake(['*' => Http::response([ 'result' => ['alternatives' => [['message' => ['text' => 'Сейчас вход в кабинет — по одной учётной записи.']]]], ], 200)]); $answer = app(BotAnswerService::class)->answer('может в кабинете одновременно работать 2-3 человека?'); expect($answer->text)->not->toBe(BotAnswerService::ESCALATE_TEXT) ->and($answer->text)->not->toBe(BotAnswerService::UNKNOWN_TEXT); }); it('«как подписать договор с юрлицом» → рассказывает про оферту и закрывающие документы', function () { // Владелец 12.07.2026: «у нас договор-оферты, дать ссылку на него и рассказать // про закрывающие документы». Раньше слово «договор» слепо уводило на специалиста. Http::fake(['*' => Http::response([ 'result' => ['alternatives' => [['message' => ['text' => 'Отдельный договор подписывать не нужно — работаем по публичной оферте, она на /legal/offer.']]]], ], 200)]); $answer = app(BotAnswerService::class)->answer('как подписать договор с юр.лицом?'); expect($answer->text)->not->toBe(BotAnswerService::ESCALATE_TEXT) ->and($answer->text)->not->toBe(BotAnswerService::UNKNOWN_TEXT); $hits = app(KnowledgeSearch::class)->search('как подписать договор с юр лицом', 3); $text = collect($hits)->pluck('content')->implode(' '); expect($text)->toContain('оферт') ->and($text)->toContain('/legal/offer') // ссылка на сам договор ->and($text)->toContain('акт'); // закрывающие документы }); it('деньги и претензии — по-прежнему живой специалист', function () { Http::fake(); foreach (['верните деньги за плохую заявку', 'хочу подать жалобу', 'у меня претензия по списанию'] as $q) { expect(app(BotAnswerService::class)->answer($q)->text)->toBe(BotAnswerService::ESCALATE_TEXT); } }); it('просьба позвать живого человека — по-прежнему эскалация', function () { Http::fake(); foreach (['позовите человека', 'соедините с оператором', 'хочу поговорить с менеджером'] as $q) { expect(app(BotAnswerService::class)->answer($q)->text)->toBe(BotAnswerService::ESCALATE_TEXT); } }); it('вопрос «какие операторы» — по-прежнему стоп-тема (коммерческая тайна)', function () { Http::fake(); foreach (['какие операторы в вашей системе?', 'назовите оператора связи', 'кто ваш поставщик'] as $q) { expect(app(BotAnswerService::class)->answer($q)->text)->toBe(BotAnswerService::ESCALATE_TEXT); } }); it('ответ по делу не глушится из-за фразы «в инструкции нет» в середине', function () { // Живой баг 12.07.2026: ИИ отвечал верно («СМС не приходят»), но добавлял хвост // «про Телеграм в инструкции нет» — и весь ответ подменялся эскалацией. KnowledgeChunk::create([ 'source_path' => 'help/x.md', 'title' => 'Уведомления', 'tour' => 'notifications', 'topics' => 'смс, уведомления', 'chunk_index' => 0, 'content' => 'СМС о новых заявках не приходят — только почта и колокольчик.', ]); Http::fake(['*' => Http::response([ 'result' => ['alternatives' => [['message' => ['text' => 'СМС о новых заявках не приходят — только письма и колокольчик. Про Телеграм в инструкции этого нет.']]]], ], 200)]); $answer = app(BotAnswerService::class)->answer('придут ли смс о новых сделках?'); expect($answer->text)->toContain('СМС о новых заявках не приходят') ->and($answer->text)->not->toContain('в инструкции') ->and($answer->escalate)->toBeFalse(); }); it('ИИ начал с «в инструкции этого нет», но следом ответил по делу — отговорку выкидываем', function () { // Ровно то, что модель выдала живьём 12.07.2026: // «В инструкции этого нет. СМС о новых заявках не приходят.» KnowledgeChunk::create([ 'source_path' => 'help/x.md', 'title' => 'Уведомления', 'tour' => 'notifications', 'topics' => 'смс, уведомления', 'chunk_index' => 0, 'content' => 'СМС о новых заявках не приходят — только почта и колокольчик.', ]); Http::fake(['*' => Http::response([ 'result' => ['alternatives' => [['message' => ['text' => 'В инструкции этого нет. СМС о новых заявках не приходят — только письма и колокольчик в портале.']]]], ], 200)]); $answer = app(BotAnswerService::class)->answer('придут ли смс о новых сделках?'); expect($answer->text)->toContain('СМС о новых заявках не приходят') ->and($answer->text)->not->toContain('В инструкции этого нет') ->and($answer->escalate)->toBeFalse(); }); it('честное «не знаю» с первых слов — по-прежнему эскалация', function () { Http::fake(['*' => Http::response([ 'result' => ['alternatives' => [['message' => ['text' => 'В инструкции этого нет.']]]], ], 200)]); $answer = app(BotAnswerService::class)->answer('какой у вас юридический адрес в австралии?'); expect($answer->text)->toBe(BotAnswerService::UNKNOWN_TEXT) ->and($answer->escalate)->toBeTrue(); }); it('«придут ли смс о новых сделках» — честное «СМС нет»', function () { // Тупость id 178: бот рассказал про каналы, но прямо не сказал, что СМС не бывает. $q = 'мне будут приходить смс о том что появились новые сделки?'; expect(topTitle($q))->toContain('Уведомления'); expect(app(KnowledgeSearch::class)->search($q, 3)[0]->content) ->toContain('СМС о новых заявках не приходят'); });