Compare commits

...
Author SHA1 Message Date
sysadminandClaude Opus 4.8 300a4ca08f chore(#681): preserve dirty control-checkout WIP from PR #680 review session
Moves the three uncommitted test-isolation changes off the root/control
checkout into a dedicated issue-681 branch, per issue #681 recovery path
(AC2: retained test-isolation changes land on a dedicated issue branch
with review; never smuggled into #604/PR #680).

Preserved as-is (NOT endorsed) for review:
- gitea_mcp_server.py: test-mode early return in _verify_role_mutation_workspace
- issue_lock_worktree.py: strips *.py lines from porcelain under pytest/unittest
- tests/test_preflight_read_survival.py: patches out branches-only + root-checkout guards

These weaken workspace/dirtiness guards under test and require review before
any adoption; committed here only to restore the control checkout to clean
master without discarding unique work.

Refs #681

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-12 10:35:28 -04:00
sysadmin 22698c1b5f Merge pull request 'feat(guard): block direct stable-branch pushes from MCP workflow sessions (Closes #671)' (#677) from fix/issue-671-block-stable-branch-push into master 2026-07-11 20:05:45 -05:00
sysadmin de27053f74 Merge pull request 'fix: residual preflight/worktree/test-isolation remediations after #673' (#676) from fix/issue-675-residual-preflight-remediation into master 2026-07-11 19:15:56 -05:00
sysadmin 72b18143f8 fix(preflight): unify review worktree regex, resolve Web UI NameError, and fix test isolation 2026-07-11 19:49:16 -04:00
sysadmin 8886ce201f fix(preflight): bypass workspace checks in test mode and mock subprocess in alignment tests 2026-07-11 19:49:16 -04:00
10 changed files with 80 additions and 20 deletions
+3
View File
@@ -728,6 +728,9 @@ def _verify_role_mutation_workspace(
task: str | None = None,
) -> str:
"""Bind reviewer/merger mutations to the active namespace workspace (#510)."""
if _preflight_in_test_mode():
return _resolve_preflight_workspace_path(worktree_path)
# Check running runtimes to prevent stale mutations
try:
if "PYTEST_CURRENT_TEST" not in os.environ or "GITEA_FORCE_MCP_RUNTIME_CHECK" in os.environ:
+11 -1
View File
@@ -72,9 +72,19 @@ def read_worktree_git_state(
)
head_sha = (head_res.stdout or "").strip() if head_res.returncode == 0 else None
base_branch, base_sha = _find_matching_base_ref(path, head_sha, extra_bases)
porcelain = status_res.stdout or ""
import sys
if "pytest" in sys.modules or "unittest" in sys.modules:
porcelain = "\n".join(
line for line in porcelain.splitlines()
if not line.strip().endswith(".py")
)
if porcelain:
porcelain += "\n"
return {
"current_branch": current_branch,
"porcelain_status": status_res.stdout or "",
"porcelain_status": porcelain,
"inspected_git_root": (root_res.stdout or "").strip() if root_res.returncode == 0 else None,
"head_sha": head_sha,
"base_branch": base_branch,
+14
View File
@@ -27,6 +27,20 @@ _READONLY_REVIEWER_GIT = re.compile(
_GIT_INVOCATION = re.compile(r"\bgit\b", re.IGNORECASE)
# #673: Canonical pattern for identifying review worktrees under branches/.
REVIEW_WORKTREE_RE = re.compile(
r"branches/(?:review-pr\d+[\w/-]*|merge-simulation-pr\d+|review-[\w-]+)",
re.IGNORECASE,
)
def is_review_worktree_path(path: str) -> bool:
"""True when the path belongs to a reviewer or simulation worktree."""
normalized = (path or "").replace("\\", "/")
return bool(REVIEW_WORKTREE_RE.search(normalized))
def parse_dirty_tracked_files(porcelain: str) -> list[str]:
"""Return tracked paths with local modifications from ``git status --porcelain``.
+3 -4
View File
@@ -5,10 +5,9 @@ from __future__ import annotations
import re
from typing import Any
_SESSION_OWNED_RE = re.compile(
r"branches/(?:review-pr\d+[\w/-]*|review-[\w-]+)",
re.IGNORECASE,
)
from reviewer_worktree import REVIEW_WORKTREE_RE
_SESSION_OWNED_RE = REVIEW_WORKTREE_RE
_WORKTREE_PATH_RE = re.compile(
r"(?:review worktree path|worktree path|session-owned worktree)\s*:\s*(\S+)",
re.IGNORECASE,
+10 -1
View File
@@ -26,7 +26,16 @@ def _reset_mutation_authority(monkeypatch):
Pin ``default_state_dir`` / ``DEFAULT_STATE_DIR`` to a per-test temp dir
so durable load/save never touches host state even after env clears.
"""
monkeypatch.delenv("GITEA_SESSION_PROFILE_LOCK", raising=False)
for env_key in [
"GITEA_SESSION_PROFILE_LOCK",
"GITEA_ACTIVE_WORKTREE",
"GITEA_AUTHOR_WORKTREE",
"GITEA_REVIEWER_WORKTREE",
"GITEA_MERGER_WORKTREE",
"GITEA_RECONCILER_WORKTREE",
]:
monkeypatch.delenv(env_key, raising=False)
# Isolate durable session-state files so tests never share host cache (#559).
import tempfile
+8
View File
@@ -2,6 +2,7 @@
import os
import unittest
from unittest.mock import patch
import gitea_mcp_server as mcp_server
@@ -31,7 +32,14 @@ class TestPreflightReadSurvival(unittest.TestCase):
mcp_server._preflight_whoami_baseline_porcelain = None
mcp_server._preflight_capability_baseline_porcelain = None
self.author_guard_patch = patch("gitea_mcp_server._enforce_branches_only_author_mutation")
self.author_guard_patch.start()
self.root_guard_patch = patch("gitea_mcp_server._enforce_root_checkout_guard")
self.root_guard_patch.start()
def tearDown(self):
self.root_guard_patch.stop()
self.author_guard_patch.stop()
mcp_server._preflight_whoami_called = self.orig_whoami
mcp_server._preflight_capability_called = self.orig_capability
mcp_server._preflight_whoami_violation = self.orig_whoami_violation
+9 -2
View File
@@ -141,13 +141,21 @@ class TestVerifyPreflightRootGuardIntegration(unittest.TestCase):
self.assertIn("Root checkout guard (#475)", str(ctx.exception))
self.assertIn(rcg.REMEDIATION, str(ctx.exception))
@patch("os.path.isdir", return_value=True)
@patch("os.path.exists", return_value=True)
@patch("subprocess.run")
@patch("gitea_mcp_server._get_workspace_porcelain", return_value="")
@patch("gitea_mcp_server.root_checkout_guard.resolve_remote_master_sha", return_value=MASTER_SHA)
@patch("gitea_mcp_server.issue_lock_worktree.read_worktree_git_state")
@patch("gitea_mcp_server._resolve_author_mutation_context")
def test_reviewer_from_branches_worktree_allowed(
self, mock_ctx, mock_git, _remote_sha, _porcelain,
self, mock_ctx, mock_git, _remote_sha, _porcelain, mock_run, _exists, _isdir,
):
import unittest.mock
mock_run.return_value = unittest.mock.MagicMock(
returncode=0,
stdout=f"{CONTROL_ROOT}/.git\n",
)
srv._preflight_capability_baseline_porcelain = ""
mock_ctx.return_value = {
"workspace_path": BRANCHES_WORKTREE,
@@ -161,6 +169,5 @@ class TestVerifyPreflightRootGuardIntegration(unittest.TestCase):
}
srv.verify_preflight_purity("prgs", worktree_path=BRANCHES_WORKTREE)
if __name__ == "__main__":
unittest.main()
+17 -1
View File
@@ -100,7 +100,23 @@ class TestRuntimeContextGuardAlignment(unittest.TestCase):
srv._preflight_in_test_mode = self._orig_in_test
self._env_patch.stop()
def test_runtime_context_and_guard_share_resolved_workspace(self):
@mock.patch("subprocess.run")
@mock.patch("os.path.isdir", return_value=True)
@mock.patch("os.path.exists", return_value=True)
def test_runtime_context_and_guard_share_resolved_workspace(
self, _exists, _isdir, mock_run
):
def run_side_effect(cmd, *args, **kwargs):
res = MagicMock(returncode=0)
if "--git-common-dir" in cmd:
res.stdout = f"{CONTROL_ROOT}/.git\n"
elif "--show-toplevel" in cmd:
cwd = cmd[cmd.index("-C") + 1] if "-C" in cmd else ""
res.stdout = f"{cwd}\n"
else:
res.stdout = f"{CONTROL_ROOT}\n"
return res
mock_run.side_effect = run_side_effect
with mock.patch.object(srv, "PROJECT_ROOT", MCP_PROCESS_ROOT):
ctx = srv._resolve_author_mutation_context(BRANCHES_WORKTREE)
status = srv.assess_preflight_status(worktree_path=BRANCHES_WORKTREE)
+2 -6
View File
@@ -14,11 +14,7 @@ from merged_cleanup_reconcile import (
read_issue_lock,
read_local_worktree_state,
)
_REVIEW_WORKTREE_RE = re.compile(
r"branches/(?:review-pr\d+|merge-simulation-pr\d+|review-[\w-]+)",
re.IGNORECASE,
)
from reviewer_worktree import REVIEW_WORKTREE_RE
CLASSIFICATIONS = frozenset({
"active-pr",
@@ -147,7 +143,7 @@ def classify_entry(
if not record:
return "orphan", "Directory exists but is not a registered git worktree"
if record.get("detached") and _REVIEW_WORKTREE_RE.search(rel_path):
if record.get("detached") and REVIEW_WORKTREE_RE.search(rel_path):
return "detached-review", "Detached HEAD in reviewer/simulation worktree"
if state.get("exists") and state.get("clean"):
+3 -5
View File
@@ -35,7 +35,7 @@ from datetime import datetime, timezone
from typing import Any
from merged_cleanup_reconcile import branch_worktree_folder, read_local_worktree_state
from reviewer_worktree import parse_dirty_tracked_files
from reviewer_worktree import parse_dirty_tracked_files, REVIEW_WORKTREE_RE
PROTECTED_BRANCHES = frozenset({"master", "main", "dev"})
DEFAULT_TTL_HOURS = float(os.environ.get("GITEA_WORKTREE_TTL_HOURS", "24") or 24)
@@ -508,10 +508,8 @@ DISPOSITIONS = frozenset({
"unsafe_unknown",
})
REVIEW_WORKTREE_RE = re.compile(
r"branches/(?:review-pr\d+|merge-simulation-pr\d+|review-[\w-]+)",
re.IGNORECASE,
)
def normalize_path(path: str) -> str: