fix(run-hardening): don't respawn-loop when a merge target branch is gone from origin

When an integration (cell/root) branch is deleted from origin — e.g. after a sibling cell->root merge, stranding a late straggler leaf — pr_merge's post-merge _sync_target_branch ran 'git fetch origin <branch>' (check=True) and raised 'couldn't find remote ref'. That surfaced as a retryable SERVICE_ERROR, so complete() re-blocked the task and respawn-looped the PM on an already-landed merge (observed live on cell branches feature/backend/31ae12fc--0e49e04e and 7aeee245--dcfe9fc2, blocking be-pm's complete() 5+ cycles).

pr_merge reaches the post-merge sync only after the authoritative GitHub merge has already succeeded (_merge_with_retry raises otherwise), so refreshing the local workspace copy of the target branch is cosmetic. Route it through a new _sync_target_branch_best_effort that logs and returns None instead of raising. merge_pull_request (CEO path) keeps the strict sync — its target is the default branch, which always exists on origin.
This commit is contained in:
Renn F
2026-06-25 07:44:07 +02:00
parent e1651edeb3
commit a51c3d312a
2 changed files with 77 additions and 1 deletions
+44
View File
@@ -885,3 +885,47 @@ async def test_sync_target_branch_raises_when_origin_also_missing() -> None:
await svc._sync_target_branch(
Path("/tmp/ws"), "feature/backend/parent", "token"
)
# ---------------------------------------------------------------------------
# _sync_target_branch_best_effort — post-merge local sync must never re-block
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_sync_target_branch_best_effort_swallows_missing_branch() -> None:
"""A post-merge local sync of a target branch gone from origin must NOT raise.
``pr_merge`` / ``merge_pull_request`` reach the post-merge sync only after the
authoritative GitHub merge already succeeded, so updating the local workspace
copy of the (possibly-deleted) target branch is cosmetic. Letting it raise
re-blocks the just-merged task and respawn-loops the PM — the live failure
where an integration branch deleted from origin made ``complete()`` loop on
``fetch origin <cell-branch> — fatal: couldn't find remote ref``.
"""
svc = _service()
_bind(
svc,
"_sync_target_branch",
AsyncMock(
side_effect=GitCommandError(
"fetch origin feature/backend/31ae12fc--0e49e04e",
"fatal: couldn't find remote ref feature/backend/31ae12fc--0e49e04e",
)
),
)
result = await svc._sync_target_branch_best_effort(
Path("/tmp/ws"), "feature/backend/31ae12fc--0e49e04e", "token"
)
assert result is None
@pytest.mark.asyncio
async def test_sync_target_branch_best_effort_returns_sha_on_success() -> None:
"""On success it passes the synced tip sha straight through."""
svc = _service()
_bind(svc, "_sync_target_branch", AsyncMock(return_value="merged-tip-sha"))
result = await svc._sync_target_branch_best_effort(
Path("/tmp/ws"), "feature/backend/parent", "token"
)
assert result == "merged-tip-sha"