'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(); });