---
pr: 124
title: fix(memory): keyword search out-of-box + doctor gaps + tests
---

## Что сделано

- **Dockerfile**: добавлен `ripgrep` в `apt-get install` (строки 3-16).
  System rg = fallback; `@vscode/ripgrep` в `npm install -g` (строка 38)
  остаётся primary path.
- **ci.yml** (job `test`): после `setup-node@v4` добавлены
  `apt-get install -y ripgrep` и `npm ci` (working-directory: .opencode) —
  CI теперь устанавливает ripgrep и npm-deps для keyword search.
- **memory-search.ts** (~строка 312): при `rgBin===null` пишет warning в
  stderr (`ripgrep not resolvable — keyword search disabled`) вместо
  молчаливого skip. Аналогичный warning в category-fallback блоке.
- **memory-doctor.ts** — закрыты 4 gap'а:
  - **G9**: `checkRipgrep` возвращает `{ lines, rgWorks }` где
    `rgWorks = rgBin !== null && r.status === 0`; `formatDoctorReport`
    и `allGreen` используют `rgWorks`, а не `rgBin !== null`.
  - **G5**: `resolveRgBinary({ allowSystemFallback: true })` в execute
    (консистентно с memory-search.ts:305).
  - **G3**: при `rgBin===null` (npm-пакет не найден) — строка с подсказкой
    `npm package missing — run npm ci in .opencode/`.
  - **G2**: `detectArchMismatch()` — если rgBin path содержит 'darwin'
    при `process.platform === 'linux'` (и обратные комбинации) + arch
    mismatch (arm64 vs x64) → строка `arch mismatch: binary platform !=
    runtime platform` в отчёт.
- **tests/test_memory_keyword_search.ts** (новый): 5 TS unit-тестов для
  `resolveRgBinary()` (npm-package, system-fallback, null-nothing,
  null-no-fallback) + memory-search stderr warning при rgBin===null.
- **tests/test_memory_doctor.ts** (новый): 4 TS unit-теста — allGreen
  false при битом бинарнике (G9), allGreen true при рабочем (regression
  guard), arch-mismatch detection (G2), npm-missing hint в report (G3).
- **tests/test_memory_tools_e2e.py**: убран глобальный `pytestmark`
  skipif(RUN_LIVE). Keyword-тест `test_zero_config_keyword_only` теперь
  запускается БЕЗ RUN_LIVE. Semantic-тесты (hybrid, fallback, deletion)
  имеют individual `skipif(not RUN_LIVE)`.

Проверки: `pytest tests/ -x -q` 427 passed, 6 skipped; `ruff check` OK;
`ruff format --check` OK.

## Почему

README заявлял keyword search как "always works", но:
1. System `rg` не был установлен нигде (Dockerfile/CI/host) — только
   хрупкий volume-mount npm-пакета.
2. CI не устанавливал ripgrep ни через apt, ни через npm ci для
   `.opencode/package.json`.
3. `memory-search` молча пропускал keyword search при `rgBin===null` —
   без warning, что затрудняло диагностику.
4. `memory-doctor` имел 4 расхождения: allGreen проверял `rgBin!==null`
   а не `r.status===0`, `allowSystemFallback` расходился с memory-search,
   нет arch-mismatch detection, require-ошибки не попадали в отчёт.
5. E2E-тесты скипались без `RUN_LIVE` целиком — даже keyword-тесты,
   которые не требуют embeddings API.

## Pending

—

## Watch out

- TS unit-тесты (`test_memory_keyword_search.ts`, `test_memory_doctor.ts`)
  runnable under `bun test` — bun runtime отсутствует на CI host; файлы
  валидны как TS, но не запускаются pytest'ом. Это тот же pattern что
  `test_commit_tool.ts` / `test_merge_pr_tool.ts`.
- `detectArchMismatch()` использует эвристику по имени path (contains
  'darwin'/'linux'/'win32' + 'arm64'/'x64') — не вызывает `file` command.
  Покрывает основной случай (@vscode/ripgrep platform dir names).
- `resolveRgBinary({ allowSystemFallback: true })` в memory-doctor теперь
  консистентен с memory-search — но меняет поведение: если npm-пакет
  отсутствует но system rg есть, doctor покажет ✅ вместо ❌. Это
  намеренно (doctor = зеркало реального поведения search).
- CI: `npm ci` в `.opencode/` требует `package-lock.json` — проверь что
  он есть в репо (иначе CI упадёт).