mcp: поиск индексирует коллекцию на холодном старте
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
Проект: /root/WORK/memo (Kotlin/JVM). Заказ по холодному старту MCP-поиска.
|
||||||
|
|
||||||
|
## Доказанные дефекты (воспроизведено зондом)
|
||||||
|
|
||||||
|
1. **Пустой старт не работает.** Команда
|
||||||
|
rm -rf /root/WORK/memo-e2e/*/.memo
|
||||||
|
python3 scripts/mcp_probe.py --cmd "./memo-mcp/build/install/memo-mcp/bin/memo-mcp" \
|
||||||
|
--call memo_search --args '{"path":"/root/WORK/memo-e2e/infra","query":"основа для разрешения имён в сети","k":3}'
|
||||||
|
возвращает текст `коллекции не найдены`, потому что `selectTargets()` оставляет только коллекции,
|
||||||
|
у которых УЖЕ существует `.memo/index.db`. То есть главный сценарий — агент пишет .md штатными
|
||||||
|
инструментами и сразу ищет — не работает: сначала надо вручную звать CLI `index`.
|
||||||
|
Ожидаемое поведение: `memo_search` сам создаёт `.memo/index.db` и индексирует коллекцию,
|
||||||
|
если индекса ещё нет, и возвращает результаты.
|
||||||
|
|
||||||
|
2. **Модель грузится по разу на каждую коллекцию.** В `toolSearch` `Embedder(...)` создаётся внутри цикла
|
||||||
|
по коллекциям. На трёх коллекциях это три загрузки модели.
|
||||||
|
|
||||||
|
## Что сделать
|
||||||
|
|
||||||
|
Правки ТОЛЬКО в `memo-mcp/src/main/kotlin/memo/mcp/McpServer.kt` (+ новый тестовый файл). Логику memo-core не менять.
|
||||||
|
|
||||||
|
1. `toolSearch(path, query, k, mode)`:
|
||||||
|
- Определить цели так: если `File(path)` сам является коллекцией (в нём есть `*.md` напрямую) —
|
||||||
|
цель ровно он; иначе — все коллекции внутри `findCollections(File(path))`; если и их нет —
|
||||||
|
вернуть `коллекции не найдены`.
|
||||||
|
- Для каждой цели: если `<коллекция>/.memo/index.db` не существует — создать каталог `.memo`,
|
||||||
|
`Db(...)`, `db.init()`, проиндексировать `Indexer(db, embedder).indexTree(коллекция)`.
|
||||||
|
Затем обычный поиск с refresh-хуком.
|
||||||
|
- `Embedder` создать ОДИН РАЗ до цикла по коллекциям и закрыть один раз после (try/finally).
|
||||||
|
`Db` — по одному на коллекцию, как сейчас.
|
||||||
|
- Не глотать ошибки молча: если по коллекции поиск упал, дописать в результат строку
|
||||||
|
`ошибка в <коллекция>: <текст>` (в конец возвращаемого текста), остальные коллекции всё равно обрабатывать.
|
||||||
|
2. `toolStatus` — оставить как есть (статус по несуществующему индексу законно говорит «нет индекса»).
|
||||||
|
3. `toolReindex` — оставить как есть, но `Embedder` тоже вынести из цикла (один на все коллекции).
|
||||||
|
|
||||||
|
## Тест: новый файл memo-mcp/src/test/kotlin/memo/mcp/McpColdStartTest.kt
|
||||||
|
|
||||||
|
Ровно 2 теста, имена ровно такие:
|
||||||
|
|
||||||
|
1. `searchIndexesCollectionOnColdStart` — временный каталог-коллекция с двумя .md (например
|
||||||
|
"# Прокси\nвнутренние домены ходят через шлюз 76.1" и "# Прочее\nсовсем другая заметка про книгу"),
|
||||||
|
модель берётся из MEMO_MODEL_DIR (как в CoreSmokeTest). До вызова `.memo` НЕ существует.
|
||||||
|
Вызвать `toolSearch(dir.absolutePath, "внутренние домены шлюз", 5, SearchMode.HYBRID)`.
|
||||||
|
Проверить: (а) в ответе нет подстроки "коллекции не найдены"; (б) ответ содержит "76.1";
|
||||||
|
(в) после вызова `File(dir, ".memo/index.db").exists()` == true.
|
||||||
|
2. `searchOnEmptyDirReportsNoCollections` — пустой временный каталог без .md:
|
||||||
|
ответ содержит "коллекции не найдены", каталог `.memo` не создан.
|
||||||
|
|
||||||
|
Тесты должны работать без запуска процесса (через `toolSearch` напрямую).
|
||||||
|
|
||||||
|
## Обязательная сквозная проверка (приложить вывод)
|
||||||
|
|
||||||
|
cd /root/WORK/memo
|
||||||
|
./gradlew :memo-mcp:installDist -q
|
||||||
|
cp -r /root/WORK/memo-e2e /tmp/memo-cold && rm -rf /tmp/memo-cold/*/.memo /tmp/memo-cold/.memo
|
||||||
|
MEMO_MODEL_DIR=/root/WORK/memo/models/siglip2 python3 scripts/mcp_probe.py \
|
||||||
|
--cmd "./memo-mcp/build/install/memo-mcp/bin/memo-mcp" \
|
||||||
|
--call memo_search --args '{"path":"/tmp/memo-cold/infra","query":"основа для разрешения имён в сети","k":3}'
|
||||||
|
MEMO_MODEL_DIR=/root/WORK/memo/models/siglip2 python3 scripts/mcp_probe.py \
|
||||||
|
--cmd "./memo-mcp/build/install/memo-mcp/bin/memo-mcp" \
|
||||||
|
--call memo_status --args '{"path":"/tmp/memo-cold/infra"}'
|
||||||
|
|
||||||
|
Ожидается: поиск отдаёт непустой текст с `infra/domains.md` (или infra/hosts.md) и без "коллекции не найдены";
|
||||||
|
status после этого показывает ненулевые файлы и чанки. Также приложить `find /tmp/memo-cold -name index.db`.
|
||||||
|
|
||||||
|
После: ./gradlew test --rerun-tasks — ВСЕ тесты проекта зелёные.
|
||||||
|
Коммит: git add -A && git commit -m "mcp: поиск индексирует коллекцию на холодном старте"
|
||||||
|
|
||||||
|
СТРОГИЕ ЗАПРЕТЫ:
|
||||||
|
- Не выводить план текстом; сразу правь файлы.
|
||||||
|
- Не менять memo-core, memo-cli, memo-watch.
|
||||||
|
- Не добавлять зависимости.
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
# Вопросы приёмки T2/T3. Формат: question<TAB>path<TAB>expected_file<TAB>kind
|
# Вопросы приёмки T2/T3. Формат: question<TAB>path<TAB>expected_file<TAB>kind
|
||||||
# kind: sem — смысловой (должен вытянуть вектор), lex — точное значение (должен вытянуть BM25)
|
# kind: sem — смысловой (должен вытянуть вектор), lex — точное значение (должен вытянуть BM25)
|
||||||
# Ожидаемый файл должен попасть в top-5. Порог: recall@5 >= 16/20 по sem, 3/3 по lex.
|
# Ожидаемый файл должен попасть в top-5. Порог: recall@5 >= 16/20 по sem, 3/3 по lex.
|
||||||
как понять, почему сборка съедает место на диске infra work/jira/ci.md sem
|
как понять, почему сборка съедает место на диске work/jira work/jira/ci.md sem
|
||||||
чем разметить диск, если привычной утилиты нет infra infra/servers.md sem
|
чем разметить диск, если привычной утилиты нет infra infra/servers.md sem
|
||||||
на какой карте разрешено ставить опыты infra infra/servers.md sem
|
на какой карте разрешено ставить опыты infra infra/servers.md sem
|
||||||
что служит основой для разрешения имён в сети infra infra/domains.md sem
|
что служит основой для разрешения имён в сети infra infra/domains.md sem
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
до какого правителя доходит первый том life/books life/books/history.md sem
|
до какого правителя доходит первый том life/books life/books/history.md sem
|
||||||
как я фиксирую цитаты из прочитанного life/books life/books/notes.md sem
|
как я фиксирую цитаты из прочитанного life/books life/books/notes.md sem
|
||||||
чем слушают книги в очках life/books life/books/audiobooks.md sem
|
чем слушают книги в очках life/books life/books/audiobooks.md sem
|
||||||
что запрещено трогать на машине с двумя картами life/books infra/servers.md sem
|
что запрещено трогать на машине с двумя картами infra infra/servers.md sem
|
||||||
где живёт хранилище контейнерных образов life/books infra/hosts.md sem
|
где живёт хранилище контейнерных образов infra infra/hosts.md sem
|
||||||
76.132 infra infra/hosts.md lex
|
76.132 infra infra/hosts.md lex
|
||||||
TEST-4173 work/jira work/jira/flow.md lex
|
TEST-4173 work/jira work/jira/flow.md lex
|
||||||
192.168.88.35:8080 infra infra/hosts.md lex
|
192.168.88.35:8080 infra infra/hosts.md lex
|
||||||
|
|||||||
|
@@ -7,6 +7,7 @@ import memo.core.RefreshHook
|
|||||||
import memo.core.SearchMode
|
import memo.core.SearchMode
|
||||||
import memo.core.Searcher
|
import memo.core.Searcher
|
||||||
import memo.core.findCollections
|
import memo.core.findCollections
|
||||||
|
import memo.core.isCollection
|
||||||
import memo.core.resolveCollection
|
import memo.core.resolveCollection
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -116,15 +117,20 @@ fun toolSearch(path: String, query: String, k: Int, mode: SearchMode): String {
|
|||||||
if (targets.isEmpty()) return "коллекции не найдены"
|
if (targets.isEmpty()) return "коллекции не найдены"
|
||||||
val (modelPath, tokenizerPath) = modelPaths()
|
val (modelPath, tokenizerPath) = modelPaths()
|
||||||
val allHits = ArrayList<memo.core.Hit>()
|
val allHits = ArrayList<memo.core.Hit>()
|
||||||
|
val errors = ArrayList<String>()
|
||||||
|
val embedder = Embedder(modelPath, tokenizerPath)
|
||||||
|
try {
|
||||||
for (coll in targets) {
|
for (coll in targets) {
|
||||||
val dbPath = File(coll, ".memo/index.db")
|
val memoDir = File(coll, ".memo")
|
||||||
if (!dbPath.exists()) continue
|
val dbPath = File(memoDir, "index.db")
|
||||||
runCatching {
|
memoDir.mkdirs()
|
||||||
|
val existedBefore = dbPath.exists()
|
||||||
val db = Db(dbPath.absolutePath)
|
val db = Db(dbPath.absolutePath)
|
||||||
try {
|
try {
|
||||||
db.init()
|
db.init()
|
||||||
val embedder = Embedder(modelPath, tokenizerPath)
|
if (!existedBefore) {
|
||||||
try {
|
Indexer(db, embedder).indexTree(coll)
|
||||||
|
}
|
||||||
val refresh = RefreshHook { r ->
|
val refresh = RefreshHook { r ->
|
||||||
val innerDbPath = File(r, ".memo/index.db")
|
val innerDbPath = File(r, ".memo/index.db")
|
||||||
if (innerDbPath.exists()) {
|
if (innerDbPath.exists()) {
|
||||||
@@ -139,21 +145,26 @@ fun toolSearch(path: String, query: String, k: Int, mode: SearchMode): String {
|
|||||||
}
|
}
|
||||||
val hits = Searcher(db, embedder, refresh).search(coll, query, k, mode)
|
val hits = Searcher(db, embedder, refresh).search(coll, query, k, mode)
|
||||||
allHits.addAll(hits)
|
allHits.addAll(hits)
|
||||||
} finally {
|
} catch (t: Throwable) {
|
||||||
embedder.close()
|
errors.add("ошибка в ${coll.name}: ${t.message ?: t.javaClass.simpleName}")
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
db.close()
|
db.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
embedder.close()
|
||||||
}
|
}
|
||||||
val merged = allHits.sortedByDescending { it.score }.take(k)
|
val merged = allHits.sortedByDescending { it.score }.take(k)
|
||||||
if (merged.isEmpty()) return "ничего не найдено"
|
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
for (h in merged) {
|
for (h in merged) {
|
||||||
sb.append("${h.path}:${h.line} ${h.heading}\n${h.text}\n")
|
sb.append("${h.path}:${h.line} ${h.heading}\n${h.text}\n")
|
||||||
}
|
}
|
||||||
return sb.toString().trimEnd('\n')
|
for (e in errors) {
|
||||||
|
if (sb.isNotEmpty()) sb.append('\n')
|
||||||
|
sb.append(e)
|
||||||
|
}
|
||||||
|
val out = sb.toString().trimEnd('\n')
|
||||||
|
return if (out.isEmpty()) "ничего не найдено" else out
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toolStatus(path: String): String {
|
fun toolStatus(path: String): String {
|
||||||
@@ -191,6 +202,8 @@ fun toolReindex(path: String): String {
|
|||||||
if (collections.isEmpty()) return "коллекции не найдены"
|
if (collections.isEmpty()) return "коллекции не найдены"
|
||||||
val (modelPath, tokenizerPath) = modelPaths()
|
val (modelPath, tokenizerPath) = modelPaths()
|
||||||
var totalUpdated = 0
|
var totalUpdated = 0
|
||||||
|
val embedder = Embedder(modelPath, tokenizerPath)
|
||||||
|
try {
|
||||||
for (coll in collections) {
|
for (coll in collections) {
|
||||||
val memoDir = File(coll, ".memo")
|
val memoDir = File(coll, ".memo")
|
||||||
if (memoDir.exists()) {
|
if (memoDir.exists()) {
|
||||||
@@ -201,23 +214,21 @@ fun toolReindex(path: String): String {
|
|||||||
val db = Db(dbPath.absolutePath)
|
val db = Db(dbPath.absolutePath)
|
||||||
try {
|
try {
|
||||||
db.init()
|
db.init()
|
||||||
val embedder = Embedder(modelPath, tokenizerPath)
|
|
||||||
try {
|
|
||||||
totalUpdated += Indexer(db, embedder).indexTree(coll)
|
totalUpdated += Indexer(db, embedder).indexTree(coll)
|
||||||
} finally {
|
|
||||||
embedder.close()
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
db.close()
|
db.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
embedder.close()
|
||||||
|
}
|
||||||
return "переиндексировано файлов: $totalUpdated"
|
return "переиндексировано файлов: $totalUpdated"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun selectTargets(root: File): List<File> {
|
private fun selectTargets(root: File): List<File> {
|
||||||
if (!root.exists()) return emptyList()
|
if (!root.exists()) return emptyList()
|
||||||
if (File(root, ".memo/index.db").exists()) return listOf(root)
|
if (isCollection(root)) return listOf(root)
|
||||||
return findCollections(root).filter { File(it, ".memo/index.db").exists() }
|
return findCollections(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun modelPaths(): Pair<String, String> {
|
private fun modelPaths(): Pair<String, String> {
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package memo.mcp
|
||||||
|
|
||||||
|
import memo.core.SearchMode
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import kotlin.test.fail
|
||||||
|
|
||||||
|
class McpColdStartTest {
|
||||||
|
|
||||||
|
private fun modelPaths(): Pair<String, String> {
|
||||||
|
val modelDir = System.getenv("MEMO_MODEL_DIR") ?: "/root/WORK/memo/models/siglip2"
|
||||||
|
return "$modelDir/text_model_int8.onnx" to "$modelDir/tokenizer.model"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requireModel() {
|
||||||
|
val (modelPath, tokenizerPath) = modelPaths()
|
||||||
|
if (!File(modelPath).exists() || !File(tokenizerPath).exists()) {
|
||||||
|
fail("модель не найдена: ${File(modelPath).parent}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun searchIndexesCollectionOnColdStart() {
|
||||||
|
requireModel()
|
||||||
|
|
||||||
|
val base = File.createTempFile("memo-coldstart-", "")
|
||||||
|
assertTrue(base.delete(), "temp cleanup")
|
||||||
|
assertTrue(base.mkdir(), "temp create")
|
||||||
|
base.deleteOnExit()
|
||||||
|
|
||||||
|
File(base, "proxy.md").writeText(
|
||||||
|
"# Прокси\nвнутренние домены ходят через шлюз 76.1\n"
|
||||||
|
)
|
||||||
|
File(base, "other.md").writeText(
|
||||||
|
"# Прочее\nсовсем другая заметка про книгу\n"
|
||||||
|
)
|
||||||
|
File(base, "proxy.md").deleteOnExit()
|
||||||
|
File(base, "other.md").deleteOnExit()
|
||||||
|
|
||||||
|
val memoDir = File(base, ".memo")
|
||||||
|
assertFalse(memoDir.exists(), "до поиска .memo не должен существовать")
|
||||||
|
|
||||||
|
val resp = toolSearch(base.absolutePath, "внутренние домены шлюз", 5, SearchMode.HYBRID)
|
||||||
|
assertFalse(
|
||||||
|
resp.contains("коллекции не найдены"),
|
||||||
|
"холодный старт обязан сам индексировать, ответ: $resp"
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
resp.contains("76.1"),
|
||||||
|
"ожидалось упоминание 76.1 в ответе, получено: $resp"
|
||||||
|
)
|
||||||
|
|
||||||
|
val dbFile = File(base, ".memo/index.db")
|
||||||
|
assertTrue(dbFile.exists(), "после поиска .memo/index.db обязан существовать")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun searchOnEmptyDirReportsNoCollections() {
|
||||||
|
val base = File.createTempFile("memo-empty-", "")
|
||||||
|
assertTrue(base.delete(), "temp cleanup")
|
||||||
|
assertTrue(base.mkdir(), "temp create")
|
||||||
|
base.deleteOnExit()
|
||||||
|
|
||||||
|
val resp = toolSearch(base.absolutePath, "что угодно", 5, SearchMode.HYBRID)
|
||||||
|
assertEquals(
|
||||||
|
"коллекции не найдены",
|
||||||
|
resp,
|
||||||
|
"пустой каталог без .md обязан вернуть 'коллекции не найдены'"
|
||||||
|
)
|
||||||
|
assertFalse(
|
||||||
|
File(base, ".memo").exists(),
|
||||||
|
"в пустом каталоге .memo не должен создаваться"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#!/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. Сквозной recall@5 на эталонном корпусе (e2e/run_e2e.py).
|
||||||
|
# 4. 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. мутационная проверка"
|
||||||
|
if python3 e2e/mutation_check.py --gradle ./gradlew > /tmp/memo-mut.log 2>&1 \
|
||||||
|
&& grep -q "ВСЕ МУТАЦИИ УБИТЫ" /tmp/memo-mut.log; then
|
||||||
|
echo "OK все мутации убиты"
|
||||||
|
else
|
||||||
|
echo "ПРОВАЛ: есть выжившие мутации"; tail -20 /tmp/memo-mut.log; FAILED+=("мутации"); fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "3. сквозной индекс эталонного корпуса"
|
||||||
|
rm -rf "$CORPUS"/*/.memo "$CORPUS"/.memo 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. MCP: протокол + холодный старт"
|
||||||
|
COLD=/tmp/memo-accept-cold
|
||||||
|
rm -rf "$COLD"; cp -r "$CORPUS" "$COLD"
|
||||||
|
rm -rf "$COLD"/*/.memo "$COLD"/.memo
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user