memo: спека, ТЗ, тест-план, MCP-зонд, gradle wrapper

This commit is contained in:
Hermes Agent
2026-10-02 00:59:33 +03:00
commit cefc17d782
10 changed files with 1077 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
# 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` попали в коммит.