--- pr: 102 title: E2E coverage for 4 hybrid-memory scenarios --- ## Что сделано Создан `tests/test_memory_tools_e2e.py` с 4 E2E сценариями, покрывающими гибридную память (keyword + semantic): 1. **zero-config keyword-only** — поиск работает без `OPENAI_BASE_URL` (semantic возвращает `[]`), keyword через ripgrep находит файл. 2. **full hybrid** — semantic находит по точному термину и синониму (через embedding), keyword находит по точному слову. 3. **fallback при падении OpenRouter** — при невалидном `OPENAI_API_KEY` semantic возвращает `[]` (401 не retryable → embedder returns None), keyword работает. 4. **удаление файла** — после удаления .md + reindex, ни semantic, ни keyword не находят удалённый файл. Все тесты маркированы `@pytest.mark.skipif(not RUN_LIVE)` — запускаются только с `RUN_LIVE=1`. Без env — 4 skipped. Вспомогательные функции: - `_rg_search(pattern, dir)` — keyword через ripgrep (subprocess), fallback на `/root/.cache/opencode/bin/rg` если нет в PATH. - `_semantic_search(query, index_dir, env)` — semantic через `python3 -m src.memory search --json` (subprocess), с передачей кастомного env. - `_reindex(memory_dir, index_dir)` — reindex + poll `reindex.log` до `done:`. - `_make_memory_file(dir, term, synonym)` — tmp .md с frontmatter + уникальным термином `zzuniqtestterm42` + опциональным синонимом. ## Почему Фаза 1 (PR #100) сделала Python backend safe (lazy-init, return None). Фаза 2 (PR #101) создала 5 TS tools. Нужна E2E гарантия что keyword fallback работает когда semantic недоступен — без E2E регрессии: lazy-init поломался → все tools падают; fallback сломался → semantic-up → memory-down. Подробное обоснование в `technical/memory-keyword-vs-semantic-decision.md` — гибрид выбран вместо semantic-only именно из-за стабильности keyword path. ## Pending — ## Watch out - Тесты требуют `OPENAI_BASE_URL`, `OPENAI_API_KEY`, `OPENAI_EMBEDDING_MODEL` в env (реальный OpenRouter, не mock). Запуск: `RUN_LIVE=1 uv run pytest tests/test_memory_tools_e2e.py -v --no-cov`. - Сценарий 4 (deletion) требует dummy-файл в memory_dir — иначе `run_index` возвращает по `if not md_files` без обновления `index.json`, и удалённая запись остаётся в индексе. Добавлен `_make_dummy_file`. - `rg` может быть не в PATH — `_rg_bin()` пробует `shutil.which` затем `/root/.cache/opencode/bin/rg`, pytest.skip если нет. - Timeout: сценарий 3 — 300с (хотя 401 не retryable и embedder returns None быстро, на всякий случай), сценарии 2/4 — 180с. - Ruff: `subprocess.run` требует `check=False` (PLW1510) — добавлено явно.