fix(create-pr): pass base field for Forgejo POST /pulls #33

Merged
slaid098 merged 3 commits from fix/create-pr/forgejo-base-field into main 2026-08-08 22:08:42 +03:00
Owner

Что сделано

  • create-pr.ts: auto-detect default_branch через getRepo() (эталон — merge-pr.ts) и передача --base <defaultBranch> в runGh(["pr", "create", ...]). Импортированы resolveForgejoRepo и getRepo из _shared.ts.
  • _shared.ts: добавлен симметричный fail-loud guard для base в callForgejoGh (после guard'а для head) — POST body теперь { title, body, head, base } вместо условного ...(base ? { base } : {}). При отсутствии base возвращается явная ошибка вместо молчаливой отправки malformed-запроса, который Forgejo всё равно отвергнет 422.
  • tests/test_create_pr_tool.py: добавлена REPO_OK_RESPONSE (по образцу test_merge_pr_tool.py:50); перевернут assertion test_head_explicit_override (base теперь обязателен, не omitted); обновлены 9 success-path тестов — добавлен REPO_OK_RESPONSE первым в массив responses (getRepo вызывается ДО POST /pulls), fetch_calls 1→2, assertions на POST body перенесены с fetch_calls[0] на fetch_calls[1]; test_execute_uses_cwd_from_context — git_calls 2→3, remote_calls 1→2 (появился 2-й remote lookup от create-pr.ts).

Почему

Forgejo POST /repos/{owner}/{repo}/pulls требует поле base (target branch) безусловно — в отличие от GitHub API, где base дефолтится к default_branch. Без base Forgejo возвращает 422 {"message":"[Base]: Required"}. Корневая причина бага: create-pr.ts:70 не передавал --base в argv, а _shared.ts:220 через ...(base ? { base } : {}) опускал поле, когда base был undefined — то есть всегда. Тест test_head_explicit_override закреплял этот баг как "expected behavior" с ложным комментарием "Forgejo defaults to default_branch".

Watch out

  • Теперь 2 git remote lookup на success-path вместо 1: resolveForgejoRepo в create-pr.ts (для getRepo pre-flight) + resolveForgejoRepo внутри runGh → callForgejoGh (для POST /pulls). Это не баг — acceptable overhead (2 spawnSync вместо 1, ~миллисекунды). Тесты обновлены: test_execute_uses_cwd_from_context ожидает 3 git-вызова (rev-parse + 2 remote) и 2 remote lookup.
  • Тесты для validation-fail путей (отсутствующие headings, latin-only body, detached HEAD, git failure) НЕ трогались — валидация падает до любого fetch/git-вызова, REPO_OK_RESPONSE там не нужен.
  • formatResult используется только для ошибки POST /pulls; ошибка getRepo (GET /repos) имеет собственный формат ⚠️ create_pr failed for issue #N: ... — сознательно отличается (pre-flight failure, не gh-вызов).

Pending

  • Можно оптимизировать: передать pre-resolved full напрямую в runGh (новая сигнатура опций), чтобы избежать 2-го remote lookup внутри callForgejoGh. Отдельный issue если будет нужно — текущий overhead пренебрежимо мал.

Closes #26

## Что сделано - `create-pr.ts`: auto-detect `default_branch` через `getRepo()` (эталон — `merge-pr.ts`) и передача `--base <defaultBranch>` в `runGh(["pr", "create", ...])`. Импортированы `resolveForgejoRepo` и `getRepo` из `_shared.ts`. - `_shared.ts`: добавлен симметричный fail-loud guard для `base` в `callForgejoGh` (после guard'а для `head`) — POST body теперь `{ title, body, head, base }` вместо условного `...(base ? { base } : {})`. При отсутствии `base` возвращается явная ошибка вместо молчаливой отправки malformed-запроса, который Forgejo всё равно отвергнет 422. - `tests/test_create_pr_tool.py`: добавлена `REPO_OK_RESPONSE` (по образцу `test_merge_pr_tool.py:50`); перевернут assertion `test_head_explicit_override` (`base` теперь обязателен, не omitted); обновлены 9 success-path тестов — добавлен `REPO_OK_RESPONSE` первым в массив responses (getRepo вызывается ДО POST /pulls), `fetch_calls` 1→2, assertions на POST body перенесены с `fetch_calls[0]` на `fetch_calls[1]`; `test_execute_uses_cwd_from_context` — `git_calls` 2→3, `remote_calls` 1→2 (появился 2-й remote lookup от create-pr.ts). ## Почему Forgejo `POST /repos/{owner}/{repo}/pulls` требует поле `base` (target branch) безусловно — в отличие от GitHub API, где base дефолтится к default_branch. Без `base` Forgejo возвращает `422 {"message":"[Base]: Required"}`. Корневая причина бага: `create-pr.ts:70` не передавал `--base` в argv, а `_shared.ts:220` через `...(base ? { base } : {})` опускал поле, когда base был undefined — то есть всегда. Тест `test_head_explicit_override` закреплял этот баг как "expected behavior" с ложным комментарием "Forgejo defaults to default_branch". ## Watch out - Теперь **2 git remote lookup** на success-path вместо 1: `resolveForgejoRepo` в `create-pr.ts` (для getRepo pre-flight) + `resolveForgejoRepo` внутри `runGh` → `callForgejoGh` (для POST /pulls). Это не баг — acceptable overhead (2 spawnSync вместо 1, ~миллисекунды). Тесты обновлены: `test_execute_uses_cwd_from_context` ожидает 3 git-вызова (rev-parse + 2 remote) и 2 remote lookup. - Тесты для **validation-fail путей** (отсутствующие headings, latin-only body, detached HEAD, git failure) НЕ трогались — валидация падает до любого fetch/git-вызова, REPO_OK_RESPONSE там не нужен. - `formatResult` используется только для ошибки POST /pulls; ошибка getRepo (GET /repos) имеет собственный формат `⚠️ create_pr failed for issue #N: ...` — сознательно отличается (pre-flight failure, не gh-вызов). ## Pending - Можно оптимизировать: передать pre-resolved `full` напрямую в `runGh` (новая сигнатура опций), чтобы избежать 2-го remote lookup внутри `callForgejoGh`. Отдельный issue если будет нужно — текущий overhead пренебрежимо мал. Closes #26
test(create-pr): assert base field present in Forgejo POST body
All checks were successful
CI (always) / bootstrap (pull_request) Successful in 2s
CI / bootstrap (pull_request) Successful in 5s
CI / lint (pull_request) Successful in 21s
CI / complexity (pull_request) Successful in 21s
CI / typecheck (pull_request) Successful in 22s
CI / test (3.13) (pull_request) Successful in 1m25s
4eef302ec1
Author
Owner

Code Review Summary

Чистый баг-фикс: create-pr.ts не передавал --base в Forgejo POST /pulls → 422 {"message":"[Base]: Required"}. Корневая причина (условное ...(base ? { base } : {}) опускало поле всегда) устранена: auto-detect default_branch через getRepo() (эталон — merge-pr.ts:16-36) + симметричный fail-loud guard для base в _shared.ts + обновление 9 success-path тестов. Перевёрнутый assertion в test_head_explicit_override правильно фиксирует регрессию (старый тест закреплял баг с ложным комментарием "Forgejo defaults to default_branch").

Positives

  • Симметрия с merge-pr.ts: паттерн resolveForgejoRepo → getRepo → JSON.parse(stdout).default_branch (create-pr.ts:70-76) точно повторяет merge-pr.ts:16-36 — консистентность между tool'ами.
  • Симметричный guard: fail-loud для base (_shared.ts:220-228) зеркалит существующий guard для head (_shared.ts:205-216) — одинаковая структура, понятные сообщения.
  • Тесты обновлены корректно: REPO_OK_RESPONSE первым в массиве (getRepo вызывается ДО POST), fetch_calls 1→2, assertions перенесены fetch_calls[0]→fetch_calls[1], git_calls 2→3, remote_calls 1→2. Validation-fail пути обоснованно не тронуты (падают до fetch/git).
  • Перевёрнутый assertion: assert "base" not in body → assert body.get("base") == "main" — правильно ловит фикс. Обновлённый docstring объясняет почему base обязателен.
  • PR body: ## Watch out документирует overhead 2 remote lookups (acceptable, ~ms), ## Pending предлагает оптимизацию (передать pre-resolved full в runGh). Оба осмысленны.
  • Коммиты: 3 коммита, все conventional format, ≤72 chars, English, без period. Логичная декомпозиция (fix create-pr → fix _shared guard → test).

Suggestions (info, not blocking)

  • test_create_pr_tool.py:18 [docs] Module docstring устарел: create-pr.ts makes 1 spawnSync call (gh pr create) on the success path. Теперь success path делает 3 spawnSync (rev-parse + 2 remote lookup) + 2 fetch (getRepo + POST /pulls). Обновить до актуального счета — иначе следующий ревьюер запутается.
  • create-pr.ts:71 [consistency] Error message ⚠️ create_pr failed: cannot resolve Forgejo repo не включает issue_number, в отличие от line 74 (⚠️ create_pr failed for issue #${args.issue_number || "?"}: ...). Для консистентности добавить issue number и в первый message — хотя resolveForgejoRepo failure происходит до знания repo, issue_number уже доступен из args.
  • _shared.ts:220-228 [testing] Fail-loud guard для base не имеет прямого unit-теста (вызов callForgejoGh с pr create argv без --base). Guard косвенно покрыт (create-pr всегда передаёт --base), но прямой тест защитил бы от регрессии, если кто-то вызовет runGh(["pr","create",...]) без --base напрямую. Опционально — текущее покрытие достаточное для fix.

Verdict: APPROVE

## Code Review Summary Чистый баг-фикс: `create-pr.ts` не передавал `--base` в Forgejo POST /pulls → 422 `{"message":"[Base]: Required"}`. Корневая причина (условное `...(base ? { base } : {})` опускало поле всегда) устранена: auto-detect `default_branch` через `getRepo()` (эталон — `merge-pr.ts:16-36`) + симметричный fail-loud guard для `base` в `_shared.ts` + обновление 9 success-path тестов. Перевёрнутый assertion в `test_head_explicit_override` правильно фиксирует регрессию (старый тест закреплял баг с ложным комментарием "Forgejo defaults to default_branch"). ### Positives - **Симметрия с merge-pr.ts**: паттерн `resolveForgejoRepo → getRepo → JSON.parse(stdout).default_branch` (create-pr.ts:70-76) точно повторяет merge-pr.ts:16-36 — консистентность между tool'ами. - **Симметричный guard**: fail-loud для `base` (_shared.ts:220-228) зеркалит существующий guard для `head` (_shared.ts:205-216) — одинаковая структура, понятные сообщения. - **Тесты обновлены корректно**: `REPO_OK_RESPONSE` первым в массиве (getRepo вызывается ДО POST), `fetch_calls` 1→2, assertions перенесены `fetch_calls[0]`→`fetch_calls[1]`, `git_calls` 2→3, `remote_calls` 1→2. Validation-fail пути обоснованно не тронуты (падают до fetch/git). - **Перевёрнутый assertion**: `assert "base" not in body` → `assert body.get("base") == "main"` — правильно ловит фикс. Обновлённый docstring объясняет почему `base` обязателен. - **PR body**: `## Watch out` документирует overhead 2 remote lookups (acceptable, ~ms), `## Pending` предлагает оптимизацию (передать pre-resolved `full` в `runGh`). Оба осмысленны. - **Коммиты**: 3 коммита, все conventional format, ≤72 chars, English, без period. Логичная декомпозиция (fix create-pr → fix _shared guard → test). ### Suggestions (info, not blocking) - **test_create_pr_tool.py:18** [docs] Module docstring устарел: `create-pr.ts makes 1 spawnSync call (gh pr create) on the success path.` Теперь success path делает 3 spawnSync (rev-parse + 2 remote lookup) + 2 fetch (getRepo + POST /pulls). Обновить до актуального счета — иначе следующий ревьюер запутается. - **create-pr.ts:71** [consistency] Error message `⚠️ create_pr failed: cannot resolve Forgejo repo` не включает `issue_number`, в отличие от line 74 (`⚠️ create_pr failed for issue #${args.issue_number || "?"}: ...`). Для консистентности добавить issue number и в первый message — хотя resolveForgejoRepo failure происходит до знания repo, issue_number уже доступен из args. - **_shared.ts:220-228** [testing] Fail-loud guard для `base` не имеет прямого unit-теста (вызов `callForgejoGh` с `pr create` argv без `--base`). Guard косвенно покрыт (create-pr всегда передаёт `--base`), но прямой тест защитил бы от регрессии, если кто-то вызовет `runGh(["pr","create",...])` без `--base` напрямую. Опционально — текущее покрытие достаточное для fix. ### Verdict: APPROVE
slaid098 deleted branch fix/create-pr/forgejo-base-field 2026-08-08 22:08:42 +03:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
slaid098/opencode-config!33
No description provided.