Files
computer-playbook/tests/unit/scripts/test_worktree_lib.py
T
kevinveenbirkenbachandClaude Opus 5 4f888ee80f fix(worktree): release branch on pinned metadata
git worktree remove deletes the checkout first and the metadata dir
second. Under the agent sandbox, which holds config.worktree and
commondir as read-only bind mounts, it therefore dies with EBUSY after
the checkout is already gone, aborting make worktree-down with rc 255
even though the branch is in fact free. down.sh now falls back to the
unregistration prune.sh already used (drop gitdir + HEAD, best-effort
rm -rf) and reports any dir the sandbox still pins. That routine moves
into lib.sh so both callers cannot drift apart, and gains a third
outcome for the case where gitdir/HEAD themselves survive: only then is
the branch still claimed, and only then must the teardown fail.

The discriminator for the fallback is whether the checkout survived,
not a match on "Device or resource busy" — the identical half-removed
state arises from EACCES, EPERM and EROFS.

A failing git status probe no longer reads as clean: on a broken .git
link it exits 128 with empty output, which used to satisfy the guard,
tear down the compose stack, and only then die at git worktree remove
with an unexplained 128.

Measured against the unfixed scripts with real fixtures: pinned
metadata dir 255 -> 0, broken .git link 128 -> 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:59:08 +02:00

104 lines
3.8 KiB
Python

"""Contract of the shared worktree metadata helpers.
``worktree_unregister`` is what lets a teardown succeed under the agent sandbox,
which pins some files inside ``.git/worktrees/<id>`` as read-only bind mounts so
the directory cannot be unlinked. Its three outcomes drive both callers: fully
removed, unregistered but pinned, and still registered. The last one is the only
state that keeps a branch claimed, so it must never be reported as success.
"""
from __future__ import annotations
import os
import stat
import subprocess
import tempfile
import unittest
from pathlib import Path
from utils.cache.files import PROJECT_ROOT
LIB = PROJECT_ROOT / "scripts" / "system" / "worktree" / "lib.sh"
def _call(func: str, *args: str) -> subprocess.CompletedProcess:
return subprocess.run(
["bash", "-c", f'source "$1"; shift; {func} "$@"', "bash", str(LIB), *args],
capture_output=True,
text=True,
)
class TestWorktreeMetaDir(unittest.TestCase):
def test_reads_the_gitdir_pointer(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / ".git").write_text("gitdir: /somewhere/.git/worktrees/feat\n")
result = _call("worktree_meta_dir", tmp)
self.assertEqual(result.stdout.strip(), "/somewhere/.git/worktrees/feat")
def test_missing_git_file_yields_nothing(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
result = _call("worktree_meta_dir", tmp)
self.assertEqual(result.stdout.strip(), "")
def test_directory_git_yields_nothing(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / ".git").mkdir()
result = _call("worktree_meta_dir", tmp)
self.assertEqual(result.stdout.strip(), "")
class TestWorktreeUnregister(unittest.TestCase):
def _entry(self, tmp: str) -> Path:
entry = Path(tmp) / "feat"
entry.mkdir()
(entry / "gitdir").write_text("/checkout/.git\n")
(entry / "HEAD").write_text("ref: refs/heads/feat\n")
(entry / "commondir").write_text("../..\n")
return entry
def test_removes_the_whole_entry(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
entry = self._entry(tmp)
result = _call("worktree_unregister", str(entry))
self.assertEqual(result.returncode, 0)
self.assertFalse(entry.exists())
def test_pinned_directory_still_unregisters(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
entry = self._entry(tmp)
parent_mode = os.stat(tmp).st_mode
(entry / "gitdir").unlink()
(entry / "HEAD").unlink()
os.chmod(tmp, parent_mode & ~stat.S_IWUSR)
try:
result = _call("worktree_unregister", str(entry))
finally:
os.chmod(tmp, parent_mode)
self.assertEqual(result.returncode, 1)
self.assertTrue(entry.exists())
def test_undeletable_pointers_report_still_registered(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
entry = self._entry(tmp)
mode = os.stat(entry).st_mode
os.chmod(entry, mode & ~stat.S_IWUSR)
try:
result = _call("worktree_unregister", str(entry))
finally:
os.chmod(entry, mode)
self.assertEqual(result.returncode, 2)
self.assertTrue((entry / "gitdir").exists())
def test_is_idempotent_on_a_half_emptied_entry(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
entry = self._entry(tmp)
(entry / "HEAD").unlink()
result = _call("worktree_unregister", str(entry))
self.assertEqual(result.returncode, 0)
self.assertFalse(entry.exists())
if __name__ == "__main__":
unittest.main()