#706 introduced the immutable `canonical_repository_root` and #739/#740 routed
three consumption paths through it. The paths #740 left behind all shared one
shape: the *filesystem* guards had been migrated to the canonical root, but
*repository identity* still bottomed out in `_local_git_remote_url`, which ran
`git remote get-url` with `cwd=PROJECT_ROOT` — always the Gitea-Tools install
checkout. The two halves of a single assessment therefore described two
different repositories.
For a namespace bound to another repository this inverts the guards rather than
merely weakening them: an operation naming the genuinely bound target is
rejected, while one naming Gitea-Tools is accepted.
Central helper
--------------
Adds `_canonical_local_git_root()` as the single place a target-repository root
is resolved: the configured canonical root when one is declared, else
`PROJECT_ROOT`. A configured-but-invalid root is never silently replaced by the
install checkout. Single-repository behaviour is byte-for-byte unchanged.
Defects fixed
-------------
* `_local_git_remote_url` now runs in the canonical target root, which corrects
every downstream identity consumer at once (`_resolve`, the #530 remote/repo
guard, the anti-stomp org/repo fill, `_workspace_repository_slug`).
* `_verify_role_mutation_workspace` omitted `configured_canonical_root`, so the
#274 branches-only / worktree-membership guards validated
`Gitea-Tools/branches/` instead of the bound target. It now threads the root
exactly as `_resolve_namespace_mutation_context` does.
* `_resolve` derived omitted coordinates by looking a remote up *by name*. A
target checkout commonly names its remote `origin` rather than `prgs`, so the
lookup returned None and the coordinates fell through to the remote-wide
default target — an unrelated repository. It now prefers
`_canonical_repository_slug`, which probes candidate remote names.
* Explicit `org`/`repo` short-circuit the #530 match check, so caller
coordinates bypassed validation entirely. They may now only *confirm* a
canonical binding, never override it, and fail closed in both directions.
* Reconciler ancestry, fetch, worktree-inventory and cleanup call sites, the
author worktree derivation in `gitea_lock_issue`/`gitea_create_pr`, and the
`control_clean` porcelain probe now use the canonical target root.
* `gitea_config`: v2-`environments` silently *dropped* `canonical_repository_root`
during flattening, so such a namespace fell back to the install root — a
fail-open. It is now validated and propagated. The v1 path validates it too,
so all three loaders behave identically.
Preserved as installation-scoped
--------------------------------
Server parity (`master_parity_gate`), workflow/schema/skill loading, self-code
hashing and stale-runtime detection, and `mirror_refs.sh` lookup remain anchored
to `PROJECT_ROOT`. The `server_implementation` vs `target_repository` parity
dimensions from #740 stay separately labelled, and only the server dimension
gates mutations.
Tests
-----
`tests/test_issue_741_canonical_root_consumers.py` (51 tests, 52 subtests) drives
a role-by-operation matrix over author/reviewer/merger/reconciler against:
correct cross-repository target, wrong repository, wrong worktree, explicit
matching and mismatched coordinates, missing/invalid canonical root, immutable
first-bind, and request-override attempts — plus both cross-repository
directions and all three config loaders. Real git repositories, no network, no
branch deletion, no merge.
The module reinstalls the genuine `_local_git_remote_url` per test: an autouse
conftest fixture permanently reassigns it to a working-directory-independent
stub, which is precisely the behaviour under test.
Full suite: 3408 passed, 6 skipped, 2 failed. Both failures
(`test_issue_702_review_findings_f1_f6`, `test_reconciler_supersession_close`)
reproduce identically on clean master c908ed6050 and are pre-existing.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01AYGdWAwuA6UNDc9c3CGipU
769 lines
33 KiB
Python
769 lines
33 KiB
Python
"""Complete PROJECT_ROOT elimination in cross-repository MCP operations (#741).
|
|
|
|
#706 introduced the immutable ``canonical_repository_root``; #739/#740 routed
|
|
three consumption paths through it. This module covers the paths #740 left
|
|
behind, whose shape is uniform: the *filesystem* guards were migrated to the
|
|
canonical root, but *repository identity* still bottomed out in
|
|
``_local_git_remote_url``'s hardcoded ``cwd=PROJECT_ROOT`` — always the
|
|
Gitea-Tools installation checkout.
|
|
|
|
The consequences asserted here:
|
|
|
|
* **Identity inversion.** ``_resolve``, the #530 remote/repo guard and the
|
|
anti-stomp org/repo fill all derived the *target* repository from the
|
|
*install* checkout, so a cross-repository namespace resolved Gitea-Tools
|
|
coordinates while its branch/parity facts came from the target repo.
|
|
* **Guard disagreement.** ``_verify_role_mutation_workspace`` omitted
|
|
``configured_canonical_root``, so the #274 branches-only / worktree-membership
|
|
guards validated ``Gitea-Tools/branches/`` rather than the bound target.
|
|
* **Explicit-coordinate override.** Both-explicit ``org``/``repo`` short-circuit
|
|
``assess_remote_repo_match``, so caller coordinates bypassed validation
|
|
entirely rather than merely *confirming* the binding.
|
|
* **Configuration fail-open.** ``_flatten_identity`` silently dropped
|
|
``canonical_repository_root``, so a v2-``environments`` namespace fell back to
|
|
the install root; the v1 path never validated the field at all.
|
|
|
|
A role-by-operation matrix drives author, reviewer, merger and reconciler
|
|
against: correct cross-repository target, wrong repository, wrong worktree,
|
|
explicit matching coordinates, explicit mismatched coordinates, missing
|
|
canonical root, invalid canonical root, immutable binding after first bind, and
|
|
request-override attempts.
|
|
|
|
Real git repositories are used throughout. No network calls are made, no branch
|
|
is deleted, and no merge is performed.
|
|
"""
|
|
|
|
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 mcp_server # 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"
|
|
|
|
THIRD_REPO = "Timesheet"
|
|
THIRD_SLUG = f"{INSTALL_ORG}/{THIRD_REPO}"
|
|
|
|
# tests/conftest.py installs an autouse fixture
|
|
# (mutation_profile_fixture.install_deterministic_remote_urls) that *permanently
|
|
# reassigns* srv._local_git_remote_url to a stub mapping remote names to fixed
|
|
# URLs. That stub deliberately ignores the working directory, which is exactly
|
|
# the behaviour this module must verify — so these tests would silently assert
|
|
# against the stub rather than production code in a full-suite run. Capture the
|
|
# genuine implementation at import time (before any fixture executes) and
|
|
# reinstall it per test.
|
|
_REAL_LOCAL_GIT_REMOTE_URL = srv._local_git_remote_url
|
|
|
|
|
|
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)
|
|
# Distinct content per repo: identical seed content, author and timestamp
|
|
# otherwise produce byte-identical commits and therefore an identical SHA,
|
|
# which would make the parity-dimension assertions vacuously true.
|
|
(path / "README.md").write_text(f"seed {remote_url}\n")
|
|
_git(str(path), "add", "README.md")
|
|
_git(str(path), "commit", "-q", "-m", f"seed {remote_name}")
|
|
return os.path.realpath(str(path))
|
|
|
|
|
|
_ROLE_OPS = {
|
|
"author": (
|
|
[
|
|
"gitea.read",
|
|
"gitea.issue.create",
|
|
"gitea.issue.comment",
|
|
"gitea.pr.create",
|
|
"gitea.pr.comment",
|
|
"gitea.branch.create",
|
|
"gitea.branch.push",
|
|
"gitea.repo.commit",
|
|
],
|
|
["gitea.pr.approve", "gitea.pr.merge", "gitea.pr.request_changes"],
|
|
),
|
|
"reviewer": (
|
|
[
|
|
"gitea.read",
|
|
"gitea.pr.approve",
|
|
"gitea.pr.request_changes",
|
|
"gitea.pr.comment",
|
|
"gitea.issue.comment",
|
|
],
|
|
["gitea.pr.create", "gitea.branch.push", "gitea.pr.merge"],
|
|
),
|
|
"merger": (
|
|
["gitea.read", "gitea.pr.merge", "gitea.pr.comment"],
|
|
[
|
|
"gitea.pr.create",
|
|
"gitea.branch.push",
|
|
"gitea.pr.approve",
|
|
"gitea.pr.request_changes",
|
|
],
|
|
),
|
|
"reconciler": (
|
|
["gitea.read", "gitea.pr.close", "gitea.pr.comment", "gitea.branch.delete"],
|
|
[
|
|
"gitea.pr.create",
|
|
"gitea.branch.push",
|
|
"gitea.pr.approve",
|
|
"gitea.pr.merge",
|
|
"gitea.pr.request_changes",
|
|
],
|
|
),
|
|
}
|
|
|
|
ROLES = tuple(_ROLE_OPS)
|
|
|
|
|
|
def _profile(role: str, *, canonical_root: str | None = None) -> dict:
|
|
allowed, forbidden = _ROLE_OPS[role]
|
|
profile = {
|
|
"enabled": True,
|
|
"context": "prgs",
|
|
"role": role,
|
|
"username": "jcwalker3",
|
|
"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}",
|
|
"allowed_repositories": [TARGET_SLUG],
|
|
}
|
|
if canonical_root is not None:
|
|
profile["canonical_repository_root"] = canonical_root
|
|
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"},
|
|
}
|
|
},
|
|
"profiles": profiles,
|
|
}
|
|
|
|
|
|
class _CrossRepoHarness(unittest.TestCase):
|
|
"""Real install + target git repos, temp profiles.json, no network."""
|
|
|
|
def setUp(self):
|
|
self._dir = tempfile.TemporaryDirectory()
|
|
self.tmp = self._dir.name
|
|
self.config_path = os.path.join(self.tmp, "profiles.json")
|
|
|
|
# The real install checkout carries a remote literally named `prgs`;
|
|
# a freshly cloned target repository normally names its remote `origin`.
|
|
# Reproducing that asymmetry is the point: identity derivation must not
|
|
# depend on the remote happening to share the `remote=` argument's name.
|
|
self.install_root = _init_repo(
|
|
Path(self.tmp) / "install", INSTALL_URL, remote_name="prgs"
|
|
)
|
|
self.target_root = _init_repo(
|
|
Path(self.tmp) / "target", TARGET_URL, remote_name="origin"
|
|
)
|
|
self.third_root = _init_repo(
|
|
Path(self.tmp) / "third", f"https://gitea.prgs.cc/{THIRD_SLUG}.git"
|
|
)
|
|
self.not_a_repo = os.path.join(self.tmp, "plain-dir")
|
|
os.makedirs(self.not_a_repo, exist_ok=True)
|
|
|
|
session_ctx._reset_session_context_for_testing()
|
|
gitea_config._active_profile_override = None
|
|
mcp_server._IDENTITY_CACHE.clear()
|
|
srv._MUTATION_AUTHORITY = None
|
|
|
|
# PROJECT_ROOT is wherever the server file physically lives; pin it to a
|
|
# real Gitea-Tools-identified checkout so "install-derived" is
|
|
# deterministic regardless of the developer's layout.
|
|
self._project_root = patch.object(srv, "PROJECT_ROOT", self.install_root)
|
|
self._project_root.start()
|
|
# Undo the autouse deterministic-remote stub for this module only.
|
|
self._real_remote_url = patch.object(
|
|
srv, "_local_git_remote_url", _REAL_LOCAL_GIT_REMOTE_URL
|
|
)
|
|
self._real_remote_url.start()
|
|
|
|
def tearDown(self):
|
|
self._real_remote_url.stop()
|
|
self._project_root.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 _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, role: str, **extra) -> dict:
|
|
env = {
|
|
"GITEA_MCP_CONFIG": self.config_path,
|
|
"GITEA_MCP_PROFILE": f"prgs-{role}",
|
|
"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 _bind(self, role: str, canonical_root: str | None):
|
|
"""Activate *role* with *canonical_root* and return an env patch ctx."""
|
|
self._write_config(
|
|
{f"prgs-{role}": _profile(role, canonical_root=canonical_root)}
|
|
)
|
|
return patch.dict(os.environ, self._env(role), clear=True)
|
|
|
|
|
|
# ===========================================================================
|
|
# 1. The central helper (single source of root resolution)
|
|
# ===========================================================================
|
|
class TestCanonicalLocalGitRoot(_CrossRepoHarness):
|
|
"""_canonical_local_git_root is the one place a target root is derived."""
|
|
|
|
def test_unconfigured_namespace_uses_installation_root(self):
|
|
with self._bind("author", None):
|
|
self.assertEqual(srv._canonical_local_git_root(), self.install_root)
|
|
|
|
def test_configured_namespace_uses_target_root(self):
|
|
with self._bind("author", self.target_root):
|
|
self.assertEqual(srv._canonical_local_git_root(), self.target_root)
|
|
|
|
def test_configured_root_resolves_to_git_toplevel(self):
|
|
nested = os.path.join(self.target_root, "branches", "wt")
|
|
os.makedirs(nested, exist_ok=True)
|
|
with self._bind("author", nested):
|
|
# A subdirectory of the target repo still resolves to its toplevel.
|
|
self.assertEqual(srv._canonical_local_git_root(), self.target_root)
|
|
|
|
def test_invalid_root_never_silently_becomes_installation_root(self):
|
|
"""A configured-but-broken root must not fall back to Gitea-Tools."""
|
|
with self._bind("author", self.not_a_repo):
|
|
resolved = srv._canonical_local_git_root()
|
|
self.assertNotEqual(resolved, self.install_root)
|
|
self.assertEqual(resolved, os.path.realpath(self.not_a_repo))
|
|
|
|
def test_env_override_beats_profile_binding(self):
|
|
self._write_config(
|
|
{"prgs-author": _profile("author", canonical_root=self.third_root)}
|
|
)
|
|
env = self._env("author", GITEA_CANONICAL_REPOSITORY_ROOT=self.target_root)
|
|
with patch.dict(os.environ, env, clear=True):
|
|
self.assertEqual(srv._canonical_local_git_root(), self.target_root)
|
|
|
|
|
|
# ===========================================================================
|
|
# 2. Repository identity — the upstream inversion
|
|
# ===========================================================================
|
|
class TestRepositoryIdentityDerivation(_CrossRepoHarness):
|
|
"""_local_git_remote_url and its consumers must read the target repo."""
|
|
|
|
def test_remote_url_reads_target_not_installation(self):
|
|
with self._bind("author", self.target_root):
|
|
self.assertEqual(srv._local_git_remote_url("origin"), TARGET_URL)
|
|
|
|
def test_remote_url_unconfigured_still_reads_installation(self):
|
|
with self._bind("author", None):
|
|
# The install checkout's remote is named `prgs`, matching production.
|
|
self.assertEqual(srv._local_git_remote_url("prgs"), INSTALL_URL)
|
|
|
|
def test_workspace_slug_follows_canonical_root(self):
|
|
with self._bind("author", self.target_root):
|
|
self.assertEqual(srv._workspace_repository_slug("origin"), TARGET_SLUG)
|
|
|
|
def test_workspace_slug_unconfigured_is_installation(self):
|
|
with self._bind("author", None):
|
|
self.assertEqual(srv._workspace_repository_slug("prgs"), INSTALL_SLUG)
|
|
|
|
def test_every_role_derives_the_same_target_identity(self):
|
|
for role in ROLES:
|
|
with self.subTest(role=role):
|
|
with self._bind(role, self.target_root):
|
|
self.assertEqual(
|
|
srv._workspace_repository_slug("origin"), TARGET_SLUG
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# 3. _resolve — omitted and explicit coordinates
|
|
# ===========================================================================
|
|
class TestResolveTargetCoordinates(_CrossRepoHarness):
|
|
"""Omitted coordinates follow the binding; explicit ones may only confirm."""
|
|
|
|
def test_omitted_coordinates_resolve_to_target_repository(self):
|
|
for role in ROLES:
|
|
with self.subTest(role=role):
|
|
with self._bind(role, self.target_root):
|
|
_host, org, repo = srv._resolve("prgs", None, None, None)
|
|
self.assertEqual((org, repo), (TARGET_ORG, TARGET_REPO))
|
|
|
|
def test_omitted_coordinates_unconfigured_resolve_to_installation(self):
|
|
with self._bind("author", None):
|
|
_host, org, repo = srv._resolve("prgs", None, None, None)
|
|
self.assertEqual((org, repo), (INSTALL_ORG, INSTALL_REPO))
|
|
|
|
def test_explicit_matching_coordinates_confirm_the_binding(self):
|
|
for role in ROLES:
|
|
with self.subTest(role=role):
|
|
with self._bind(role, self.target_root):
|
|
_host, org, repo = srv._resolve(
|
|
"prgs", None, TARGET_ORG, TARGET_REPO
|
|
)
|
|
self.assertEqual((org, repo), (TARGET_ORG, TARGET_REPO))
|
|
|
|
def test_explicit_mismatched_repository_cannot_override_binding(self):
|
|
for role in ROLES:
|
|
with self.subTest(role=role):
|
|
with self._bind(role, self.target_root):
|
|
with self.assertRaises(RuntimeError) as ctx:
|
|
srv._resolve("prgs", None, TARGET_ORG, INSTALL_REPO)
|
|
msg = str(ctx.exception)
|
|
self.assertIn("#741", msg)
|
|
self.assertIn("fail closed", msg)
|
|
|
|
def test_explicit_mismatched_organization_cannot_override_binding(self):
|
|
with self._bind("author", self.target_root):
|
|
with self.assertRaises(RuntimeError):
|
|
srv._resolve("prgs", None, "SomeoneElse", TARGET_REPO)
|
|
|
|
def test_gitea_tools_rooted_namespace_cannot_target_control_plane(self):
|
|
"""Direction 1: an install-rooted namespace must not reach the target."""
|
|
with self._bind("author", self.install_root):
|
|
with self.assertRaises(RuntimeError):
|
|
srv._resolve("prgs", None, TARGET_ORG, TARGET_REPO)
|
|
|
|
def test_control_plane_rooted_namespace_cannot_target_gitea_tools(self):
|
|
"""Direction 2: a target-rooted namespace must not reach Gitea-Tools."""
|
|
with self._bind("author", self.target_root):
|
|
with self.assertRaises(RuntimeError):
|
|
srv._resolve("prgs", None, INSTALL_ORG, INSTALL_REPO)
|
|
|
|
def test_unconfigured_namespace_keeps_existing_explicit_behaviour(self):
|
|
"""Same-repository behaviour is unchanged (no new fail-closed path)."""
|
|
with self._bind("author", None):
|
|
_host, org, repo = srv._resolve("prgs", None, INSTALL_ORG, INSTALL_REPO)
|
|
self.assertEqual((org, repo), (INSTALL_ORG, INSTALL_REPO))
|
|
|
|
|
|
# ===========================================================================
|
|
# 4. #274 workspace guard — the two guard paths must agree
|
|
# ===========================================================================
|
|
class TestMutationWorkspaceGuardBinding(_CrossRepoHarness):
|
|
"""Both guard paths must agree on which repository they protect."""
|
|
|
|
def _contexts(self, worktree: str | None = None):
|
|
return srv._resolve_namespace_mutation_context(worktree)
|
|
|
|
def test_mutation_context_uses_target_root(self):
|
|
with self._bind("author", self.target_root):
|
|
self.assertEqual(self._contexts()["canonical_repo_root"], self.target_root)
|
|
|
|
def test_mutation_context_unconfigured_uses_installation_root(self):
|
|
with self._bind("author", None):
|
|
self.assertEqual(self._contexts()["canonical_repo_root"], self.install_root)
|
|
|
|
def test_role_mutation_workspace_guard_agrees_with_mutation_context(self):
|
|
"""The omitted configured_canonical_root made these two disagree."""
|
|
import namespace_workspace_binding as nwb
|
|
|
|
for role in ROLES:
|
|
with self.subTest(role=role):
|
|
with self._bind(role, self.target_root):
|
|
configured, _src = srv._configured_canonical_root()
|
|
assessment = nwb.assess_namespace_mutation_workspace(
|
|
role_kind=role,
|
|
worktree_path=None,
|
|
worktree=None,
|
|
process_project_root=srv.PROJECT_ROOT,
|
|
profile_name=f"prgs-{role}",
|
|
configured_canonical_root=configured,
|
|
)
|
|
self.assertEqual(
|
|
assessment["canonical_repo_root"],
|
|
self._contexts()["canonical_repo_root"],
|
|
)
|
|
self.assertEqual(
|
|
assessment["canonical_repo_root"], self.target_root
|
|
)
|
|
|
|
def test_wrong_worktree_is_rejected_against_target_root(self):
|
|
"""A worktree belonging to the install repo is not in the target repo."""
|
|
import author_mutation_worktree as amw
|
|
|
|
with self._bind("author", self.target_root):
|
|
foreign = os.path.join(self.install_root, "branches", "wt")
|
|
os.makedirs(foreign, exist_ok=True)
|
|
membership = amw.assess_workspace_repo_membership(
|
|
workspace_path=foreign,
|
|
canonical_repo_root=self.target_root,
|
|
)
|
|
self.assertTrue(membership["block"])
|
|
|
|
|
|
# ===========================================================================
|
|
# 5. Canonical-root validation — missing / invalid / mismatched
|
|
# ===========================================================================
|
|
class TestCanonicalRootFailClosed(_CrossRepoHarness):
|
|
"""Missing, invalid, ambiguous and mismatched roots fail closed."""
|
|
|
|
def _assess(self, value, *, expected=TARGET_SLUG, require=True):
|
|
return crr.assess_canonical_repository_root(
|
|
configured_value=value,
|
|
source="test",
|
|
expected_slug=expected,
|
|
process_project_root=self.install_root,
|
|
remote="origin",
|
|
require_binding=require,
|
|
)
|
|
|
|
def test_missing_root_fails_closed_when_binding_required(self):
|
|
result = self._assess(None)
|
|
self.assertTrue(result["block"])
|
|
self.assertFalse(result["configured"])
|
|
|
|
def test_missing_root_is_the_single_repo_default_when_not_required(self):
|
|
result = self._assess(None, expected=None, require=False)
|
|
self.assertFalse(result["block"])
|
|
self.assertEqual(result["canonical_repo_root"], self.install_root)
|
|
|
|
def test_nonexistent_root_fails_closed(self):
|
|
result = self._assess(os.path.join(self.tmp, "nope"))
|
|
self.assertTrue(result["block"])
|
|
self.assertIn("does not exist", " ".join(result["reasons"]))
|
|
|
|
def test_non_git_directory_fails_closed(self):
|
|
result = self._assess(self.not_a_repo)
|
|
self.assertTrue(result["block"])
|
|
self.assertIn("not a git repository", " ".join(result["reasons"]))
|
|
|
|
def test_mismatched_repository_identity_fails_closed(self):
|
|
result = self._assess(self.third_root)
|
|
self.assertTrue(result["block"])
|
|
self.assertIn("mismatch", " ".join(result["reasons"]))
|
|
|
|
def test_matching_repository_identity_is_proven(self):
|
|
result = self._assess(self.target_root)
|
|
self.assertFalse(result["block"])
|
|
self.assertEqual(result["resolved_slug"], TARGET_SLUG)
|
|
self.assertEqual(result["canonical_repo_root"], self.target_root)
|
|
|
|
def test_unresolvable_root_yields_fail_closed_reasons_not_fallback(self):
|
|
with self._bind("author", self.not_a_repo):
|
|
slug, reasons = srv._canonical_repository_slug(srv.get_profile(), "origin")
|
|
self.assertIsNone(slug)
|
|
self.assertTrue(reasons)
|
|
|
|
|
|
# ===========================================================================
|
|
# 6. Immutability — first bind wins, requests cannot replace the root
|
|
# ===========================================================================
|
|
class TestCanonicalRootImmutability(_CrossRepoHarness):
|
|
"""No request-supplied value can establish or swap the pinned root."""
|
|
|
|
def test_first_bind_pins_target_root(self):
|
|
with self._bind("author", self.target_root):
|
|
with patch(
|
|
"gitea_mcp_server.api_request",
|
|
side_effect=lambda *a, **k: {
|
|
"login": "jcwalker3",
|
|
"full_name": "T",
|
|
"id": 1,
|
|
"email": "[email protected]",
|
|
},
|
|
):
|
|
srv.gitea_whoami(remote="prgs")
|
|
bound = session_ctx.get_session_context()
|
|
self.assertEqual(bound["canonical_repository_root"], self.target_root)
|
|
|
|
def test_binding_is_first_write_wins(self):
|
|
with self._bind("author", self.target_root):
|
|
session_ctx.seed_session_context_if_unbound(
|
|
profile_name="prgs-author",
|
|
remote="prgs",
|
|
host="gitea.prgs.cc",
|
|
identity="jcwalker3",
|
|
org=TARGET_ORG,
|
|
repository=TARGET_REPO,
|
|
role_kind="author",
|
|
source="test",
|
|
canonical_repository_root=self.target_root,
|
|
)
|
|
# A second, contradictory seed must not replace the pin.
|
|
session_ctx.seed_session_context_if_unbound(
|
|
profile_name="prgs-author",
|
|
remote="prgs",
|
|
host="gitea.prgs.cc",
|
|
identity="jcwalker3",
|
|
org=INSTALL_ORG,
|
|
repository=INSTALL_REPO,
|
|
role_kind="author",
|
|
source="test-2",
|
|
canonical_repository_root=self.install_root,
|
|
)
|
|
bound = session_ctx.get_session_context()
|
|
self.assertEqual(bound["canonical_repository_root"], self.target_root)
|
|
self.assertEqual(bound["repository"], TARGET_REPO)
|
|
|
|
def test_request_supplied_worktree_cannot_replace_the_root(self):
|
|
with self._bind("author", self.target_root):
|
|
ctx = srv._resolve_namespace_mutation_context(self.install_root)
|
|
# The workspace argument may be demoted/inspected, but the canonical
|
|
# repository root stays the configured target.
|
|
self.assertEqual(ctx["canonical_repo_root"], self.target_root)
|
|
|
|
def test_request_supplied_coordinates_cannot_replace_the_root(self):
|
|
with self._bind("author", self.target_root):
|
|
with self.assertRaises(RuntimeError):
|
|
srv._resolve("prgs", None, INSTALL_ORG, INSTALL_REPO)
|
|
self.assertEqual(srv._canonical_local_git_root(), self.target_root)
|
|
|
|
|
|
# ===========================================================================
|
|
# 7. Parity dimensions stay separately labelled (#739 F3 preserved)
|
|
# ===========================================================================
|
|
class TestParityDimensionSeparation(_CrossRepoHarness):
|
|
"""Server-implementation parity stays anchored to the install checkout."""
|
|
|
|
def test_server_dimension_is_installation_not_target(self):
|
|
import master_parity_gate
|
|
|
|
with self._bind("author", self.target_root):
|
|
head = master_parity_gate.read_git_head(srv.PROJECT_ROOT)
|
|
self.assertEqual(head, _git(self.install_root, "rev-parse", "HEAD"))
|
|
self.assertNotEqual(head, _git(self.target_root, "rev-parse", "HEAD"))
|
|
|
|
def test_target_dimension_reads_the_target_checkout(self):
|
|
with self._bind("author", self.target_root):
|
|
self.assertEqual(
|
|
_git(srv._canonical_local_git_root(), "rev-parse", "HEAD"),
|
|
_git(self.target_root, "rev-parse", "HEAD"),
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# 8. Configuration loaders validate the binding consistently (AC13)
|
|
# ===========================================================================
|
|
class TestConfigurationLoaderValidation(unittest.TestCase):
|
|
"""Every supported loader treats canonical_repository_root identically."""
|
|
|
|
def setUp(self):
|
|
self._dir = tempfile.TemporaryDirectory()
|
|
self.tmp = self._dir.name
|
|
|
|
def tearDown(self):
|
|
self._dir.cleanup()
|
|
|
|
def _write(self, data: dict) -> str:
|
|
path = os.path.join(self.tmp, "profiles.json")
|
|
with open(path, "w", encoding="utf-8") as fh:
|
|
fh.write(json.dumps(data))
|
|
return path
|
|
|
|
# --- v1 ---------------------------------------------------------------
|
|
def _v1(self, root):
|
|
return {
|
|
"version": 1,
|
|
"profiles": {
|
|
"prgs-author": {
|
|
"base_url": "https://gitea.prgs.cc",
|
|
"auth": {"type": "env", "name": "GITEA_TOKEN"},
|
|
"canonical_repository_root": root,
|
|
}
|
|
},
|
|
}
|
|
|
|
def test_v1_rejects_relative_canonical_root(self):
|
|
path = self._write(self._v1("relative/path"))
|
|
with self.assertRaises(gitea_config.ConfigError) as ctx:
|
|
gitea_config.load_config(path)
|
|
self.assertIn("absolute", str(ctx.exception))
|
|
|
|
def test_v1_rejects_blank_canonical_root(self):
|
|
path = self._write(self._v1(" "))
|
|
with self.assertRaises(gitea_config.ConfigError):
|
|
gitea_config.load_config(path)
|
|
|
|
def test_v1_accepts_absolute_canonical_root(self):
|
|
path = self._write(self._v1("/abs/target"))
|
|
loaded = gitea_config.load_config(path)
|
|
self.assertEqual(
|
|
loaded["profiles"]["prgs-author"]["canonical_repository_root"],
|
|
"/abs/target",
|
|
)
|
|
|
|
def test_v1_without_the_field_is_unchanged(self):
|
|
data = self._v1("/abs/target")
|
|
del data["profiles"]["prgs-author"]["canonical_repository_root"]
|
|
loaded = gitea_config.load_config(self._write(data))
|
|
self.assertNotIn("canonical_repository_root", loaded["profiles"]["prgs-author"])
|
|
|
|
# --- v2 environments --------------------------------------------------
|
|
def _v2_env(self, root):
|
|
ident = {
|
|
"auth": {"type": "env", "name": "GITEA_TOKEN"},
|
|
"base_url": "https://gitea.prgs.cc",
|
|
"username": "jcwalker3",
|
|
"allowed_operations": ["gitea.read"],
|
|
}
|
|
if root is not None:
|
|
ident["canonical_repository_root"] = root
|
|
return {
|
|
"version": 2,
|
|
"environments": {
|
|
"prgs": {"services": {"gitea": {"identities": {"author": ident}}}}
|
|
},
|
|
}
|
|
|
|
def test_v2_environments_propagates_canonical_root(self):
|
|
"""Regression: the field was silently dropped during flattening."""
|
|
loaded = gitea_config.load_config(self._write(self._v2_env("/abs/target")))
|
|
profile = loaded["profiles"]["prgs.gitea.author"]
|
|
self.assertEqual(profile["canonical_repository_root"], "/abs/target")
|
|
|
|
def test_v2_environments_rejects_relative_canonical_root(self):
|
|
with self.assertRaises(gitea_config.ConfigError) as ctx:
|
|
gitea_config.load_config(self._write(self._v2_env("relative/path")))
|
|
self.assertIn("absolute", str(ctx.exception))
|
|
|
|
def test_v2_environments_without_the_field_is_unchanged(self):
|
|
loaded = gitea_config.load_config(self._write(self._v2_env(None)))
|
|
self.assertNotIn(
|
|
"canonical_repository_root", loaded["profiles"]["prgs.gitea.author"]
|
|
)
|
|
|
|
# --- v2 contexts (already validated; guard against regression) ---------
|
|
def test_v2_contexts_still_rejects_relative_canonical_root(self):
|
|
data = {
|
|
"version": 2,
|
|
"contexts": {
|
|
"prgs": {
|
|
"enabled": True,
|
|
"gitea": {"enabled": True, "base_url": "https://gitea.prgs.cc"},
|
|
}
|
|
},
|
|
"profiles": {
|
|
"prgs-author": {
|
|
"enabled": True,
|
|
"context": "prgs",
|
|
"username": "jcwalker3",
|
|
"auth": {"type": "env", "name": "GITEA_TOKEN"},
|
|
"allowed_operations": ["gitea.read"],
|
|
"canonical_repository_root": "relative/path",
|
|
}
|
|
},
|
|
}
|
|
with self.assertRaises(gitea_config.ConfigError):
|
|
gitea_config.load_config(self._write(data))
|
|
|
|
|
|
# ===========================================================================
|
|
# 9. Role-by-operation matrix
|
|
# ===========================================================================
|
|
class TestRoleOperationMatrix(_CrossRepoHarness):
|
|
"""Each role, each cross-repository condition, one assertion per cell."""
|
|
|
|
def test_correct_target_resolves_for_every_role(self):
|
|
for role in ROLES:
|
|
with self.subTest(role=role, case="correct-target"):
|
|
with self._bind(role, self.target_root):
|
|
_h, org, repo = srv._resolve("prgs", None, None, None)
|
|
self.assertEqual((org, repo), (TARGET_ORG, TARGET_REPO))
|
|
|
|
def test_wrong_repository_is_rejected_for_every_role(self):
|
|
for role in ROLES:
|
|
with self.subTest(role=role, case="wrong-repo"):
|
|
with self._bind(role, self.target_root):
|
|
with self.assertRaises(RuntimeError):
|
|
srv._resolve("prgs", None, INSTALL_ORG, INSTALL_REPO)
|
|
|
|
def test_explicit_matching_coordinates_pass_for_every_role(self):
|
|
for role in ROLES:
|
|
with self.subTest(role=role, case="explicit-match"):
|
|
with self._bind(role, self.target_root):
|
|
_h, org, repo = srv._resolve("prgs", None, TARGET_ORG, TARGET_REPO)
|
|
self.assertEqual((org, repo), (TARGET_ORG, TARGET_REPO))
|
|
|
|
def test_explicit_mismatch_fails_closed_for_every_role(self):
|
|
for role in ROLES:
|
|
with self.subTest(role=role, case="explicit-mismatch"):
|
|
with self._bind(role, self.target_root):
|
|
with self.assertRaises(RuntimeError):
|
|
srv._resolve("prgs", None, INSTALL_ORG, THIRD_REPO)
|
|
|
|
def test_missing_canonical_root_preserves_single_repo_default(self):
|
|
for role in ROLES:
|
|
with self.subTest(role=role, case="missing-root"):
|
|
with self._bind(role, None):
|
|
self.assertEqual(srv._canonical_local_git_root(), self.install_root)
|
|
|
|
def test_invalid_canonical_root_never_becomes_install_root(self):
|
|
for role in ROLES:
|
|
with self.subTest(role=role, case="invalid-root"):
|
|
with self._bind(role, self.not_a_repo):
|
|
self.assertNotEqual(
|
|
srv._canonical_local_git_root(), self.install_root
|
|
)
|
|
|
|
def test_wrong_worktree_rejected_for_every_role(self):
|
|
import author_mutation_worktree as amw
|
|
|
|
foreign = os.path.join(self.install_root, "branches", "foreign")
|
|
os.makedirs(foreign, exist_ok=True)
|
|
for role in ROLES:
|
|
with self.subTest(role=role, case="wrong-worktree"):
|
|
with self._bind(role, self.target_root):
|
|
membership = amw.assess_workspace_repo_membership(
|
|
workspace_path=foreign,
|
|
canonical_repo_root=srv._canonical_local_git_root(),
|
|
)
|
|
self.assertTrue(membership["block"])
|
|
|
|
def test_request_override_attempt_rejected_for_every_role(self):
|
|
for role in ROLES:
|
|
with self.subTest(role=role, case="request-override"):
|
|
with self._bind(role, self.target_root):
|
|
with self.assertRaises(RuntimeError):
|
|
srv._resolve("prgs", None, "Attacker", "evil-repo")
|
|
self.assertEqual(srv._canonical_local_git_root(), self.target_root)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|