From 9d654a2201e2dbacb17b205736d2fe458b01ba86 Mon Sep 17 00:00:00 2001 From: Sergey <93754860+slaid098@users.noreply.github.com> Date: Mon, 27 Jul 2026 01:47:26 +0300 Subject: [PATCH] chore(memory): remove plugin + setup-memory.sh + Rust artifacts (#103) * chore(memory): remove plugin block from opencode.json * chore(memory): remove setup-memory.sh and memory-setup.ts * chore(memory): remove opencode-memory from Dockerfile * docs(memory): update AGENTS.md and SKILL.md for new tools * test(memory): drop tests for removed setup-memory.sh and memory-setup.ts * docs(handoff): add handoff and ADR-045 for plugin/rust removal * docs(handoff): set PR number * docs(project-map): update after plugin/rust removal --------- Co-authored-by: opencode-agent --- .opencode/opencode.json | 8 - .opencode/scripts/setup-memory.sh | 127 ------ .opencode/skills/memory/SKILL.md | 16 +- .opencode/tools/memory-setup.ts | 19 - AGENTS.md | 6 +- Dockerfile | 2 +- .../045-pr-103-remove-plugin-rust.md | 63 +++ docs/handoff/pr-103-remove-plugin-rust.md | 86 ++++ docs/project-map/README.md | 13 +- tests/test_commit_tool.py | 2 +- tests/test_dockerfile.py | 60 +-- tests/test_memory_setup_tool.py | 104 ----- tests/test_memory_setup_tool.ts | 56 --- tests/test_setup_memory.py | 410 ------------------ 14 files changed, 198 insertions(+), 774 deletions(-) delete mode 100755 .opencode/scripts/setup-memory.sh delete mode 100644 .opencode/tools/memory-setup.ts create mode 100644 docs/decisions/045-pr-103-remove-plugin-rust.md create mode 100644 docs/handoff/pr-103-remove-plugin-rust.md delete mode 100644 tests/test_memory_setup_tool.py delete mode 100644 tests/test_memory_setup_tool.ts delete mode 100644 tests/test_setup_memory.py diff --git a/.opencode/opencode.json b/.opencode/opencode.json index 44965d1..e091af6 100644 --- a/.opencode/opencode.json +++ b/.opencode/opencode.json @@ -1,13 +1,5 @@ { "$schema": "https://opencode.ai/config.json", - "plugin": [ - [ - "@mathew-cf/opencode-memory", - { - "memoryDir": "{env:OPENCODE_MEMORY_DIR}" - } - ] - ], "skills": { "paths": [ ".opencode/skills" diff --git a/.opencode/scripts/setup-memory.sh b/.opencode/scripts/setup-memory.sh deleted file mode 100755 index 78d916b..0000000 --- a/.opencode/scripts/setup-memory.sh +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -MEMORY_DIR="${OPENCODE_MEMORY_DIR:-/root/.local/share/opencode/opencode-memory}" -HOOK="$MEMORY_DIR/.git/hooks/post-commit" -BRANCH="master" -WRAPPER_PATH="${MEMORY_WRAPPER_PATH:-/usr/local/lib/node_modules/@mathew-cf/opencode-memory/node_modules/@mathew-cf/rag-cli/bin/rag.js}" - -# Pick a Python that can import src.memory: prefer workspace .venv, then CWD -# .venv, then system python3. Override via MEMORY_WRAPPER_PYTHON for tests. -if [ -n "${OPENCODE_WORKSPACE:-}" ] && [ -x "${OPENCODE_WORKSPACE}/.venv/bin/python" ]; then - MEMORY_PYTHON="${MEMORY_WRAPPER_PYTHON:-${OPENCODE_WORKSPACE}/.venv/bin/python}" -elif [ -x ".venv/bin/python" ]; then - MEMORY_PYTHON="${MEMORY_WRAPPER_PYTHON:-.venv/bin/python}" -else - MEMORY_PYTHON="${MEMORY_WRAPPER_PYTHON:-python3}" -fi -EXPECTED_HOOK="#!/bin/bash -git push origin ${BRANCH} 2>/dev/null || true" - -if [ -z "${OPENCODE_MEMORY_REMOTE:-}" ]; then - echo "ERROR: OPENCODE_MEMORY_REMOTE not set" >&2 - exit 1 -fi -REMOTE="${OPENCODE_MEMORY_REMOTE}" - -echo "memory setup: $MEMORY_DIR" - -# 1. MEMORY_DIR exists? → mkdir -p -if [ ! -d "$MEMORY_DIR" ]; then - mkdir -p "$MEMORY_DIR" - echo " [1/6] created directory: $MEMORY_DIR" -else - echo " [1/6] directory exists" -fi - -# 2. .git exists? → clone (no) | pull --ff-only (yes) -if [ ! -d "$MEMORY_DIR/.git" ]; then - echo " [2/6] cloning remote: $REMOTE" - # Ensure GITHUB_TOKEN is used for HTTPS git operations (non-interactive). - # Idempotent: git config --global overwrites (not duplicates) the value. - if [ -n "${GITHUB_TOKEN:-}" ]; then - git config --global url."https://x-access-token:${GITHUB_TOKEN}@github.com/".insteadOf https://github.com/ - fi - git clone --origin origin "$REMOTE" "$MEMORY_DIR" || { echo "ERROR: clone failed" >&2; exit 1; } - git -C "$MEMORY_DIR" checkout "$BRANCH" 2>/dev/null || true -else - echo " [2/6] pulling latest (ff-only)" - git -C "$MEMORY_DIR" pull --ff-only origin "$BRANCH" 2>/dev/null || \ - echo " pull skipped (no upstream or offline)" -fi - -# 3. remote origin correct? → set-url (no) | noop (yes) -CURRENT_REMOTE="$(git -C "$MEMORY_DIR" remote get-url origin 2>/dev/null || echo "")" -if [ "$CURRENT_REMOTE" != "$REMOTE" ]; then - echo " [3/6] fixing remote: $CURRENT_REMOTE → $REMOTE" - git -C "$MEMORY_DIR" remote set-url origin "$REMOTE" -else - echo " [3/6] remote correct" -fi - -# 4. post-commit hook exists + content correct? → create/fix (no) | noop (yes) -NEEDS_HOOK=0 -if [ ! -f "$HOOK" ]; then - NEEDS_HOOK=1 -elif [ "$(cat "$HOOK")" != "$EXPECTED_HOOK" ]; then - NEEDS_HOOK=1 -fi -if [ "$NEEDS_HOOK" -eq 1 ]; then - echo " [4/6] installing post-commit hook (auto-push)" - mkdir -p "$(dirname "$HOOK")" - printf '%s\n' "$EXPECTED_HOOK" > "$HOOK" - chmod +x "$HOOK" -else - echo " [4/6] hook correct" -fi - -# 5. .rag/index.json exists? → memory index (no) | noop (yes) -if "$MEMORY_PYTHON" -c "import src.memory" >/dev/null 2>&1; then - if [ ! -f "$MEMORY_DIR/.rag/index.json" ]; then - echo " [5/7] building RAG index (memory CLI)" - ( "$MEMORY_PYTHON" -m src.memory index "$MEMORY_DIR" -o "$MEMORY_DIR/.rag" ) 2>&1 \ - || echo " memory index failed — continuing" - else - echo " [5/7] RAG index exists" - fi -else - echo " [5/7] memory CLI not available — skipping index" -fi - -# 5b. JS wrapper for opencode-memory plugin — delegates rag CLI calls to -# Python memory CLI. Idempotent (content check via cmp), backs up -# original once. -WRAPPER_DIR="$(dirname "$WRAPPER_PATH")" -mkdir -p "$WRAPPER_DIR" -WRAPPER_CONTENT='#!/usr/bin/env node -// Generated by setup-memory.sh — delegates rag CLI calls to Python memory CLI -const { spawnSync } = require("child_process"); -const py = process.env.MEMORY_WRAPPER_PYTHON || "'"$MEMORY_PYTHON"'"; -const r = spawnSync(py, ["-m", "src.memory", ...process.argv.slice(2)], { - stdio: "inherit", - cwd: process.env.OPENCODE_WORKSPACE || process.cwd() -}); -process.exit(r.status || 0); -' - -if [ -f "$WRAPPER_PATH" ] && cmp -s "$WRAPPER_PATH" /dev/stdin <<<"$WRAPPER_CONTENT"; then - echo " [5b/7] wrapper correct" -else - if [ -f "$WRAPPER_PATH" ]; then - if [ ! -f "${WRAPPER_PATH}.orig" ]; then - cp "$WRAPPER_PATH" "${WRAPPER_PATH}.orig" - echo " [5b/7] backed up original wrapper → ${WRAPPER_PATH}.orig" - fi - fi - printf '%s\n' "$WRAPPER_CONTENT" > "$WRAPPER_PATH" - chmod +x "$WRAPPER_PATH" - echo " [5b/7] generated wrapper (delegates → python memory CLI)" -fi - -# 6. status -echo " [7/7] done" -echo "" -echo "memory: ready at $MEMORY_DIR" -echo " remote: $REMOTE" -echo " branch: $BRANCH" -echo " auto-push: enabled (post-commit hook)" \ No newline at end of file diff --git a/.opencode/skills/memory/SKILL.md b/.opencode/skills/memory/SKILL.md index 33956a7..c885fd8 100644 --- a/.opencode/skills/memory/SKILL.md +++ b/.opencode/skills/memory/SKILL.md @@ -6,17 +6,17 @@ description: Инструкция по работе с файловой памя # File Memory (opencode-memory) Графовая память (Graphiti/FalkorDB) удалена — была нестабильна и забагована. -Теперь память работает через `@mathew-cf/opencode-memory` — файловая система с keyword + semantic search. +Память работает через файловую систему с keyword + semantic search — 5 TS tools (`.opencode/tools/memory-*.ts`), вызывают `python3 -m src.memory`. Плагин `@mathew-cf/opencode-memory` удалён. ## Инструменты | Инструмент | Назначение | |---|---| -| `memory_search(query, category?)` | Гибридный поиск (keyword + semantic) | -| `memory_list(category?)` | Список категорий / файлов | -| `memory_save()` | Commit + re-index после записи/редактирования | -| `memory_access(path)` | Отметить файл как прочитанный | -| `memory-setup()` | Проверить статус бэкендов | +| `memory-search({ query, category? })` | Гибридный поиск (ripgrep keyword + Python semantic) | +| `memory-list({ category? })` | Список категорий / файлов | +| `memory-save()` | Commit + reindex после записи/редактирования (auto-setup: git init + hook если `OPENCODE_MEMORY_REMOTE` set) | +| `memory-access({ path })` | Отметить файл как прочитанный (bump `last_accessed`/`access_count`) | +| `memory-doctor()` | Read-only диагностика: ripgrep, Python `src.memory`, env vars, index | ## Категории @@ -24,7 +24,7 @@ description: Инструкция по работе с файловой памя ## Как работать -1. **Перед началом работы** — `memory_search` по теме +1. **Перед началом работы** — `memory-search` по теме 2. **В процессе** — сохранять находки сразу (контекст свежий) 3. **В конце сессии** — retrospective: что узнал → сохранить, что было в памяти → обновить, чего не хватало → создать @@ -61,7 +61,7 @@ related: [category/file.md] ## Путь для репозиториев -Плагинный default: `~/opencode-memory` (переопределяется через `OPENCODE_MEMORY_DIR`, например `app_data/opencode-memory`). +Default: `/root/.local/share/opencode/opencode-memory` (переопределяется через `OPENCODE_MEMORY_DIR`). ``` {memory-dir}/repos/{host}/{org}/{repo}.md diff --git a/.opencode/tools/memory-setup.ts b/.opencode/tools/memory-setup.ts deleted file mode 100644 index 71ee7cd..0000000 --- a/.opencode/tools/memory-setup.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { spawnSync } from "child_process" -import path from "path" -import { tool } from "@opencode-ai/plugin" - -export default tool({ - description: "Initialize or sync opencode-memory. Clones remote repo, installs post-commit hook for auto-push, rebuilds RAG index. Idempotent — safe to run multiple times. No arguments needed.", - args: {}, - async execute(_args, context) { - const script = path.join(import.meta.dir, "..", "scripts", "setup-memory.sh") - const r = spawnSync("bash", [script], { - encoding: "utf-8", - cwd: context.worktree, - }) - if (r.status !== 0) { - return `⚠️ memory-setup failed (exit ${r.status}): ${r.stderr || r.stdout}` - } - return r.stdout.trim() - }, -}) \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 8205b39..b0d13fa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,7 +35,11 @@ fallback на raw bash, НЕ импровизируй обход через `gh | `post-docs-review({ pr_number, verdict, body })` | `gh pr comment` для verdict | Docs review verdict (heading `## Docs Review Summary` + verdict) | Сообщи оркестратору, не `gh pr comment` | | `pipeline-status({ pr_number })` | `python3 .opencode/scripts/pipeline-status.py` | Read-only oracle: статус фаз PR + NEXT action | Сообщи оркестратору, не bash-запуск скрипта | | `spec-status({})` | `python3 .opencode/scripts/spec-status.py` | Read-only oracle: текущая фаза spec + NEXT action | Сообщи оркестратору, не bash-запуск скрипта | -| `memory-setup()` | `bash .opencode/scripts/setup-memory.sh` | Инициализация/синхронизация opencode-memory (clone + hook + reindex) | Сообщи оркестратору, не bash-запуск скрипта | +| `memory-doctor()` | — | Read-only диагностика памяти: ripgrep, Python src.memory, env vars, index (замена `memory-setup`) | Сообщи оркестратору, не raw bash | +| `memory-save()` | — | Commit + reindex opencode-memory после записи/редактирования (auto-setup: git init + hook если remote set) | Сообщи оркестратору, не raw bash | +| `memory-search({ query, category? })` | — | Гибридный поиск (ripgrep keyword + Python semantic) по opencode-memory | Сообщи оркестратору, не raw bash | +| `memory-list({ category? })` | — | Список категорий / файлов opencode-memory | Сообщи оркестратору, не raw bash | +| `memory-access({ path })` | — | Отметить memory-файл как прочитанный (bump `last_accessed`/`access_count`) | Сообщи оркестратору, не raw bash | | `tunnel()` | `bash .opencode/scripts/tunnel.sh` | Cloudflare tunnel toggle (1-й вызов — start, 2-й — stop) | Сообщи оркестратору, не bash-запуск скрипта | `gh pr comment*` остаётся в allow-list для обратной совместимости (ADR-019 diff --git a/Dockerfile b/Dockerfile index 43f3f0e..79a7752 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,7 @@ RUN curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/c ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true -RUN npm install -g opencode-ai repomix @mathew-cf/opencode-memory --unsafe-perm +RUN npm install -g opencode-ai repomix --unsafe-perm WORKDIR /root/workspace diff --git a/docs/decisions/045-pr-103-remove-plugin-rust.md b/docs/decisions/045-pr-103-remove-plugin-rust.md new file mode 100644 index 0000000..18179ef --- /dev/null +++ b/docs/decisions/045-pr-103-remove-plugin-rust.md @@ -0,0 +1,63 @@ +# ADR-045: Remove @mathew-cf/opencode-memory plugin + Rust artifacts + +## Статус + +Accepted (2026-07-26) + +## Контекст + +После миграции на единую гибридную систему памяти (5 TS tools, PR #101) + E2E +покрытия (PR #102) сохранилась двойная система: + +- **Plugin `@mathew-cf/opencode-memory`** прописан в `opencode.json` блоке + `plugin` и устанавливается в `Dockerfile` через `npm install -g`. Но plugin + не вызывается из-за бага wrapper-пути (см. ADR-043, handoff pr-101) — + фактически мёртвый код, съедающий ~85 MB в image + cache. +- **Rust `rag-cli` binary** (внутри плагина) пишет `.rag/index.bin` (7.5 MB) + + `.rag/meta.json` (12 KB) в Rust-формате (`hidden_size`, `model_id`, + `file_hashes`). Python `src/memory/index.py` пишет `.rag/index.json` (210 MB, + 4096-dim embeddings) + `.rag/meta.json` в Python-формате (`version`, `files`). + **Конфликт**: оба пишут `meta.json` по одному пути, разные форматы → Rust + перетирает Python → broken индекс при сосуществовании. +- **`setup-memory.sh`** (4.6 KB) — shell-скрипт инициализации, заменён на + auto-setup внутри `memory-save.ts` (git init + hook если remote set). +- **`memory-setup.ts`** (716 B) — TS tool-обёртка над `setup-memory.sh`, + заменён на `memory-doctor.ts` (read-only диагностика, не модифицирующий). + +Cleanup нужно делать ПОСЛЕ подтверждения что TS tools работают (PR #101) и E2E +тесты проходят (PR #102) — иначе остались бы без памяти. Issue #97 (E2E green) +был blocker для #98. + +## Решение + +Удалить все артефакты старой системы за один PR (issue #98, фаза 3 эпика #94): + +1. `opencode.json` — убрать блок `plugin` целиком. JSON остался валидным. +2. `Dockerfile` — убрать `@mathew-cf/opencode-memory` из `npm install -g`. +3. `.opencode/scripts/setup-memory.sh` — удалить (auto-setup в `memory-save.ts`). +4. `.opencode/tools/memory-setup.ts` — удалить (заменён на `memory-doctor.ts`). +5. Rust артефакты на хосте: `.rag/index.bin`, `.rag/meta.json` (Rust-формат), + `~/.cache/opencode/packages/@mathew-cf/opencode-memory@latest/` + sibling + `opencode-memory/` (оба 85 MB). НЕ трогать `.rag/index.json` (Python). +6. `AGENTS.md` + `SKILL.md` — обновить Tool Usage Policy: убрать + `memory-setup()`, добавить 5 kebab-case tools (`memory-doctor`, `memory-save`, + `memory-search`, `memory-list`, `memory-access`). +7. Тесты: инвертировать assertions `test_dockerfile.py` (плагин должен + ОТСУТСТВОВАТЬ), удалить тесты для `setup-memory.sh` и `memory-setup.ts`. + +Имя tool-casing: **kebab-case** для локальных `.opencode/tools/*.ts` (runtime +registry = filename), confirmed ADR-009 + memory `tool-name-casing-kebab-vs-mcp-snake`. +MCP plugin tools были snake_case, но плагин удалён — больше не актуально. + +## Альтернативы + +- **Оставить plugin как fallback** — отклонено: plugin не вызывается из-за + wrapper-бага, Rust meta.json конфилктует с Python, ~170 MB мёртвого веса. + Двойная система = каша, двойной maintenance, риск что Rust перетрёт Python + индекс. +- **Удалить только Rust артефакты, оставить plugin в конфиге** — отклонено: + plugin без `npm install` = silent skip (opencode логирует warning), но + оставляет мёртвый блок в конфиге + вводит в заблуждение. +- **Поэтапный cleanup (несколько PR)** — отклонено: артефакты тесно связаны + (plugin → setup-memory.sh → memory-setup.ts → Rust binary), частичный cleanup + оставляет поломанное состояние. \ No newline at end of file diff --git a/docs/handoff/pr-103-remove-plugin-rust.md b/docs/handoff/pr-103-remove-plugin-rust.md new file mode 100644 index 0000000..fd17adc --- /dev/null +++ b/docs/handoff/pr-103-remove-plugin-rust.md @@ -0,0 +1,86 @@ +--- +pr: 103 +title: Remove @mathew-cf/opencode-memory plugin + setup-memory.sh + Rust artifacts +--- + +## Что сделано + +Cleanup двойной системы памяти после перехода на единую гибридную (5 TS tools +PR #101 + E2E PR #102). Удалено всё, что связано со старой plugin-based системой: + +1. **`opencode.json`** — убран блок `plugin` с `@mathew-cf/opencode-memory` + (строки 3-10). JSON остался валидным. +2. **`Dockerfile:34`** — `@mathew-cf/opencode-memory` убран из `npm install -g` + (`opencode-ai repomix --unsafe-perm`). +3. **Удалены файлы**: + - `.opencode/scripts/setup-memory.sh` — заменён на auto-setup в `memory-save.ts` + - `.opencode/tools/memory-setup.ts` — заменён на `memory-doctor.ts` (read-only) +4. **Rust артефакты на хосте** (не в git): + - `~/.local/share/opencode/opencode-memory/.rag/index.bin` (7.5 MB, Rust binary) + - `~/.local/share/opencode/opencode-memory/.rag/meta.json` (12 KB, Rust формат + `hidden_size`/`model_id`/`file_hashes` — перетёр бы Python `version`/`files`) + - `~/.cache/opencode/packages/@mathew-cf/opencode-memory@latest/` (85 MB) + - `~/.cache/opencode/packages/@mathew-cf/opencode-memory/` (85 MB, sibling без + `@latest` — тот же плагин, удалён для полноты cleanup) + - Итого освобождено: ~177.5 MB + - Python `index.json` (210 MB, 4096-dim embeddings) НЕ тронут. +5. **`AGENTS.md`** (project) — таблица Tool Usage Policy обновлена: убран + `memory-setup()`, добавлены `memory-doctor`, `memory-save`, `memory-search`, + `memory-list`, `memory-access` (kebab-case для TS tools). +6. **`SKILL.md`** (`.opencode/skills/memory/`) — обновлены имена tools + (`memory_search` → `memory-search`), описание (плагин удалён, 5 TS tools), + default путь памяти (`/root/.local/share/opencode/opencode-memory`). +7. **Тесты** адаптированы под cleanup: + - `tests/test_dockerfile.py` — assertions инвертированы: плагин должен + ОТСУТСТВОВАТЬ в Dockerfile (issue #98). Regression-guards для `opencode-ai` + и `repomix` сохранены. + - Удалены `tests/test_setup_memory.py`, `tests/test_memory_setup_tool.py`, + `tests/test_memory_setup_tool.ts` — тесты для удалённых `setup-memory.sh` + и `memory-setup.ts`. + - `tests/test_commit_tool.py` — docstring обновлён (ссылка на `memory-setup.ts` + заменена на `spec-status.ts`). + +## Почему + +Двойная система (plugin + 5 TS tools) = каша. Plugin `@mathew-cf/opencode-memory` +не вызывался из-за бага wrapper-пути, Rust `rag-cli` binary конфликтовал с Python +`index.json` (разные форматы `meta.json` по одному пути). Cleanup ПОСЛЕ того как +5 TS tools работают (PR #101) и E2E тесты проходят (PR #102) — иначе остались бы +без памяти. + +Часть эпика #94 (migration plan), фаза 3 (cleanup). Blocked by #97 (E2E green). + +## Pending + +- Глобальный `/root/.config/opencode/AGENTS.md` и + `/root/.config/opencode/skills/memory/SKILL.md` НЕ обновлены — Write tool + заблокирован для путей вне workspace (`FileSystem.writeFile` error). Это + ручная задача пользователя (копии тех же правок, что в project-файлах). +- `OPENAI_EMBEDDING_BATCH_DELAY` НЕ добавлен в `.env.example` — этот env var + НЕ существует в коде (`src/memory/embedder.py` использует только + `OPENAI_EMBEDDING_BATCH_SIZE` + `Retry-After` из 429-ответа). Добавление + мусорной переменной противоречит правилу «не добавлять мёртвый код». Issue #98 + задача 8 ссылается на «аудит» — вероятно устаревшая/ошибочная находка. +- `~/.local/share/opencode/opencode-memory/.gitignore` — отсутствует. Это + локальный репозиторий памяти (отдельный git), `.gitignore` для `.rag/` там + не относится к этому PR — задача 5 issue #98, но не блокер (`.rag/` уже не + содержит Rust артефактов). + +## Watch out + +- **Кеш `@mathew-cf/opencode-memory/` (без `@latest`)** — кроме `@latest/` + существовал sibling-алиас `opencode-memory/` (85 MB, тот же плагин). Удалён + тоже — иначе кеш не полностью очищен. Issue #98 явно указывает только + `@latest/`, но критерий приёмки «Cache удалён» подразумевает полный cleanup. +- **meta.json** — Python `index.py` и Rust `rag-cli` пишут `meta.json` в ОДИН + путь (`/.rag/meta.json`) но в РАЗНЫХ форматах. Rust: `{model_id, + hidden_size, num_chunks, file_hashes}`. Python: `{version, files:{rel:{sha256, + chunks}}}`. Текущий был Rust-формат → удалён; Python пересоздаст при следующем + reindex. Если снова поставить плагин — он перетрёт Python meta → broken. +- **Тест `test_memory_plugin_in_npm_install`** переименован в + `test_memory_plugin_removed_from_npm_install` (assertion инвертирован). Если + кто-то откатит cleanup — тест провалится с понятным сообщением. +- **Plugin auto-load** — после удаления блока `plugin` из `opencode.json` + плагин НЕ грузится при старте opencode (критерий #98 выполнен). +- **`memory-doctor` tool** — после cleanup НЕ репортит проблем с plugin/Rust + (он их и не проверял — проверяет ripgrep/Python/env/index, которые intact). \ No newline at end of file diff --git a/docs/project-map/README.md b/docs/project-map/README.md index 3a1f198..06f1baa 100644 --- a/docs/project-map/README.md +++ b/docs/project-map/README.md @@ -2,7 +2,7 @@ opencode-config — Docker-based AI coding assistant with persistent memory (opencode configuration). Runs in Docker via `docker-compose.yml` (dind + opencode services). -Root `AGENTS.md` — orchestrator directive (chat = plan only, all via subagents) + global rules (pipeline, code style, language RU) + `## Tool Usage Policy` (таблица 10 tools: commit/create-pr/create-issue/merge-pr/post-review/post-docs-review/pipeline-status/spec-status/memory-setup/tunnel; raw bash заблокирован deny, при сбое tool — STOP, НЕ fallback). Auto-loaded for project + bind-mounted globally in container — PR#31, PR#63. +Root `AGENTS.md` — orchestrator directive (chat = plan only, all via subagents) + global rules (pipeline, code style, language RU) + `## Tool Usage Policy` (таблица tools: commit/create-pr/create-issue/merge-pr/post-review/post-docs-review/pipeline-status/spec-status/memory-doctor/memory-save/memory-search/memory-list/memory-access/tunnel; raw bash заблокирован deny, при сбое tool — STOP, НЕ fallback; `memory-setup` REMOVED PR#103). Auto-loaded for project + bind-mounted globally in container — PR#31, PR#63. ## Structure @@ -49,7 +49,6 @@ opencode-config/ │ │ ├── memory-list.ts # memory-list tool (pure TS, categories count .md or files in category with frontmatter) — PR#101 │ │ ├── memory-save.ts # memory-save tool (auto-setup mkdir+git init/clone+hook+7 categories, git commit, async reindex via spawn detached+unref) — PR#101 │ │ ├── memory-search.ts # memory-search tool (ripgrep keyword + Python semantic via spawnSync + scoring port + cross-category fallback) — PR#101 -│ │ ├── memory-setup.ts # memory-setup tool wrapper (0 args, calls setup-memory.sh) — PR#36 │ │ ├── pipeline-status.ts # pipeline-status tool wrapper │ │ ├── post-docs-review.ts # post-docs-review tool wrapper (3 args: pr_number, verdict enum, body; deterministic ## Docs Review Summary heading; optional repo?: string) — PR#46, PR#65 │ │ ├── post-review.ts # post-review tool wrapper (3 args: pr_number, verdict enum, body; deterministic ## Code Review Summary heading; optional repo?: string) — PR#46, PR#65 @@ -61,10 +60,9 @@ opencode-config/ │ │ ├── observability.py # OTel spans for tools │ │ ├── pipeline-status.py # 7-phase oracle (gh PR + CI polling, NEXT_ACTIONS with subagent_type+template) — PR#42, PR#67 (check_project_map guard) │ │ ├── scaffold-handoff.sh # Scaffold handoff + ADR stubs -│ │ ├── setup-memory.sh # opencode-memory bootstrap (7-step: mkdir/clone/remote/hook/memory index/JS wrapper/status, idempotent) — PR#36, PR#77 │ │ ├── spec-status.py # 9-phase spec oracle │ │ └── tunnel.sh # Cloudflare tunnel toggle bash (named mode via CLOUDFLARE_TUNNEL_TOKEN) — PR#34 -│ ├── opencode.json # MCP servers, providers, permissions, agents (role-based tools), plugins — PR#40, PR#69 +│ ├── opencode.json # MCP servers, providers, permissions, agents (role-based tools) — PR#40, PR#69 (plugin block REMOVED PR#103) │ ├── package.json # npm deps for tools/*.ts (deps: @vscode/ripgrep; devDeps: @types/node, typescript) — PR#101 │ └── .gitignore # Ignores node_modules, etc. ├── docs/ @@ -86,7 +84,7 @@ opencode-config/ │ ├── test_check_permissions.py # permissions-check.yml validator │ ├── test_cli.py # src/memory/cli.py │ ├── test_docker_compose.py # docker-compose.yml port exposure (0.0.0.0:4096, no 127.0.0.1) — PR#51 -│ ├── test_dockerfile.py # Dockerfile npm install (opencode-ai + repomix + @mathew-cf/opencode-memory) — PR#57 +│ ├── test_dockerfile.py # Dockerfile npm install (opencode-ai + repomix; @mathew-cf/opencode-memory REMOVED PR#103, assertions inverted) — PR#57, PR#103 │ ├── test_commit_tool.py # .opencode/tools/commit.ts (via _ts_loader.mjs exec_stub_json) — PR#38 │ ├── test_commit_tool.ts # TS wrapper test (mjs loader) — PR#38 │ ├── test_create_issue_tool.py # .opencode/tools/create-issue.ts (via _ts_loader.mjs exec_stub_json; +repo explicit/omitted/invalid) — PR#38, PR#65 @@ -97,8 +95,6 @@ opencode-config/ │ ├── test_embedder_live.py # Live embed tests (skip without RUN_LIVE=1) │ ├── test_index.py # src/memory/index.py (chunking + env override + .rag skip) │ ├── test_chunking.py # _chunk_text edge cases (empty, unicode, size``. -""" - -import json -import os -import subprocess -from pathlib import Path - -import pytest - -REPO_ROOT = Path(__file__).resolve().parent.parent -LOADER = REPO_ROOT / "tests" / "_ts_loader.mjs" -TS_FILE = REPO_ROOT / ".opencode" / "tools" / "memory-setup.ts" -TS_FILE_REL = ".opencode/tools/memory-setup.ts" - - -def _run_loader(*args: str) -> dict: - """Invoke the loader with ``TS_FILE`` env set to memory-setup.ts and parse JSON stdout.""" - env = {**os.environ, "TS_FILE": TS_FILE_REL} - proc = subprocess.run( - ["node", str(LOADER), *args], - capture_output=True, - text=True, - check=False, - cwd=str(REPO_ROOT), - timeout=60, - env=env, - ) - if proc.returncode != 0: - raise RuntimeError( - f"_ts_loader.mjs {' '.join(args)} failed (exit {proc.returncode}):\n" - f"stdout: {proc.stdout}\nstderr: {proc.stderr}" - ) - return json.loads(proc.stdout) - - -def test_loader_can_load_tool(): - """Sanity: memory-setup.ts loads and declares no args (toggle-style tool).""" - if not TS_FILE.exists(): - pytest.skip("memory-setup.ts not present") - out = _run_loader("load") - assert "description" in out - assert out["args"] == [], f"expected no args, got: {out['args']}" - - -def test_success(): - """execute() with exit 0 returns trimmed stdout. - - memory-setup.ts follows the tunnel.ts pattern: ``r.stdout.trim()`` on - success. Stub spawnSync to return ``" memory: ready \\n"`` and verify - the tool returns ``"memory: ready"`` (whitespace stripped). - """ - out = _run_loader("exec_stub", "_", "0", " memory: ready \n", "") - result = out["result"] - assert result == "memory: ready", f"expected trimmed stdout, got: {result!r}" - - -def test_failure(): - """execute() with exit 1 returns ``⚠️ memory-setup failed (exit 1): ``. - - The tool prepends ``⚠️ memory-setup failed (exit ): `` to the stderr - (falling back to stdout if stderr is empty). Stub spawnSync to return - exit 1 + a stderr string and verify the error prefix + stderr content. - """ - out = _run_loader("exec_stub", "_", "1", "", "ERROR: OPENCODE_MEMORY_REMOTE not set") - result = out["result"] - assert "⚠️ memory-setup failed" in result, f"missing error prefix: {result!r}" - assert "exit 1" in result, f"missing exit code: {result!r}" - assert "OPENCODE_MEMORY_REMOTE not set" in result, f"missing stderr: {result!r}" - - -def test_execute_uses_cwd_from_context(): - """execute passes ``cwd=context.worktree`` to spawnSync (ADR-023 pattern). - - Like pipeline-status.ts / spec-status.ts / tunnel.ts, the memory-setup - tool wrapper propagates ``context.worktree`` as ``cwd`` to spawnSync so - the bash script runs in the git worktree root (where .opencode/ lives), - not in the opencode process cwd. - """ - out = _run_loader("exec_stub", "_", "0", "ok", "") - calls = out["calls"] - assert len(calls) == 1, f"expected 1 spawnSync call, got {len(calls)}" - opts = calls[0]["opts"] - assert opts is not None, "spawnSync called without opts — expected cwd kwarg" - assert "cwd" in opts, f"opts missing 'cwd' key — got: {opts}" - assert opts["cwd"] == str(REPO_ROOT), ( - f"cwd must equal context.worktree ({REPO_ROOT}), got: {opts['cwd']!r}" - ) diff --git a/tests/test_memory_setup_tool.ts b/tests/test_memory_setup_tool.ts deleted file mode 100644 index c94f0cc..0000000 --- a/tests/test_memory_setup_tool.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Tests for .opencode/tools/memory-setup.ts — the memory-setup custom tool. - * - * Mirror of tests/test_pipeline_status_tool.ts / test_spec_status_tool.ts: - * the tool is a thin spawnSync wrapper around - * ``.opencode/scripts/setup-memory.sh`` with no args. - * - * Runtime note: opencode ships a standalone binary with Bun bundled inside; - * there is no separate ``bun`` CLI on the host (CI runner uses node + pytest). - * The CI runs the equivalent Python tests in - * ``tests/test_memory_setup_tool.py`` via the JS loader ``tests/_ts_loader.mjs``. - * This file documents the intended TS-side test cases and is runnable under - * ``bun test`` once a bun runtime is available on the host. - * - * Test cases (mirror tests/test_memory_setup_tool.py): - * - test_success — script exit 0 → returns trimmed stdout - * - test_failure — script exit 1 → returns ``⚠️ memory-setup failed (exit 1): `` - */ - -import { describe, test, expect, mock } from "bun:test" with { type: "'bun-test'" } -import { spawnSync } from "child_process" -import path from "path" - -const TOOL_SRC = path.resolve(import.meta.dir, "..", ".opencode", "tools", "memory-setup.ts") - -describe("memory-setup tool", () => { - test("test_success — script exit 0 returns trimmed stdout", async () => { - mock.module("child_process", () => ({ - spawnSync: () => ({ status: 0, stdout: " memory: ready \n", stderr: "" }), - })) - const mod = await import(TOOL_SRC + "?t=" + Date.now()) - const result = await mod.default.execute({}, { - sessionID: "t", messageID: "t", agent: "t", - directory: ".", worktree: ".", - abort: new AbortController().signal, - metadata() {}, async ask() {}, - }) - expect(result).toBe("memory: ready") - }) - - test("test_failure — script exit 1 returns error message", async () => { - mock.module("child_process", () => ({ - spawnSync: () => ({ status: 1, stdout: "", stderr: "ERROR: OPENCODE_MEMORY_REMOTE not set" }), - })) - const mod = await import(TOOL_SRC + "?t=" + Date.now()) - const result = await mod.default.execute({}, { - sessionID: "t", messageID: "t", agent: "t", - directory: ".", worktree: ".", - abort: new AbortController().signal, - metadata() {}, async ask() {}, - }) - expect(result).toContain("⚠️ memory-setup failed") - expect(result).toContain("exit 1") - expect(result).toContain("OPENCODE_MEMORY_REMOTE not set") - }) -}) \ No newline at end of file diff --git a/tests/test_setup_memory.py b/tests/test_setup_memory.py deleted file mode 100644 index 7b1019c..0000000 --- a/tests/test_setup_memory.py +++ /dev/null @@ -1,410 +0,0 @@ -"""Tests for .opencode/scripts/setup-memory.sh — deterministic memory init. - -Uses a local mock remote (``git init --bare``) instead of GitHub so the -tests are hermetic and offline-safe. ``tmp_path`` provides isolation. - -The script is invoked via ``subprocess`` with env vars pointing at the -mock remote + a tmp dir. Each test asserts one of the deterministic -flow steps. - -Script contract (``setup-memory.sh``): - 1. mkdir -p MEMORY_DIR - 2. clone REMOTE | git pull --ff-only - 3. remote set-url if origin != REMOTE - 4. install post-commit hook (auto-push) if missing/wrong - 5. memory index if .rag missing (best-effort, CLI optional) - 5b. generate JS wrapper for opencode-memory plugin (idempotent + backup) - 6. echo status - -Exit 1 when ``OPENCODE_MEMORY_REMOTE`` is unset (no default — the -``.env.example`` provides the value; absence is a config error). -""" - -import os -import subprocess -import sys -from pathlib import Path - -import pytest - -REPO_ROOT = Path(__file__).resolve().parent.parent -SCRIPT = REPO_ROOT / ".opencode" / "scripts" / "setup-memory.sh" -REPO_PYTHON = str(REPO_ROOT / ".venv" / "bin" / "python") - -EXPECTED_HOOK = "#!/bin/bash\ngit push origin master 2>/dev/null || true\n" - - -def _seed_remote(remote_dir: Path) -> None: - """Seed a bare remote with one commit on ``master``.""" - remote_dir.mkdir(parents=True, exist_ok=True) - subprocess.run(["git", "init", "--bare", "-q", str(remote_dir)], check=True) - seed = remote_dir.parent / "seed" - seed.mkdir() - subprocess.run(["git", "init", "-q", str(seed)], check=True) - (seed / "README.md").write_text("# memory\n") - subprocess.run(["git", "-C", str(seed), "add", "README.md"], check=True) - subprocess.run( - [ - "git", - "-C", - str(seed), - "-c", - "user.email=t@t", - "-c", - "user.name=t", - "commit", - "-qm", - "init", - ], - check=True, - ) - subprocess.run(["git", "-C", str(seed), "branch", "-M", "master"], check=True) - subprocess.run( - ["git", "-C", str(seed), "remote", "add", "origin", str(remote_dir)], - check=True, - ) - subprocess.run(["git", "-C", str(seed), "push", "-q", "origin", "master"], check=True) - - -def _run_script( - memory_dir: Path, - remote: str | None, - wrapper: Path | None = None, -) -> subprocess.CompletedProcess[str]: - """Run setup-memory.sh with env pointing at tmp paths. - - When ``remote`` is None, OPENCODE_MEMORY_REMOTE is removed from env - (simulating the "no remote env" error path). ``wrapper`` sets - ``MEMORY_WRAPPER_PATH`` so tests don't write to the real plugin dir. - """ - env = { - **os.environ, - "OPENCODE_MEMORY_DIR": str(memory_dir), - "GIT_TERMINAL_PROMPT": "0", - } - if remote is not None: - env["OPENCODE_MEMORY_REMOTE"] = remote - else: - env.pop("OPENCODE_MEMORY_REMOTE", None) - if wrapper is not None: - env["MEMORY_WRAPPER_PATH"] = str(wrapper) - wrapper.parent.mkdir(parents=True, exist_ok=True) - env["MEMORY_WRAPPER_PYTHON"] = REPO_PYTHON - env["OPENCODE_WORKSPACE"] = str(REPO_ROOT) - return subprocess.run( - ["bash", str(SCRIPT)], - capture_output=True, - text=True, - check=False, - env=env, - ) - - -def test_fresh_init(tmp_path: Path) -> None: - """Empty dir → run → clone, hook, remote, files present.""" - remote = tmp_path / "remote.git" - _seed_remote(remote) - mem = tmp_path / "mem" - r = _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js") - assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" - assert (mem / ".git").is_dir(), "repo not cloned" - assert (mem / "README.md").exists(), "file not pulled" - assert (mem / ".git" / "hooks" / "post-commit").exists(), "hook missing" - got = subprocess.run( - ["git", "-C", str(mem), "remote", "get-url", "origin"], - capture_output=True, - text=True, - check=True, - ).stdout.strip() - assert got == str(remote), f"remote mismatch: {got}" - - -def test_existing_repo(tmp_path: Path) -> None: - """.git exists → run → pull --ff-only, no destructive changes.""" - remote = tmp_path / "remote.git" - _seed_remote(remote) - mem = tmp_path / "mem" - assert _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js").returncode == 0 - # capture state before second run - head_before = subprocess.run( - ["git", "-C", str(mem), "rev-parse", "HEAD"], - capture_output=True, - text=True, - check=True, - ).stdout.strip() - r = _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js") - assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" - head_after = subprocess.run( - ["git", "-C", str(mem), "rev-parse", "HEAD"], - capture_output=True, - text=True, - check=True, - ).stdout.strip() - assert head_before == head_after, "pull --ff-only changed HEAD unexpectedly" - assert "pulling latest" in r.stdout - - -def test_wrong_remote(tmp_path: Path) -> None: - """remote ≠ expected → run → set-url corrects the remote.""" - remote = tmp_path / "remote.git" - _seed_remote(remote) - mem = tmp_path / "mem" - assert _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js").returncode == 0 - # corrupt remote URL (tmp path — avoids S108 insecure-tmp warning) - wrong = tmp_path / "wrong-remote" - subprocess.run( - ["git", "-C", str(mem), "remote", "set-url", "origin", str(wrong)], - check=True, - ) - r = _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js") - assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" - assert "fixing remote" in r.stdout - got = subprocess.run( - ["git", "-C", str(mem), "remote", "get-url", "origin"], - capture_output=True, - text=True, - check=True, - ).stdout.strip() - assert got == str(remote), f"remote not fixed: {got}" - - -def test_missing_hook(tmp_path: Path) -> None: - """hook missing → run → hook created with correct content.""" - remote = tmp_path / "remote.git" - _seed_remote(remote) - mem = tmp_path / "mem" - assert _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js").returncode == 0 - hook = mem / ".git" / "hooks" / "post-commit" - hook.unlink() - assert not hook.exists() - r = _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js") - assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" - assert hook.exists(), "hook not recreated" - assert hook.read_text() == EXPECTED_HOOK, f"hook content wrong: {hook.read_text()!r}" - assert "installing post-commit hook" in r.stdout - - -def test_idempotent(tmp_path: Path) -> None: - """run 3x → state identical after run 1 and run 3.""" - remote = tmp_path / "remote.git" - _seed_remote(remote) - mem = tmp_path / "mem" - - def _snapshot() -> dict[str, str | None]: - head = subprocess.run( - ["git", "-C", str(mem), "rev-parse", "HEAD"], - capture_output=True, - text=True, - check=True, - ).stdout.strip() - hook = mem / ".git" / "hooks" / "post-commit" - return { - "head": head, - "hook": hook.read_text() if hook.exists() else None, - "remote": subprocess.run( - ["git", "-C", str(mem), "remote", "get-url", "origin"], - capture_output=True, - text=True, - check=True, - ).stdout.strip(), - } - - assert _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js").returncode == 0 - snap1 = _snapshot() - assert _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js").returncode == 0 - assert _run_script(mem, str(remote), wrapper=tmp_path / "wrapper" / "rag.js").returncode == 0 - snap3 = _snapshot() - assert snap1 == snap3, f"non-idempotent:\n run1={snap1}\n run3={snap3}" - - -def test_no_remote_env(tmp_path: Path) -> None: - """OPENCODE_MEMORY_REMOTE unset → exit 1, error message to stderr/stdout.""" - mem = tmp_path / "mem" - r = _run_script(mem, remote=None) - assert r.returncode == 1, f"expected exit 1, got {r.returncode}" - combined = r.stderr + r.stdout - assert "OPENCODE_MEMORY_REMOTE" in combined, f"missing error msg: {combined!r}" - assert "ERROR" in combined or "not set" in combined - - -# ── git insteadOf helper (GITHUB_TOKEN) ────────────────────────────────────── - - -def test_script_contains_git_insteadof() -> None: - """setup-memory.sh contains `git config --global url.insteadOf` before clone.""" - src = SCRIPT.read_text() - assert "git config --global url." in src, "missing git config --global url. call" - assert ".insteadOf https://github.com/" in src, "missing insteadOf directive" - # The insteadOf block must appear BEFORE the git clone line. - insteadof_pos = src.index(".insteadOf") - clone_pos = src.index("git clone --origin") - assert insteadof_pos < clone_pos, "insteadOf helper must precede git clone" - - -def test_script_contains_github_token_check() -> None: - """setup-memory.sh checks GITHUB_TOKEN (guarded, not hard-fail if empty).""" - src = SCRIPT.read_text() - assert "GITHUB_TOKEN" in src, "missing GITHUB_TOKEN reference" - # Must be a conditional check (if [ -n ... ]), not an unconditional hard fail. - assert '-n "${GITHUB_TOKEN:-}"' in src or '-n "$GITHUB_TOKEN"' in src, ( - "GITHUB_TOKEN must be a guarded check (if [ -n ... ]), not a hard requirement" - ) - - -def test_insteadof_idempotent(tmp_path: Path) -> None: - """Running the script N times does not duplicate the insteadOf entry. - - `git config --global url..insteadOf` overwrites the value on each - call (idempotent by git semantics). We verify by invoking setup-memory.sh - twice with a fake GITHUB_TOKEN against a mock remote and checking that - `git config --global --get-all` returns exactly one match after each run. - """ - remote = tmp_path / "remote.git" - _seed_remote(remote) - mem = tmp_path / "mem" - - # Use an isolated HOME so --global writes to a tmp gitconfig (no pollution). - home = tmp_path / "home" - home.mkdir() - - env = { - **os.environ, - "OPENCODE_MEMORY_DIR": str(mem), - "OPENCODE_MEMORY_REMOTE": str(remote), - "GITHUB_TOKEN": "fake-token-for-test", - "HOME": str(home), - "GIT_TERMINAL_PROMPT": "0", - "MEMORY_WRAPPER_PATH": str(tmp_path / "wrapper" / "rag.js"), - "MEMORY_WRAPPER_PYTHON": REPO_PYTHON, - "OPENCODE_WORKSPACE": str(REPO_ROOT), - } - (tmp_path / "wrapper").mkdir(exist_ok=True) - - # First run: clone path triggers the insteadOf helper. - r1 = subprocess.run(["bash", str(SCRIPT)], capture_output=True, text=True, check=False, env=env) - assert r1.returncode == 0, f"run1 failed: stdout={r1.stdout}\nstderr={r1.stderr}" - - # Read the global gitconfig written by the script. - gitconfig = home / ".gitconfig" - assert gitconfig.exists(), f"gitconfig not created at {gitconfig}" - content1 = gitconfig.read_text() - assert "x-access-token:fake-token-for-test@github.com" in content1 - assert "insteadOf" in content1 - - # Count insteadOf occurrences — should be exactly 1 (no duplication). - insteadof_count_1 = content1.count("insteadOf") - assert insteadof_count_1 == 1, f"expected 1 insteadOf after run1, got {insteadof_count_1}" - - # Second run: existing repo → pull path (clone branch skipped). The helper - # is inside the clone branch, so it won't re-fire — but we assert the - # config is NOT duplicated regardless (single insteadOf entry preserved). - r2 = subprocess.run(["bash", str(SCRIPT)], capture_output=True, text=True, check=False, env=env) - assert r2.returncode == 0, f"run2 failed: stdout={r2.stdout}\nstderr={r2.stderr}" - content2 = gitconfig.read_text() - insteadof_count_2 = content2.count("insteadOf") - assert insteadof_count_2 == 1, ( - f"expected 1 insteadOf after run2, got {insteadof_count_2} (duplicate?)" - ) - - -def test_no_github_token_skips_insteadof(tmp_path: Path) -> None: - """When GITHUB_TOKEN is unset, the insteadOf helper is skipped (no error).""" - remote = tmp_path / "remote.git" - _seed_remote(remote) - mem = tmp_path / "mem" - - home = tmp_path / "home" - home.mkdir() - - env = { - **os.environ, - "OPENCODE_MEMORY_DIR": str(mem), - "OPENCODE_MEMORY_REMOTE": str(remote), - "HOME": str(home), - "GIT_TERMINAL_PROMPT": "0", - "MEMORY_WRAPPER_PATH": str(tmp_path / "wrapper" / "rag.js"), - "MEMORY_WRAPPER_PYTHON": REPO_PYTHON, - "OPENCODE_WORKSPACE": str(REPO_ROOT), - } - env.pop("GITHUB_TOKEN", None) - (tmp_path / "wrapper").mkdir(exist_ok=True) - - r = subprocess.run(["bash", str(SCRIPT)], capture_output=True, text=True, check=False, env=env) - assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" - gitconfig = home / ".gitconfig" - if gitconfig.exists(): - content = gitconfig.read_text() - assert "insteadOf" not in content, "insteadOf set without GITHUB_TOKEN" - - -# ── JS wrapper for opencode-memory plugin (step 5b) ────────────────────────── - - -def test_wrapper_generated(tmp_path: Path) -> None: - """Run → wrapper file exists at MEMORY_WRAPPER_PATH with delegate content.""" - remote = tmp_path / "remote.git" - _seed_remote(remote) - mem = tmp_path / "mem" - wrapper = tmp_path / "wrapper" / "rag.js" - r = _run_script(mem, str(remote), wrapper=wrapper) - assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" - assert wrapper.exists(), f"wrapper not generated at {wrapper}" - assert wrapper.stat().st_mode & 0o100, "wrapper not executable" - assert "generated wrapper" in r.stdout, f"missing generate msg: {r.stdout!r}" - - -def test_wrapper_idempotent(tmp_path: Path) -> None: - """Run 2x → wrapper content identical, 2nd run reports 'wrapper correct'.""" - remote = tmp_path / "remote.git" - _seed_remote(remote) - mem = tmp_path / "mem" - wrapper = tmp_path / "wrapper" / "rag.js" - r1 = _run_script(mem, str(remote), wrapper=wrapper) - assert r1.returncode == 0, f"run1: stdout={r1.stdout}\nstderr={r1.stderr}" - content1 = wrapper.read_text() - r2 = _run_script(mem, str(remote), wrapper=wrapper) - assert r2.returncode == 0, f"run2: stdout={r2.stdout}\nstderr={r2.stderr}" - content2 = wrapper.read_text() - assert content1 == content2, "wrapper content changed between runs" - assert "wrapper correct" in r2.stdout, f"2nd run should report correct: {r2.stdout!r}" - - -def test_wrapper_content(tmp_path: Path) -> None: - """Wrapper contains python memory CLI delegation + spawnSync.""" - remote = tmp_path / "remote.git" - _seed_remote(remote) - mem = tmp_path / "mem" - wrapper = tmp_path / "wrapper" / "rag.js" - r = _run_script(mem, str(remote), wrapper=wrapper) - assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" - body = wrapper.read_text() - assert "python" in body, f"wrapper missing python ref: {body!r}" - assert "-m" in body and "src.memory" in body, "wrapper missing src.memory module" - assert "spawnSync" in body, "wrapper missing spawnSync" - assert "process.argv.slice" in body, "wrapper missing argv passthrough" - - -def test_wrapper_backup_original(tmp_path: Path) -> None: - """Pre-existing wrapper with different content → .orig backup saved once.""" - remote = tmp_path / "remote.git" - _seed_remote(remote) - mem = tmp_path / "mem" - wrapper = tmp_path / "wrapper" / "rag.js" - wrapper.parent.mkdir(parents=True, exist_ok=True) - original = "# old rag-cli shim\nconsole.log('old');\n" - wrapper.write_text(original) - r = _run_script(mem, str(remote), wrapper=wrapper) - assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" - backup = wrapper.with_suffix(".js.orig") - assert backup.exists(), ".orig backup not created" - assert backup.read_text() == original, ".orig does not preserve original content" - assert wrapper.read_text() != original, "wrapper not replaced with delegate" - # Second run with different wrong content: .orig must NOT be overwritten. - wrapper.write_text("# another wrong\n") - r2 = _run_script(mem, str(remote), wrapper=wrapper) - assert r2.returncode == 0, f"run2: stdout={r2.stdout}\nstderr={r2.stderr}" - assert backup.read_text() == original, ".orig overwritten on 2nd backup" - - -if __name__ == "__main__": - sys.exit(pytest.main([__file__, "-v"]))