# TESTING — приёмочный тест-план Тестирует **заказчик** (Hermes) на реальном железе, а не исполнитель. Исполнитель пишет тесты; приёмка идёт независимыми проверками ниже. Правило приёмки: **«зелёная сборка» ничего не доказывает, пока мутация не валит нужный тест.** ## 0. Подготовка стенда ```bash export MEMO_MODEL_DIR=/root/WORK/memo/models/siglip2 mkdir -p "$MEMO_MODEL_DIR" curl -fSL -o "$MEMO_MODEL_DIR/text_model_int8.onnx" http://static.binom.pw/models/siglip2/text_model_int8.onnx # 283M curl -fSL -o "$MEMO_MODEL_DIR/tokenizer.model" http://static.binom.pw/models/siglip2/tokenizer.model # 4.2M # эталонный корпус: 12-15 заметок в 3 папках-коллекциях export MEMO_ROOT=/root/WORK/memo-e2e # infra/*.md (серверы, домены), work/jira/*.md, life/books/*.md — обычный markdown с заголовками ``` Пороговое требование: файлов не меньше 12 в трёх папках, в текстах есть и кириллица, и латиница, и хотя бы по одному точному значению на папку (IP, номер тикета, год издания). ## T1. Индексация и её повтор ```bash cd /root/WORK/memo && ./gradlew installDist -q CLI=./memo-cli/build/install/memo/bin/memo $CLI index "$MEMO_ROOT"; $CLI status "$MEMO_ROOT" find "$MEMO_ROOT" -name index.db | sort # ожидается 3 пути, по одному на коллекцию $CLI index "$MEMO_ROOT" # второй прогон ``` Ожидается: в каждой коллекции появился `.memo/index.db` (**ровно три** — вложенные папки без своих заметок коллекциями не считаются); второй прогон **не переэмбеддивает** ни одного файла (в выводе 0 обновлённых, `status` показывает те же `indexed_at`). Провал: нет `.memo/index.db`; баз больше трёх (значит, коллекцией считается каждая папка с .md внутри); второй прогон молча переиндексирует всё. ## T2. Смысловой поиск (вектор работает) 20 вопросов, сформулированных **другими словами**, чем в заметках. Ожидание: нужный раздел в top-5 не реже 16 раз из 20 (recall@5 ≥ 0.8). Формулировки и ожидаемый файл — в `e2e/questions.tsv` (создаёт заказчик). ```bash $CLI search "$MEMO_ROOT/infra" "как пережить перезагрузку сервера, чтобы операция не умерла" --json ``` Провал: в результатах нет нужного файла; возвращаются чанки без текста (агент не сможет ими воспользоваться); поиск идёт по всему корню вместо указанной папки. ## T3. Лексический поиск (BM25 работает) Вопросы с точными значениями: IP-адрес, номер тикета, версия, год. Ожидание: точное совпадение найдено, и именно через лексику. ```bash $CLI search "$MEMO_ROOT/infra" "76.132" --json # должен найти заметку с этим IP $CLI search "$MEMO_ROOT/infra" "76.132" --mode lex --json ``` Провал: находит «похожее по смыслу», но не строку с точным значением; `--mode lex` даёт тот же результат, что `--mode vec` (значит, режимы не разделены). ## T4. Живая правка файла ```bash echo "\n## Свежий раздел\nуникальное_слово_дзынь_47" >> "$MEMO_ROOT/infra/hosts.md" sleep 1 $CLI search "$MEMO_ROOT/infra" "уникальное слово дзынь" --json # ≤1 с после правки echo "## Ещё раздел\nвторое_слово_бамс_93" >> "$MEMO_ROOT/infra/hosts.md" $CLI search "$MEMO_ROOT/infra" "второе слово бамс" --json # БЕЗ sleep: тот же вызов ``` Ожидается: первая правка подхвачена watcher'ом ≤1 с; вторая видна **в том же вызове поиска** (stat-догон до поиска). Провал: поиск не видит свежую правку; `database is locked` при одновременной записи watcher'ом и чтении (признак: WAL не включён). ## T5. MCP-сервер по протоколу (не по самоотчёту) Инструменты подключаются к Hermes **только в новой сессии**, поэтому проверяем протоколом: ```bash python3 scripts/mcp_probe.py --cmd "./memo-mcp/build/install/memo-mcp/bin/memo-mcp" \ --call memo_search --args '{"path":"'"$MEMO_ROOT"'/infra","query":"домены","k":3}' ``` Скрипт-зонд: пишет `initialize`, `notifications/initialized`, `tools/list`, `tools/call` в stdin процесса, читает JSON-RPC-ответы. Ожидается: `tools/list` вернул 3 инструмента; `tools/call` — непустой `content[0].text` с результатами; `isError` отсутствует. **Тестовые ручки (то же без транспорта):** ```bash $CLI mcp-probe --tool memo_search --args '{"path":"'"$MEMO_ROOT"'/infra","query":"домены"}' # плюс режим "одного вызова": разобрать строку запроса и вернуть ответ, не поднимая stdio ``` Провал: сервер печатает что-то в stdout помимо JSON-RPC (ломает клиента); на `tools/call` отвечает `-32601`; результат — пустая строка. ## T6. Офлайн ```bash # отключаем внешний интерфейс у процесса: без сети индексация и поиск обязаны работать $CLI index "$MEMO_ROOT" && $CLI search "$MEMO_ROOT" "любой вопрос" --json ``` Провал: любой сетевой вызов в рантайме (модель качается при каждом старте, эмбеддинг уходит в API). ## M. Мутационная приёмка (обязательна) Для каждого пункта: сломать ровно одно место в боевом коде (python-replace с `assert old in s`), прогнать `./gradlew cleanTest test`, **сверить XML-отчёты и их mtime**, затем `git checkout -- <файл>`. | Мутация | Какой тест обязан упасть | |---|---| | Чанкер: резать по 4000 символов, игнорируя заголовки | T2 (recall), тест чанкера | | Поиск: `mode=lex` уходит в вектор | T3, тест разделения режимов | | RRF: убрать слияние (только вектор) | T3 | | Индексация: игнорировать `mtime`/хэш, всегда переиндексировать | T1 (второй прогон не no-op) | | Watcher: убрать вызов `pollEvents`/регистрацию | T4 (первая часть) | | Поиск: убрать stat-догон перед поиском | T4 (вторая часть) | | MCP: `tools/list` возвращает пустой список | T5 | | Схема: `chunks_vec` дименсия 384 вместо 768 | T3-KNN, тест round-trip | Мутация, которая **не валит ни одного теста**, означает дыру в покрытии: тест дописать, находку — в отчёт. ## Признаки провала исполнителя - Вывод — «план реализации» текстом, `git status` пуст, exit 0. - Тесты зелёные, но мутации не валят (тесты написаны под код, а не под поведение). - Изменён боевой код вне заказанного списка файлов (`git diff --stat` не совпал с заказом). - Появились зависимости, которых нет в `TASK.md` §2. - Файлы модели или `.db` попали в коммит.