78 lines
3.9 KiB
PHP
78 lines
3.9 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
use App\Models\KnowledgeChunk;
|
||
|
|
use App\Services\Bot\BotAnswerService;
|
||
|
|
use App\Services\Bot\ChatTextCleaner;
|
||
|
|
use App\Services\Bot\KnowledgeSearch;
|
||
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
|
|
use Illuminate\Support\Facades\Http;
|
||
|
|
|
||
|
|
uses(RefreshDatabase::class);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Слепое сравнение моделей 12.07.2026: Gemini выиграл у Яндекса 62:36, но судьи нашли
|
||
|
|
* пять болезней — враньё про платный повтор сбора, цены по памяти, «приглашу специалиста»,
|
||
|
|
* гарантии и markdown-разметка в чате.
|
||
|
|
*/
|
||
|
|
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');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('в чат не уходит markdown-разметка — Jivo её не рисует', function () {
|
||
|
|
$cleaner = app(ChatTextCleaner::class);
|
||
|
|
|
||
|
|
expect($cleaner->clean('**Важно:** пишите на `support@liderra.ru`'))
|
||
|
|
->toBe('Важно: пишите на support@liderra.ru')
|
||
|
|
->and($cleaner->clean("Причины:\n* первая\n* вторая"))
|
||
|
|
->toBe("Причины:\n— первая\n— вторая")
|
||
|
|
->and($cleaner->clean('### Заголовок'))->toBe('Заголовок');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('ответ бота приходит клиенту без звёздочек и кавычек', function () {
|
||
|
|
Http::fake(['*' => Http::response([
|
||
|
|
'result' => ['alternatives' => [['message' => ['text' => '**Списания** видно в разделе `Биллинг`.']]]],
|
||
|
|
], 200)]);
|
||
|
|
|
||
|
|
$answer = app(BotAnswerService::class)->answer('где посмотреть списания');
|
||
|
|
|
||
|
|
expect($answer->text)->not->toContain('**')
|
||
|
|
->and($answer->text)->not->toContain('`')
|
||
|
|
->and($answer->text)->toContain('Списания');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('статья честно предупреждает: повторный сбор платный даже без результата', function () {
|
||
|
|
// Судья 12.07.2026: обе модели соврали на «источников не нашлось — деньги спишутся?».
|
||
|
|
// Первый сбор без результата бесплатен, ПОВТОРНЫЙ списывается всегда.
|
||
|
|
$hits = app(KnowledgeSearch::class)->search('источников не нашлось, деньги спишутся?', 3);
|
||
|
|
$text = collect($hits)->pluck('content')->implode(' ');
|
||
|
|
|
||
|
|
expect($text)->toContain('Повторный сбор списывается всегда')
|
||
|
|
->and($text)->toContain('даже если ничего не нашлось');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('правила для модели запрещают гарантии, цены по памяти и «приглашу специалиста»', function () {
|
||
|
|
// Правила живут в системном промпте — проверяем, что они там есть и доезжают до модели.
|
||
|
|
Http::fake(['*' => Http::response([
|
||
|
|
'result' => ['alternatives' => [['message' => ['text' => 'Ответ.']]]],
|
||
|
|
], 200)]);
|
||
|
|
|
||
|
|
app(BotAnswerService::class)->answer('сколько стоит заявка');
|
||
|
|
|
||
|
|
Http::assertSent(function ($request) {
|
||
|
|
$system = (string) ($request->data()['messages'][0]['text'] ?? '');
|
||
|
|
|
||
|
|
return str_contains($system, 'Никогда не называй цены по памяти')
|
||
|
|
&& str_contains($system, 'Не обещай «приглашу специалиста»')
|
||
|
|
&& str_contains($system, 'Не давай гарантий')
|
||
|
|
&& str_contains($system, 'Без markdown');
|
||
|
|
});
|
||
|
|
});
|