# ADR-027: Shared module for GitHub tools + repo parameter + tunnel tests ## Статус Accepted (2026-07-25) ## Контекст Research (subagent explore, PR#63) выявил 3 проблемы в GitHub tools инфраструктуре репо `slaid098/opencode-config`: ### Проблема 1: Дублирование spawnSync логики Каждый из 5 GitHub tools (`create-issue.ts`, `create-pr.ts`, `post-review.ts`, `post-docs-review.ts`, `merge-pr.ts`) дублировал: - `spawnSync("gh", [...], { encoding: "utf-8", cwd: context.worktree })` — ~5 строк на tool - `if (r.status !== 0) return "⚠️ ... failed (exit ...): ..."` — ~3 строки на tool - Итого: ~10 строк × 5 файлов = ~50 строк дублирования Дублирование означает: изменение canonical error-формата или spawnSync options (например, добавление timeout) требует правки 5 файлов, дрейф вероятен. ### Проблема 2: Нет `repo` параметра Tools работали только через auto-detect: `gh` CLI сам определяет owner/repo из `git remote get-url origin` в текущей директории (`context.worktree`). Ограничения: - Из не-git-директории (например `/root/workspace`) — `gh` не может определить remote → fall back на hardcoded `--repo` (что было багом issue #60, исправлено в PR#61 / ADR-025). - Нет явного способа указать repo при вызове tool — теряется гибкость. - Для pipeline оркестрации было бы полезно передавать repo явно (единый стандарт вместо полагания на cwd). ADR-025 (PR#61) убрал хардкод `--repo slaid098/opencode-config`, оставив auto-detect. Этот PR делает следующий шаг: `repo` становится опциональным параметром (явный путь вместо хардкода), auto-detect остаётся default. ### Проблема 3: Tunnel без тестов Tool `tunnel` — единственный из 10 tools без парных `.py` + `.ts` тестов. Все остальные tools покрыты (`tests/test_commit_tool.py/.ts`, `tests/test_create_issue_tool.py/.ts`, etc.). Tunnel tool: беспараметровый, toggle-логика в `tunnel.sh` (PID-файл `/tmp/tunnel.pid`, `kill -0` проверка живости, stale cleanup, `CLOUDFLARE_TUNNEL_TOKEN` обязателен). Регрессии в toggle/PID-логике проходят незамеченными. ## Решение ### 1. Shared module `.opencode/tools/_shared.ts` Создан модуль с 3 функциями: - `parseRepo(repo?: string): string[]` — если `repo` передан, возвращает `["--repo", repo]`; если нет — `[]` (auto-detect через gh из cwd). - `runGh(args: string[], repo?: string, opts?: { cwd?: string })` — `spawnSync("gh", [...parseRepo(repo), ...args], { encoding: "utf-8", cwd: opts?.cwd })`. Prepends `--repo ` к gh argv когда repo явный. - `formatResult(r, toolName: string): string` — на успехе (exit 0) `r.stdout.trim()`, на ошибке canonical `⚠️ failed (exit ): `. 5 GitHub tools используют `runGh` (устраняет дублирование spawnSync). `formatResult` используют только `create-issue.ts` / `create-pr.ts` (их error-формат точно совпадает). `post-review.ts` / `post-docs-review.ts` / `merge-pr.ts` сохраняют кастомный error с PR номером (`⚠️ ... failed for PR #N (exit K): ...`) для диагностики — `formatResult` потерял бы PR номер. ### 2. `repo?: string` параметр (backward-compatible) Все 5 GitHub tools приняли опциональный `repo?: string`: - При `repo` передан → `runGh` prepends `["--repo", repo]` → gh targetит явный owner/name независимо от cwd. - При `repo` omitted → `parseRepo` возвращает `[]` → `--repo` НЕ добавляется → gh auto-detect'ит из `context.worktree` (cwd) — поведение идентично pre-refactor (ADR-025 / PR#61). Backward-compatibility подтверждена: существующие тесты `test_spawnsync_args` (ассертят `"--repo" not in args`) + новые `test_repo_omitted_no_repo_flag` проходят. 51→401 тестов (375 baseline + 26 новых). ### 3. `_ts_loader.mjs` extension для relative imports Loader (`tests/_ts_loader.mjs`) использует `new Function` sandbox и умел только `child_process` / `path` / `@opencode-ai/plugin`. Relative imports (`./_shared`) падали с ENOENT. Расширение: - `stripTs` стриппает `import { ... } from "./..."` + `export` на top-level declarations + type annotations в function params (`function foo(a: Type): Ret {` → `function foo(a) {`). - `inlineShared()` инлайнит код shared-модуля в sandbox: читает оригинальный файл для детекта import, грузит + стриппет shared-модуль, prepends к `new Function` body. Авто-добавление `.ts` к specifier (`./_shared` → `./_shared.ts`). Без этого тесты отрефакторенных tools падали бы — loader не поддерживал local imports. ### 4. Tunnel tests Созданы `tests/test_tunnel_tool.py` (6 pytest) + `tests/test_tunnel_tool.ts` (4 TS). `.py` тесты запускают `tunnel.sh` напрямую с изоляцией (копия в `tmp_path`, переписанные PID/LOG пути, fake `cloudflared` = `sleep 30` на PATH). Покрыты все ветви: start без токена (exit 1), start с токеном, start с `TUNNEL_DOMAIN`, stop при живом процессе, stale PID cleanup + start, toggle (повторный start → stop). ### 5. 5 test-файлов обновлены для `repo` параметра Каждый из 5 tools получил 3 новых кейса: `test_repo_explicit_passed_to_gh` (`--repo foo/bar` prepended), `test_repo_omitted_no_repo_flag` (no `--repo`, backward-compat), `test_repo_invalid_gh_error` (invalid repo + gh failure → tool-specific error). `test_merge_pr_tool.py/.ts` созданы с нуля (merge-pr не имел тестов ранее) — 8 .py + 5 .ts покрывают базовые кейсы + repo. ## Альтернативы - **Оставить дублирование как есть** — отклонено: ~50 строк дублирования растёт с каждым новым gh tool, дрейф в error-формате/spawnSync options вероятен. Shared module — canonical источник, изменение в 1 месте. - **Mandatory `repo` параметр (убрать auto-detect)** — отклонено: теряет backward-compatibility. Существующие вызовы tools (из pipeline, из агентов) не передают `repo` — полагаются на auto-detect из `context.worktree`. Mandatory `repo` сломал бы все вызовы, потребовал бы обновления skills/agents. Опциональный `repo?: string` сохраняет auto-detect default + даёт явный путь когда нужно. - **`formatResult` для всех 5 tools (убрать кастомный error с PR номером)** — отклонено: post-review/post-docs-review/merge-pr error-сообщения содержат `for PR #N` — полезно для диагностики (какой PR fail'нул). `formatResult` дал бы generic `⚠️ post-review failed (exit K): ...` без PR. Потеря информации > экономия 3 строк × 3 файла. `formatResult` используется где error-формат точно совпадает (create-issue/create-pr). - **Параметризовать `--repo` через `git remote get-url origin` (как ADR-007 для pipeline-status.py)** — отклонено: достаточно опционального `repo` + auto-detect. Параметризация через git remote добавила бы spawn вызов в каждый tool без выгоды (auto-detect из cwd справляется, а явный `repo` покрывает edge-cases). ADR-025 rejected alt пересмотрена. - **Inline shared-логику в каждый tool без отдельного модуля** — отклонено: это и есть текущее дублирование (проблема 1). Вынос в `_shared.ts` — canonical источник, тестируемый отдельно. - **Не расширять `_ts_loader.mjs`, тестировать только через `.ts` (bun)** — отклонено: CI runner не имеет `bun` (opencode binary с bundled bun, нет отдельного CLI). `.ts` тесты документационные; реальные проверки идут через `.py` + loader. Без расширения loader'а `.py` тесты отрефакторенных tools падали бы.