core: каркас сборки, embedder, схема БД, 4 теста
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# Вопросы приёмки T2/T3. Формат: question<TAB>path<TAB>expected_file<TAB>kind
|
||||
# kind: sem — смысловой (должен вытянуть вектор), lex — точное значение (должен вытянуть BM25)
|
||||
# Ожидаемый файл должен попасть в top-5. Порог: recall@5 >= 16/20 по sem, 3/3 по lex.
|
||||
как понять, почему сборка съедает место на диске infra work/jira/ci.md sem
|
||||
чем разметить диск, если привычной утилиты нет infra infra/servers.md sem
|
||||
на какой карте разрешено ставить опыты infra infra/servers.md sem
|
||||
что служит основой для разрешения имён в сети infra infra/domains.md sem
|
||||
куда конвейер отдаёт готовые артефакты infra infra/hosts.md sem
|
||||
как корпоративные имена ходят наружу infra infra/hosts.md sem
|
||||
восстановление прошло мгновенно, а данных не видно infra infra/backup.md sem
|
||||
куда складываются зеркала для очков infra infra/backup.md sem
|
||||
где лежат ключи доступа к корпоративным ресурсам work/jira work/jira/access.md sem
|
||||
как оформляют задачу, пришедшую от робота work/jira work/jira/flow.md sem
|
||||
после обновления пропали настройки выпуска work/jira work/jira/pitfalls.md sem
|
||||
какой номер тикета закрыли после починки тестов work/jira work/jira/flow.md sem
|
||||
кто ведёт рассказ в романе life/books life/books/iphuck.md sem
|
||||
в каком году вышла книга life/books life/books/iphuck.md sem
|
||||
где чинят карточку, если подтянулось чужое издание life/books life/books/audiobooks.md sem
|
||||
до какого правителя доходит первый том life/books life/books/history.md sem
|
||||
как я фиксирую цитаты из прочитанного life/books life/books/notes.md sem
|
||||
чем слушают книги в очках life/books life/books/audiobooks.md sem
|
||||
что запрещено трогать на машине с двумя картами life/books infra/servers.md sem
|
||||
где живёт хранилище контейнерных образов life/books infra/hosts.md sem
|
||||
76.132 infra infra/hosts.md lex
|
||||
TEST-4173 work/jira work/jira/flow.md lex
|
||||
192.168.88.35:8080 infra infra/hosts.md lex
|
||||
|
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Приёмка recall@5 для memo (TESTING.md T2/T3).
|
||||
|
||||
Читает e2e/questions.tsv, гоняет CLI и считает, попал ли ожидаемый файл в top-5.
|
||||
Использование:
|
||||
python3 e2e/run_e2e.py --cli ./memo-cli/build/install/memo-cli/bin/memo \
|
||||
--root /root/WORK/memo-e2e [--k 5] [--min-sem-recall 0.8]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def normalize(p: str, root: str) -> str:
|
||||
p = p.replace("\\", "/")
|
||||
root = root.rstrip("/")
|
||||
if p.startswith(root + "/"):
|
||||
p = p[len(root) + 1:]
|
||||
return p.lstrip("./")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--cli", required=True)
|
||||
ap.add_argument("--root", required=True)
|
||||
ap.add_argument("--questions", default="e2e/questions.tsv")
|
||||
ap.add_argument("--k", type=int, default=5)
|
||||
ap.add_argument("--min-sem-recall", type=float, default=0.8)
|
||||
a = ap.parse_args()
|
||||
|
||||
rows = []
|
||||
for line in open(a.questions, encoding="utf-8"):
|
||||
line = line.rstrip("\n")
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
q, scope, expected, kind = line.split("\t")
|
||||
rows.append((q, scope, expected, kind))
|
||||
|
||||
hits = {"sem": 0, "lex": 0}
|
||||
total = {"sem": 0, "lex": 0}
|
||||
fails = []
|
||||
for q, scope, expected, kind in rows:
|
||||
total[kind] += 1
|
||||
cmd = shlex.split(a.cli) + ["search", f"{a.root}/{scope}", q, "--k", str(a.k), "--json"]
|
||||
try:
|
||||
out = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
|
||||
except subprocess.TimeoutExpired:
|
||||
fails.append((q, kind, "TIMEOUT", []))
|
||||
continue
|
||||
if out.returncode != 0:
|
||||
fails.append((q, kind, f"exit {out.returncode}: {out.stderr.strip()[:200]}", []))
|
||||
continue
|
||||
try:
|
||||
data = json.loads(out.stdout)
|
||||
except json.JSONDecodeError:
|
||||
fails.append((q, kind, f"не JSON: {out.stdout[:200]}", []))
|
||||
continue
|
||||
items = data if isinstance(data, list) else data.get("results", [])
|
||||
got = [normalize(str(it.get("path", "")), a.root) for it in items][:a.k]
|
||||
if normalize(expected, a.root) in got:
|
||||
hits[kind] += 1
|
||||
else:
|
||||
fails.append((q, kind, "не найдено в top-%d" % a.k, got))
|
||||
|
||||
sem_total = total["sem"] or 1
|
||||
lex_total = total["lex"] or 1
|
||||
sem_recall = hits["sem"] / sem_total
|
||||
lex_recall = hits["lex"] / lex_total
|
||||
print(f"sem: {hits['sem']}/{sem_total} = {sem_recall:.2f} (порог {a.min_sem_recall})")
|
||||
print(f"lex: {hits['lex']}/{lex_total} = {lex_recall:.2f} (порог 1.00)")
|
||||
if fails:
|
||||
print("\nпромахи:")
|
||||
for q, kind, why, got in fails:
|
||||
print(f" [{kind}] {q} -> {why}")
|
||||
if got:
|
||||
print(f" получено: {got}")
|
||||
ok = sem_recall >= a.min_sem_recall and lex_recall >= 1.0
|
||||
print("\nИТОГ:", "ПРОЙДЕНО" if ok else "ПРОВАЛ")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user