"""Tests for opt-in subagent worktree isolation (tools/subagent_worktree.py). Inspired by Muse Code's --subagent-worktree-isolation (clean-room implementation from documented behavior). """ import os import subprocess import sys import tempfile import shutil import unittest from pathlib import Path from unittest import mock sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) from tools import subagent_worktree as sw # noqa: E402 def _git(args, cwd): return subprocess.run( ["git", *args], cwd=cwd, capture_output=True, text=True, check=True ) def _make_repo(root: Path) -> Path: repo = root / "repo" repo.mkdir() _git(["init", "-q"], repo) _git(["config", "user.email", "test@test"], repo) _git(["config", "user.name", "Test"], repo) (repo / "README.md").write_text("hello\n", encoding="utf-8") _git(["add", "-A"], repo) _git(["commit", "-q", "-m", "seed"], repo) return repo class SubagentWorktreeTests(unittest.TestCase): def setUp(self): self.tmp = Path(tempfile.mkdtemp(prefix="hermes-sw-test-")) self.addCleanup(shutil.rmtree, self.tmp, True) # ── resolve_repo_root ────────────────────────────────────────────── def test_resolve_repo_root_in_repo(self): repo = _make_repo(self.tmp) sub = repo / "src" sub.mkdir() root = sw.resolve_repo_root(str(sub)) assert root is not None self.assertEqual(Path(root).resolve(), repo.resolve()) def test_resolve_repo_root_non_git(self): plain = self.tmp / "plain" plain.mkdir() self.assertIsNone(sw.resolve_repo_root(str(plain))) def test_resolve_repo_root_none_and_missing(self): self.assertIsNone(sw.resolve_repo_root(None)) self.assertIsNone(sw.resolve_repo_root(str(self.tmp / "nope"))) # ── create_subagent_worktree ─────────────────────────────────────── def test_create_in_non_git_returns_none(self): plain = self.tmp / "plain" plain.mkdir() self.assertIsNone(sw.create_subagent_worktree(str(plain), "abc")) def test_create_makes_isolated_worktree(self): repo = _make_repo(self.tmp) info = sw.create_subagent_worktree(str(repo), "abc123") self.assertIsNotNone(info) assert info is not None self.assertTrue(os.path.isdir(info["path"])) self.assertIn(".worktrees", info["path"]) self.assertEqual(info["branch"], "hermes-subagent/subagent-abc123") self.assertTrue(info["base_commit"]) # Worktree carries the committed file self.assertTrue((Path(info["path"]) / "README.md").exists()) # .gitignore gained the .worktrees/ entry self.assertIn( ".worktrees/", (repo / ".gitignore").read_text(encoding="utf-8").splitlines() ) # A write in the worktree does not touch the parent checkout (Path(info["path"]) / "child.txt").write_text("x", encoding="utf-8") self.assertFalse((repo / "child.txt").exists()) def test_create_unborn_head_returns_none(self): repo = self.tmp / "empty" repo.mkdir() _git(["init", "-q"], repo) self.assertIsNone(sw.create_subagent_worktree(str(repo), "abc")) # ── finalize_subagent_worktree ───────────────────────────────────── def test_finalize_prunes_clean_worktree(self): repo = _make_repo(self.tmp) info = sw.create_subagent_worktree(str(repo), "clean1") assert info is not None payload = sw.finalize_subagent_worktree(info) self.assertTrue(payload["pruned"]) self.assertEqual(payload["commits"], 0) self.assertFalse(payload["dirty"]) self.assertFalse(os.path.isdir(info["path"])) # branch deleted too branches = _git(["branch", "--list", info["branch"]], repo).stdout self.assertEqual(branches.strip(), "") def test_finalize_keeps_worktree_with_commits(self): repo = _make_repo(self.tmp) info = sw.create_subagent_worktree(str(repo), "work1") assert info is not None wt = Path(info["path"]) (wt / "feature.py").write_text("print('hi')\n", encoding="utf-8") _git(["add", "-A"], wt) _git(["config", "user.email", "child@test"], wt) _git(["config", "user.name", "Child"], wt) _git(["commit", "-q", "-m", "child work"], wt) payload = sw.finalize_subagent_worktree(info) self.assertFalse(payload["pruned"]) self.assertEqual(payload["commits"], 1) self.assertTrue(os.path.isdir(info["path"])) def test_finalize_keeps_dirty_worktree(self): repo = _make_repo(self.tmp) info = sw.create_subagent_worktree(str(repo), "dirty1") assert info is not None (Path(info["path"]) / "wip.txt").write_text("uncommitted\n", encoding="utf-8") payload = sw.finalize_subagent_worktree(info) self.assertFalse(payload["pruned"]) self.assertTrue(payload["dirty"]) self.assertTrue(os.path.isdir(info["path"])) def test_finalize_missing_path_reports_pruned(self): payload = sw.finalize_subagent_worktree( {"path": str(self.tmp / "gone"), "branch": "b", "repo_root": "", "base_commit": ""} ) self.assertTrue(payload["pruned"]) # ── local_backend_active ─────────────────────────────────────────── def test_local_backend_active_local(self): with mock.patch( "hermes_cli.config.load_config_readonly", return_value={"terminal": {"backend": "local"}}, ): self.assertTrue(sw.local_backend_active()) def test_local_backend_active_docker(self): with mock.patch( "hermes_cli.config.load_config_readonly", return_value={"terminal": {"backend": "docker"}}, ): self.assertFalse(sw.local_backend_active()) # ── context note ─────────────────────────────────────────────────── def test_context_note_names_path_and_branch(self): note = sw.build_worktree_context_note( {"path": "/x/wt", "branch": "hermes-subagent/subagent-1"} ) self.assertIn("/x/wt", note) self.assertIn("hermes-subagent/subagent-1", note) self.assertIn("WORKTREE ISOLATION", note) class DelegationConfigGateTests(unittest.TestCase): def test_worktree_isolation_default_off(self): from tools import delegate_tool with mock.patch.object(delegate_tool, "_load_config", return_value={}): self.assertFalse(delegate_tool._get_worktree_isolation()) def test_worktree_isolation_enabled(self): from tools import delegate_tool with mock.patch.object( delegate_tool, "_load_config", return_value={"worktree_isolation": True}, ): self.assertTrue(delegate_tool._get_worktree_isolation()) if __name__ == "__main__": unittest.main()