bd918c1dce
Прогон 46 диалогами (250 реплик подряд в одном чате) вскрыл то, чего не видел прогон отдельными вопросами: - Клиент оставил телефон → бот не отвечал ВООБЩЕ: письмо в поддержку слалось синхронно внутри ответа, SMTP не отозвался → упала вся задача (tries=1). Теперь номер в чате = заявка на звонок: принимаем всегда (даже без просьбы бота), письмо уходит через очередь и не может уронить ответ. - Бот «забывал» себя: сторож резал цену, взятую из истории чата, ответ пустел → эскалация. Числа из своих прошлых ответов теперь разрешены; память чата 3 → 5 пар реплик; в эскалационную ветку тоже передаётся история. - AnswerGuard: + телефон поддержки (его нет), + юр-гарантии в новых оборотах («Да, законно», «Штраф не прилетит»), + враньё «на паузе списаний нет», + выдумки про 18:00/ночь, + «цена зависит от ниши»; снимает висящее «Но…» и голое «Правильно, не будут» после вырезанной лжи. - Промпт: не повторять сказанное, не поддакивать клиенту, без нумерованных списков. - pravilo-18-00.md: 18:00 — граница для ИЗМЕНЕНИЙ, а не конец приёма заявок. Тесты 120/120. Бот НЕ на проде. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
92 lines
4.5 KiB
PHP
92 lines
4.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Jobs\Bot\ProcessJivoMessageJob;
|
|
use App\Mail\BotContactRequestMail;
|
|
use App\Models\BotDialog;
|
|
use App\Models\KnowledgeChunk;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Mail;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
/**
|
|
* Правило владельца 12.07.2026: бот не «передаёт диалог оператору» (оператор
|
|
* не сидит в чате круглосуточно), а ПРОСИТ телефон и присылает контакт письмом.
|
|
*/
|
|
beforeEach(function () {
|
|
config()->set('services.jivo_bot.outbound_url', 'https://bot.jivosite.com/webhooks/p/t');
|
|
config()->set('services.support.email', 'support@liderra.ru');
|
|
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::create([
|
|
'source_path' => 'help/p.md', 'title' => 'Что такое проект', 'tour' => null,
|
|
'topics' => 'создать проект', 'chunk_index' => 0,
|
|
'content' => 'Проект — это заявка на поток клиентов.',
|
|
]);
|
|
Http::fake(['bot.jivosite.com/*' => Http::response(['ok' => true])]);
|
|
});
|
|
|
|
it('эскалация просит телефон, а не зовёт оператора в чат', function () {
|
|
Mail::fake();
|
|
|
|
(new ProcessJivoMessageJob('chat-c1', 'client-1', 'какой у меня баланс?'))->handle();
|
|
|
|
$out = BotDialog::where('direction', 'out')->firstOrFail();
|
|
expect($out->escalated)->toBeTrue()
|
|
->and($out->message)->toContain('номер телефона');
|
|
|
|
Http::assertNotSent(fn ($r) => ($r['event'] ?? '') === 'INVITE_AGENT');
|
|
Mail::assertNothingSent();
|
|
});
|
|
|
|
it('клиент прислал телефон после просьбы → письмо с контактом и вопросом уходит в поддержку', function () {
|
|
Mail::fake();
|
|
|
|
(new ProcessJivoMessageJob('chat-c2', 'client-2', 'какой у меня баланс?'))->handle();
|
|
(new ProcessJivoMessageJob('chat-c2', 'client-2', 'мой номер +7 916 123-45-67'))->handle();
|
|
|
|
// Письмо уходит ЧЕРЕЗ ОЧЕРЕДЬ: почта не должна ронять ответ клиенту (прогон диалогами
|
|
// 12.07.2026 — SMTP не ответил, задача упала, клиент с номером остался без ответа).
|
|
Mail::assertQueued(BotContactRequestMail::class, function (BotContactRequestMail $mail) {
|
|
return $mail->hasTo('support@liderra.ru')
|
|
&& $mail->phone === '79161234567'
|
|
&& str_contains($mail->question, 'баланс');
|
|
});
|
|
|
|
$last = BotDialog::where('direction', 'out')->latest('id')->firstOrFail();
|
|
expect($last->message)->toContain('Спасибо');
|
|
});
|
|
|
|
it('телефон без просьбы бота — тоже принимаем: человек хочет, чтобы перезвонили', function () {
|
|
// Правило изменено 12.07.2026 по итогам прогона диалогами: раньше номер «без просьбы»
|
|
// игнорировался, и бот отвечал «не могу работать с персональными данными в чате».
|
|
// Номер в чате = заявка на звонок. Не теряем его никогда.
|
|
Mail::fake();
|
|
|
|
(new ProcessJivoMessageJob('chat-c3', 'client-3', 'звоните мне на +79161234567 если что'))->handle();
|
|
|
|
Mail::assertQueued(BotContactRequestMail::class, fn ($m) => $m->phone === '79161234567');
|
|
$last = BotDialog::where('direction', 'out')->latest('id')->firstOrFail();
|
|
expect($last->message)->toContain('Спасибо');
|
|
});
|
|
|
|
it('номер распознаётся в разных форматах', function () {
|
|
Mail::fake();
|
|
|
|
$variants = ['8 (916) 123-45-67', '+7-916-123-45-67', '79161234567'];
|
|
foreach ($variants as $i => $phone) {
|
|
$chat = 'chat-fmt-'.$i;
|
|
(new ProcessJivoMessageJob($chat, 'c', 'какой у меня баланс?'))->handle();
|
|
(new ProcessJivoMessageJob($chat, 'c', $phone))->handle();
|
|
}
|
|
|
|
Mail::assertQueued(BotContactRequestMail::class, 3);
|
|
Mail::assertQueued(BotContactRequestMail::class, fn ($m) => $m->phone === '79161234567');
|
|
});
|