#!/usr/bin/env bash # Единый приёмочный прогон memo. Запускает ВСЁ, что должен пройти проект, и печатает # один вердикт. Возвращает 0, если все контуры зелёные. # # bash scripts/accept.sh # полный прогон # bash scripts/accept.sh --fast # без мутационной проверки (она самая долгая) # # Контуры: # 1. Юнит-тесты всех модулей (gradle test --rerun-tasks). # 2. Мутационная проверка тестов (e2e/mutation_check.py) — тесты обязаны падать на сломанном коде. # 3. Сквозной индекс эталонного корпуса: идемпотентность и ровно 3 коллекции. # 4. Сквозной recall@5 на эталонном корпусе (e2e/run_e2e.py). # 5. Демон слежения: первичный проход, 3 базы, отсутствие «locked» при параллельном чтении. # 6. MCP-протокол живым клиентом (scripts/mcp_probe.py), включая холодный старт. set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" CORPUS="${MEMO_CORPUS:-/root/WORK/memo-e2e}" export MEMO_MODEL_DIR="${MEMO_MODEL_DIR:-$ROOT/models/siglip2}" FAST=0 [ "${1:-}" = "--fast" ] && FAST=1 FAILED=() section() { printf '\n=== %s ===\n' "$1"; } section "0. сборка" if ./gradlew installDist -q >/tmp/memo-build.log 2>&1; then echo "OK сборка и установка дистрибутивов" else echo "ПРОВАЛ сборки:"; tail -30 /tmp/memo-build.log; FAILED+=("сборка"); fi CLI="$ROOT/memo-cli/build/install/memo/bin/memo" MCP="$ROOT/memo-mcp/build/install/memo-mcp/bin/memo-mcp" section "1. юнит-тесты" if ./gradlew test --rerun-tasks >/tmp/memo-test.log 2>&1; then python3 - <<'EOF' import glob, xml.etree.ElementTree as ET tot = fail = 0 for f in glob.glob('**/build/test-results/test/*.xml', recursive=True): r = ET.parse(f).getroot() tot += int(r.get('tests') or 0); fail += int(r.get('failures') or 0) + int(r.get('errors') or 0) print(f"OK тестов {tot}, провалов {fail}") EOF else echo "ПРОВАЛ тестов:"; grep -E "FAILED|error:" /tmp/memo-test.log | head -20; FAILED+=("тесты"); fi if [ "$FAST" = "0" ]; then section "2. мутационная проверка" python3 e2e/mutation_check.py --gradle ./gradlew > /tmp/memo-mut.log 2>&1 MUT_RC=$? if [ "$MUT_RC" = "0" ]; then echo "OK все мутации убиты" elif [ "$MUT_RC" = "2" ]; then echo "ПРОВАЛ: таблица мутаций недействительна (no-op или неприменимая мутация)" grep -E "ОШИБКА ТАБЛИЦЫ|ПРОВАЛ" /tmp/memo-mut.log | head -10; FAILED+=("таблица мутаций") else echo "ПРОВАЛ: есть выжившие мутации"; tail -20 /tmp/memo-mut.log; FAILED+=("мутации"); fi fi section "3. сквозной индекс эталонного корпуса" # .memo лежит не только на верхнем уровне (life/books, work/jira), поэтому ищем по дереву, # иначе часть баз переживает очистку и прогон перестаёт быть чистым. find "$CORPUS" -name .memo -type d -prune -exec rm -rf {} + 2>/dev/null FIRST=$("$CLI" index "$CORPUS" 2>&1 | tail -1) SECOND=$("$CLI" index "$CORPUS" 2>&1 | tail -1) DBS=$(find "$CORPUS" -name index.db | wc -l) echo " первый прогон : $FIRST" echo " второй прогон : $SECOND" echo " баз индекса : $DBS (ожидается 3)" if echo "$SECOND" | grep -q "итого: 0 обновлено" && [ "$DBS" = "3" ]; then echo "OK индексация идемпотентна, коллекций 3" else echo "ПРОВАЛ индексации (ожидалось «итого: 0 обновлено» и 3 базы)"; FAILED+=("индексация"); fi section "4. recall@5" if python3 e2e/run_e2e.py --cli "$CLI" --root "$CORPUS" --k 5 > /tmp/memo-e2e.log 2>&1 \ && grep -q "ИТОГ: ПРОЙДЕНО" /tmp/memo-e2e.log; then grep -E "^(sem|lex):" /tmp/memo-e2e.log; echo "OK recall" else echo "ПРОВАЛ recall:"; tail -25 /tmp/memo-e2e.log; FAILED+=("recall"); fi section "5. демон слежения (memo-watch)" # Демон проверяется отдельным контуром: три его дефекта (6 баз вместо 3, отсутствие первичного # прохода, «locked» при параллельном чтении) прошли мимо юнит-тестов и зелёной приёмки. WATCH="$ROOT/memo-watch/build/install/memo-watch/bin/memo-watch" WROOT=/tmp/memo-accept-watch rm -rf "$WROOT"; cp -r "$CORPUS" "$WROOT" find "$WROOT" -name .memo -type d -prune -exec rm -rf {} + 2>/dev/null "$WATCH" "$WROOT" > /tmp/memo-watch.log 2>&1 & WPID=$! WOK=1 # ждём не «на глаз», а по факту: пока в каждой коллекции не появятся чанки (максимум 120 с) for _ in $(seq 1 120); do READY=$(python3 - "$WROOT" <<'EOF' import sqlite3, glob, sys root = sys.argv[1] dbs = sorted(glob.glob(root + "/**/.memo/index.db", recursive=True)) if len(dbs) != 3: print(0); raise SystemExit n = 0 for db in dbs: try: c = sqlite3.connect("file:" + db + "?mode=ro", uri=True) if c.execute("SELECT COUNT(*) FROM chunks").fetchone()[0] > 0: n += 1 c.close() except Exception: pass print(n) EOF ) [ "$READY" = "3" ] && break sleep 1 done WDBS=$(find "$WROOT" -name index.db | wc -l) echo " баз индекса : $WDBS (ожидается 3)" if [ "$WDBS" != "3" ]; then echo "ПРОВАЛ: демон завёл $WDBS баз вместо 3"; WOK=0 fi if [ "$READY" = "3" ]; then echo "OK первичный проход: все 3 коллекции наполнены сразу после старта" else echo "ПРОВАЛ: первичного прохода нет — наполнено коллекций: $READY из 3"; WOK=0 fi # параллельно с работающим демоном — тот же файл читаем поиском WSEARCH=$("$CLI" search "$WROOT/life/books" "кто ведёт рассказ в романе" --k 1 2>&1) if echo "$WSEARCH" | grep -qi "locked"; then echo "ПРОВАЛ: database is locked при чтении во время работы демона"; WOK=0 elif [ -n "$WSEARCH" ]; then echo "OK поиск при работающем демоне отвечает без locked" else echo "ПРОВАЛ: поиск при работающем демоне ничего не вернул"; WOK=0 fi kill "$WPID" 2>/dev/null; wait "$WPID" 2>/dev/null [ "$WOK" = "1" ] || FAILED+=("демон слежения") section "6. MCP: протокол + холодный старт" COLD=/tmp/memo-accept-cold rm -rf "$COLD"; cp -r "$CORPUS" "$COLD" find "$COLD" -name .memo -type d -prune -exec rm -rf {} + 2>/dev/null if python3 scripts/mcp_probe.py --cmd "$MCP" \ --call memo_search \ --args "{\"path\":\"$COLD/infra\",\"query\":\"основа для разрешения имён в сети\",\"k\":3}" \ > /tmp/memo-mcp.log 2>&1; then if grep -q "коллекции не найдены" /tmp/memo-mcp.log; then echo "ПРОВАЛ: холодный старт не индексирует"; FAILED+=("MCP холодный старт") else echo "OK MCP: протокол отвечает, холодный старт индексирует" grep -E "^OK" /tmp/memo-mcp.log | sed 's/^/ /' fi else echo "ПРОВАЛ MCP:"; tail -20 /tmp/memo-mcp.log; FAILED+=("MCP протокол"); fi section "ВЕРДИКТ" if [ ${#FAILED[@]} -eq 0 ]; then echo "ПРОЙДЕНО — все контуры зелёные" exit 0 else printf 'ПРОВАЛ — контуры: %s\n' "$(IFS=', '; echo "${FAILED[*]}")" exit 1 fi