Skip to content

test(prune): Guard the property that makes a force-delete safe - #77

Merged
epilande merged 1 commit into
mainfrom
test/a1-regression-guard
Jul 29, 2026
Merged

test(prune): Guard the property that makes a force-delete safe#77
epilande merged 1 commit into
mainfrom
test/a1-regression-guard

Conversation

@epilande

Copy link
Copy Markdown
Owner

Follow-up to #76.

Why

pr-merged is the only prune reason that force-deletes a branch with git branch -D. What makes that safe is a single property: a PR only counts as this branch's PR when its headRefOid equals the local branch tip. If that check ever weakens, this is the failure it produces:

  1. A PR is squash-merged. The user keeps working in the worktree and commits.
  2. Those commits exist nowhere else — not on any remote, and not in the squash.
  3. The run deletes the directory, runs git worktree prune (dropping the per-worktree reflog), and force-deletes the branch, taking all three recovery handles at once.

During review of #76 that scenario was proposed as unfixed and a git rev-list --count <branch> --not --remotes == 0 gate was suggested three separate times. The gate is measurably wrong — it reports 0 while the remote branch still exists and 1 after auto-delete-on-merge, so with delete_branch_on_merge enabled it would refuse to clean up exactly the squash-merged branches the feature exists for. The real protection is the identity check, one function away, which is easy to miss from the deletion site. #76 documents that in a comment; this pins it as tests.

What is asserted

Three tests, at both levels:

  • selectPRForBranch: a merged PR whose headRefOid is not the local tip returns nothing. Carries an explicit "do not delete as a duplicate" note, since it resembles the branch-name-reuse case but exists for a different reason.
  • End to end through scanRepo against a real git fixture, driving the real ghPRStateLookup through a stub gh on PATH. Injecting a lookupPR stub instead would bypass the exact code under test, because scanRepo does no identity filtering itself.
  • Through a real runPrune, asserting the outcome that actually matters: branchDeleted === false, the branch still listed, and the unpublished commit still resolvable by SHA.

Verification

The guards were confirmed capable of failing, not merely passing: removing the tip check turns all three red, including the one proving the commit is destroyed. Source restored afterwards.

bun run typecheck clean; bun test 3402 pass / 0 fail (main's 3399 plus these three). Test-only change, +144 lines in one file.

`pr-merged` is the only reason that force-deletes a branch, and nothing in
`branchDeletionFor` gates that force. What makes it safe is one line in
`selectPRForBranch`: a merged PR only counts when its head SHA equals the
local tip, so a branch carrying commits made after the merge stops matching
and falls through to a reason that uses `-d`.

Three review lanes independently proposed adding a
`rev-list --not --remotes` gate on the force, and that proposal was measured
wrong (the count is > 0 for every correctly squash-merged branch, so it would
refuse the feature's main path). A comment now records why it is absent; this
pins the property that comment depends on, so it cannot regress quietly.

Guarded at both levels: at `selectPRForBranch`, and end to end through
`scanRepo` driving the real `ghPRStateLookup` against a stub `gh`, because
`scanRepo` does no identity filtering of its own and a `lookupPR` stub would
bypass the code under test. The end-to-end pair asserts the outcome that
actually matters: after a real prune run the branch survives and the
unpublished commit is still reachable.

Verified to fail: removing the tip check makes all three go red, including
the one that proves the commit would be destroyed.
@epilande
epilande merged commit 4a2cf37 into main Jul 29, 2026
1 check passed
@epilande
epilande deleted the test/a1-regression-guard branch July 29, 2026 13:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant