133 lines
7.5 KiB
PHP
133 lines
7.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\KnowledgeChunk;
|
|
use App\Services\Bot\KnowledgeSearch;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
/**
|
|
* Экскурсия «Показать на портале» цеплялась, если у вопроса и статьи совпало ХОТЯ БЫ ОДНО
|
|
* слово, — поэтому на болтовню («первое сообщение подряд») бот предлагал экскурсию по
|
|
* уведомлениям, а на вопрос про поддержку уводил в другой раздел (живая проверка 13.07.2026,
|
|
* до этого два десятка таких ссылок в прогоне 12.07).
|
|
*
|
|
* Правило: экскурсия только тогда, когда реплика — ВОПРОС (есть «?» или вопросительное
|
|
* слово) И слово из него попало в заголовок статьи или её синонимы. «Показать на портале»
|
|
* имеет смысл, только если человек о чём-то спрашивает.
|
|
*/
|
|
beforeEach(function () {
|
|
KnowledgeChunk::create([
|
|
'source_path' => 'help/create.md', 'title' => 'Как создать проект', 'tour' => 'create-project',
|
|
'topics' => 'создать проект, новый проект, запустить сбор', 'chunk_index' => 0,
|
|
'content' => 'Проект создаётся в разделе «Проекты», кнопка «Создать».',
|
|
]);
|
|
KnowledgeChunk::create([
|
|
'source_path' => 'help/notify.md', 'title' => 'Уведомления портала', 'tour' => 'notifications',
|
|
'topics' => 'уведомления, сообщение, письмо, колокольчик', 'chunk_index' => 0,
|
|
'content' => 'Уведомления приходят в колокольчик и на почту.',
|
|
]);
|
|
});
|
|
|
|
it('на болтовню без сути экскурсию не предлагает', function () {
|
|
$search = app(KnowledgeSearch::class);
|
|
$notify = KnowledgeChunk::where('tour', 'notifications')->firstOrFail();
|
|
|
|
foreach (['привет', 'ага', 'слушаю', 'спасибо'] as $chatter) {
|
|
expect($search->isOnTopic($notify, $chatter))->toBeFalse();
|
|
}
|
|
});
|
|
|
|
it('реплика не-вопрос экскурсию не получает, даже если слово совпало', function () {
|
|
$search = app(KnowledgeSearch::class);
|
|
$notify = KnowledgeChunk::where('tour', 'notifications')->firstOrFail();
|
|
|
|
// «сообщение» есть в ключевых словах статьи про уведомления — но человек ни о чём
|
|
// не спрашивает. Именно так бот и водил на экскурсию по уведомлениям (13.07.2026).
|
|
expect($search->isOnTopic($notify, 'первое сообщение подряд'))->toBeFalse();
|
|
});
|
|
|
|
it('вопрос по теме статьи экскурсию получает', function () {
|
|
$search = app(KnowledgeSearch::class);
|
|
$create = KnowledgeChunk::where('tour', 'create-project')->firstOrFail();
|
|
|
|
expect($search->isOnTopic($create, 'как создать проект?'))->toBeTrue();
|
|
});
|
|
|
|
it('совпадение по двум ключевым словам тоже считается', function () {
|
|
$search = app(KnowledgeSearch::class);
|
|
$notify = KnowledgeChunk::where('tour', 'notifications')->firstOrFail();
|
|
|
|
expect($search->isOnTopic($notify, 'куда приходит письмо и колокольчик?'))->toBeTrue();
|
|
});
|
|
|
|
/**
|
|
* Живая проверка 13.07.2026 (гость с лендинга, 10 вопросов, 3 промаха): частые вопросы
|
|
* находят статью, но ссылки на разбор не получают — либо статья не находится вовсе
|
|
* («что такое канбан»), либо isOnTopic() не узнаёт свою же статью в найденном чанке
|
|
* («как пополнить баланс», «где смотреть заявки»). Проверяем на РЕАЛЬНЫХ статьях
|
|
* resources/help — синтетические фикстуры этот баг не ловят.
|
|
*/
|
|
it('«что такое канбан» — статья находится и признаётся по теме', function () {
|
|
KnowledgeChunk::query()->delete();
|
|
Artisan::call('help:rebuild-knowledge');
|
|
|
|
$search = app(KnowledgeSearch::class);
|
|
$hits = $search->search('что такое канбан', 3);
|
|
|
|
expect($hits)->not->toBeEmpty()
|
|
->and($hits[0]->title)->toContain('Канбан');
|
|
expect($search->isOnTopic($hits[0], 'что такое канбан'))->toBeTrue();
|
|
});
|
|
|
|
it('«как пополнить баланс» — статья находится и признаётся по теме', function () {
|
|
KnowledgeChunk::query()->delete();
|
|
Artisan::call('help:rebuild-knowledge');
|
|
|
|
$search = app(KnowledgeSearch::class);
|
|
$hits = $search->search('как пополнить баланс', 3);
|
|
|
|
expect($hits)->not->toBeEmpty()
|
|
->and($hits[0]->title)->toContain('Как пополнить баланс');
|
|
expect($search->isOnTopic($hits[0], 'как пополнить баланс'))->toBeTrue();
|
|
});
|
|
|
|
it('«где смотреть заявки» — находит именно «Сделки» (а не соседнюю статью) и признаёт по теме', function () {
|
|
// «заявка» слишком частое слово в синонимах ДРУГИХ статей (например «Замена заявки»
|
|
// с пятью вхождениями в topics) — поиск не должен путать частоту слова с темой вопроса.
|
|
KnowledgeChunk::query()->delete();
|
|
Artisan::call('help:rebuild-knowledge');
|
|
|
|
$search = app(KnowledgeSearch::class);
|
|
$hits = $search->search('где смотреть заявки', 3);
|
|
|
|
expect($hits)->not->toBeEmpty()
|
|
->and($hits[0]->title)->toContain('Сделки');
|
|
expect($search->isOnTopic($hits[0], 'где смотреть заявки'))->toBeTrue();
|
|
});
|
|
|
|
it('контроль: случайное общее слово НЕ делает чужую статью «по теме» (регистрация ≠ импорт)', function () {
|
|
// Живой урок 12.07.2026: спросили про регистрацию — бот повёл на статью про импорт
|
|
// (совпало одно случайное слово). Это правило беречь — не сломать при починке выше.
|
|
KnowledgeChunk::query()->delete();
|
|
Artisan::call('help:rebuild-knowledge');
|
|
|
|
$search = app(KnowledgeSearch::class);
|
|
$import = KnowledgeChunk::where('source_path', 'help/import-dannyh.md')->firstOrFail();
|
|
|
|
expect($search->isOnTopic($import, 'как мне зарегистрироваться на портале'))->toBeFalse();
|
|
});
|
|
|
|
it('контроль: «привет» не по теме ни для одной статьи', function () {
|
|
KnowledgeChunk::query()->delete();
|
|
Artisan::call('help:rebuild-knowledge');
|
|
|
|
$search = app(KnowledgeSearch::class);
|
|
$any = KnowledgeChunk::first();
|
|
|
|
expect($search->isOnTopic($any, 'привет'))->toBeFalse();
|
|
});
|