delete(); Artisan::call('help:rebuild-knowledge'); }); function topArticle(string $question): string { $hits = app(KnowledgeSearch::class)->search($question, 3); expect($hits)->not->toBeEmpty(); return $hits[0]->title; } it('поиск берёт статью ПО ТЕМЕ, а не соседнюю с теми же словами', function () { $cases = [ 'как создать проект' => 'Создать проект — пошагово', 'какие поля надо заполнить при создании проекта' => 'Создать проект — пошагово', 'как запустить проект обратно' => 'Пауза и запуск проекта', 'что такое сделка' => 'Сделки', 'можно скачать заявки в csv' => 'Выгрузить сделки', 'есть ли абонентская плата' => 'Тарифы', 'в какое время дня приходят лиды' => 'Выгрузка по проекту', 'у меня один и тот же конкурент два раза' => 'Дубли конкурентов', 'нашел источник, что дальше делать' => 'Проект из найденного источника', 'а бесплатно можно?' => 'Бесплатный тест', 'дубликаты номеров бывают?' => 'Повторы и дубли номеров', 'насколько свежие заявки, сколько времени прошло' => 'Что вы получаете', ]; foreach ($cases as $q => $expected) { expect(topArticle($q))->toContain($expected); } }); it('в инструкции нет выдуманных статусов «Отказ» и «Успешная»', function () { // Судья 12.07.2026: бот советовал ставить статус «Отказ» — его нет. Пять статусов: // Новая сделка / Просмотрено / В работе / Сделка / Не реализовано. $all = KnowledgeChunk::query()->pluck('content')->implode(' '); expect($all)->not->toContain('статус «Отказ»') ->and($all)->not->toContain('статус «Успешная»'); $deals = KnowledgeChunk::query() ->where('source_path', 'help/sdelki.md') ->orderBy('chunk_index') ->pluck('content') ->implode(' '); expect($deals)->toContain('«Не реализовано»'); }); it('значение источника менять можно — бот не имеет права говорить «нельзя»', function () { // Судья 12.07.2026: на «могу ли я поменять сайт в проекте» бот ответил «нельзя, // заводите новый». Неправда: не меняется только ТИП источника, а сам сайт — меняется. $hits = app(KnowledgeSearch::class)->search('могу ли я поменять сайт в уже созданном проекте', 3); $text = collect($hits)->pluck('content')->implode(' '); expect($hits[0]->title)->toContain('Сменить источник') ->and($text)->toContain('Менять можно значение'); }); it('за повтор номера деньги не берутся, а списание идёт за каждую заявку', function () { // Судья 12.07.2026: на «списываются ли деньги за дубли» бот ответил, что деньги // списываются ТОЛЬКО за услуги Конкурентного поля. Это враньё про деньги. $hits = app(KnowledgeSearch::class)->search('списывается ли деньги за дубли', 3); expect($hits[0]->title)->toContain('Повторы и дубли номеров'); $article = KnowledgeChunk::query() ->where('source_path', 'help/dubli-nomerov.md') ->orderBy('chunk_index') ->pluck('content') ->implode(' '); expect($article)->toContain('30 дней') ->and($article)->toContain('дважды за него не заплатите'); }); it('про штрафы бот не даёт гарантий — ответственность на клиенте', function () { // Судья 12.07.2026: бот категорично обещал «Нет, вас не оштрафуют». Гарантий не даём. $hits = app(KnowledgeSearch::class)->search('меня оштрафуют если я буду звонить по вашим номерам', 3); $text = collect($hits)->pluck('content')->implode(' '); expect($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('support@liderra.ru'); }); it('на старте не требует пополнения — подарок уже на балансе', function () { foreach (['с чего начать?', 'я только зарегистрировался что дальше'] as $q) { $hits = app(KnowledgeSearch::class)->search($q, 3); $text = collect($hits)->pluck('content')->implode(' '); expect($text)->toContain('1000 ₽'); } }); /** * Правило владельца 13.07.2026: замены заявки НЕТ (недозвон браком не считается). * Прежний текст статьи обещал «заменим» — это была ошибка, и бот её повторял клиентам. */ it('«верните деньги за заявку» — находится статья про недозвон, и она НЕ обещает замену', function () { $hits = app(KnowledgeSearch::class)->search('верните деньги за плохую заявку', 3); expect(collect($hits)->pluck('title')->implode(' | '))->toContain('не берёт трубку'); $text = collect($hits)->pluck('content')->implode(' '); expect($text)->not->toContain('заменим') ->and($text)->not->toContain('можем заменить'); }); it('ночью и после лимита — отвечает по статье, без выдумок', function () { $hits = app(KnowledgeSearch::class)->search('ночью заявки приходят?', 3); $text = collect($hits)->pluck('content')->implode(' '); expect($text)->toContain('Ночью заявки не приходят'); });