Fix review 646 blockers B1-B7 for cross-repo canonical roots (#973)

This commit is contained in:
2026-07-29 14:01:17 -04:00
parent 6a53308473
commit 7978008709
4 changed files with 380 additions and 80 deletions
+51 -11
View File
@@ -500,10 +500,38 @@ def _resolve_preflight_workspace_path(worktree_path: str | None = None) -> str:
return workspace return workspace
def _resolve_namespace_mutation_context(worktree_path: str | None = None) -> dict: def _resolve_expected_repository_slug(
remote: str | None = None,
org: str | None = None,
repo: str | None = None,
) -> str | None:
"""Resolve expected repository slug from parameters, session context, or remote URL."""
if org and repo:
return session_ctx.format_repository_slug(org, repo)
bound = session_ctx.get_session_context() or {}
b_org = bound.get("org")
b_repo = bound.get("repository")
if b_org and b_repo:
return session_ctx.format_repository_slug(b_org, b_repo)
eff_remote = remote or bound.get("remote") or _effective_remote()
parsed = remote_repo_guard.parse_org_repo_from_remote_url(
_local_git_remote_url(eff_remote)
)
if parsed:
return session_ctx.format_repository_slug(parsed[0], parsed[1])
return None
def _resolve_namespace_mutation_context(
worktree_path: str | None = None,
remote: str | None = None,
) -> dict:
"""Canonical namespace workspace + repository root for guards (#460/#510/#706/#618).""" """Canonical namespace workspace + repository root for guards (#460/#510/#706/#618)."""
role = _effective_workspace_role() role = _effective_workspace_role()
configured_root, _source = _configured_canonical_root() configured_root, _source = _configured_canonical_root()
bound = session_ctx.get_session_context() or {}
eff_remote = remote or bound.get("remote") or _effective_remote()
expected_slug = _resolve_expected_repository_slug(eff_remote)
return nwb.resolve_namespace_mutation_context( return nwb.resolve_namespace_mutation_context(
role_kind=role, role_kind=role,
worktree_path=worktree_path, worktree_path=worktree_path,
@@ -516,9 +544,12 @@ def _resolve_namespace_mutation_context(worktree_path: str | None = None) -> dic
), ),
profile_name=get_profile().get("profile_name"), profile_name=get_profile().get("profile_name"),
configured_canonical_root=configured_root, configured_canonical_root=configured_root,
expected_slug=expected_slug,
remote=eff_remote,
) )
def _resolve_author_mutation_context(worktree_path: str | None = None) -> dict: def _resolve_author_mutation_context(worktree_path: str | None = None) -> dict:
"""Backward-compatible alias for namespace workspace context.""" """Backward-compatible alias for namespace workspace context."""
return _resolve_namespace_mutation_context(worktree_path) return _resolve_namespace_mutation_context(worktree_path)
@@ -620,6 +651,9 @@ def _preflight_workspace_details(worktree_path: str | None, dirty_files: list[st
inspected_root = _get_git_root(workspace) inspected_root = _get_git_root(workspace)
process_root = ctx["process_project_root"] process_root = ctx["process_project_root"]
canonical_root = ctx["canonical_repo_root"] canonical_root = ctx["canonical_repo_root"]
crr_assessment = ctx.get("canonical_root_assessment") or {}
resolved_slug = crr_assessment.get("resolved_slug")
expected_slug = ctx.get("expected_slug")
active_root = os.path.realpath(inspected_root or workspace) active_root = os.path.realpath(inspected_root or workspace)
if active_root == canonical_root: if active_root == canonical_root:
dirty_scope = "control checkout" dirty_scope = "control checkout"
@@ -649,6 +683,10 @@ def _preflight_workspace_details(worktree_path: str | None, dirty_files: list[st
"workspace_healthy": not bool( "workspace_healthy": not bool(
ctx.get("bound_worktree_missing") or ctx.get("author_worktree_block") ctx.get("bound_worktree_missing") or ctx.get("author_worktree_block")
), ),
"canonical_root_assessment": crr_assessment,
"expected_repository_slug": expected_slug,
"observed_repository_identity": resolved_slug,
"worktree_registration_result": ctx.get("in_git_worktree_list"),
} }
if not ctx["roots_aligned"]: if not ctx["roots_aligned"]:
details["workspace_root_mismatch"] = ( details["workspace_root_mismatch"] = (
@@ -658,6 +696,7 @@ def _preflight_workspace_details(worktree_path: str | None, dirty_files: list[st
return details return details
def _format_preflight_workspace_details(details: dict) -> str: def _format_preflight_workspace_details(details: dict) -> str:
parts = [ parts = [
f"MCP server process root: {details.get('mcp_server_process_root')}", f"MCP server process root: {details.get('mcp_server_process_root')}",
@@ -1866,6 +1905,9 @@ def _verify_role_mutation_workspace(
# back to the install checkout and validated Gitea-Tools/branches/ instead # back to the install checkout and validated Gitea-Tools/branches/ instead
# of the target repository the namespace is actually bound to. # of the target repository the namespace is actually bound to.
_configured_root, _configured_source = _configured_canonical_root() _configured_root, _configured_source = _configured_canonical_root()
bound = session_ctx.get_session_context() or {}
eff_remote = remote or bound.get("remote") or _effective_remote()
expected_slug = _resolve_expected_repository_slug(eff_remote, org=org, repo=repo)
assessment = nwb.assess_namespace_mutation_workspace( assessment = nwb.assess_namespace_mutation_workspace(
role_kind=role, role_kind=role,
worktree_path=worktree_path, worktree_path=worktree_path,
@@ -1880,6 +1922,8 @@ def _verify_role_mutation_workspace(
profile_name=get_profile().get("profile_name"), profile_name=get_profile().get("profile_name"),
current_branch=git_state.get("current_branch"), current_branch=git_state.get("current_branch"),
configured_canonical_root=_configured_root, configured_canonical_root=_configured_root,
expected_slug=expected_slug,
remote=eff_remote,
) )
if assessment["block"]: if assessment["block"]:
raise RuntimeError( raise RuntimeError(
@@ -3414,7 +3458,7 @@ def cleanup_in_progress_for_pr(
# ── Helpers ─────────────────────────────────────────────────────────────────── # ── Helpers ───────────────────────────────────────────────────────────────────
def _effective_remote(remote: str) -> str: def _effective_remote(remote: str = "dadeschools") -> str:
"""If remote is the default ('dadeschools') but the active profile base_url maps to a known remote, use that remote instead.""" """If remote is the default ('dadeschools') but the active profile base_url maps to a known remote, use that remote instead."""
try: try:
profile = get_profile() profile = get_profile()
@@ -15277,22 +15321,17 @@ def _current_runtime_mode_report(refresh: bool = False) -> dict:
workspace_root = None workspace_root = None
aligned = None aligned = None
canonical_root = None canonical_root = None
resolved_slug = None
try: try:
ctx = _resolve_namespace_mutation_context(None) ctx = _resolve_namespace_mutation_context(None)
workspace_root = ctx.get("workspace_path") workspace_root = ctx.get("workspace_path")
canonical_root = ctx.get("canonical_repo_root") canonical_root = ctx.get("canonical_repo_root")
# Alignment keeps its established repository-level meaning (#615 F1):
# does this namespace target the repository the process is installed in,
# i.e. canonical_repo_root == process_project_root. It is deliberately
# NOT path equality between the task workspace and the process root --
# the global worktree rule requires task work to live in a branches/
# worktree, so that comparison would classify every correctly bound
# author, reviewer, and merger session as unsafe.
aligned = ctx.get("roots_aligned") aligned = ctx.get("roots_aligned")
crr_assessment = ctx.get("canonical_root_assessment") or {}
resolved_slug = crr_assessment.get("resolved_slug")
except Exception: except Exception:
# An unresolvable binding is reported as unknown alignment, never as
# proof of alignment.
aligned = None aligned = None
resolved_slug = None
try: try:
profile_name = get_profile()["profile_name"] profile_name = get_profile()["profile_name"]
except Exception: except Exception:
@@ -15305,6 +15344,7 @@ def _current_runtime_mode_report(refresh: bool = False) -> dict:
dirty_files=dirty_files, dirty_files=dirty_files,
active_task_workspace=workspace_root, active_task_workspace=workspace_root,
canonical_repository_root=canonical_root, canonical_repository_root=canonical_root,
repository_slug=resolved_slug,
workspace_roots_aligned=aligned, workspace_roots_aligned=aligned,
profile=profile_name, profile=profile_name,
declared_mode=stable_control_runtime.declared_runtime_mode(), declared_mode=stable_control_runtime.declared_runtime_mode(),
+54 -40
View File
@@ -189,23 +189,23 @@ def verify_git_common_directory_membership(
real_common = os.path.realpath(common_dir) real_common = os.path.realpath(common_dir)
canonical_git = os.path.realpath(os.path.join(real_root, ".git")) canonical_git = os.path.realpath(os.path.join(real_root, ".git"))
if real_common in (canonical_git, real_root) or os.path.dirname(real_common) == real_root: if real_common in (canonical_git, real_root):
return True, None return True, None
return ( return (
False, False,
f"workspace '{real_ws}' git common directory '{real_common}' does not match " f"workspace '{real_ws}' git common directory '{real_common}' does not match "
f"canonical repository root '{real_root}' (.git at '{canonical_git}')" f"canonical repository root '{real_root}' (.git at '{canonical_git}')"
) )
except Exception: else:
pass return (
False,
if amw.is_path_under_branches(real_ws, real_root): f"workspace '{real_ws}' is not a valid git repository or git rev-parse failed"
return True, None )
except Exception as exc:
return ( return (
False, False,
f"workspace '{real_ws}' does not belong to canonical repository root '{real_root}'" f"failed to inspect git common directory for workspace '{real_ws}': {exc}"
) )
def resolve_namespace_mutation_context( def resolve_namespace_mutation_context(
@@ -313,6 +313,8 @@ def resolve_namespace_mutation_context(
"canonical_repo_root": canonical_root, "canonical_repo_root": canonical_root,
"roots_aligned": roots_aligned, "roots_aligned": roots_aligned,
"canonical_root_assessment": crr_assessment, "canonical_root_assessment": crr_assessment,
"expected_slug": expected_slug,
"remote": remote,
} }
if durable is not None: if durable is not None:
result["author_worktree_resolution"] = durable result["author_worktree_resolution"] = durable
@@ -324,6 +326,15 @@ def resolve_namespace_mutation_context(
result["author_worktree_reasons"] = list(durable.get("reasons") or []) result["author_worktree_reasons"] = list(durable.get("reasons") or [])
result["author_worktree_blocker_kind"] = durable.get("blocker_kind") result["author_worktree_blocker_kind"] = durable.get("blocker_kind")
result["operator_recovery"] = durable.get("operator_recovery") result["operator_recovery"] = durable.get("operator_recovery")
else:
path_exists = os.path.exists(workspace)
result["path_exists"] = path_exists
result["in_git_worktree_list"] = (
amw.path_in_git_worktree_list(workspace, canonical_root)
if path_exists
else False
)
result["bound_worktree_missing"] = not path_exists
return result return result
@@ -456,8 +467,8 @@ def format_namespace_workspace_binding_error(
def assess_namespace_mutation_workspace( def assess_namespace_mutation_workspace(
*, *,
role_kind: str, role_kind: str,
worktree_path: str | None, worktree_path: str | None = None,
worktree: str | None, worktree: str | None = None,
process_project_root: str, process_project_root: str,
env: dict[str, str] | os._Environ | None = None, env: dict[str, str] | os._Environ | None = None,
session_lease_worktree: str | None = None, session_lease_worktree: str | None = None,
@@ -509,14 +520,16 @@ def assess_namespace_mutation_workspace(
if crr_reasons: if crr_reasons:
reasons.extend(crr_reasons) reasons.extend(crr_reasons)
if not ctx.get("roots_aligned"): path_exists = ctx.get("path_exists")
if not crr_reasons: if path_exists is None:
reasons.append( path_exists = os.path.exists(mutation_workspace)
f"unsafe_process_root_workspace_alignment: process root '{process_root}' "
f"and canonical root '{ctx['canonical_repo_root']}' disagree"
)
if os.path.exists(mutation_workspace): if not path_exists:
if role != "author":
reasons.append(
f"{role} mutation blocked: configured workspace directory '{mutation_workspace}' does not exist (nonexistent worktree)"
)
else:
valid_common, common_err = verify_git_common_directory_membership( valid_common, common_err = verify_git_common_directory_membership(
mutation_workspace, ctx["canonical_repo_root"] mutation_workspace, ctx["canonical_repo_root"]
) )
@@ -539,26 +552,27 @@ def assess_namespace_mutation_workspace(
) )
if branches["block"]: if branches["block"]:
reasons.extend(branches["reasons"]) reasons.extend(branches["reasons"])
elif ( elif role in {"reviewer", "merger"}:
role == "reviewer" if mutation_workspace == process_root and not amw.is_path_under_branches(mutation_workspace, ctx["canonical_repo_root"]):
and mutation_workspace == process_root reasons.append(
and not amw.is_path_under_branches(mutation_workspace, ctx["canonical_repo_root"]) f"{role} mutation blocked: workspace is the stable control checkout; "
): f"create or reconnect to a session-owned worktree under branches/ "
reasons.append( f"or set {ROLE_WORKTREE_ENVS.get(role, ACTIVE_WORKTREE_ENV)} / "
f"{role} mutation blocked: workspace is the stable control checkout; " f"{ACTIVE_WORKTREE_ENV}"
f"create or reconnect to a session-owned worktree under branches/ " )
f"or set {ROLE_WORKTREE_ENVS.get(role, ACTIVE_WORKTREE_ENV)} / " elif mutation_workspace != process_root and not amw.is_path_under_branches(mutation_workspace, ctx["canonical_repo_root"]):
f"{ACTIVE_WORKTREE_ENV}" reasons.append(
) f"{role} mutation blocked: workspace '{mutation_workspace}' is not under "
elif ( f"'{ctx['canonical_repo_root']}/branches/'"
role in {"reviewer", "merger"} )
and mutation_workspace != process_root
and not amw.is_path_under_branches(mutation_workspace, ctx["canonical_repo_root"]) if path_exists and amw.is_path_under_branches(mutation_workspace, ctx["canonical_repo_root"]):
): in_list = ctx.get("in_git_worktree_list")
reasons.append( if in_list is False:
f"{role} mutation blocked: workspace '{mutation_workspace}' is not under " reasons.append(
f"'{ctx['canonical_repo_root']}/branches/'" f"{role} mutation blocked: workspace '{mutation_workspace}' is under branches/ "
) f"but is not registered in git worktree list for '{ctx['canonical_repo_root']}'"
)
block = bool(reasons) block = bool(reasons)
return { return {
@@ -6,12 +6,15 @@ import os
import shutil import shutil
import tempfile import tempfile
import unittest import unittest
from unittest.mock import patch, MagicMock
from pathlib import Path from pathlib import Path
import subprocess import subprocess
import gitea_config import gitea_config
import namespace_workspace_binding as nwb import namespace_workspace_binding as nwb
import canonical_repository_root as crr import canonical_repository_root as crr
import stable_control_runtime
import gitea_mcp_server as mcp_server
class TestIssue973RecognizedEnvKeys(unittest.TestCase): class TestIssue973RecognizedEnvKeys(unittest.TestCase):
@@ -53,29 +56,44 @@ class TestIssue973CrossRepoCanonicalRoots(unittest.TestCase):
self.install_root = os.path.join(self.tmp_dir, "Gitea-Tools") self.install_root = os.path.join(self.tmp_dir, "Gitea-Tools")
os.makedirs(self.install_root) os.makedirs(self.install_root)
subprocess.run(["git", "init", "-b", "master"], cwd=self.install_root, check=True) subprocess.run(["git", "init", "-b", "master"], cwd=self.install_root, check=True)
subprocess.run(["git", "config", "user.email", "[email protected]"], cwd=self.install_root, check=True)
subprocess.run(["git", "config", "user.name", "Test User"], cwd=self.install_root, check=True)
with open(os.path.join(self.install_root, "README.md"), "w") as f:
f.write("install\n")
subprocess.run(["git", "add", "README.md"], cwd=self.install_root, check=True)
subprocess.run(["git", "commit", "-m", "initial"], cwd=self.install_root, check=True)
# Create simulated target repository root # Create simulated target repository root
self.target_root = os.path.join(self.tmp_dir, "mcp-control-plane") self.target_root = os.path.join(self.tmp_dir, "mcp-control-plane")
os.makedirs(self.target_root) os.makedirs(self.target_root)
subprocess.run(["git", "init", "-b", "master"], cwd=self.target_root, check=True) subprocess.run(["git", "init", "-b", "master"], cwd=self.target_root, check=True)
subprocess.run(["git", "config", "user.email", "[email protected]"], cwd=self.target_root, check=True)
subprocess.run(["git", "config", "user.name", "Test User"], cwd=self.target_root, check=True)
with open(os.path.join(self.target_root, "README.md"), "w") as f:
f.write("target\n")
subprocess.run(["git", "add", "README.md"], cwd=self.target_root, check=True)
subprocess.run(["git", "commit", "-m", "initial"], cwd=self.target_root, check=True)
# Create branches/ directory and a valid worktree in target repository # Create branches/ directory and a valid registered worktree in target repository
self.target_branches = os.path.join(self.target_root, "branches") self.target_branches = os.path.join(self.target_root, "branches")
os.makedirs(self.target_branches)
self.target_worktree = os.path.join(self.target_branches, "rev-pr-99") self.target_worktree = os.path.join(self.target_branches, "rev-pr-99")
os.makedirs(self.target_worktree) subprocess.run(["git", "worktree", "add", "-b", "rev-pr-99", self.target_worktree], cwd=self.target_root, check=True)
# Create git common dir linking target_worktree to target_root
dot_git_file = os.path.join(self.target_worktree, ".git")
git_dir_target = os.path.join(self.target_root, ".git", "worktrees", "rev-pr-99")
os.makedirs(git_dir_target, exist_ok=True)
with open(dot_git_file, "w") as f:
f.write(f"gitdir: {git_dir_target}\n")
with open(os.path.join(git_dir_target, "commondir"), "w") as f:
f.write("../../..\n")
def tearDown(self): def tearDown(self):
self._tmp.cleanup() self._tmp.cleanup()
def test_valid_same_repository_configuration(self):
ctx = nwb.resolve_namespace_mutation_context(
role_kind="reviewer",
worktree_path=None,
process_project_root=self.install_root,
env={},
configured_canonical_root=None,
)
self.assertEqual(ctx["canonical_repo_root"], self.install_root)
self.assertTrue(ctx["roots_aligned"])
self.assertTrue(ctx["canonical_root_assessment"]["proven"])
def test_valid_cross_repo_canonical_root(self): def test_valid_cross_repo_canonical_root(self):
ctx = nwb.resolve_namespace_mutation_context( ctx = nwb.resolve_namespace_mutation_context(
role_kind="reviewer", role_kind="reviewer",
@@ -88,6 +106,54 @@ class TestIssue973CrossRepoCanonicalRoots(unittest.TestCase):
self.assertTrue(ctx["roots_aligned"]) self.assertTrue(ctx["roots_aligned"])
self.assertTrue(ctx["canonical_root_assessment"]["proven"]) self.assertTrue(ctx["canonical_root_assessment"]["proven"])
def test_expected_repository_identity_match(self):
with patch("canonical_repository_root.repository_identity_slug", return_value="Scaled-Tech-Consulting/Gitea-Tools"):
assessment = crr.assess_canonical_repository_root(
configured_value=self.target_root,
source="test",
expected_slug="Scaled-Tech-Consulting/Gitea-Tools",
process_project_root=self.install_root,
)
self.assertTrue(assessment["proven"])
self.assertFalse(assessment["block"])
def test_foreign_repository_identity_mismatch(self):
with patch("canonical_repository_root.repository_identity_slug", return_value="Someone-Else/Evil-Repo"):
assessment = crr.assess_canonical_repository_root(
configured_value=self.target_root,
source="test",
expected_slug="Scaled-Tech-Consulting/Gitea-Tools",
process_project_root=self.install_root,
)
self.assertFalse(assessment["proven"])
self.assertTrue(assessment["block"])
self.assertTrue(any("identity mismatch" in r for r in assessment["reasons"]))
def test_native_repository_binding_mismatch(self):
with patch("canonical_repository_root.repository_identity_slug", return_value="Foreign/Repo"):
ctx = nwb.resolve_namespace_mutation_context(
role_kind="reviewer",
worktree_path=self.target_worktree,
process_project_root=self.install_root,
env={},
configured_canonical_root=self.target_root,
expected_slug="Scaled-Tech-Consulting/Gitea-Tools",
)
self.assertFalse(ctx["roots_aligned"])
self.assertFalse(ctx["canonical_root_assessment"]["proven"])
self.assertTrue(any("identity mismatch" in r for r in ctx["canonical_root_assessment"]["reasons"]))
def test_missing_canonical_root(self):
ctx = nwb.resolve_namespace_mutation_context(
role_kind="author",
worktree_path=None,
process_project_root=self.install_root,
env={},
configured_canonical_root="",
)
self.assertEqual(ctx["canonical_repo_root"], self.install_root)
self.assertTrue(ctx["roots_aligned"])
def test_nonexistent_configured_canonical_root(self): def test_nonexistent_configured_canonical_root(self):
nonexistent = os.path.join(self.tmp_dir, "nonexistent-repo") nonexistent = os.path.join(self.tmp_dir, "nonexistent-repo")
ctx = nwb.resolve_namespace_mutation_context( ctx = nwb.resolve_namespace_mutation_context(
@@ -122,38 +188,213 @@ class TestIssue973CrossRepoCanonicalRoots(unittest.TestCase):
self.assertTrue(valid, err) self.assertTrue(valid, err)
self.assertIsNone(err) self.assertIsNone(err)
def test_git_common_directory_membership_foreign_repo_blocks(self): def test_foreign_git_common_directory(self):
# Foreign worktree created under install_root # Foreign worktree created under install_root
foreign_wt = os.path.join(self.install_root, "branches", "foreign-wt") install_branches = os.path.join(self.install_root, "branches")
os.makedirs(foreign_wt) foreign_wt = os.path.join(install_branches, "foreign-wt")
dot_git_file = os.path.join(foreign_wt, ".git") subprocess.run(["git", "worktree", "add", "-b", "foreign-wt", foreign_wt], cwd=self.install_root, check=True)
git_dir_install = os.path.join(self.install_root, ".git", "worktrees", "foreign-wt")
os.makedirs(git_dir_install, exist_ok=True)
with open(dot_git_file, "w") as f:
f.write(f"gitdir: {git_dir_install}\n")
with open(os.path.join(git_dir_install, "commondir"), "w") as f:
f.write("../../..\n")
valid, err = nwb.verify_git_common_directory_membership( valid, err = nwb.verify_git_common_directory_membership(
foreign_wt, self.target_root foreign_wt, self.target_root
) )
self.assertFalse(valid) self.assertFalse(valid)
self.assertIn("does not belong", err) self.assertIn("does not match", err)
def test_reviewer_worktree_outside_branches_blocks(self): def test_normalized_path_aliases(self):
outside_wt = os.path.join(self.target_root, "outside_branches_wt") alias_path = self.target_worktree + "/../rev-pr-99/./"
os.makedirs(outside_wt) valid, err = nwb.verify_git_common_directory_membership(
alias_path, self.target_root
)
self.assertTrue(valid, err)
def test_safe_symlink_identity(self):
link_path = os.path.join(self.target_branches, "symlink-rev-99")
try:
os.symlink(self.target_worktree, link_path)
valid, err = nwb.verify_git_common_directory_membership(
link_path, self.target_root
)
self.assertTrue(valid, err)
finally:
if os.path.exists(link_path):
os.unlink(link_path)
def test_symlink_escape_or_foreign_alias(self):
outside_dir = os.path.join(self.tmp_dir, "outside-target")
os.makedirs(outside_dir)
link_escape = os.path.join(self.target_branches, "escape-link")
try:
os.symlink(outside_dir, link_escape)
assessment = nwb.assess_namespace_mutation_workspace(
role_kind="reviewer",
worktree_path=link_escape,
worktree=None,
process_project_root=self.install_root,
env={},
configured_canonical_root=self.target_root,
)
self.assertTrue(assessment["block"])
finally:
if os.path.exists(link_escape):
os.unlink(link_escape)
def test_reviewer_worktree_registered_and_valid(self):
assessment = nwb.assess_namespace_mutation_workspace( assessment = nwb.assess_namespace_mutation_workspace(
role_kind="reviewer", role_kind="reviewer",
worktree_path=outside_wt, worktree_path=self.target_worktree,
worktree=None,
process_project_root=self.install_root,
env={},
configured_canonical_root=self.target_root,
)
self.assertFalse(assessment["block"])
def test_reviewer_worktree_unregistered_blocks(self):
unreg_wt = os.path.join(self.target_branches, "unregistered-reviewer")
os.makedirs(unreg_wt)
assessment = nwb.assess_namespace_mutation_workspace(
role_kind="reviewer",
worktree_path=unreg_wt,
worktree=None, worktree=None,
process_project_root=self.install_root, process_project_root=self.install_root,
env={}, env={},
configured_canonical_root=self.target_root, configured_canonical_root=self.target_root,
) )
self.assertTrue(assessment["block"]) self.assertTrue(assessment["block"])
self.assertTrue(any("is not under" in r for r in assessment["reasons"])) self.assertTrue(any("is not registered in git worktree list" in r for r in assessment["reasons"]))
def test_merger_worktree_registered_and_valid(self):
assessment = nwb.assess_namespace_mutation_workspace(
role_kind="merger",
worktree_path=self.target_worktree,
worktree=None,
process_project_root=self.install_root,
env={},
configured_canonical_root=self.target_root,
)
self.assertFalse(assessment["block"])
def test_merger_worktree_unregistered_blocks(self):
unreg_wt = os.path.join(self.target_branches, "unregistered-merger")
os.makedirs(unreg_wt)
assessment = nwb.assess_namespace_mutation_workspace(
role_kind="merger",
worktree_path=unreg_wt,
worktree=None,
process_project_root=self.install_root,
env={},
configured_canonical_root=self.target_root,
)
self.assertTrue(assessment["block"])
self.assertTrue(any("is not registered in git worktree list" in r for r in assessment["reasons"]))
def test_reviewer_or_merger_worktree_outside_branches_blocks(self):
outside_wt = os.path.join(self.target_root, "outside_branches_wt")
os.makedirs(outside_wt)
for r_kind in ("reviewer", "merger"):
assessment = nwb.assess_namespace_mutation_workspace(
role_kind=r_kind,
worktree_path=outside_wt,
worktree=None,
process_project_root=self.install_root,
env={},
configured_canonical_root=self.target_root,
)
self.assertTrue(assessment["block"])
self.assertTrue(any("is not under" in r for r in assessment["reasons"]))
def test_nonexistent_reviewer_or_merger_worktree_blocks(self):
nonexistent_wt = os.path.join(self.target_branches, "nonexistent-wt")
for r_kind in ("reviewer", "merger"):
assessment = nwb.assess_namespace_mutation_workspace(
role_kind=r_kind,
worktree_path=nonexistent_wt,
worktree=None,
process_project_root=self.install_root,
env={},
configured_canonical_root=self.target_root,
)
self.assertTrue(assessment["block"])
self.assertTrue(any("does not exist" in r for r in assessment["reasons"]))
def test_safe_and_unsafe_mutation_alignment_outcomes(self):
# Safe alignment (same repo)
report_safe = stable_control_runtime.build_runtime_report(
process_root=self.install_root,
checkout_branch="master",
runtime_head="abcdef123456",
active_task_workspace=self.install_root,
canonical_repository_root=self.install_root,
workspace_roots_aligned=True,
)
gate_safe = stable_control_runtime.assess_runtime_mutation_gate(report_safe)
self.assertFalse(gate_safe["block"])
# Unsafe alignment
report_unsafe = stable_control_runtime.build_runtime_report(
process_root=self.install_root,
checkout_branch="master",
runtime_head="abcdef123456",
active_task_workspace=self.target_worktree,
canonical_repository_root=self.target_root,
workspace_roots_aligned=False,
)
gate_unsafe = stable_control_runtime.assess_runtime_mutation_gate(report_unsafe)
self.assertTrue(gate_unsafe["block"])
def test_reviewer_lease_lifecycle_production_path(self):
"""B5: Automated regression for reviewer lease acquire and release through production path."""
mock_whoami = {
"authenticated": True,
"username": "sysadmin",
"remote": "prgs",
"profile": {
"profile_name": "prgs-reviewer",
"role": "reviewer",
"role_kind": "reviewer",
"allowed_operations": ["gitea.read", "gitea.pr.comment", "gitea.pr.approve", "gitea.pr.request_changes"],
"forbidden_operations": [],
},
}
def mock_api_request(method, url, auth=None, json_data=None):
if method == "GET":
return {"number": 99, "head": {"sha": "abc1234"}, "state": "open", "merged": False, "merged_at": None}
elif method == "POST":
return {"id": 9999, "body": (json_data or {}).get("body", "")}
return {}
with patch.object(mcp_server, "gitea_whoami", return_value=mock_whoami), \
patch.object(mcp_server, "get_profile", return_value=mock_whoami["profile"]), \
patch.object(mcp_server, "_effective_workspace_role", return_value="reviewer"), \
patch.object(mcp_server, "_configured_canonical_root", return_value=(self.target_root, "env")), \
patch.object(mcp_server, "_reviewer_session_worktree", return_value=self.target_worktree), \
patch.object(mcp_server, "_auth", return_value="token mock-token"), \
patch.object(mcp_server, "_fetch_pr_comments", return_value=[]), \
patch.object(mcp_server, "api_request", side_effect=mock_api_request), \
patch("reviewer_pr_lease.assess_acquire_lease", return_value={"acquire_allowed": True, "reasons": [], "lease_body": "<!-- LEASE -->"}), \
patch("reviewer_pr_lease.find_active_reviewer_lease", return_value={"session_id": "sid-123", "reviewer": "sysadmin"}), \
patch("reviewer_pr_lease.get_session_lease", return_value={"session_id": "sid-123", "reviewer": "sysadmin"}), \
patch("reviewer_pr_lease.clear_session_lease") as mock_clear:
acq_res = mcp_server.gitea_acquire_reviewer_pr_lease(
pr_number=99,
remote="prgs",
worktree=self.target_worktree,
org="Scaled-Tech-Consulting",
repo="Gitea-Tools",
)
self.assertTrue(acq_res.get("success"), acq_res)
rel_res = mcp_server.gitea_release_reviewer_pr_lease(
pr_number=99,
worktree=self.target_worktree,
remote="prgs",
org="Scaled-Tech-Consulting",
repo="Gitea-Tools",
)
self.assertTrue(rel_res.get("success"), rel_res)
mock_clear.assert_called_once()
if __name__ == "__main__": if __name__ == "__main__":
+7 -2
View File
@@ -245,10 +245,15 @@ class TestNamespaceWorkspaceIntegration(unittest.TestCase):
def test_pr487_style_merge_binds_clean_merger_workspace( def test_pr487_style_merge_binds_clean_merger_workspace(
self, _exists, _isdir, mock_run self, _exists, _isdir, mock_run
): ):
mock_run.return_value = MagicMock(returncode=0, stdout=f"{CONTROL_ROOT}/.git\n") def mock_git(cmd, *args, **kwargs):
if "rev-parse" in cmd:
return MagicMock(returncode=0, stdout=f"{CONTROL_ROOT}/.git\n")
return MagicMock(returncode=0, stdout=f"worktree {CONTROL_ROOT}\nworktree {MERGER_CLEAN}\n")
mock_run.side_effect = mock_git
os.environ[nwb.AUTHOR_WORKTREE_ENV] = AUTHOR_DIRTY os.environ[nwb.AUTHOR_WORKTREE_ENV] = AUTHOR_DIRTY
os.environ[nwb.MERGER_WORKTREE_ENV] = MERGER_CLEAN
srv._preflight_resolved_role = "reviewer" srv._preflight_resolved_role = "reviewer"
with mock.patch.object(srv, "PROJECT_ROOT", MCP_PROCESS_ROOT): with mock.patch.object(srv, "PROJECT_ROOT", MCP_PROCESS_ROOT):
with mock.patch("gitea_mcp_server.get_profile", return_value=self._merger_profile()): with mock.patch("gitea_mcp_server.get_profile", return_value=self._merger_profile()):
resolved = srv._verify_role_mutation_workspace("prgs") resolved = srv._verify_role_mutation_workspace("prgs")
self.assertEqual(resolved, os.path.realpath(MCP_PROCESS_ROOT)) self.assertEqual(resolved, os.path.realpath(MERGER_CLEAN))