opencode-config/docs/decisions/027-pr-65-tools-refactor-shared-module.md
Sergey 15fc7d014d
refactor(tools): add repo parameter + shared module + tunnel tests (#65)
* refactor(tools): extract shared module for gh spawnSync logic

* feat(tools): add repo parameter to 5 GitHub tools

* test(tools): add tunnel tool tests

* test(tools): add repo parameter test cases for 5 tools

* docs(handoff): scaffold handoff and ADR for PR

* docs(handoff): set PR number

* docs(project-map): update after PR#65 structural changes

---------

Co-authored-by: opencode-agent <agent@opencode.local>
2026-07-25 19:19:19 +03:00

9.4 KiB
Raw Blame History

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