8.7 KiB
TESTING — приёмочный тест-план
Тестирует заказчик (Hermes) на реальном железе, а не исполнитель. Исполнитель пишет тесты; приёмка идёт независимыми проверками ниже. Правило приёмки: «зелёная сборка» ничего не доказывает, пока мутация не валит нужный тест.
0. Подготовка стенда
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. Индексация и её повтор
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 (создаёт заказчик).
$CLI search "$MEMO_ROOT/infra" "как пережить перезагрузку сервера, чтобы операция не умерла" --json
Провал: в результатах нет нужного файла; возвращаются чанки без текста (агент не сможет ими воспользоваться); поиск идёт по всему корню вместо указанной папки.
T3. Лексический поиск (BM25 работает)
Вопросы с точными значениями: IP-адрес, номер тикета, версия, год. Ожидание: точное совпадение найдено, и именно через лексику.
$CLI search "$MEMO_ROOT/infra" "76.132" --json # должен найти заметку с этим IP
$CLI search "$MEMO_ROOT/infra" "76.132" --mode lex --json
Провал: находит «похожее по смыслу», но не строку с точным значением; --mode lex даёт тот же
результат, что --mode vec (значит, режимы не разделены).
T4. Живая правка файла
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 только в новой сессии, поэтому проверяем протоколом:
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 отсутствует.
Тестовые ручки (то же без транспорта):
$CLI mcp-probe --tool memo_search --args '{"path":"'"$MEMO_ROOT"'/infra","query":"домены"}'
# плюс режим "одного вызова": разобрать строку запроса и вернуть ответ, не поднимая stdio
Провал: сервер печатает что-то в stdout помимо JSON-RPC (ломает клиента); на tools/call отвечает
-32601; результат — пустая строка.
T6. Офлайн
# отключаем внешний интерфейс у процесса: без сети индексация и поиск обязаны работать
$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попали в коммит.