feat(bot): задача очереди отдаёт мозгу карточку фактов вошедшего
This commit is contained in:
@@ -5,8 +5,12 @@ declare(strict_types=1);
|
||||
use App\Jobs\Bot\ProcessChatMessageJob;
|
||||
use App\Models\BotDialog;
|
||||
use App\Models\KnowledgeChunk;
|
||||
use App\Services\Bot\BotAnswer;
|
||||
use App\Services\Bot\BotAnswerService;
|
||||
use App\Services\Bot\ClientFacts;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
@@ -56,10 +60,10 @@ it('happy path: ответ пишется в журнал (in+out) с latency',
|
||||
});
|
||||
|
||||
it('личный вопрос без карточки фактов: приглашение в кабинет в чате, журнал escalated=false', function () {
|
||||
// Правило изменено 13.07.2026: ProcessChatMessageJob карточку фактов ClientFacts пока
|
||||
// не передаёт (отдельная задача) — с точки зрения BotAnswerService это гость, и на
|
||||
// личный вопрос он получает приглашение войти в кабинет, а не эскалацию к специалисту
|
||||
// (см. BotContactCaptureTest, PersonalAnswersTest).
|
||||
// chatMessage() пишет реплику ГОСТЯ (user_id не задан) — карточку фактов ClientFacts
|
||||
// джоба спрашивает только у вошедшего клиента (см. тесты ниже про user_id), у гостя
|
||||
// facts остаётся null и BotAnswerService приглашает войти в кабинет, а не зовёт
|
||||
// специалиста (см. BotContactCaptureTest, PersonalAnswersTest).
|
||||
chatMessage('chat-2', 'какой у меня баланс?');
|
||||
|
||||
$out = BotDialog::where('direction', 'out')->firstOrFail();
|
||||
@@ -110,3 +114,110 @@ it('джоба объявлена с queue=bot и timeout ≤ 12 сек', functi
|
||||
expect($job->queue)->toBe('bot')
|
||||
->and($job->timeout)->toBeLessThanOrEqual(12);
|
||||
});
|
||||
|
||||
it('вошедший клиент (у строки журнала есть user_id): джоба берёт карточку у ClientFacts и передаёт её в BotAnswerService', function () {
|
||||
$facts = new class
|
||||
{
|
||||
/** @var list<int> */
|
||||
public array $calledWith = [];
|
||||
|
||||
public function card(int $userId): ?string
|
||||
{
|
||||
$this->calledWith[] = $userId;
|
||||
|
||||
return 'ДАННЫЕ ЭТОГО КЛИЕНТА: Баланс: 777 ₽.';
|
||||
}
|
||||
};
|
||||
app()->instance(ClientFacts::class, $facts);
|
||||
|
||||
$botAnswer = new class
|
||||
{
|
||||
public ?string $receivedFacts = 'UNSET';
|
||||
|
||||
public function answer(string $question, array $history = [], bool $fromPortal = true, ?string $facts = null): BotAnswer
|
||||
{
|
||||
$this->receivedFacts = $facts;
|
||||
|
||||
return new BotAnswer('ответ', escalate: false);
|
||||
}
|
||||
};
|
||||
app()->instance(BotAnswerService::class, $botAnswer);
|
||||
|
||||
$row = BotDialog::create([
|
||||
'chat_id' => 'chat-user',
|
||||
'direction' => 'in',
|
||||
'message' => 'какой у меня баланс?',
|
||||
'user_id' => 777,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
(new ProcessChatMessageJob('chat-user', (int) $row->id, 'какой у меня баланс?'))->handle();
|
||||
|
||||
expect($facts->calledWith)->toBe([777])
|
||||
->and($botAnswer->receivedFacts)->toContain('777 ₽');
|
||||
});
|
||||
|
||||
it('гость (у строки журнала user_id пуст): ClientFacts не вызывается, в BotAnswerService приходит facts=null', function () {
|
||||
$facts = new class
|
||||
{
|
||||
public int $calls = 0;
|
||||
|
||||
public function card(int $userId): ?string
|
||||
{
|
||||
$this->calls++;
|
||||
|
||||
return 'ЭТО НЕ ДОЛЖНО БЫТЬ ВЫЗВАНО У ГОСТЯ';
|
||||
}
|
||||
};
|
||||
app()->instance(ClientFacts::class, $facts);
|
||||
|
||||
$botAnswer = new class
|
||||
{
|
||||
public ?string $receivedFacts = 'UNSET';
|
||||
|
||||
public function answer(string $question, array $history = [], bool $fromPortal = true, ?string $facts = null): BotAnswer
|
||||
{
|
||||
$this->receivedFacts = $facts;
|
||||
|
||||
return new BotAnswer('ответ', escalate: false);
|
||||
}
|
||||
};
|
||||
app()->instance(BotAnswerService::class, $botAnswer);
|
||||
|
||||
chatMessage('chat-guest-facts', 'что такое проект?');
|
||||
|
||||
expect($facts->calls)->toBe(0)
|
||||
->and($botAnswer->receivedFacts)->toBeNull();
|
||||
});
|
||||
|
||||
it('ClientFacts бросил исключение: клиент всё равно получает ответ (журнал out), ошибка уходит в лог', function () {
|
||||
Log::spy();
|
||||
|
||||
app()->instance(ClientFacts::class, new class
|
||||
{
|
||||
public function card(int $userId): ?string
|
||||
{
|
||||
throw new RuntimeException('база моргнула');
|
||||
}
|
||||
});
|
||||
|
||||
Http::fake([
|
||||
'llm.api.cloud.yandex.net/*' => Http::response([
|
||||
'result' => ['alternatives' => [['message' => ['role' => 'assistant', 'text' => 'Ок.']]]],
|
||||
]),
|
||||
]);
|
||||
|
||||
$row = BotDialog::create([
|
||||
'chat_id' => 'chat-boom',
|
||||
'direction' => 'in',
|
||||
'message' => 'что такое проект?',
|
||||
'user_id' => 999,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
(new ProcessChatMessageJob('chat-boom', (int) $row->id, 'что такое проект?'))->handle();
|
||||
|
||||
expect(BotDialog::where('chat_id', 'chat-boom')->where('direction', 'out')->exists())->toBeTrue();
|
||||
|
||||
Log::shouldHaveReceived('error')->once();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user