Author mutation tools now resolve workspace via explicit worktree_path,
env bindings, or the active author issue lock — never silent fallback to
the control checkout/master. Missing configured bindings fail closed with
operator recovery; create_issue and create_issue_comment agree.
Recovered onto 0568f44 from preserved candidate cbf56ccd (AUTHOR_RECOVERY).
LLM_LOCK_ID=author-618-recovery-508eb3162d01-1784569919
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
159 lines
5.9 KiB
Python
159 lines
5.9 KiB
Python
"""Tests for branches-only author mutation worktree guard (#274)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
import author_mutation_worktree as amw # noqa: E402
|
|
|
|
|
|
class TestPathUnderBranches(unittest.TestCase):
|
|
ROOT = "/repo/Gitea-Tools"
|
|
|
|
def test_branches_subdirectory_passes(self):
|
|
self.assertTrue(
|
|
amw.is_path_under_branches(f"{self.ROOT}/branches/issue-274", self.ROOT)
|
|
)
|
|
|
|
def test_control_checkout_fails(self):
|
|
self.assertFalse(amw.is_path_under_branches(self.ROOT, self.ROOT))
|
|
|
|
def test_sibling_directory_fails(self):
|
|
self.assertFalse(
|
|
amw.is_path_under_branches("/repo/other-checkout", self.ROOT)
|
|
)
|
|
|
|
|
|
class TestAssessAuthorMutationWorktree(unittest.TestCase):
|
|
ROOT = "/repo/Gitea-Tools"
|
|
|
|
def test_branches_worktree_passes(self):
|
|
result = amw.assess_author_mutation_worktree(
|
|
workspace_path=f"{self.ROOT}/branches/issue-274",
|
|
project_root=self.ROOT,
|
|
current_branch="feat/issue-274-branches-only-worktrees",
|
|
)
|
|
self.assertTrue(result["proven"])
|
|
self.assertFalse(result["block"])
|
|
|
|
def test_control_checkout_blocks(self):
|
|
result = amw.assess_author_mutation_worktree(
|
|
workspace_path=self.ROOT,
|
|
project_root=self.ROOT,
|
|
current_branch="master",
|
|
)
|
|
self.assertFalse(result["proven"])
|
|
self.assertTrue(result["block"])
|
|
self.assertIn("control checkout", result["reasons"][0])
|
|
|
|
def test_drifted_control_branch_blocks(self):
|
|
result = amw.assess_author_mutation_worktree(
|
|
workspace_path=self.ROOT,
|
|
project_root=self.ROOT,
|
|
current_branch="feat/some-task",
|
|
)
|
|
self.assertTrue(result["block"])
|
|
self.assertTrue(any("drift" in r for r in result["reasons"]))
|
|
|
|
def test_branches_worktree_as_project_root_passes(self):
|
|
"""MCP launched from a branches/ worktree uses that path as PROJECT_ROOT."""
|
|
worktree_root = f"{self.ROOT}/branches/issue-274"
|
|
result = amw.assess_author_mutation_worktree(
|
|
workspace_path=worktree_root,
|
|
project_root=worktree_root,
|
|
current_branch="feat/issue-274-branches-only-worktrees",
|
|
)
|
|
self.assertTrue(result["proven"])
|
|
self.assertFalse(result["block"])
|
|
|
|
|
|
class TestPreflightIntegration(unittest.TestCase):
|
|
def test_verify_preflight_blocks_control_checkout_with_test_porcelain(self):
|
|
import mcp_server
|
|
|
|
mcp_server._preflight_whoami_called = True
|
|
mcp_server._preflight_capability_called = True
|
|
mcp_server._preflight_resolved_role = "author"
|
|
mcp_server._preflight_resolved_task = None
|
|
control_root = "/repo/Gitea-Tools"
|
|
with mock.patch.object(mcp_server, "PROJECT_ROOT", control_root):
|
|
with mock.patch.object(mcp_server, "_enforce_root_checkout_guard"):
|
|
with mock.patch(
|
|
"gitea_mcp_server._session_author_lock_worktree",
|
|
return_value=None,
|
|
):
|
|
with mock.patch(
|
|
"gitea_auth.get_profile",
|
|
return_value={"profile_name": "gitea-author"},
|
|
):
|
|
with mock.patch.dict(
|
|
"os.environ",
|
|
{"GITEA_TEST_PORCELAIN": ""},
|
|
clear=False,
|
|
):
|
|
with self.assertRaises(RuntimeError) as ctx:
|
|
mcp_server.verify_preflight_purity()
|
|
blob = str(ctx.exception)
|
|
self.assertTrue(
|
|
"Branches-only mutation guard" in blob
|
|
or "control checkout" in blob
|
|
or "author worktree" in blob.lower()
|
|
or "#618" in blob,
|
|
msg=blob,
|
|
)
|
|
|
|
def test_verify_preflight_allows_branches_worktree(self):
|
|
import mcp_server
|
|
|
|
mcp_server._preflight_whoami_called = True
|
|
mcp_server._preflight_capability_called = True
|
|
mcp_server._preflight_resolved_role = "author"
|
|
mcp_server._preflight_resolved_task = None
|
|
worktree = "/repo/Gitea-Tools/branches/issue-274"
|
|
healthy_ctx = {
|
|
"workspace_path": worktree,
|
|
"workspace_binding_source": "worktree_path argument",
|
|
"workspace_role_kind": "author",
|
|
"ignored_bindings": [],
|
|
"process_project_root": "/repo/Gitea-Tools",
|
|
"canonical_repo_root": "/repo/Gitea-Tools",
|
|
"roots_aligned": True,
|
|
"bound_worktree_missing": False,
|
|
"author_worktree_block": False,
|
|
"author_worktree_reasons": [],
|
|
"author_worktree_resolution": {
|
|
"proven": True,
|
|
"block": False,
|
|
"bound_worktree_missing": False,
|
|
"workspace_path": worktree,
|
|
"workspace_binding_source": "worktree_path argument",
|
|
"reasons": [],
|
|
},
|
|
"path_exists": True,
|
|
"in_git_worktree_list": True,
|
|
"inspected_git_root": worktree,
|
|
}
|
|
with mock.patch.object(mcp_server, "_enforce_root_checkout_guard"):
|
|
with mock.patch.object(
|
|
mcp_server, "_session_author_lock_worktree", return_value=None
|
|
):
|
|
with mock.patch.object(
|
|
mcp_server,
|
|
"_resolve_namespace_mutation_context",
|
|
return_value=healthy_ctx,
|
|
):
|
|
with mock.patch.dict(
|
|
"os.environ",
|
|
{"GITEA_TEST_PORCELAIN": ""},
|
|
clear=False,
|
|
):
|
|
mcp_server.verify_preflight_purity(worktree_path=worktree)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main() |