2026-07-12 15:30:49 +03:00
< ? php
declare ( strict_types = 1 );
use App\Models\KnowledgeChunk ;
use App\Services\Bot\BotAnswerService ;
use App\Services\Bot\KnowledgeSearch ;
use Illuminate\Foundation\Testing\RefreshDatabase ;
use Illuminate\Support\Facades\Http ;
uses ( RefreshDatabase :: class );
/**
* Живой прогон владельца 12.07.2026 (журнал bot_dialogs id 127– 182): «тупежа жуть».
* Каждый тест — конкретная тупость из переписки.
*/
beforeEach ( function () {
config () -> 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 ;
}
2026-07-12 16:13:34 +03:00
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 ( 'соглас' ); // причина — согласие абонента, требование закона
});
2026-07-12 15:30:49 +03:00
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 );
});
2026-07-12 16:13:34 +03:00
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 );
}
});
2026-07-12 15:30:49 +03:00
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 ( 'СМС о новых заявках не приходят' );
});