Files
pkgmgr/tests/integration/test_branch_tree.py
T
Kevin Veen-BirkenbachandClaude Opus 5.5 102c74f1e0 feat(worktree): add worktree up/down and harden branch gc
pkgmgr worktree up and down create and release a branch worktree with its own slot and run the operator-pinned worktree-init and worktree-fini hooks; branch gc removes merged worktrees, now also patch-merged ones (git cherry), and the hooks of each repository run only from worktree_hooks pins of a managed repository whose git directory is its own. Removal walks to the worktree without following a symbolic link and skips paths that are not their own realpath. A bare-name hook program resolves once to a root-controlled file on a root-controlled PATH: root is uid 0 only in the initial user namespace and the overflow uid elsewhere, nosuid mounts never count on the host, and the hook runs the resolved file with only those PATH entries, stdin closed and a timeout over its process group.

🎯 Fix: 93 %
💥 Break: 3 %
💸 Cost: 114 %
⏱️ Time: 129 %
🛡️ Security: 100 %
🏆 Score: 61
🚦 Gate: cost 114 % and time 129 %: accepted by the operator for B (2026-11-07) after skeptic round 6 found no hole and MEDUSA reached 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 11:18:57 +00:00

443 lines
18 KiB
Python

from __future__ import annotations
import io
import json
import os
import shutil
import stat
import subprocess
import tempfile
import time
import unittest
from contextlib import redirect_stdout
from unittest.mock import patch
import yaml
from pkgmgr.actions.branch import gc, tree
from pkgmgr.actions.branch.gc_forge import ForgeFacts
from pkgmgr.cli.commands.branch import handle_branch
from pkgmgr.cli.context import CLIContext
from pkgmgr.cli.parser import create_parser
from pkgmgr.core.git.run import run
GIT_ENV = {
**os.environ,
"GIT_AUTHOR_NAME": "t",
"GIT_AUTHOR_EMAIL": "t@example.org",
"GIT_COMMITTER_NAME": "t",
"GIT_COMMITTER_EMAIL": "t@example.org",
}
VOLATILE = ("generated_at", "scanned_at")
def git(cwd: str, *args: str) -> str:
return subprocess.run(
["git", "-c", "commit.gpgsign=false", *args],
cwd=cwd,
env=GIT_ENV,
check=True,
capture_output=True,
text=True,
).stdout.strip()
def stable(data):
if isinstance(data, dict):
return {
key: stable(value) for key, value in data.items() if key not in VOLATILE
}
if isinstance(data, list):
return [stable(value) for value in data]
return data
class TestBranchTree(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.addCleanup(self._tmp.cleanup)
self.root = os.path.realpath(self._tmp.name)
self.origin = os.path.join(self.root, "origin.git")
self.repo = os.path.join(self.root, "repos", "repo")
self.shared = os.path.join(self.root, "shared-worktrees")
os.makedirs(self.shared)
git(self.root, "init", "--bare", "-b", "main", self.origin)
git(self.root, "init", "-b", "main", self.repo)
git(self.repo, "remote", "add", "origin", self.origin)
self._commit("init")
git(self.repo, "push", "-u", "origin", "main")
git(self.repo, "remote", "set-head", "origin", "main")
self._write(os.path.join(self.repo, ".git", "info"), "exclude", ".worktrees/\n")
self._pushed("merged")
git(self.repo, "merge", "--no-ff", "-m", "merge merged", "merged")
git(self.repo, "push", "origin", "main")
self._pushed("ahead")
git(self.repo, "checkout", "ahead")
self._commit("ahead-local")
git(self.repo, "checkout", "main")
self._pushed("behind", commits=2)
git(self.repo, "branch", "-f", "behind", "behind~1")
self._pushed("gone")
git(self.repo, "push", "origin", "--delete", "gone")
self.changed = os.path.join(self.root, "repos", "repo", ".worktrees", "changed")
git(self.repo, "worktree", "add", "-b", "changed", self.changed, "main")
self._write(self.changed, "staged.txt", "s")
git(self.changed, "add", "staged.txt")
self._write(self.changed, "init.txt", "edited")
self._write(self.changed, "loose.txt", "u")
self.detached = os.path.join(self.shared, "repo", "detached")
git(self.repo, "worktree", "add", "--detach", self.detached, "main")
self.missing = os.path.join(self.root, "wt-missing")
git(self.repo, "worktree", "add", "-b", "missing", self.missing, "main")
shutil.rmtree(self.missing)
self.facts = ForgeFacts(merged_heads={}, open_heads={})
self.tree_file = os.path.join(self.root, "state", "tree", "branch-tree.json")
self.env = patch.dict(
os.environ,
{
"PKGMGR_TREE_FILE": self.tree_file,
"XDG_CACHE_HOME": os.path.join(self.root, "cache"),
},
)
self.env.start()
self.addCleanup(self.env.stop)
self.patches = [
patch.object(gc, "host_pid_namespace", return_value=True),
patch.object(gc, "list_compose_containers", return_value=[]),
patch.object(gc, "_process_cwds", return_value={}),
patch.object(
tree,
"secret_service_token",
return_value=(None, "Secret Service locked"),
),
]
for patcher in self.patches:
patcher.start()
self.addCleanup(patcher.stop)
def _write(self, cwd: str, name: str, text: str) -> None:
with open(os.path.join(cwd, name), "w", encoding="utf-8") as fh:
fh.write(text)
def _commit(self, name: str, cwd: str | None = None) -> None:
self._write(cwd or self.repo, f"{name}.txt", name)
git(cwd or self.repo, "add", f"{name}.txt")
git(cwd or self.repo, "commit", "-m", name)
def _pushed(self, name: str, commits: int = 1) -> None:
git(self.repo, "checkout", "-b", name, "main")
for number in range(commits):
self._commit(f"{name}-{number}")
git(self.repo, "push", "-u", "origin", name)
git(self.repo, "checkout", "main")
def _tree(self, fmt: str = "json", repos: list[str] | None = None) -> str:
out = io.StringIO()
with redirect_stdout(out):
code = tree.branch_tree(
repos or [self.repo], fmt=fmt, output=None, worktrees_root=self.shared
)
self.assertEqual(code, 0)
return out.getvalue()
def _entry(self) -> dict:
data = json.loads(self._tree())
return next(repo for repo in data["repos"] if repo["path"] == self.repo)
def _by_branch(self, entry: dict) -> dict[str, dict]:
return {item["branch"]: item for item in entry["items"] if item["branch"]}
def test_json_carries_repository_branch_and_worktree_fields(self) -> None:
data = json.loads(self._tree())
entry = next(repo for repo in data["repos"] if repo["path"] == self.repo)
branches = self._by_branch(entry)
self.assertIn("generated_at", data)
self.assertEqual(
(entry["remote"], entry["default"], entry["missing"]),
(self.origin, "main", False),
)
self.assertEqual(
(
branches["ahead"]["upstream"],
branches["ahead"]["ahead"],
branches["ahead"]["behind"],
),
("origin/ahead", 1, 0),
)
self.assertEqual(
(branches["behind"]["ahead"], branches["behind"]["behind"]), (0, 1)
)
self.assertTrue(branches["gone"]["gone"])
self.assertFalse(branches["merged"]["gone"])
changed = branches["changed"]
self.assertEqual(
(changed["worktree"], changed["worktree_state"]), (self.changed, "changed")
)
self.assertEqual(
(changed["staged"], changed["unstaged"], changed["untracked"]), (1, 1, 1)
)
self.assertEqual(changed["head"], git(self.repo, "rev-parse", "main"))
self.assertEqual(changed["state"], "dirty")
self.assertEqual(branches["missing"]["worktree_state"], "missing")
self.assertEqual(branches["main"]["worktree_state"], "clean")
detached = [item for item in entry["items"] if item["detached"]]
self.assertEqual(
[(item["worktree"], item["worktree_state"]) for item in detached],
[(self.detached, "clean")],
)
self.assertEqual(branches["merged"]["merged_by"], "ancestor")
self.assertEqual(entry["forge"], "none")
self.assertEqual(
(data["delete_ready"], data["delete_reason"]),
(False, "Secret Service locked"),
)
def test_yaml_prints_the_same_data(self) -> None:
self.assertEqual(
stable(yaml.safe_load(self._tree("yaml"))),
stable(json.loads(self._tree("json"))),
)
def test_tree_and_gc_share_every_state(self) -> None:
out = io.StringIO()
with (
patch.object(gc, "fetch_forge_facts", return_value=ForgeFacts()),
redirect_stdout(out),
):
gc.cleanup(
[self.repo],
scope="all",
apply=False,
as_json=True,
include=[],
exclude=[],
merged_into=[],
sizes=False,
min_age=None,
hooks={},
)
planned = {
(item["branch"], item["worktree"]): (
item["state"],
item["merged_by"],
item["skip"],
)
for item in json.loads(out.getvalue())["repos"][0]["items"]
}
listed = {
(item["branch"], item["worktree"]): (
item["state"],
item["merged_by"],
item["skip"],
)
for item in self._entry()["items"]
}
self.assertEqual(listed, planned)
def test_the_scan_runs_no_hook_fsmonitor_or_local_filter(self) -> None:
marker = os.path.join(self.root, "ran")
script = os.path.join(self.root, "plant.sh")
self._write(self.root, "plant.sh", f"#!/bin/sh\ntouch {marker}\n")
os.chmod(script, stat.S_IRWXU)
hooks = os.path.join(self.root, "hooks")
os.makedirs(hooks)
for hook in ("post-index-change", "reference-transaction", "post-checkout"):
shutil.copy(script, os.path.join(hooks, hook))
git(self.repo, "config", "core.fsmonitor", script)
git(self.repo, "config", "core.hooksPath", hooks)
git(self.repo, "config", "core.pager", script)
git(self.repo, "config", "filter.planted.clean", script)
self._write(self.changed, ".gitattributes", "* filter=planted\n")
with patch("subprocess.run", wraps=subprocess.run) as spy:
changed = self._by_branch(self._entry())["changed"]
self.assertFalse(os.path.exists(marker))
self.assertEqual(
(changed["worktree_state"], changed["staged"]), ("changed", None)
)
self.assertIn("filter.planted.clean", changed["reason"])
scans = [
call
for call in spy.call_args_list
if call.args and call.args[0][:1] == ["git"]
]
self.assertTrue(scans)
for call in scans:
with self.subTest(argv=call.args[0][:6]):
self.assertNotIn("fetch", call.args[0])
self.assertEqual(call.kwargs["env"]["GIT_NO_LAZY_FETCH"], "1")
self.assertIn("core.fsmonitor=false", call.args[0])
def test_a_worktree_with_submodules_is_not_read(self) -> None:
head = git(self.changed, "rev-parse", "HEAD")
git(self.changed, "update-index", "--add", "--cacheinfo", f"160000,{head},sub")
changed = self._by_branch(self._entry())["changed"]
self.assertEqual((changed["state"], changed["staged"]), ("dirty", None))
self.assertIn("submodules", changed["reason"])
def test_the_scan_never_runs_a_planted_gpg_program(self) -> None:
marker = os.path.join(self.root, "verified")
self._write(self.root, "gpg.sh", f"#!/bin/sh\ntouch {marker}\nexit 1\n")
os.chmod(os.path.join(self.root, "gpg.sh"), stat.S_IRWXU)
git(self.repo, "config", "log.showSignature", "true")
git(self.repo, "config", "gpg.program", os.path.join(self.root, "gpg.sh"))
body = git(self.repo, "cat-file", "commit", "HEAD").replace(
"\n\n",
"\ngpgsig -----BEGIN PGP SIGNATURE-----\n \n -----END PGP SIGNATURE-----\n\n",
1,
)
signed = subprocess.run(
["git", "hash-object", "-t", "commit", "-w", "--stdin"],
cwd=self.repo,
input=body,
capture_output=True,
text=True,
check=True,
).stdout.strip()
git(self.repo, "log", "-1", signed)
self.assertTrue(os.path.exists(marker))
os.unlink(marker)
run(["log", "-1", "--format=%ct", signed], cwd=self.repo, scan=True)
self.assertFalse(os.path.exists(marker))
def test_output_is_written_atomically(self) -> None:
target = os.path.join(self.root, "out", "branch-tree.json")
code = tree.branch_tree(
[self.repo], fmt="json", output=target, worktrees_root=self.shared
)
self.assertEqual(code, 0)
with open(target, encoding="utf-8") as fh:
self.assertEqual(json.load(fh)["repos"][0]["path"], self.repo)
self.assertEqual(
sorted(os.listdir(os.path.dirname(target))),
[".branch-tree.json.lock", "branch-tree.json"],
)
def test_lists_every_known_repository_also_plain_and_missing(self) -> None:
plain = os.path.join(self.root, "repos", "plain")
git(self.root, "init", "-b", "main", plain)
self._commit("plain", cwd=plain)
absent = os.path.join(self.root, "repos", "absent")
ctx = CLIContext(
config_merged={},
repositories_base_dir=os.path.join(self.root, "base"),
all_repositories=[
{
"provider": "x",
"account": "y",
"repository": "repo",
"directory": self.repo,
},
{
"provider": "x",
"account": "y",
"repository": "plain",
"directory": plain,
},
{
"provider": "x",
"account": "y",
"repository": "absent",
"directory": absent,
},
],
binaries_dir="",
user_config_path="",
)
args = create_parser("test").parse_args(["branch", "tree", "--format", "json"])
out = io.StringIO()
with (
patch.object(tree, "WORKTREES_ROOT", self.shared),
redirect_stdout(out),
self.assertRaises(SystemExit) as done,
):
handle_branch(args, ctx)
self.assertEqual(done.exception.code, 0)
repos = {repo["path"]: repo for repo in json.loads(out.getvalue())["repos"]}
self.assertEqual(set(repos), {self.repo, plain, absent})
self.assertEqual([item["branch"] for item in repos[plain]["items"]], ["main"])
self.assertEqual((repos[absent]["missing"], repos[absent]["items"]), (True, []))
def test_a_shared_worktree_of_an_unknown_repository_adds_it(self) -> None:
other = os.path.join(self.root, "elsewhere", "other")
git(self.root, "init", "-b", "main", other)
self._commit("other", cwd=other)
orphan = os.path.join(self.shared, "other", "feature")
git(other, "worktree", "add", "-b", "feature", orphan, "main")
repos = {repo["path"]: repo for repo in json.loads(self._tree())["repos"]}
self.assertEqual(
(repos[self.repo]["known"], repos[other]["known"]), (True, False)
)
self.assertIn(orphan, [item["worktree"] for item in repos[other]["items"]])
def test_forge_facts_come_from_the_secret_service_at_most_every_ten_minutes(
self,
) -> None:
git(self.repo, "remote", "set-url", "origin", "https://github.com/me/repo.git")
locked = self._entry()
self.assertEqual(locked["forge"], "unavailable: Secret Service locked")
self.assertEqual(self._by_branch(locked)["merged"]["merged_by"], "ancestor")
self.assertEqual(self._by_branch(locked)["ahead"]["skip"], "forge unavailable")
facts = ForgeFacts(
merged_heads={
git(self.repo, "rev-parse", ref): 9 for ref in ("ahead", "origin/ahead")
},
open_heads={},
)
with (
patch.object(tree, "secret_service_token", return_value=("t0ken", None)),
patch.object(tree, "facts_with_token", return_value=facts) as live,
):
first = self._by_branch(self._entry())
second = self._by_branch(self._entry())
self.assertEqual(live.call_count, 1)
with patch.object(tree, "now", return_value=time.time() + 601):
self._entry()
self.assertEqual(live.call_count, 2)
self.assertEqual(
(first["ahead"]["merged_by"], second["ahead"]["merged_by"]),
("pr#9", "pr#9"),
)
data = json.loads(self._tree())
self.assertEqual(
(data["delete_ready"], data["delete_reason"]),
(False, "Secret Service locked"),
)
self.assertEqual(data["repos"][0]["forge"], "cached")
with patch.object(tree, "secret_service_token", return_value=("t0ken", None)):
self.assertTrue(json.loads(self._tree())["delete_ready"])
def test_an_unwritable_cache_keeps_the_scan_and_names_it(self) -> None:
git(self.repo, "remote", "set-url", "origin", "https://github.com/me/repo.git")
blocker = os.path.join(self.root, "cache-is-a-file")
self._write(self.root, "cache-is-a-file", "")
with (
patch.dict(os.environ, {"XDG_CACHE_HOME": blocker}),
patch.object(tree, "secret_service_token", return_value=("t0ken", None)),
patch.object(tree, "facts_with_token", return_value=ForgeFacts()),
):
entry = self._entry()
self.assertTrue(entry["forge"].startswith("live: cache not written"))
self.assertIn("main", self._by_branch(entry))
def test_branch_actions_rewrite_the_tree_file_when_it_exists(self) -> None:
tree.branch_tree(
[self.repo], fmt="json", output=self.tree_file, worktrees_root=self.shared
)
args = create_parser("test").parse_args(["branch", "open", "fresh"])
cwd = os.getcwd()
os.chdir(self.repo)
self.addCleanup(os.chdir, cwd)
handle_branch(args, ctx=None)
with open(self.tree_file, encoding="utf-8") as fh:
entry = json.load(fh)["repos"][0]
self.assertIn("fresh", [item["branch"] for item in entry["items"]])
if __name__ == "__main__":
unittest.main()