133 lines
8.3 KiB
Markdown
133 lines
8.3 KiB
Markdown
# 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 :memo-cli:installDist -q
|
||
CLI=./memo-cli/build/install/memo-cli/bin/memo
|
||
$CLI index "$MEMO_ROOT"; $CLI status "$MEMO_ROOT"
|
||
ls -l "$MEMO_ROOT"/*/.memo/index.db
|
||
$CLI index "$MEMO_ROOT" # второй прогон
|
||
```
|
||
|
||
Ожидается: в каждой папке появился `.memo/index.db`; второй прогон **не переэмбеддивает** ни одного файла
|
||
(в выводе 0 обновлённых, `status` показывает те же `indexed_at`). Провал: нет `.memo/index.db`;
|
||
второй прогон молча переиндексирует всё.
|
||
|
||
## 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` попали в коммит.
|