* 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>
9.4 KiB
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 строк на toolif (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передан →runGhprepends["--repo", repo]→ gh targetит явный owner/name независимо от cwd. - При
repoomitted →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 Functionbody. Авто-добавление.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. Mandatoryrepoсломал бы все вызовы, потребовал бы обновления 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 падали бы.