fix(robomp): prune pool metadata on any failed worktree remove

`remove_workspace` guarded its rmtree+prune fallback on `repo_dir.exists()`.
But a `git worktree remove` that is killed (incl. a 124 timeout) mid-operation
can delete the checkout *before* it clears the pool's worktree registration.
In that window `repo_dir` is already gone, so the exists() guard skipped the
prune and left dangling metadata — the next `git worktree add` for the same
path then failed with "missing but already registered worktree".

Guard the fallback on the remove command's return code instead; prune runs on
any nonzero exit regardless of whether the checkout was already deleted. Added
a regression test for the remove-deleted-checkout-then-died case, which the
existing test (checkout survives) never covered.

Op: correct
Restores: spec:failed-worktree-remove-must-prune-dangling-pool-metadata
This commit is contained in:
metaphorics
2026-07-02 10:24:06 +09:00
parent b7bcc0bbf0
commit dfaa41f6b3
2 changed files with 45 additions and 7 deletions
+9 -7
View File
@@ -958,13 +958,15 @@ class SandboxManager:
repo_dir = ws_root / "repo"
if repo_dir.exists():
pool = self.pool_path(repo)
_safe_run(["git", "worktree", "remove", "--force", str(repo_dir)], cwd=pool)
if repo_dir.exists():
# `git worktree remove` did not clean up (nonzero exit, incl. a
# 124 timeout). Delete the checkout ourselves, then prune the
# pool's now-dangling worktree registration so a later
# `git worktree add` for the same path does not trip on stale
# metadata.
removed = _safe_run(["git", "worktree", "remove", "--force", str(repo_dir)], cwd=pool)
if removed.returncode != 0:
# A failed `git worktree remove` (nonzero exit, incl. a 124
# timeout) may have deleted the checkout but left the pool's
# worktree registration dangling, or vice versa. Delete any
# leftover checkout ourselves, then prune the dangling pool
# registration so a later `git worktree add` for the same path
# does not trip on stale metadata. `repo_dir.exists()` is not a
# reliable proxy: a killed remove can clear the checkout first.
shutil.rmtree(repo_dir, ignore_errors=True)
_safe_run(["git", "worktree", "prune"], cwd=pool)
if ws_root.exists():