--- pr: 65 title: refactor(tools): add repo parameter + shared module + tunnel tests --- ## Что сделано - **`.opencode/tools/_shared.ts`** — новый shared module: `parseRepo(repo?)` (возвращает `["--repo", repo]` или `[]` для auto-detect), `runGh(args, repo?, opts?)` (spawnSync с prepended `--repo` при явном repo + `cwd` из opts), `formatResult(r, toolName)` (canonical `⚠️ failed (exit K): ...` на ошибке, `stdout.trim()` на успехе). Устраняет ~50 строк дублирования spawnSync-логики в 5 GitHub tools. - **`tests/_ts_loader.mjs`** — расширение для поддержки relative imports (`./_shared`): `stripTs` стриппает `import { ... } from "./..."` + `export` на top-level declarations + type annotations в function params; новая `inlineShared()` инлайнит код shared-модуля в `new Function` sandbox (с авто-добавлением `.ts` расширения к specifier). Без этого loader падал на ENOENT — local imports не резолвились. - **5 GitHub tools отрефакторены** на `runGh`/`formatResult` + добавлен опциональный `repo?: string` параметр (backward-compatible): - `create-issue.ts` — `runGh` + `formatResult(r, "gh issue create")` - `create-pr.ts` — `runGh` + `formatResult(r, "gh pr create")` - `post-review.ts` — `runGh` (error-ветка оставлена кастомной с PR номером) - `post-docs-review.ts` — `runGh` (аналогично) - `merge-pr.ts` — `runGh` (аналогично) При `repo` omitted → `parseRepo` возвращает `[]`, gh auto-detect'ит из `context.worktree` (cwd) — поведение идентично pre-refactor (ADR-025 / PR#61). - **Tunnel tests созданы** (issue #64 проблема 3 — единственный tool без тестов): - `tests/test_tunnel_tool.py` — 6 pytest-тестов: start без токена (exit 1), start с токеном (`started (PID: N)`), start с `TUNNEL_DOMAIN` (`domain: ...`), stop при живом процессе (`stopped` + PID-файл удалён), stale PID-файл (чистка + start), toggle (повторный start → stop). Изоляция: копия `tunnel.sh` в tmp с переписанными PID/LOG путями + fake `cloudflared` (sleep 30s) на PATH. - `tests/test_tunnel_tool.ts` — 4 TS-теста: spawnSync с `bash` + путём к скрипту, success → trimmed stdout, failure → `⚠️ tunnel failed (exit K)`, cwd из context. - **5 test-файлов обновлены** кейсами для `repo?: string` (явный / auto-detect / invalid): - `test_create_issue_tool.py/.ts` — +3 кейса (explicit, omitted, invalid) - `test_create_pr_tool.py/.ts` — +3 кейса - `test_post_review_tool.py/.ts` — +3 кейса - `test_post_docs_review_tool.py/.ts` — +3 кейса - `test_merge_pr_tool.py/.ts` — созданы с нуля (8 .py + 5 .ts: базовые кейсы + repo; merge-pr не имел тестов ранее) - **ADR-027** + этот handoff. ## Почему Research (subagent explore, PR#63) выявил 3 проблемы в GitHub tools инфраструктуре: 1. **Дублирование spawnSync логики** — каждый из 5 GitHub tools дублировал `spawnSync("gh", [...], { encoding, cwd })` + `if (r.status !== 0) return "⚠️ ... failed (exit ...): ..."` — ~10 строк × 5 файлов = ~50 строк дублирования. Shared module устраняет дублирование, единый canonical error-формат. 2. **Нет `repo` параметра** — tools работали только через auto-detect (`gh` определяет owner/repo из `git remote` в `context.worktree`). Ограничение: из не-git-директории `gh` не может определить remote → fall back на хардкод (баг issue #60, исправлен в PR#61). Явный `repo?: string` даёт гибкость для pipeline оркестрации без потери auto-detect по умолчанию (backward-compatible). Связано с ADR-025 (PR#61 убрал хардкод, этот PR делает `repo` параметром — явный путь вместо хардкода). 3. **Tunnel без тестов** — `tunnel` единственный из 10 tools без парных `.py` + `.ts` тестов. Toggle-логика (PID-файл, kill -0, stale cleanup) не покрыта → регрессии проходят незамеченными. 6 .py + 4 .ts тестов покрывают все ветви `tunnel.sh`. ## Pending - После merge: на хосте `git pull` + рестарт opencode-контейнера чтобы подхватились обновлённые tools (auto-discovered через @opencode-ai/plugin). - Зависимости: supersedes partial PR#61 (ADR-025) — убрал хардкод `--repo`, этот PR параметризует; related PR#63 (ADR-026) — tool-usage policy описывает 10 tools, этот PR улучшает их реализацию. ## Watch out - **`_ts_loader.mjs` extension** — `inlineShared()` добавляет `.ts` к relative import specifier если его нет (`./_shared` → `./_shared.ts`). Это необходимо потому что TS import specifiers опускают расширение, а `readFileSync` требует полный путь. Если будущие shared-модули используют другие расширения (`.tsx`, `.mts`) — потребуется расширить логику. - **`formatResult` только для create-issue/create-pr** — post-review, post-docs-review, merge-pr сохраняют кастомный error с PR номером (`⚠️ ... failed for PR #N (exit K): ...`) для диагностики. `formatResult` дал бы `⚠️ ... failed (exit K): ...` без PR — потеря информации. Тесты `test_gh_failure_returns_error` проверяют substring `"post-review failed"` / `"exit 1"` — оба варианта проходят. - **Backward-compat критична** — существующие `test_spawnsync_args` в post-review/post-docs-review ассертят `"--repo" not in args`. При `repo` omitted `parseRepo` возвращает `[]` → `--repo` не добавляется → тесты проходят (51→62 на 4 tools). `test_repo_omitted_no_repo_flag` явно закрепляет это поведение. - **Tunnel test isolation** — `tunnel.sh` хардкодит `/tmp/tunnel.pid`. Тесты копируют скрипт в `tmp_path` и переписывают PID/LOG пути через `replace()`. Если в `tunnel.sh` изменится формат строк `PID_FILE="..."` — паттерн replace сломается, тесты упадут (раннее обнаружение). - **`commit.ts` НЕ в списке 5** — это `git`, не `gh` tool (нет `--repo` флага). Оставлен как есть. - **`.opencode/package-lock.json` untracked** — не относится к этому PR, оставлен вне коммитов.