fix(mcp): complete canonical-root consumption for cross-repo namespaces (Closes #739)
#706 routed the #274 filesystem guards and the session repository slug through
the configured canonical_repository_root. Three consumption paths were left
deriving from the Gitea-Tools installation checkout.
F1 — gitea_get_runtime_context did not normalize its `remote` argument, while
gitea_whoami did. On a prgs-hosted namespace whose first native call was the
runtime-context path, the 'dadeschools' argument default was pinned: identity
resolved against the wrong host, and because first-bind is first-write-wins a
later correct gitea_whoami(remote="prgs") could not repair the binding. It now
calls _effective_remote before any host lookup, identity resolution, or session
seeding. Explicit remotes pass through untouched and a dadeschools-hosted
profile still resolves to dadeschools.
F2 — _delete_branch_repository_binding_block derived its expected slug from
_workspace_repository_slug, which reads _local_git_remote_url in PROJECT_ROOT
(always Gitea-Tools). For a cross-repository namespace this inverted the guard:
the genuinely bound target was rejected and Gitea-Tools was accepted. The
canonical-root branch already present in _trusted_session_repository is
extracted as _canonical_repository_slug and shared by both call sites. A
configured-but-unresolvable root now fails closed instead of falling back to
the installation identity; unconfigured namespaces keep the install-derived
default unchanged.
F3 — gitea_assess_master_parity measures Gitea-Tools server implementation
parity only. That is intentional and is preserved: startup_head, current_head,
in_parity, stale, and restart_required keep their existing meaning and values,
and only the server dimension gates mutations. Two separately labelled
dimensions are added — server_implementation (installation root and commit) and
target_repository (canonical root, repository slug, checkout head, last-known
remote master head, staleness) — so cross-repository commissioning evidence can
distinguish them. The target assessment makes no network call: the remote side
is read from the existing remote-tracking ref, and an unfetched target is
reported indeterminate rather than guessed at.
Verified intentional and left unchanged: the #274 workspace-membership,
author-mutation-worktree, and branches-only guards already validate against the
configured canonical root; explicit org/repo may confirm but never authorize a
binding; and the four-role repository-specific configuration surface already
passes audit and bind-time validation with a wrong root failing closed.
Tests: 31 new cases in tests/test_cross_repo_canonical_consumption.py using
real git repositories and no network. Full suite 3298 passed / 6 skipped
against a clean-baseline 3267; the 2 failures
(test_issue_702_review_findings_f1_f6, test_reconciler_supersession_close)
reproduce identically on unmodified master a8d2087 and are pre-existing.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_013jUxaLVLkPHuTQwAzFzztW
This commit is contained in:
@@ -0,0 +1,704 @@
|
||||
"""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", "[email protected]")
|
||||
_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": "[email protected]",
|
||||
}
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user