Files
portal/app/tests/Feature/Bot/LiveDialogFixesTest.php
T
Дмитрий beb0528612 fix(bot): продукт, 50% номеров и договор-оферта — по живым замечаниям владельца
1. 🔴 МОЙ КОСЯК: на «как получить номер человека, заходящего на сайт» бот отвечал
   «нельзя» — то есть отговаривал клиента от того, ЧТО МЫ И ПРОДАЁМ. Владелец:
   «как нельзя, а мы чем занимаемся?!». Теперь: «это ровно то, что делает Лидерра —
   создайте проект, укажите сайт, контакты пойдут в Сделки».
2. «Все ли номера с источника цепляет система?» — нет, ПРИМЕРНО ПОЛОВИНУ: передавать
   можно только абонентов с согласием (требование закона). Формулировка владельца.
3. «Как подписать договор с юрлицом?» — у нас публичная ОФЕРТА, подписывать нечего.
   Слово «договор» убрано из стоп-тем; новая статья: оферта (/legal/offer), политика,
   возврат + закрывающие документы (счёт → оплата → акт, УСН без НДС).
   Деньги/жалобы/претензии — по-прежнему живой специалист.

Все три проверены ЖИВЬЁМ через настоящий вебхук чата (а не в обход очереди).
Инструкция: 41 статья. Pest бот 58/58.

🪤 Урок: после правки кода бота ОБЯЗАТЕЛЬНО перезапускать queue:work — воркер
кэширует бут. Владелец видел старые ответы, пока я радовался зелёным тестам.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 16:13:34 +03:00

211 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 127182): «тупежа жуть».
* Каждый тест — конкретная тупость из переписки.
*/
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;
}
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('СМС о новых заявках не приходят');
});