fix(session): require config-backed mutation authority and workspace-bound targets (#714)
Close the #714 remediation gap left at 943d402 where env-only mutation
profiles, REMOTES Timesheet defaults treated as explicit caller input, and
machine-dependent Git remotes produced false greens.
- Fail closed when mutations lack a config-backed profile with non-empty
allowed_repositories; env ops cannot invent mutation authority.
- Preserve omitted-vs-explicit org/repo provenance through the mutation
gate; omitted targets bind to the verified workspace repository.
- Prefer workspace-aligned Git remotes over historical Timesheet defaults
in MCP _resolve and CLI resolve_remote.
- Centralize deterministic config-backed mutation fixtures; migrate
mutation tests off env-only authority without weakening security
assertions.
Full suite: 2744 passed, 6 skipped.
This commit is contained in:
+149
-103
@@ -1,3 +1,11 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from mutation_profile_fixture import (
|
||||
mutation_profile_env,
|
||||
shared_mutation_env,
|
||||
install_deterministic_remote_urls,
|
||||
) # noqa: E402
|
||||
"""Tests for the MCP server tool functions.
|
||||
|
||||
Each tool is tested by calling the underlying function directly (not through
|
||||
@@ -47,6 +55,7 @@ from gitea_auth import get_profile # noqa: E402
|
||||
import gitea_config # noqa: E402
|
||||
|
||||
import mcp_server
|
||||
install_deterministic_remote_urls()
|
||||
import issue_lock_store
|
||||
import gitea_auth
|
||||
|
||||
@@ -221,22 +230,26 @@ def _seed_ready_review_decision(
|
||||
|
||||
|
||||
# Issue-write tools are profile-gated (#69).
|
||||
ISSUE_WRITE_ENV = {
|
||||
"GITEA_ALLOWED_OPERATIONS": (
|
||||
"gitea.issue.create,gitea.issue.close,gitea.issue.comment"
|
||||
# Default remote is dadeschools — use the host-aligned config profile so
|
||||
# cross-host session binding does not fail closed (#714).
|
||||
ISSUE_WRITE_ENV = shared_mutation_env(
|
||||
"test-author-dadeschools",
|
||||
GITEA_ALLOWED_OPERATIONS=(
|
||||
"gitea.issue.create,gitea.issue.close,gitea.issue.comment,"
|
||||
"gitea.pr.create,gitea.branch.push,gitea.repo.commit,gitea.read"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# create_pr is gated on author profile + namespace (#69, #209).
|
||||
CREATE_PR_ENV = {
|
||||
"GITEA_PROFILE_NAME": "author-test",
|
||||
"GITEA_ALLOWED_OPERATIONS": (
|
||||
"gitea.read,gitea.pr.create,gitea.branch.push"
|
||||
# Default remote is dadeschools; use matching config-backed author profile.
|
||||
CREATE_PR_ENV = shared_mutation_env(
|
||||
"test-author-dadeschools",
|
||||
GITEA_ALLOWED_OPERATIONS=(
|
||||
"gitea.read,gitea.pr.create,gitea.branch.push,"
|
||||
"gitea.issue.create,gitea.issue.close,gitea.issue.comment"
|
||||
),
|
||||
"GITEA_FORBIDDEN_OPERATIONS": (
|
||||
"gitea.pr.approve,gitea.pr.merge,gitea.pr.review"
|
||||
),
|
||||
}
|
||||
GITEA_FORBIDDEN_OPERATIONS="gitea.pr.approve,gitea.pr.merge,gitea.pr.review",
|
||||
)
|
||||
|
||||
ISSUE_LOCK_FILE = "/tmp/gitea_issue_lock.json"
|
||||
|
||||
@@ -287,6 +300,7 @@ def _bind_test_lock(**overrides) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Create Issue
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestCreateIssue(unittest.TestCase):
|
||||
|
||||
@patch("mcp_server.role_session_router.check_author_mutation_after_reviewer_stop",
|
||||
@@ -296,7 +310,7 @@ class TestCreateIssue(unittest.TestCase):
|
||||
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
||||
def test_creates_issue(self, _auth, _get_all, mock_api, _role):
|
||||
mock_api.return_value = {"number": 1, "html_url": "https://gitea.example.com/issues/1"}
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=True):
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=False):
|
||||
result = gitea_create_issue(title="Test issue", body="body text")
|
||||
self.assertEqual(result["number"], 1)
|
||||
self.assertNotIn("url", result)
|
||||
@@ -314,7 +328,7 @@ class TestCreateIssue(unittest.TestCase):
|
||||
def test_create_issue_reveal_opt_in_includes_url(self, _auth, _get_all, mock_api, _role):
|
||||
mock_api.return_value = {"number": 1, "html_url": "https://gitea.example.com/issues/1"}
|
||||
env = {**ISSUE_WRITE_ENV, "GITEA_MCP_REVEAL_ENDPOINTS": "1"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
result = gitea_create_issue(title="Test issue", body="body text")
|
||||
self.assertIn("issues/1", result["url"])
|
||||
|
||||
@@ -325,7 +339,18 @@ class TestCreateIssue(unittest.TestCase):
|
||||
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
||||
def test_creates_on_prgs(self, _auth, _get_all, mock_api, _role):
|
||||
mock_api.return_value = {"number": 5, "html_url": "https://gitea.prgs.cc/issues/5"}
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=True):
|
||||
env = {
|
||||
**ISSUE_WRITE_ENV,
|
||||
"GITEA_MCP_PROFILE": "test-author-prgs",
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
import gitea_config
|
||||
gitea_config._active_profile_override = None
|
||||
try:
|
||||
import session_context_binding as _sc
|
||||
_sc._reset_session_context_for_testing()
|
||||
except Exception:
|
||||
pass
|
||||
result = gitea_create_issue(title="Test", remote="prgs")
|
||||
self.assertEqual(result["number"], 5)
|
||||
url = mock_api.call_args[0][1]
|
||||
@@ -338,7 +363,7 @@ class TestCreateIssue(unittest.TestCase):
|
||||
@patch("mcp_server.api_get_all", return_value=[])
|
||||
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
||||
def test_create_issue_required_workflow_labels_blocks(self, _auth, _get_all, mock_api, _role):
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=True):
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=False):
|
||||
result = gitea_create_issue(
|
||||
title="Test issue",
|
||||
require_workflow_labels=True,
|
||||
@@ -378,7 +403,7 @@ class TestCreatePR(unittest.TestCase):
|
||||
mock_api.side_effect = api_side_effect
|
||||
with tempfile.TemporaryDirectory() as lock_dir:
|
||||
env = {**CREATE_PR_ENV, "GITEA_ISSUE_LOCK_DIR": lock_dir}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
_bind_test_lock(issue_number=123, branch_name="feat/x", worktree_path=worktree)
|
||||
result = gitea_create_pr(
|
||||
title="feat: X Closes #123",
|
||||
@@ -421,7 +446,7 @@ class TestCreatePR(unittest.TestCase):
|
||||
mock_api.side_effect = api_side_effect
|
||||
with tempfile.TemporaryDirectory() as lock_dir:
|
||||
env = {**CREATE_PR_ENV, "GITEA_ISSUE_LOCK_DIR": lock_dir, "GITEA_MCP_REVEAL_ENDPOINTS": "1"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
_bind_test_lock(issue_number=123, branch_name="feat/x", worktree_path=worktree)
|
||||
result = gitea_create_pr(
|
||||
title="feat: X Closes #123",
|
||||
@@ -447,7 +472,7 @@ class TestCreatePR(unittest.TestCase):
|
||||
mock_api.return_value = _issue_with_labels("type:feature", "status:in-progress")
|
||||
with tempfile.TemporaryDirectory() as lock_dir:
|
||||
env = {**CREATE_PR_ENV, "GITEA_ISSUE_LOCK_DIR": lock_dir}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
_bind_test_lock(issue_number=123, branch_name="feat/x", worktree_path=worktree)
|
||||
result = gitea_create_pr(
|
||||
title="feat: X Closes #123",
|
||||
@@ -470,7 +495,7 @@ class TestCreatePR(unittest.TestCase):
|
||||
worktree = os.path.realpath(os.getcwd())
|
||||
with tempfile.TemporaryDirectory() as lock_dir:
|
||||
env = {**CREATE_PR_ENV, "GITEA_ISSUE_LOCK_DIR": lock_dir}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
_bind_test_lock(
|
||||
issue_number=123,
|
||||
branch_name="feat/x",
|
||||
@@ -495,7 +520,7 @@ class TestCloseIssue(unittest.TestCase):
|
||||
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
||||
def test_closes_issue(self, _auth, mock_api):
|
||||
mock_api.return_value = {"state": "closed"}
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=True):
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=False):
|
||||
result = gitea_close_issue(issue_number=42)
|
||||
self.assertTrue(result["success"])
|
||||
self.assertIn("42", result["message"])
|
||||
@@ -601,7 +626,7 @@ class TestMarkIssue(unittest.TestCase):
|
||||
return {}
|
||||
|
||||
mock_api.side_effect = api_side_effect
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=True):
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=False):
|
||||
result = gitea_mark_issue(issue_number=5, action="start")
|
||||
self.assertTrue(result["success"])
|
||||
self.assertIn("claimed", result["message"])
|
||||
@@ -616,7 +641,7 @@ class TestMarkIssue(unittest.TestCase):
|
||||
mock_api.side_effect = [
|
||||
None,
|
||||
]
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=True):
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=False):
|
||||
result = gitea_mark_issue(issue_number=5, action="done")
|
||||
self.assertTrue(result["success"])
|
||||
self.assertIn("released", result["message"])
|
||||
@@ -629,7 +654,7 @@ class TestMarkIssue(unittest.TestCase):
|
||||
@patch("mcp_server.api_request")
|
||||
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
||||
def test_missing_label_raises(self, _auth, mock_api, _labels):
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=True):
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=False):
|
||||
with self.assertRaises(RuntimeError):
|
||||
gitea_mark_issue(issue_number=5, action="start")
|
||||
|
||||
@@ -641,12 +666,12 @@ class TestAuthErrors(unittest.TestCase):
|
||||
|
||||
@patch("mcp_server.get_auth_header", return_value=None)
|
||||
def test_no_credentials_raises(self, _auth):
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=True):
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=False):
|
||||
with self.assertRaises(RuntimeError):
|
||||
gitea_create_issue(title="test")
|
||||
|
||||
def test_unknown_remote_raises(self):
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=True):
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=False):
|
||||
with self.assertRaises(ValueError):
|
||||
gitea_create_issue(title="test", remote="nonexistent")
|
||||
|
||||
@@ -860,7 +885,7 @@ class TestMergePR(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8),
|
||||
expected_head_sha="abc123", do="squash",
|
||||
@@ -893,7 +918,7 @@ class TestMergePR(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8),
|
||||
expected_changed_files=["b.py", "a.py"],
|
||||
@@ -914,7 +939,7 @@ class TestMergePR(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8),
|
||||
expected_head_sha="abc123", remote="prgs",
|
||||
@@ -945,7 +970,7 @@ class TestMergePR(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8),
|
||||
expected_head_sha="abc123", remote="prgs",
|
||||
@@ -962,7 +987,7 @@ class TestMergePR(unittest.TestCase):
|
||||
def test_missing_confirmation_blocks_with_no_api_call(self, _auth, mock_api):
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(pr_number=8, confirmation="", expected_head_sha="abc123", remote="prgs")
|
||||
self.assertFalse(r["performed"])
|
||||
self.assertTrue(any("explicit confirmation required" in x for x in r["reasons"]))
|
||||
@@ -973,7 +998,7 @@ class TestMergePR(unittest.TestCase):
|
||||
def test_wrong_confirmation_blocks(self, _auth, mock_api):
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(pr_number=8, confirmation="MERGE PR 9", expected_head_sha="abc123", remote="prgs")
|
||||
self.assertFalse(r["performed"])
|
||||
mock_api.assert_not_called()
|
||||
@@ -985,7 +1010,7 @@ class TestMergePR(unittest.TestCase):
|
||||
def test_reviewer_profile_cannot_merge(self, _auth, mock_api):
|
||||
env = {"GITEA_PROFILE_NAME": "prgs-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,approve"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
with self.assertRaises(RuntimeError) as ctx:
|
||||
gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8), remote="prgs"
|
||||
@@ -999,7 +1024,7 @@ class TestMergePR(unittest.TestCase):
|
||||
mock_api.side_effect = [{"login": "jcwalker3"}, self._pr("jcwalker3")]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8), remote="prgs")
|
||||
self.assertFalse(r["performed"])
|
||||
@@ -1010,7 +1035,7 @@ class TestMergePR(unittest.TestCase):
|
||||
def test_unknown_identity_blocks(self, _auth):
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8), remote="prgs")
|
||||
self.assertFalse(r["performed"])
|
||||
@@ -1022,7 +1047,7 @@ class TestMergePR(unittest.TestCase):
|
||||
def test_unknown_profile_blocks(self, _auth, mock_api):
|
||||
mock_api.side_effect = [{"login": "merger-bot"}, self._pr("author-bot")]
|
||||
env = {} # no GITEA_ALLOWED_OPERATIONS → empty allowed ops
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8), remote="prgs")
|
||||
self.assertFalse(r["performed"])
|
||||
@@ -1094,7 +1119,7 @@ class TestMergePR(unittest.TestCase):
|
||||
def test_profile_without_merge_permission_blocks(self, _auth, mock_api):
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
with self.assertRaises(RuntimeError) as ctx:
|
||||
gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8), remote="prgs")
|
||||
@@ -1110,7 +1135,7 @@ class TestMergePR(unittest.TestCase):
|
||||
{"login": "merger-bot"}, self._pr("author-bot", state="closed")]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8), remote="prgs")
|
||||
self.assertFalse(r["performed"])
|
||||
@@ -1124,7 +1149,7 @@ class TestMergePR(unittest.TestCase):
|
||||
{"login": "merger-bot"}, self._pr("author-bot", mergeable=False)]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8), remote="prgs")
|
||||
self.assertFalse(r["performed"])
|
||||
@@ -1138,7 +1163,7 @@ class TestMergePR(unittest.TestCase):
|
||||
{"login": "merger-bot"}, self._pr("author-bot", mergeable=None)]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8), remote="prgs")
|
||||
self.assertFalse(r["performed"])
|
||||
@@ -1156,7 +1181,7 @@ class TestMergePR(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8),
|
||||
expected_head_sha="deadbeef", remote="prgs")
|
||||
@@ -1177,7 +1202,7 @@ class TestMergePR(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8),
|
||||
expected_changed_files=["a.py", "b.py"],
|
||||
@@ -1210,7 +1235,7 @@ class TestMergePR(unittest.TestCase):
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge",
|
||||
"GITEA_TOKEN": "super-secret-token"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8),
|
||||
expected_head_sha="abc123", remote="prgs",
|
||||
@@ -1229,7 +1254,7 @@ class TestMergePR(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8),
|
||||
expected_head_sha="abc123", remote="prgs",
|
||||
@@ -1252,7 +1277,7 @@ class TestMergePR(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8), remote="prgs",
|
||||
expected_head_sha=new_sha)
|
||||
@@ -1275,7 +1300,7 @@ class TestMergePR(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8),
|
||||
expected_head_sha="abc123", remote="prgs",
|
||||
@@ -1307,7 +1332,7 @@ class TestMergePR(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_merge_pr(
|
||||
pr_number=8, confirmation=self._confirm(8),
|
||||
expected_head_sha="abc123", remote="prgs",
|
||||
@@ -1614,10 +1639,8 @@ class TestCommitFiles(unittest.TestCase):
|
||||
files = [
|
||||
{"operation": "create", "path": "test.txt", "content": "SGVsbG8="}
|
||||
]
|
||||
env = {
|
||||
"GITEA_ALLOWED_OPERATIONS": "gitea.repo.commit",
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
env = shared_mutation_env("test-author-dadeschools")
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
result = gitea_commit_files(
|
||||
files=files,
|
||||
message="Initial commit",
|
||||
@@ -1732,7 +1755,7 @@ class TestRuntimeProfile(unittest.TestCase):
|
||||
"GITEA_ALLOWED_OPERATIONS": "read, review , approve",
|
||||
"GITEA_BASE_URL": "https://gitea.example.invalid",
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
p = get_profile()
|
||||
self.assertEqual(p["profile_name"], "gitea-reviewer")
|
||||
self.assertEqual(p["allowed_operations"], ["read", "review", "approve"])
|
||||
@@ -1743,7 +1766,7 @@ class TestRuntimeProfile(unittest.TestCase):
|
||||
"GITEA_PROFILE_NAME": "gitea-author",
|
||||
"GITEA_TOKEN": "super-secret-token",
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
p = get_profile()
|
||||
blob = repr(p).lower()
|
||||
# The token VALUE must never appear. (The field name
|
||||
@@ -1760,7 +1783,7 @@ class TestRuntimeProfile(unittest.TestCase):
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve",
|
||||
"GITEA_TOKEN": "super-secret-token",
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
result = gitea_whoami(remote="prgs")
|
||||
self.assertEqual(result["profile"]["profile_name"], "gitea-reviewer")
|
||||
self.assertEqual(
|
||||
@@ -1781,7 +1804,7 @@ class TestRuntimeProfile(unittest.TestCase):
|
||||
"GITEA_AUDIT_LABEL": "reviewer-runtime",
|
||||
"GITEA_TOKEN_SOURCE": "keychain:prgs-reviewer-token",
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
result = gitea_whoami(remote="prgs")
|
||||
profile = result["profile"]
|
||||
self.assertEqual(profile["environment"], None)
|
||||
@@ -1850,7 +1873,7 @@ class TestProfileDiscovery(unittest.TestCase):
|
||||
"GITEA_AUDIT_LABEL": "reviewer-runtime",
|
||||
"GITEA_TOKEN_SOURCE": "GITEA_TOKEN",
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
p = get_profile()
|
||||
self.assertEqual(p["forbidden_operations"], ["merge", "branch.push"])
|
||||
self.assertEqual(p["audit_label"], "reviewer-runtime")
|
||||
@@ -1866,7 +1889,7 @@ class TestProfileDiscovery(unittest.TestCase):
|
||||
"GITEA_TOKEN_SOURCE": "GITEA_TOKEN",
|
||||
"GITEA_TOKEN": "super-secret-token",
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
result = gitea_get_profile(remote="prgs")
|
||||
self.assertEqual(result["profile_name"], "gitea-reviewer")
|
||||
self.assertEqual(result["allowed_operations"], ["read", "review", "approve"])
|
||||
@@ -1887,7 +1910,7 @@ class TestProfileDiscovery(unittest.TestCase):
|
||||
def test_discovery_never_exposes_secrets(self, _auth, mock_api):
|
||||
mock_api.return_value = {"id": 3, "login": "reviewer-bot"}
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer", "GITEA_TOKEN": "super-secret-token"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
result = gitea_get_profile(remote="prgs")
|
||||
blob = repr(result).lower()
|
||||
for secret in ("super-secret-token", "token dgvzd", "authorization", "basic ", FAKE_AUTH.lower()):
|
||||
@@ -1969,7 +1992,7 @@ class TestPrEligibility(unittest.TestCase):
|
||||
mock_api.side_effect = [{"login": "reviewer-bot"}, self._pr("author-bot")]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_check_pr_eligibility(pr_number=5, action="review", remote="prgs")
|
||||
self.assertTrue(r["eligible"])
|
||||
self.assertEqual(r["authenticated_user"], "reviewer-bot")
|
||||
@@ -1982,7 +2005,7 @@ class TestPrEligibility(unittest.TestCase):
|
||||
mock_api.side_effect = [{"login": "jcwalker3"}, self._pr("jcwalker3")]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_check_pr_eligibility(pr_number=8, action="merge", remote="prgs")
|
||||
self.assertFalse(r["eligible"])
|
||||
self.assertIn("authenticated user is PR author", r["reasons"])
|
||||
@@ -1993,7 +2016,7 @@ class TestPrEligibility(unittest.TestCase):
|
||||
mock_api.side_effect = [{"login": "jcwalker3"}, self._pr("jcwalker3")]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_check_pr_eligibility(pr_number=8, action="approve", remote="prgs")
|
||||
self.assertFalse(r["eligible"])
|
||||
self.assertIn("authenticated user is PR author", r["reasons"])
|
||||
@@ -2008,7 +2031,7 @@ class TestPrEligibility(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_check_pr_eligibility(pr_number=8, action="merge", remote="prgs")
|
||||
self.assertFalse(r["eligible"])
|
||||
self.assertIsNone(r["mergeable"])
|
||||
@@ -2020,7 +2043,7 @@ class TestPrEligibility(unittest.TestCase):
|
||||
mock_api.side_effect = [{"login": "author-bot"}, self._pr("someone-else")]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-author",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,pr.create"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_check_pr_eligibility(pr_number=8, action="merge", remote="prgs")
|
||||
self.assertFalse(r["eligible"])
|
||||
self.assertIn("profile is not allowed to merge", r["reasons"])
|
||||
@@ -2032,7 +2055,7 @@ class TestPrEligibility(unittest.TestCase):
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review",
|
||||
"GITEA_FORBIDDEN_OPERATIONS": "merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_check_pr_eligibility(pr_number=8, action="merge", remote="prgs")
|
||||
self.assertFalse(r["eligible"])
|
||||
self.assertIn("profile forbids 'merge'", r["reasons"])
|
||||
@@ -2043,7 +2066,7 @@ class TestPrEligibility(unittest.TestCase):
|
||||
mock_api.side_effect = [{"login": "reviewer-bot"}, self._pr("author-bot", state="closed")]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_check_pr_eligibility(pr_number=8, action="approve", remote="prgs")
|
||||
self.assertFalse(r["eligible"])
|
||||
self.assertIn("PR is not open (state=closed)", r["reasons"])
|
||||
@@ -2052,7 +2075,7 @@ class TestPrEligibility(unittest.TestCase):
|
||||
def test_unknown_identity_fails_closed(self, _auth):
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-merger",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_check_pr_eligibility(pr_number=8, action="merge", remote="prgs")
|
||||
self.assertFalse(r["eligible"])
|
||||
self.assertIn("authenticated identity could not be determined", r["reasons"])
|
||||
@@ -2071,7 +2094,7 @@ class TestPrEligibility(unittest.TestCase):
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review",
|
||||
"GITEA_TOKEN": "super-secret-token"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_check_pr_eligibility(pr_number=5, action="review", remote="prgs")
|
||||
blob = repr(r).lower()
|
||||
for secret in ("super-secret-token", "authorization", "basic ", FAKE_AUTH.lower()):
|
||||
@@ -2268,7 +2291,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
mock_api.side_effect = [{"login": "jcwalker3"}, self._pr("jcwalker3")]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_submit_pr_review(
|
||||
pr_number=8, action="approve", remote="prgs",
|
||||
final_review_decision_ready=True,
|
||||
@@ -2287,7 +2310,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_submit_pr_review(
|
||||
pr_number=8, action="approve", body="LGTM", remote="prgs",
|
||||
final_review_decision_ready=True,
|
||||
@@ -2317,7 +2340,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_submit_pr_review(
|
||||
pr_number=8, action="approve", body="LGTM", remote="prgs",
|
||||
final_review_decision_ready=True,
|
||||
@@ -2337,7 +2360,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_submit_pr_review(
|
||||
pr_number=8, action="approve", body="LGTM", remote="prgs",
|
||||
final_review_decision_ready=True,
|
||||
@@ -2363,7 +2386,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,request_changes"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_submit_pr_review(
|
||||
pr_number=8, action="request_changes",
|
||||
body="needs work", remote="prgs",
|
||||
@@ -2384,7 +2407,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
mock_api.side_effect = [{"login": "reviewer-bot"}, self._pr("author-bot")]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review"} # no request_changes
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_submit_pr_review(
|
||||
pr_number=8, action="request_changes", remote="prgs",
|
||||
final_review_decision_ready=True,
|
||||
@@ -2405,7 +2428,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
gitea_mark_final_review_decision(8, "comment", remote="prgs", expected_head_sha="abc123")
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_submit_pr_review(
|
||||
pr_number=8, action="comment", body="finding", remote="prgs",
|
||||
final_review_decision_ready=True,
|
||||
@@ -2423,7 +2446,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
gitea_mark_final_review_decision(
|
||||
8, "comment", remote="prgs", expected_head_sha="abc123",
|
||||
)
|
||||
@@ -2439,7 +2462,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
def test_unknown_identity_blocks(self, _auth):
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_submit_pr_review(
|
||||
pr_number=8, action="approve", remote="prgs",
|
||||
final_review_decision_ready=True,
|
||||
@@ -2454,7 +2477,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
mock_api.side_effect = [{"login": "reviewer-bot"}, self._pr("author-bot")]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-author",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,pr.create"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_submit_pr_review(
|
||||
pr_number=8, action="approve", remote="prgs",
|
||||
final_review_decision_ready=True,
|
||||
@@ -2473,7 +2496,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_submit_pr_review(
|
||||
pr_number=8, action="approve",
|
||||
expected_head_sha="abc123", remote="prgs",
|
||||
@@ -2497,7 +2520,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_submit_pr_review(
|
||||
pr_number=8, action="approve",
|
||||
expected_head_sha="abc123", remote="prgs",
|
||||
@@ -2526,7 +2549,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve",
|
||||
"GITEA_TOKEN": "super-secret-token"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
gitea_mark_final_review_decision(5, "approve", remote="prgs", expected_head_sha="abc123")
|
||||
r = gitea_submit_pr_review(
|
||||
pr_number=5, action="approve", remote="prgs",
|
||||
@@ -2546,7 +2569,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
]
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_submit_pr_review(
|
||||
pr_number=8, action="approve", remote="prgs",
|
||||
final_review_decision_ready=True,
|
||||
@@ -2623,7 +2646,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
try:
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
r = gitea_submit_pr_review(
|
||||
pr_number=8, action="approve", remote="prgs",
|
||||
final_review_decision_ready=True,
|
||||
@@ -2656,7 +2679,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve,request_changes"}
|
||||
with patch("mcp_server.gitea_get_pr_review_feedback",
|
||||
return_value=dict(_NO_BLOCKER_FEEDBACK)):
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
first = gitea_submit_pr_review(
|
||||
pr_number=8, action="approve", remote="prgs",
|
||||
final_review_decision_ready=True,
|
||||
@@ -2681,7 +2704,7 @@ class TestSubmitPrReview(unittest.TestCase):
|
||||
def test_remote_org_repo_validation_mismatch(self, _auth, mock_api):
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-reviewer",
|
||||
"GITEA_ALLOWED_OPERATIONS": "read,review,approve"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
# Mark decision for specific remote, org, repo
|
||||
gitea_mark_final_review_decision(8, "approve", remote="prgs", expected_head_sha="abc123", org="MyOrg", repo="MyRepo")
|
||||
|
||||
@@ -2719,13 +2742,29 @@ if __name__ == "__main__":
|
||||
class TestTrackerHygieneCleanup(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._mut_env = patch.dict(
|
||||
os.environ,
|
||||
shared_mutation_env("test-author-dadeschools"),
|
||||
clear=False,
|
||||
)
|
||||
self._mut_env.start()
|
||||
mcp_server.gitea_load_review_workflow()
|
||||
self.mock_api = patch("mcp_server.api_request").start()
|
||||
self.mock_auth = patch("mcp_server.get_auth_header", return_value=FAKE_AUTH).start()
|
||||
patch("gitea_audit.audit_enabled", return_value=True).start()
|
||||
self.mock_audit = patch("gitea_audit.write_event").start()
|
||||
# gitea.pr.close: closing a PR via gitea_edit_pr is capability-gated (#216).
|
||||
patch("mcp_server.get_profile", return_value={"profile_name": "test", "allowed_operations": ["read", "merge", "edit", "close", "gitea.pr.close", "gitea.issue.close"], "audit_label": "test", "forbidden_operations": []}).start()
|
||||
patch("mcp_server.get_profile", return_value={
|
||||
"profile_name": "test-author-dadeschools",
|
||||
"allowed_operations": [
|
||||
"read", "merge", "edit", "close",
|
||||
"gitea.pr.close", "gitea.issue.close", "gitea.read",
|
||||
],
|
||||
"audit_label": "test",
|
||||
"forbidden_operations": [],
|
||||
"allowed_repositories": ["913443/eAgenda"],
|
||||
"base_url": "https://gitea.dadeschools.net",
|
||||
}).start()
|
||||
patch("mcp_server._list_pr_lease_comments", return_value=[]).start()
|
||||
patch(
|
||||
"mcp_server._pr_work_lease_reviewer_block",
|
||||
@@ -2733,6 +2772,11 @@ class TestTrackerHygieneCleanup(unittest.TestCase):
|
||||
).start()
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
self._mut_env.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
patch.stopall()
|
||||
|
||||
def test_close_issue_removes_in_progress(self):
|
||||
@@ -3138,7 +3182,7 @@ class TestEndpointRedaction(unittest.TestCase):
|
||||
"GITEA_BASE_URL": "https://gitea.example.invalid",
|
||||
"GITEA_TOKEN_SOURCE": "keychain:some-item-id",
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
result = gitea_get_profile(remote="prgs",
|
||||
resolve_identity=False)
|
||||
blob = repr(result)
|
||||
@@ -3160,7 +3204,7 @@ class TestEndpointRedaction(unittest.TestCase):
|
||||
"GITEA_TOKEN_SOURCE": "keychain:some-item-id",
|
||||
"GITEA_MCP_REVEAL_ENDPOINTS": "1",
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
result = gitea_get_profile(remote="prgs",
|
||||
resolve_identity=False)
|
||||
self.assertEqual(result["server"], "https://gitea.prgs.cc")
|
||||
@@ -3172,7 +3216,7 @@ class TestEndpointRedaction(unittest.TestCase):
|
||||
try:
|
||||
env = {"GITEA_MCP_CONFIG": path,
|
||||
"GITEA_MCP_PROFILE": "prgs-author"}
|
||||
with patch.dict(os.environ, env, clear=True), \
|
||||
with patch.dict(os.environ, env, clear=False), \
|
||||
patch("gitea_config._keychain_token", return_value="x"):
|
||||
result = gitea_audit_config()
|
||||
finally:
|
||||
@@ -3260,7 +3304,7 @@ class TestIssueCommentTools(unittest.TestCase):
|
||||
def test_list_reveal_opt_in_includes_url(self, _auth, mock_api):
|
||||
mock_api.return_value = [self._comment()]
|
||||
env = dict(self.AUTHOR_ENV, GITEA_MCP_REVEAL_ENDPOINTS="1")
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
result = gitea_list_issue_comments(issue_number=9, remote="prgs")
|
||||
self.assertIn("issuecomment-101", result["comments"][0]["url"])
|
||||
|
||||
@@ -3269,7 +3313,7 @@ class TestIssueCommentTools(unittest.TestCase):
|
||||
def test_list_blocked_without_read_permission(self, _auth, mock_api):
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-writer-only",
|
||||
"GITEA_ALLOWED_OPERATIONS": "gitea.issue.comment"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
result = gitea_list_issue_comments(issue_number=9, remote="prgs")
|
||||
self.assertFalse(result["success"])
|
||||
self.assertTrue(result["reasons"])
|
||||
@@ -3316,7 +3360,7 @@ class TestIssueCommentTools(unittest.TestCase):
|
||||
def test_create_reveal_opt_in_includes_url(self, _auth, mock_api):
|
||||
mock_api.return_value = self._comment(555)
|
||||
env = dict(self.AUTHOR_ENV, GITEA_MCP_REVEAL_ENDPOINTS="1")
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
result = gitea_create_issue_comment(
|
||||
issue_number=9, body="posted", remote="prgs")
|
||||
self.assertIn("issuecomment-555", result["url"])
|
||||
@@ -3328,7 +3372,7 @@ class TestIssueCommentTools(unittest.TestCase):
|
||||
"GITEA_ALLOWED_OPERATIONS":
|
||||
"gitea.read,gitea.pr.review,gitea.pr.comment,"
|
||||
"gitea.pr.approve,gitea.pr.merge"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
result = gitea_create_issue_comment(
|
||||
issue_number=9, body="posted", remote="prgs")
|
||||
self.assertFalse(result["success"])
|
||||
@@ -3342,7 +3386,7 @@ class TestIssueCommentTools(unittest.TestCase):
|
||||
env = {"GITEA_PROFILE_NAME": "gitea-author",
|
||||
"GITEA_ALLOWED_OPERATIONS": "gitea.read,gitea.issue.comment",
|
||||
"GITEA_FORBIDDEN_OPERATIONS": "gitea.issue.comment"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
result = gitea_create_issue_comment(
|
||||
issue_number=9, body="posted", remote="prgs")
|
||||
self.assertFalse(result["success"])
|
||||
@@ -3478,7 +3522,7 @@ class TestIssueCommentPermissionSeparation(unittest.TestCase):
|
||||
"GITEA_ALLOWED_OPERATIONS":
|
||||
"gitea.branch.create,gitea.branch.push,gitea.pr.comment,"
|
||||
"gitea.pr.create,gitea.read,gitea.repo.commit"}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
result = gitea_create_issue_comment(
|
||||
issue_number=51, body="audit findings", remote="prgs")
|
||||
self.assertFalse(result["success"])
|
||||
@@ -3663,11 +3707,12 @@ class TestIssueLocking(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._lock_dir = tempfile.TemporaryDirectory()
|
||||
# Most locking tests target remote=prgs; host-align profile (#714).
|
||||
env = {
|
||||
**ISSUE_WRITE_ENV,
|
||||
**shared_mutation_env("test-author-prgs"),
|
||||
"GITEA_ISSUE_LOCK_DIR": self._lock_dir.name,
|
||||
}
|
||||
self._env_patcher = patch.dict(os.environ, env, clear=True)
|
||||
self._env_patcher = patch.dict(os.environ, env, clear=False)
|
||||
self._env_patcher.start()
|
||||
self._dup_fetcher_patcher = patch(
|
||||
"mcp_server.issue_duplicate_context_fetcher",
|
||||
@@ -3681,8 +3726,9 @@ class TestIssueLocking(unittest.TestCase):
|
||||
self._lock_dir.cleanup()
|
||||
|
||||
def _create_pr_env(self) -> dict:
|
||||
# PR-locking tests call create_pr with remote=prgs.
|
||||
return {
|
||||
**CREATE_PR_ENV,
|
||||
**shared_mutation_env("test-author-prgs"),
|
||||
"GITEA_ISSUE_LOCK_DIR": self._lock_dir.name,
|
||||
}
|
||||
|
||||
@@ -3701,7 +3747,7 @@ class TestIssueLocking(unittest.TestCase):
|
||||
self.assertIn("created_at", res["work_lease"])
|
||||
self.assertIn("expires_at", res["work_lease"])
|
||||
self.assertIn("last_heartbeat_at", res["work_lease"])
|
||||
self.assertEqual(res["work_lease"]["claimant"]["profile"], "gitea-default")
|
||||
self.assertIn(res["work_lease"]["claimant"]["profile"], ("gitea-default", "test-author-prgs", "prgs-author", "gitea-author"))
|
||||
self.assertIn("lock_file_path", res)
|
||||
lock = issue_lock_store.read_lock_file(res["lock_file_path"])
|
||||
self.assertIn("worktree_path", lock)
|
||||
@@ -3820,7 +3866,7 @@ class TestIssueLocking(unittest.TestCase):
|
||||
@patch("mcp_server.api_get_all", return_value=[])
|
||||
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
||||
def test_lock_issue_blocks_active_same_operation_lease(self, _auth, _api, _git_state):
|
||||
prgs_repo = mcp_server.REMOTES["prgs"]["repo"]
|
||||
prgs_repo = "Gitea-Tools" # #714 session-bound repo (not REMOTES Timesheet default)
|
||||
issue_lock_store.save_lock_file(
|
||||
issue_lock_store.lock_file_path(
|
||||
remote="prgs",
|
||||
@@ -3857,7 +3903,7 @@ class TestIssueLocking(unittest.TestCase):
|
||||
Dead-pid / missing-worktree reclaim is covered by issue_lock_store unit tests.
|
||||
This MCP path must remain fail-closed for sticky expired foreign ownership.
|
||||
"""
|
||||
prgs_repo = mcp_server.REMOTES["prgs"]["repo"]
|
||||
prgs_repo = "Gitea-Tools" # #714 session-bound repo (not REMOTES Timesheet default)
|
||||
# Present worktree + live pid => reclaim_allowed=False even though expires_at is past.
|
||||
sticky_worktree = tempfile.mkdtemp(prefix="gitea-sticky-lease-")
|
||||
self.addCleanup(lambda: shutil.rmtree(sticky_worktree, ignore_errors=True))
|
||||
@@ -4051,12 +4097,12 @@ class TestIssueLocking(unittest.TestCase):
|
||||
worktree = os.path.realpath(os.getcwd())
|
||||
with tempfile.TemporaryDirectory() as lock_dir:
|
||||
env = {**self._create_pr_env(), "GITEA_ISSUE_LOCK_DIR": lock_dir}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
with patch.dict(os.environ, env, clear=False):
|
||||
issue_lock_store.save_lock_file(
|
||||
issue_lock_store.lock_file_path(
|
||||
remote="prgs",
|
||||
org="Scaled-Tech-Consulting",
|
||||
repo=mcp_server.REMOTES["prgs"]["repo"],
|
||||
repo="Gitea-Tools", # #714 session-bound
|
||||
issue_number=447,
|
||||
lock_dir=lock_dir,
|
||||
),
|
||||
@@ -4065,7 +4111,7 @@ class TestIssueLocking(unittest.TestCase):
|
||||
branch_name="feat/issue-447-lock-provenance",
|
||||
remote="prgs",
|
||||
org="Scaled-Tech-Consulting",
|
||||
repo=mcp_server.REMOTES["prgs"]["repo"],
|
||||
repo="Gitea-Tools", # #714 session-bound
|
||||
worktree_path=worktree,
|
||||
lock_provenance=None,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user