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 <agent@opencode.local>
This commit is contained in:
parent
9d1373a51b
commit
9d654a2201
14 changed files with 198 additions and 774 deletions
|
|
@ -1,13 +1,5 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"plugin": [
|
|
||||||
[
|
|
||||||
"@mathew-cf/opencode-memory",
|
|
||||||
{
|
|
||||||
"memoryDir": "{env:OPENCODE_MEMORY_DIR}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"skills": {
|
"skills": {
|
||||||
"paths": [
|
"paths": [
|
||||||
".opencode/skills"
|
".opencode/skills"
|
||||||
|
|
|
||||||
|
|
@ -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)"
|
|
||||||
|
|
@ -6,17 +6,17 @@ description: Инструкция по работе с файловой памя
|
||||||
# File Memory (opencode-memory)
|
# File Memory (opencode-memory)
|
||||||
|
|
||||||
Графовая память (Graphiti/FalkorDB) удалена — была нестабильна и забагована.
|
Графовая память (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-search({ query, category? })` | Гибридный поиск (ripgrep keyword + Python semantic) |
|
||||||
| `memory_list(category?)` | Список категорий / файлов |
|
| `memory-list({ category? })` | Список категорий / файлов |
|
||||||
| `memory_save()` | Commit + re-index после записи/редактирования |
|
| `memory-save()` | Commit + reindex после записи/редактирования (auto-setup: git init + hook если `OPENCODE_MEMORY_REMOTE` set) |
|
||||||
| `memory_access(path)` | Отметить файл как прочитанный |
|
| `memory-access({ path })` | Отметить файл как прочитанный (bump `last_accessed`/`access_count`) |
|
||||||
| `memory-setup()` | Проверить статус бэкендов |
|
| `memory-doctor()` | Read-only диагностика: ripgrep, Python `src.memory`, env vars, index |
|
||||||
|
|
||||||
## Категории
|
## Категории
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ description: Инструкция по работе с файловой памя
|
||||||
|
|
||||||
## Как работать
|
## Как работать
|
||||||
|
|
||||||
1. **Перед началом работы** — `memory_search` по теме
|
1. **Перед началом работы** — `memory-search` по теме
|
||||||
2. **В процессе** — сохранять находки сразу (контекст свежий)
|
2. **В процессе** — сохранять находки сразу (контекст свежий)
|
||||||
3. **В конце сессии** — retrospective: что узнал → сохранить, что было в памяти → обновить, чего не хватало → создать
|
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
|
{memory-dir}/repos/{host}/{org}/{repo}.md
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
@ -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` |
|
| `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-запуск скрипта |
|
| `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-запуск скрипта |
|
| `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-запуск скрипта |
|
| `tunnel()` | `bash .opencode/scripts/tunnel.sh` | Cloudflare tunnel toggle (1-й вызов — start, 2-й — stop) | Сообщи оркестратору, не bash-запуск скрипта |
|
||||||
|
|
||||||
`gh pr comment*` остаётся в allow-list для обратной совместимости (ADR-019
|
`gh pr comment*` остаётся в allow-list для обратной совместимости (ADR-019
|
||||||
|
|
|
||||||
|
|
@ -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_EXECUTABLE_PATH=/usr/bin/chromium
|
||||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
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
|
WORKDIR /root/workspace
|
||||||
|
|
||||||
|
|
|
||||||
63
docs/decisions/045-pr-103-remove-plugin-rust.md
Normal file
63
docs/decisions/045-pr-103-remove-plugin-rust.md
Normal file
|
|
@ -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
|
||||||
|
оставляет поломанное состояние.
|
||||||
86
docs/handoff/pr-103-remove-plugin-rust.md
Normal file
86
docs/handoff/pr-103-remove-plugin-rust.md
Normal file
|
|
@ -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` в ОДИН
|
||||||
|
путь (`<output>/.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).
|
||||||
|
|
@ -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).
|
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
|
## 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-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-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-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
|
│ │ ├── 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-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
|
│ │ ├── 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
|
│ │ ├── 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)
|
│ │ ├── 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
|
│ │ ├── 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
|
│ │ ├── spec-status.py # 9-phase spec oracle
|
||||||
│ │ └── tunnel.sh # Cloudflare tunnel toggle bash (named mode via CLOUDFLARE_TUNNEL_TOKEN) — PR#34
|
│ │ └── 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
|
│ ├── package.json # npm deps for tools/*.ts (deps: @vscode/ripgrep; devDeps: @types/node, typescript) — PR#101
|
||||||
│ └── .gitignore # Ignores node_modules, etc.
|
│ └── .gitignore # Ignores node_modules, etc.
|
||||||
├── docs/
|
├── docs/
|
||||||
|
|
@ -86,7 +84,7 @@ opencode-config/
|
||||||
│ ├── test_check_permissions.py # permissions-check.yml validator
|
│ ├── test_check_permissions.py # permissions-check.yml validator
|
||||||
│ ├── test_cli.py # src/memory/cli.py
|
│ ├── 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_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.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_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
|
│ ├── 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_embedder_live.py # Live embed tests (skip without RUN_LIVE=1)
|
||||||
│ ├── test_index.py # src/memory/index.py (chunking + env override + .rag skip)
|
│ ├── test_index.py # src/memory/index.py (chunking + env override + .rag skip)
|
||||||
│ ├── test_chunking.py # _chunk_text edge cases (empty, unicode, size<overlap)
|
│ ├── test_chunking.py # _chunk_text edge cases (empty, unicode, size<overlap)
|
||||||
│ ├── test_memory_setup_tool.py # .opencode/tools/memory-setup.ts (via _ts_loader.mjs) — PR#36
|
|
||||||
│ ├── test_memory_setup_tool.ts # TS wrapper test (mjs loader) — PR#36
|
|
||||||
│ ├── test_memory_tools_e2e.py # E2E hybrid memory (4 scenarios: zero-config keyword-only, full hybrid, OpenRouter fallback, deletion; @pytest.mark.skipif(not RUN_LIVE); subprocess-based: ripgrep + python3 -m src.memory) — PR#102
|
│ ├── test_memory_tools_e2e.py # E2E hybrid memory (4 scenarios: zero-config keyword-only, full hybrid, OpenRouter fallback, deletion; @pytest.mark.skipif(not RUN_LIVE); subprocess-based: ripgrep + python3 -m src.memory) — PR#102
|
||||||
│ ├── test_merge_pr_tool.py # .opencode/tools/merge-pr.ts (via _ts_loader.mjs; 8 tests: base + repo explicit/omitted/invalid) — PR#65
|
│ ├── test_merge_pr_tool.py # .opencode/tools/merge-pr.ts (via _ts_loader.mjs; 8 tests: base + repo explicit/omitted/invalid) — PR#65
|
||||||
│ ├── test_merge_pr_tool.ts # TS wrapper test (mjs loader; 5 tests: base + repo) — PR#65
|
│ ├── test_merge_pr_tool.ts # TS wrapper test (mjs loader; 5 tests: base + repo) — PR#65
|
||||||
|
|
@ -116,7 +112,6 @@ opencode-config/
|
||||||
│ ├── test_post_review_tool.py # .opencode/tools/post-review.ts (via _ts_loader.mjs exec_stub_json; +repo cases) — PR#46, PR#65
|
│ ├── test_post_review_tool.py # .opencode/tools/post-review.ts (via _ts_loader.mjs exec_stub_json; +repo cases) — PR#46, PR#65
|
||||||
│ ├── test_post_review_tool.ts # TS wrapper test (mjs loader; +repo cases) — PR#46, PR#65
|
│ ├── test_post_review_tool.ts # TS wrapper test (mjs loader; +repo cases) — PR#46, PR#65
|
||||||
│ ├── test_search.py # src/memory/search.py
|
│ ├── test_search.py # src/memory/search.py
|
||||||
│ ├── test_setup_memory.py # .opencode/scripts/setup-memory.sh (mock remote, idempotency, JS wrapper gen/backup) — PR#36, PR#77
|
|
||||||
│ ├── test_spec_status.py # .opencode/scripts/spec-status.py
|
│ ├── test_spec_status.py # .opencode/scripts/spec-status.py
|
||||||
│ ├── test_spec_status_tool.py
|
│ ├── test_spec_status_tool.py
|
||||||
│ ├── test_tunnel_tool.py # .opencode/scripts/tunnel.sh (6 pytest: start/stop/stale PID/toggle, isolated tmp copy + fake cloudflared) — PR#65
|
│ ├── test_tunnel_tool.py # .opencode/scripts/tunnel.sh (6 pytest: start/stop/stale PID/toggle, isolated tmp copy + fake cloudflared) — PR#65
|
||||||
|
|
@ -125,7 +120,7 @@ opencode-config/
|
||||||
├── uv.lock # Locked deps for Python project
|
├── uv.lock # Locked deps for Python project
|
||||||
├── .pre-commit-config.yaml # ruff + UV hooks
|
├── .pre-commit-config.yaml # ruff + UV hooks
|
||||||
├── docker-compose.yml # 2 services (dind + opencode), opencode_network, 4 bind mounts, port 4096 on 0.0.0.0 — PR#24, PR#51
|
├── docker-compose.yml # 2 services (dind + opencode), opencode_network, 4 bind mounts, port 4096 on 0.0.0.0 — PR#24, PR#51
|
||||||
├── Dockerfile # node:22-trixie-slim (Debian 13 trixie, glibc 2.41) + uv + gh + chromium + docker.io + opencode-ai + repomix + @mathew-cf/opencode-memory + cloudflared — PR#24, PR#34, PR#57, PR#71
|
├── Dockerfile # node:22-trixie-slim (Debian 13 trixie, glibc 2.41) + uv + gh + chromium + docker.io + opencode-ai + repomix + cloudflared (@mathew-cf/opencode-memory REMOVED PR#103) — PR#24, PR#34, PR#57, PR#71, PR#103
|
||||||
├── .env.example # Placeholder-only env template (user copies to .env) — PR#24, PR#34 (TUNNEL_DOMAIN), PR#36 (OPENCODE_MEMORY_REMOTE/DIR)
|
├── .env.example # Placeholder-only env template (user copies to .env) — PR#24, PR#34 (TUNNEL_DOMAIN), PR#36 (OPENCODE_MEMORY_REMOTE/DIR)
|
||||||
├── app_data/
|
├── app_data/
|
||||||
│ ├── opencode-memory/ # Persistent memory (separate git repo, gitignored) — PR#36
|
│ ├── opencode-memory/ # Persistent memory (separate git repo, gitignored) — PR#36
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ def test_wrong_type():
|
||||||
def test_execute_uses_cwd_from_context():
|
def test_execute_uses_cwd_from_context():
|
||||||
"""execute passes cwd=context.worktree to spawnSync (ADR-023 pattern).
|
"""execute passes cwd=context.worktree to spawnSync (ADR-023 pattern).
|
||||||
|
|
||||||
Like pipeline-status.ts / memory-setup.ts, the commit tool wrapper
|
Like pipeline-status.ts / spec-status.ts, the commit tool wrapper
|
||||||
propagates ``context.worktree`` as ``cwd`` to spawnSync.
|
propagates ``context.worktree`` as ``cwd`` to spawnSync.
|
||||||
"""
|
"""
|
||||||
out = _run_exec({"message": VALID_MSG}, [STAGED_RESPONSE, COMMIT_OK_RESPONSE])
|
out = _run_exec({"message": VALID_MSG}, [STAGED_RESPONSE, COMMIT_OK_RESPONSE])
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
"""Tests for Dockerfile npm install line.
|
"""Tests for Dockerfile npm install line.
|
||||||
|
|
||||||
Covers issue #56 acceptance criteria:
|
Covers issue #98 acceptance criteria:
|
||||||
- ``Dockerfile`` installs ``@mathew-cf/opencode-memory`` alongside
|
- ``Dockerfile`` no longer installs ``@mathew-cf/opencode-memory`` — the
|
||||||
``opencode-ai`` and ``repomix`` in the global npm install step, so the
|
plugin was replaced by 5 TS tools (``.opencode/tools/memory-*.ts``) that
|
||||||
memory plugin (``memory_save`` / ``memory_search`` / ``memory_list`` MCP
|
call ``python3 -m src.memory`` directly (PR #101). The npm package is
|
||||||
tools) is available inside the container.
|
dead weight in the image and is removed from the global install step.
|
||||||
- ``opencode-ai`` is still present (regression guard — must not be dropped
|
- ``opencode-ai`` and ``repomix`` remain (regression guard — must not be
|
||||||
when editing the install line).
|
dropped when editing the install line).
|
||||||
|
|
||||||
Checks are raw-text grep assertions (no Docker build needed), mirroring the
|
Checks are raw-text grep assertions (no Docker build needed), mirroring the
|
||||||
belt-and-suspenders style of ``test_docker_compose.py``.
|
belt-and-suspenders style of ``test_docker_compose.py``.
|
||||||
|
|
@ -17,8 +17,8 @@ from pathlib import Path
|
||||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
DOCKERFILE = REPO_ROOT / "Dockerfile"
|
DOCKERFILE = REPO_ROOT / "Dockerfile"
|
||||||
|
|
||||||
REQUIRED_PACKAGES = ("opencode-ai", "repomix", "@mathew-cf/opencode-memory")
|
REMOVED_PLUGIN = "@mathew-cf/opencode-memory"
|
||||||
MEMORY_PLUGIN = "@mathew-cf/opencode-memory"
|
KEPT_PACKAGES = ("opencode-ai", "repomix")
|
||||||
OPENCODE_AI = "opencode-ai"
|
OPENCODE_AI = "opencode-ai"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -41,33 +41,33 @@ def test_dockerfile_exists():
|
||||||
assert DOCKERFILE.exists(), f"Dockerfile missing: {DOCKERFILE}"
|
assert DOCKERFILE.exists(), f"Dockerfile missing: {DOCKERFILE}"
|
||||||
|
|
||||||
|
|
||||||
# ── @mathew-cf/opencode-memory installed (issue #56 — the required plugin) ───
|
# ── @mathew-cf/opencode-memory removed (issue #98 — plugin replaced by TS tools)
|
||||||
|
|
||||||
|
|
||||||
def test_memory_plugin_in_npm_install():
|
def test_memory_plugin_removed_from_npm_install():
|
||||||
"""Dockerfile installs ``@mathew-cf/opencode-memory`` via ``npm install -g``.
|
"""Dockerfile does NOT install ``@mathew-cf/opencode-memory`` via ``npm install -g``.
|
||||||
|
|
||||||
Without the plugin the memory MCP tools (``memory_save`` /
|
The plugin was replaced by 5 TS tools (PR #101) that call
|
||||||
``memory_search`` / ``memory_list``) are unavailable — opencode silently
|
``python3 -m src.memory`` directly — the npm package is no longer needed
|
||||||
skips the uninstalled plugin referenced in opencode.json.
|
inside the container and is removed from the global install step.
|
||||||
"""
|
"""
|
||||||
lines = _npm_install_lines(_dockerfile_text())
|
lines = _npm_install_lines(_dockerfile_text())
|
||||||
assert lines, "no `npm install -g` line found in Dockerfile"
|
assert lines, "no `npm install -g` line found in Dockerfile"
|
||||||
assert any(MEMORY_PLUGIN in line for line in lines), (
|
assert not any(REMOVED_PLUGIN in line for line in lines), (
|
||||||
f"Dockerfile must install {MEMORY_PLUGIN!r} via `npm install -g` — "
|
f"Dockerfile must NOT install {REMOVED_PLUGIN!r} via `npm install -g` — "
|
||||||
"memory plugin MCP tools depend on it"
|
"plugin replaced by TS tools (PR #101, cleanup issue #98)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_memory_plugin_in_npm_install_raw():
|
def test_memory_plugin_removed_from_npm_install_raw():
|
||||||
"""Raw text check: the Dockerfile contains ``@mathew-cf/opencode-memory``.
|
"""Raw text check: the Dockerfile does NOT contain ``@mathew-cf/opencode-memory``.
|
||||||
|
|
||||||
Belt-and-suspenders alongside the parsed check — catches edge cases where
|
Belt-and-suspenders alongside the parsed check — catches any lingering
|
||||||
the line collector might miss a multi-line or differently-formatted
|
reference to the removed plugin (multi-line install, comment, etc.).
|
||||||
install statement.
|
|
||||||
"""
|
"""
|
||||||
assert MEMORY_PLUGIN in _dockerfile_text(), (
|
assert REMOVED_PLUGIN not in _dockerfile_text(), (
|
||||||
f"Dockerfile must contain {MEMORY_PLUGIN!r} so the memory plugin loads inside the container"
|
f"Dockerfile must NOT contain {REMOVED_PLUGIN!r} — plugin replaced by "
|
||||||
|
"TS tools (PR #101, cleanup issue #98)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -77,7 +77,7 @@ def test_memory_plugin_in_npm_install_raw():
|
||||||
def test_opencode_ai_present():
|
def test_opencode_ai_present():
|
||||||
"""Dockerfile still installs ``opencode-ai`` (regression guard).
|
"""Dockerfile still installs ``opencode-ai`` (regression guard).
|
||||||
|
|
||||||
Editing the npm install line to add the memory plugin must not
|
Removing the memory plugin from the npm install line must not
|
||||||
accidentally drop the primary ``opencode-ai`` package.
|
accidentally drop the primary ``opencode-ai`` package.
|
||||||
"""
|
"""
|
||||||
lines = _npm_install_lines(_dockerfile_text())
|
lines = _npm_install_lines(_dockerfile_text())
|
||||||
|
|
@ -93,11 +93,11 @@ def test_opencode_ai_present_raw():
|
||||||
assert OPENCODE_AI in _dockerfile_text(), f"Dockerfile must still contain {OPENCODE_AI!r}"
|
assert OPENCODE_AI in _dockerfile_text(), f"Dockerfile must still contain {OPENCODE_AI!r}"
|
||||||
|
|
||||||
|
|
||||||
# ── single npm install line holds all three packages ────────────────────────
|
# ── single npm install line holds the kept packages ──────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def test_single_npm_install_line_has_all_packages():
|
def test_single_npm_install_line_has_kept_packages():
|
||||||
"""One ``npm install -g`` line installs all three required packages.
|
"""One ``npm install -g`` line installs all kept packages.
|
||||||
|
|
||||||
Guards against splitting the install across multiple RUN layers (which
|
Guards against splitting the install across multiple RUN layers (which
|
||||||
would waste image layers and obscure which package is missing).
|
would waste image layers and obscure which package is missing).
|
||||||
|
|
@ -106,7 +106,7 @@ def test_single_npm_install_line_has_all_packages():
|
||||||
install_lines = [line for line in lines if OPENCODE_AI in line]
|
install_lines = [line for line in lines if OPENCODE_AI in line]
|
||||||
assert install_lines, f"no `npm install -g` line with {OPENCODE_AI!r}"
|
assert install_lines, f"no `npm install -g` line with {OPENCODE_AI!r}"
|
||||||
main_line = install_lines[0]
|
main_line = install_lines[0]
|
||||||
for pkg in REQUIRED_PACKAGES:
|
for pkg in KEPT_PACKAGES:
|
||||||
assert pkg in main_line, (
|
assert pkg in main_line, (
|
||||||
f"package {pkg!r} missing from the opencode-ai npm install line: {main_line!r}"
|
f"package {pkg!r} missing from the opencode-ai npm install line: {main_line!r}"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
"""Tests for .opencode/tools/memory-setup.ts — the memory-setup custom tool.
|
|
||||||
|
|
||||||
Mirrors ``tests/test_pipeline_status_tool.py`` / ``test_spec_status_tool.py``:
|
|
||||||
exercises the tool's ``execute()`` function via ``tests/_ts_loader.mjs``
|
|
||||||
(a node CommonJS sandbox that strips TS-only syntax, stubs
|
|
||||||
``@opencode-ai/plugin``, and replaces ``import.meta.dir`` with the real
|
|
||||||
``.opencode/tools`` directory).
|
|
||||||
|
|
||||||
The loader is parameterized via the ``TS_FILE`` env var so the same harness
|
|
||||||
works for every TS tool wrapper. These tests set
|
|
||||||
``TS_FILE=.opencode/tools/memory-setup.ts``.
|
|
||||||
|
|
||||||
Modes used (same as the pipeline-status / spec-status tool tests):
|
|
||||||
- ``load`` — sanity-check that the tool loads and declares no args.
|
|
||||||
- ``exec_stub`` — call execute with a stubbed spawnSync to verify:
|
|
||||||
(a) success path: exit 0 → trimmed stdout returned,
|
|
||||||
(b) failure path: exit 1 → ``⚠️ memory-setup failed (exit 1): <stderr>``.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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): <stderr>``.
|
|
||||||
|
|
||||||
The tool prepends ``⚠️ memory-setup failed (exit <N>): `` 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}"
|
|
||||||
)
|
|
||||||
|
|
@ -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): <stderr>``
|
|
||||||
*/
|
|
||||||
|
|
||||||
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")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -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.<prefix>.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"]))
|
|
||||||
Loading…
Add table
Reference in a new issue