"""Complete canonical-root consumption for cross-repository MCP namespaces. #706 introduced an immutable, configured ``canonical_repository_root`` and routed the #274 filesystem guards and the session repository *slug* through it. Three consumption paths were left behind, and this module drives each one through its production entry point: * **A — remote initialization.** ``gitea_get_runtime_context`` never normalized its ``remote`` argument, while ``gitea_whoami`` did. A fresh process whose first native call is the runtime-context path therefore pinned the ``dadeschools`` argument default instead of the profile's configured remote, and — because first-bind is first-write-wins — no later correct call could repair it. * **C — reconciler branch deletion.** The delete-branch repository-binding guard derived its expected slug from ``_workspace_repository_slug``, which reads the git remote of the *installation* checkout (always Gitea-Tools), rather than from the session's configured canonical root. * **D — parity evidence.** ``gitea_assess_master_parity`` proves Gitea-Tools *server implementation* parity only, which is intentional. It carried no target-repository dimension at all, so a cross-repository namespace had no evidence that its target checkout was current. The existing ``startup_head``/``current_head`` semantics are preserved unchanged and the target-repository assessment is reported under separately labelled fields. Groups B and E assert *intentional* behaviour (the #274 guards already consume the canonical root; the configuration surface already validates a repository-specific namespace) so that a regression in either is caught. Real git repositories are used throughout; no network calls are made. """ from __future__ import annotations import json import os import subprocess import sys import tempfile import unittest from pathlib import Path from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) import canonical_repository_root as crr # noqa: E402 import gitea_config # noqa: E402 import gitea_mcp_server as srv # noqa: E402 import master_parity_gate # noqa: E402 import mcp_server # noqa: E402 import namespace_workspace_binding as nwb # noqa: E402 import session_context_binding as session_ctx # noqa: E402 INSTALL_ORG = "Scaled-Tech-Consulting" INSTALL_REPO = "Gitea-Tools" INSTALL_SLUG = f"{INSTALL_ORG}/{INSTALL_REPO}" INSTALL_URL = f"https://gitea.prgs.cc/{INSTALL_SLUG}.git" TARGET_ORG = "Scaled-Tech-Consulting" TARGET_REPO = "mcp-control-plane" TARGET_SLUG = f"{TARGET_ORG}/{TARGET_REPO}" TARGET_URL = f"https://gitea.prgs.cc/{TARGET_SLUG}.git" def _git(cwd: str, *args: str) -> str: res = subprocess.run( ["git", "-C", cwd, *args], capture_output=True, text=True, check=True ) return res.stdout.strip() def _init_repo(path: Path, remote_url: str, *, remote_name: str = "origin") -> str: path.mkdir(parents=True, exist_ok=True) _git(str(path), "init", "-q") _git(str(path), "config", "user.email", "t@example.com") _git(str(path), "config", "user.name", "Test") _git(str(path), "remote", "add", remote_name, remote_url) (path / "README.md").write_text("seed\n") _git(str(path), "add", "README.md") _git(str(path), "commit", "-q", "-m", "seed") return os.path.realpath(str(path)) # --------------------------------------------------------------------------- # Shared profile/config construction # --------------------------------------------------------------------------- _AUTHOR_OPS = [ "gitea.read", "gitea.issue.create", "gitea.issue.comment", "gitea.pr.create", "gitea.pr.comment", "gitea.branch.create", "gitea.branch.push", "gitea.repo.commit", ] _AUTHOR_FORBIDDEN = [ "gitea.pr.approve", "gitea.pr.merge", "gitea.pr.request_changes", ] # A profile that may approve or merge must forbid authoring (gitea_config's # reviewer-identity deadlock rule). _REVIEWER_OPS = [ "gitea.read", "gitea.pr.approve", "gitea.pr.request_changes", "gitea.pr.comment", "gitea.issue.comment", ] _REVIEWER_FORBIDDEN = ["gitea.pr.create", "gitea.branch.push", "gitea.pr.merge"] _MERGER_OPS = ["gitea.read", "gitea.pr.merge", "gitea.pr.comment"] _MERGER_FORBIDDEN = [ "gitea.pr.create", "gitea.branch.push", "gitea.pr.approve", "gitea.pr.request_changes", ] _RECONCILER_OPS = [ "gitea.read", "gitea.pr.close", "gitea.pr.comment", "gitea.branch.delete", ] _RECONCILER_FORBIDDEN = [ "gitea.pr.create", "gitea.branch.push", "gitea.pr.approve", "gitea.pr.merge", "gitea.pr.request_changes", ] _ROLE_MATRIX = { "author": (_AUTHOR_OPS, _AUTHOR_FORBIDDEN), "reviewer": (_REVIEWER_OPS, _REVIEWER_FORBIDDEN), "merger": (_MERGER_OPS, _MERGER_FORBIDDEN), "reconciler": (_RECONCILER_OPS, _RECONCILER_FORBIDDEN), } def _profile( role: str, *, canonical_root: str | None = None, allowed_repositories: list[str] | None = None, username: str = "jcwalker3", ) -> dict: allowed, forbidden = _ROLE_MATRIX[role] profile = { "enabled": True, "context": "prgs", "role": role, "username": username, "base_url": "https://gitea.prgs.cc", "auth": {"type": "env", "name": f"GITEA_TOKEN_PRGS_{role.upper()}"}, "allowed_operations": list(allowed), "forbidden_operations": list(forbidden), "execution_profile": f"prgs-{role}", } if canonical_root is not None: profile["canonical_repository_root"] = canonical_root if allowed_repositories is not None: profile["allowed_repositories"] = list(allowed_repositories) return profile def _config(profiles: dict) -> dict: return { "version": 2, "rules": {"allow_runtime_switching": True}, "contexts": { "prgs": { "enabled": True, "gitea": {"enabled": True, "base_url": "https://gitea.prgs.cc"}, }, "mdcps": { "enabled": True, "gitea": { "enabled": True, "base_url": "https://gitea.dadeschools.net", }, }, }, "profiles": profiles, } class _ServerHarness(unittest.TestCase): """Temp profiles.json + pinned install remote + no network.""" def setUp(self): self._dir = tempfile.TemporaryDirectory() self.tmp = self._dir.name self.config_path = os.path.join(self.tmp, "profiles.json") session_ctx._reset_session_context_for_testing() gitea_config._active_profile_override = None mcp_server._IDENTITY_CACHE.clear() srv._MUTATION_AUTHORITY = None # The install checkout's remote is Gitea-Tools regardless of the # developer's layout; pin it so "install-derived" is deterministic. self._remote_url = patch.object( srv, "_local_git_remote_url", side_effect=self._install_remote_url ) self._remote_url.start() def tearDown(self): self._remote_url.stop() session_ctx._reset_session_context_for_testing() gitea_config._active_profile_override = None mcp_server._IDENTITY_CACHE.clear() srv._MUTATION_AUTHORITY = None self._dir.cleanup() def _install_remote_url(self, remote_name): return INSTALL_URL if remote_name in ("prgs", "origin") else None def _write_config(self, profiles: dict) -> None: with open(self.config_path, "w", encoding="utf-8") as fh: fh.write(json.dumps(_config(profiles))) def _env(self, profile_name: str, **extra) -> dict: env = { "GITEA_MCP_CONFIG": self.config_path, "GITEA_MCP_PROFILE": profile_name, "GITEA_TOKEN_PRGS_AUTHOR": "t", "GITEA_TOKEN_PRGS_REVIEWER": "t", "GITEA_TOKEN_PRGS_MERGER": "t", "GITEA_TOKEN_PRGS_RECONCILER": "t", } env.update(extra) return env def _api(self, method, url, header): return { "login": "jcwalker3", "full_name": "Test", "id": 1, "email": "t@example.com", } def _live(self): return patch("gitea_mcp_server.api_request", side_effect=self._api) # =========================================================================== # A. Remote initialization # =========================================================================== class TestRemoteInitializationFirstCall(_ServerHarness): """A prgs namespace must never pin the dadeschools argument default.""" def setUp(self): super().setUp() self._write_config( {"prgs-author": _profile("author", allowed_repositories=[INSTALL_SLUG])} ) def test_runtime_context_first_call_does_not_bind_default_remote(self): """Runtime-context as the FIRST native call, relying on the default.""" with patch.dict(os.environ, self._env("prgs-author"), clear=False), self._live(): srv.gitea_get_runtime_context() ctx = session_ctx.get_session_context() self.assertIsNotNone(ctx, "first call must establish a session binding") self.assertEqual(ctx["remote"], "prgs") self.assertEqual(ctx["host"], "gitea.prgs.cc") self.assertEqual(ctx["org"], INSTALL_ORG) self.assertEqual(ctx["repository"], INSTALL_REPO) def test_runtime_context_first_call_reports_effective_remote(self): with patch.dict(os.environ, self._env("prgs-author"), clear=False), self._live(): result = srv.gitea_get_runtime_context() self.assertEqual(result["remote"], "prgs") def test_whoami_first_call_remains_correct(self): """Control: the already-normalizing path is unchanged.""" with patch.dict(os.environ, self._env("prgs-author"), clear=False), self._live(): srv.gitea_whoami() ctx = session_ctx.get_session_context() self.assertEqual(ctx["remote"], "prgs") self.assertEqual(ctx["host"], "gitea.prgs.cc") def test_explicit_remote_argument_still_honoured(self): """Normalization only fills the default; explicit values are untouched.""" with patch.dict(os.environ, self._env("prgs-author"), clear=False), self._live(): result = srv.gitea_get_runtime_context(remote="prgs") self.assertEqual(result["remote"], "prgs") def test_mdcps_profile_default_is_not_rewritten_to_prgs(self): """A dadeschools-hosted profile keeps the dadeschools remote.""" profile = _profile("author", allowed_repositories=[INSTALL_SLUG]) profile["context"] = "mdcps" profile["base_url"] = "https://gitea.dadeschools.net" self._write_config({"prgs-author": profile}) with patch.dict(os.environ, self._env("prgs-author"), clear=False), self._live(): result = srv.gitea_get_runtime_context() self.assertEqual(result["remote"], "dadeschools") # =========================================================================== # B. Remote/repository guard (intentional behaviour — regression fence) # =========================================================================== class TestCanonicalRootGuardBinding(_ServerHarness): def setUp(self): super().setUp() self.target_root = _init_repo(Path(self.tmp) / "mcp-control-plane", TARGET_URL) self.install_root = _init_repo(Path(self.tmp) / "install", INSTALL_URL) def test_canonical_target_root_resolves_to_target_slug(self): got = crr.assess_canonical_repository_root( configured_value=self.target_root, source="profile.canonical_repository_root", expected_slug=None, process_project_root=self.install_root, remote="prgs", require_binding=True, ) self.assertFalse(got.get("block"), got.get("reasons")) self.assertEqual(got["resolved_slug"], TARGET_SLUG) def test_install_root_identity_remains_gitea_tools(self): got = crr.assess_canonical_repository_root( configured_value=None, source=None, expected_slug=None, process_project_root=self.install_root, remote="prgs", ) self.assertEqual( os.path.realpath(got["canonical_repo_root"]), self.install_root ) def test_guards_validate_against_canonical_target_root(self): ctx = nwb.resolve_namespace_mutation_context( role_kind="author", worktree_path=None, process_project_root=self.install_root, configured_canonical_root=self.target_root, ) self.assertEqual( os.path.realpath(ctx["canonical_repo_root"]), self.target_root ) def test_explicit_coordinates_cannot_override_canonical_binding(self): """Explicit org/repo may confirm, never authorize, a binding.""" confirm = session_ctx.assess_repository_override( requested_org=TARGET_ORG, requested_repo=TARGET_REPO, bound_org=TARGET_ORG, bound_repo=TARGET_REPO, ) self.assertFalse(confirm.get("block")) override = session_ctx.assess_repository_override( requested_org=TARGET_ORG, requested_repo=TARGET_REPO, bound_org=INSTALL_ORG, bound_repo=INSTALL_REPO, ) self.assertTrue(override.get("block")) def test_gitea_tools_rooted_namespace_cannot_reach_target_repository(self): """No canonical root configured → session stays pinned to Gitea-Tools.""" profile = _profile("author", allowed_repositories=[INSTALL_SLUG]) with patch.dict(os.environ, {}, clear=False): os.environ.pop(crr.CANONICAL_ROOT_ENV, None) resolved = srv._trusted_session_repository( profile, "prgs", for_mutation=True ) self.assertEqual(resolved["repository"], INSTALL_REPO) self.assertNotEqual(resolved["repository"], TARGET_REPO) def test_target_rooted_namespace_binds_target_repository(self): profile = _profile( "author", canonical_root=self.target_root, allowed_repositories=[TARGET_SLUG], ) resolved = srv._trusted_session_repository(profile, "prgs", for_mutation=True) self.assertEqual(resolved["org"], TARGET_ORG) self.assertEqual(resolved["repository"], TARGET_REPO) # =========================================================================== # C. Reconciler branch deletion # =========================================================================== class TestDeleteBranchRepositoryBinding(_ServerHarness): """The binding guard must consult the canonical root, not PROJECT_ROOT. No branch is ever deleted here: only the pre-deletion binding guard is exercised. """ def setUp(self): super().setUp() self.target_root = _init_repo(Path(self.tmp) / "mcp-control-plane", TARGET_URL) self._write_config( { "prgs-reconciler": _profile( "reconciler", canonical_root=self.target_root, allowed_repositories=[TARGET_SLUG], ), "gt-reconciler": _profile( "reconciler", allowed_repositories=[INSTALL_SLUG] ), } ) def test_target_rooted_reconciler_validates_target_deletion(self): with patch.dict(os.environ, self._env("prgs-reconciler"), clear=False): block = srv._delete_branch_repository_binding_block( "prgs", org=TARGET_ORG, repo=TARGET_REPO ) self.assertIsNone(block, block) def test_target_rooted_reconciler_fails_closed_for_install_repository(self): with patch.dict(os.environ, self._env("prgs-reconciler"), clear=False): block = srv._delete_branch_repository_binding_block( "prgs", org=INSTALL_ORG, repo=INSTALL_REPO ) self.assertIsNotNone(block) self.assertEqual(block["blocker_kind"], "repository_binding") self.assertFalse(block["performed"]) def test_install_rooted_reconciler_fails_closed_for_target_repository(self): with patch.dict(os.environ, self._env("gt-reconciler"), clear=False): os.environ.pop(crr.CANONICAL_ROOT_ENV, None) block = srv._delete_branch_repository_binding_block( "prgs", org=TARGET_ORG, repo=TARGET_REPO ) self.assertIsNotNone(block) self.assertEqual(block["blocker_kind"], "repository_binding") def test_env_configured_canonical_root_is_honoured(self): env = self._env("gt-reconciler") env[crr.CANONICAL_ROOT_ENV] = self.target_root with patch.dict(os.environ, env, clear=False): allowed = srv._delete_branch_repository_binding_block( "prgs", org=TARGET_ORG, repo=TARGET_REPO ) blocked = srv._delete_branch_repository_binding_block( "prgs", org=INSTALL_ORG, repo=INSTALL_REPO ) self.assertIsNone(allowed, allowed) self.assertIsNotNone(blocked) def test_unresolvable_canonical_root_fails_closed(self): env = self._env("gt-reconciler") env[crr.CANONICAL_ROOT_ENV] = os.path.join(self.tmp, "does-not-exist") with patch.dict(os.environ, env, clear=False): block = srv._delete_branch_repository_binding_block( "prgs", org=TARGET_ORG, repo=TARGET_REPO ) self.assertIsNotNone(block) self.assertEqual(block["blocker_kind"], "repository_binding") def test_no_request_parameter_can_override_canonical_identity(self): """Every explicit coordinate that is not the canonical target is refused; none of them can *establish* the binding.""" # Both repositories share an org, so a wrong *org* case must name a # genuinely different owner to be meaningful. cases = [ (INSTALL_ORG, INSTALL_REPO), (TARGET_ORG, INSTALL_REPO), (TARGET_ORG, "some-other-repo"), ("someone-else", TARGET_REPO), ] with patch.dict(os.environ, self._env("prgs-reconciler"), clear=False): for org, repo in cases: with self.subTest(org=org, repo=repo): block = srv._delete_branch_repository_binding_block( "prgs", org=org, repo=repo ) self.assertIsNotNone(block, f"{org}/{repo} must fail closed") # =========================================================================== # D. Parity semantics # =========================================================================== class TestTargetRepositoryParityAssessment(unittest.TestCase): """Server-implementation parity is preserved; target parity is additive.""" def setUp(self): self._dir = tempfile.TemporaryDirectory() self.tmp = self._dir.name def tearDown(self): self._dir.cleanup() def _target(self) -> str: return _init_repo(Path(self.tmp) / "mcp-control-plane", TARGET_URL) def test_unconfigured_target_is_reported_not_stale(self): got = master_parity_gate.assess_target_repository_parity( canonical_root=None, source=None ) self.assertFalse(got["configured"]) self.assertFalse(got["stale"]) self.assertIsNone(got["canonical_repository_root"]) def test_configured_target_reports_checkout_head_and_slug(self): root = self._target() head = _git(root, "rev-parse", "HEAD") got = master_parity_gate.assess_target_repository_parity( canonical_root=root, source="profile.canonical_repository_root" ) self.assertTrue(got["configured"]) self.assertEqual(got["canonical_repository_root"], root) self.assertEqual(got["checkout_head"], head) self.assertEqual(got["repository_slug"], TARGET_SLUG) def test_target_stale_when_remote_tracking_ref_is_ahead(self): root = self._target() head = _git(root, "rev-parse", "HEAD") # Simulate a fetched remote-tracking ref that has advanced. _git(root, "checkout", "-q", "-b", "advanced") (Path(root) / "next.md").write_text("next\n") _git(root, "add", "next.md") _git(root, "commit", "-q", "-m", "advance") advanced = _git(root, "rev-parse", "HEAD") _git(root, "update-ref", "refs/remotes/origin/master", advanced) _git(root, "checkout", "-q", "--detach", head) got = master_parity_gate.assess_target_repository_parity( canonical_root=root, source="profile.canonical_repository_root" ) self.assertEqual(got["checkout_head"], head) self.assertEqual(got["remote_tracking_head"], advanced) self.assertTrue(got["stale"]) def test_missing_root_is_not_determinable_and_fails_closed(self): got = master_parity_gate.assess_target_repository_parity( canonical_root=os.path.join(self.tmp, "absent"), source="profile.canonical_repository_root", ) self.assertTrue(got["configured"]) self.assertFalse(got["determinable"]) self.assertTrue(got["reasons"]) class TestParityToolEvidenceDimensions(_ServerHarness): def setUp(self): super().setUp() self.target_root = _init_repo(Path(self.tmp) / "mcp-control-plane", TARGET_URL) self._write_config( { "prgs-author": _profile( "author", canonical_root=self.target_root, allowed_repositories=[TARGET_SLUG], ) } ) def test_existing_server_parity_semantics_are_unchanged(self): with patch.dict(os.environ, self._env("prgs-author"), clear=False): result = srv.gitea_assess_master_parity(remote="prgs") # startup_head/current_head keep their Gitea-Tools implementation # meaning and must not be redefined to the target repository. self.assertEqual(result["process_root"], srv.PROJECT_ROOT) self.assertEqual( result["startup_head"], srv._STARTUP_PARITY.get("startup_head") ) self.assertIn("in_parity", result) def test_evidence_distinguishes_server_and_target_dimensions(self): with patch.dict(os.environ, self._env("prgs-author"), clear=False): result = srv.gitea_assess_master_parity(remote="prgs") server = result["server_implementation"] self.assertEqual(server["installation_root"], srv.PROJECT_ROOT) self.assertEqual(server["current_head"], result["current_head"]) self.assertIn("stale", server) target = result["target_repository"] self.assertTrue(target["configured"]) self.assertEqual(target["canonical_repository_root"], self.target_root) self.assertEqual(target["repository_slug"], TARGET_SLUG) self.assertEqual( target["checkout_head"], _git(self.target_root, "rev-parse", "HEAD") ) self.assertIn("stale", target) def test_unconfigured_namespace_reports_target_as_unconfigured(self): self._write_config( {"prgs-author": _profile("author", allowed_repositories=[INSTALL_SLUG])} ) env = self._env("prgs-author") with patch.dict(os.environ, env, clear=False): os.environ.pop(crr.CANONICAL_ROOT_ENV, None) result = srv.gitea_assess_master_parity(remote="prgs") self.assertFalse(result["target_repository"]["configured"]) # =========================================================================== # E. Startup / configuration validation for a candidate namespace set # =========================================================================== class TestCandidateNamespaceConfiguration(_ServerHarness): """Four repository-specific profiles for the target repository.""" def setUp(self): super().setUp() self.target_root = _init_repo(Path(self.tmp) / "mcp-control-plane", TARGET_URL) self.profiles = { f"mcpcp-{role}": _profile( role, canonical_root=self.target_root, allowed_repositories=[TARGET_SLUG], ) for role in ("author", "reviewer", "merger", "reconciler") } self._write_config(self.profiles) def test_configuration_audit_succeeds(self): with patch.dict(os.environ, self._env("mcpcp-author"), clear=False): audit = srv.gitea_audit_config() self.assertTrue(audit.get("configured")) names = {row["name"] for row in audit["profiles"]} self.assertEqual(names, set(self.profiles)) def test_no_inline_credentials_are_present(self): raw = json.loads(Path(self.config_path).read_text()) for name, profile in raw["profiles"].items(): with self.subTest(profile=name): self.assertNotIn("token", profile) self.assertNotIn("password", profile) self.assertNotIn("secret", profile) self.assertIn(profile["auth"]["type"], ("env", "keychain")) def test_bind_time_validation_succeeds_for_target_repository(self): for name, profile in self.profiles.items(): with self.subTest(profile=name): resolved = srv._trusted_session_repository( profile, "prgs", for_mutation=True ) self.assertEqual(resolved["reasons"], []) self.assertEqual(resolved["org"], TARGET_ORG) self.assertEqual(resolved["repository"], TARGET_REPO) def test_wrong_repository_root_fails_closed(self): wrong_root = _init_repo(Path(self.tmp) / "wrong", INSTALL_URL) profile = _profile( "author", canonical_root=wrong_root, allowed_repositories=[TARGET_SLUG] ) resolved = srv._trusted_session_repository(profile, "prgs", for_mutation=True) self.assertIsNone(resolved["repository"]) self.assertTrue(resolved["reasons"]) def test_existing_install_profiles_are_unchanged_in_behaviour(self): profile = _profile("author", allowed_repositories=[INSTALL_SLUG]) resolved = srv._trusted_session_repository(profile, "prgs", for_mutation=True) self.assertEqual(resolved["org"], INSTALL_ORG) self.assertEqual(resolved["repository"], INSTALL_REPO) def _denied(self, profile: dict, operation: str) -> bool: ok, _ = gitea_config.check_operation( operation, profile["allowed_operations"], profile["forbidden_operations"], ) return not ok def test_role_separation_is_enforced(self): expectations = { "author": [ "gitea.pr.approve", "gitea.pr.merge", "gitea.pr.request_changes", ], "reviewer": ["gitea.pr.create", "gitea.branch.push", "gitea.pr.merge"], "merger": [ "gitea.pr.create", "gitea.branch.push", "gitea.pr.approve", "gitea.pr.request_changes", ], "reconciler": [ "gitea.pr.create", "gitea.branch.push", "gitea.pr.approve", "gitea.pr.merge", "gitea.pr.request_changes", ], } for role, denied_ops in expectations.items(): profile = self.profiles[f"mcpcp-{role}"] for op in denied_ops: with self.subTest(role=role, operation=op): self.assertTrue( self._denied(profile, op), f"{role} must not be permitted {op}", ) def test_each_role_retains_its_own_capability(self): permitted = { "author": "gitea.pr.create", "reviewer": "gitea.pr.approve", "merger": "gitea.pr.merge", "reconciler": "gitea.branch.delete", } for role, op in permitted.items(): with self.subTest(role=role, operation=op): self.assertFalse(self._denied(self.profiles[f"mcpcp-{role}"], op)) if __name__ == "__main__": unittest.main()