# 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 <name>` к gh argv когда repo явный.
- `formatResult(r, toolName: string): string` — на успехе (exit 0)
  `r.stdout.trim()`, на ошибке canonical `⚠️ <toolName> failed (exit <code>):
  <stderr || stdout>`.

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 падали бы.