Merge branch 'master' into feat/issue-727-pr-sync-status
Resolve task_capability_map conflict by keeping PR-sync entries and #725 merger-lease map entries. Fix author ownership for PR #728 (issue #727): prove lock via linked issue/session/branch instead of issue_number==pr_number. Add regression tests for 727/728 ownership and unrelated issue rejection.
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
"""#727 / PR #728: author ownership when tracking issue number != PR number.
|
||||
|
||||
Regression for REQUEST_CHANGES: gitea_update_pr_branch_by_merge must not require
|
||||
issue_number == pr_number. Ownership is proven via linked issue + lock/branch
|
||||
context and fails closed for unrelated issues.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import patch
|
||||
|
||||
import issue_lock_store
|
||||
import gitea_mcp_server as mcp
|
||||
|
||||
|
||||
def _live_lock(
|
||||
*,
|
||||
issue_number: int,
|
||||
branch_name: str = "feat/issue-727-pr-sync-status",
|
||||
worktree_path: str = "/tmp/branches/issue-727-pr-sync-status",
|
||||
remote: str = "prgs",
|
||||
org: str = "Scaled-Tech-Consulting",
|
||||
repo: str = "Gitea-Tools",
|
||||
) -> dict:
|
||||
now = datetime.now(timezone.utc)
|
||||
return {
|
||||
"issue_number": issue_number,
|
||||
"branch_name": branch_name,
|
||||
"worktree_path": worktree_path,
|
||||
"remote": remote,
|
||||
"org": org,
|
||||
"repo": repo,
|
||||
"operation_type": issue_lock_store.AUTHOR_ISSUE_WORK_LEASE,
|
||||
"acquired_at": now.isoformat(),
|
||||
"expires_at": (now + timedelta(hours=2)).isoformat(),
|
||||
"owner_pid": os.getpid(),
|
||||
"status": "active",
|
||||
}
|
||||
|
||||
|
||||
class TestAuthorOwnershipIssuePrMismatch(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._tmp = tempfile.TemporaryDirectory(prefix="gitea-issue-locks-")
|
||||
self.lock_dir = self._tmp.name
|
||||
os.environ["GITEA_ISSUE_LOCK_DIR"] = self.lock_dir
|
||||
|
||||
def tearDown(self):
|
||||
# Clear session pointer for this process.
|
||||
try:
|
||||
ptr = issue_lock_store.session_pointer_path(self.lock_dir)
|
||||
if os.path.isfile(ptr):
|
||||
os.unlink(ptr)
|
||||
except Exception:
|
||||
pass
|
||||
os.environ.pop("GITEA_ISSUE_LOCK_DIR", None)
|
||||
self._tmp.cleanup()
|
||||
|
||||
def test_issue_727_pr_728_session_lock_accepted(self):
|
||||
"""Tracking issue #727 lock is valid ownership for PR #728."""
|
||||
lock = _live_lock(issue_number=727)
|
||||
issue_lock_store.bind_session_lock(lock)
|
||||
result = mcp._prove_author_ownership_for_pr(
|
||||
pr_number=728,
|
||||
pr_title="feat: pr sync",
|
||||
pr_body="Closes #727\n\nNative PR sync lifecycle.",
|
||||
source_branch="feat/issue-727-pr-sync-status",
|
||||
remote="prgs",
|
||||
host=None,
|
||||
org="Scaled-Tech-Consulting",
|
||||
repo="Gitea-Tools",
|
||||
worktree_path=lock["worktree_path"],
|
||||
)
|
||||
self.assertTrue(result["proven"], result)
|
||||
self.assertTrue(result["has_author_lock"])
|
||||
self.assertEqual(result["matched_issue"], 727)
|
||||
self.assertEqual(result["matched_via"], "session_lock")
|
||||
self.assertIn(727, result["linked_issues"])
|
||||
|
||||
def test_issue_727_pr_728_durable_lock_accepted(self):
|
||||
lock = _live_lock(issue_number=727)
|
||||
path = issue_lock_store.lock_file_path(
|
||||
remote="prgs",
|
||||
org="Scaled-Tech-Consulting",
|
||||
repo="Gitea-Tools",
|
||||
issue_number=727,
|
||||
lock_dir=self.lock_dir,
|
||||
)
|
||||
issue_lock_store.save_lock_file(path, lock)
|
||||
result = mcp._prove_author_ownership_for_pr(
|
||||
pr_number=728,
|
||||
pr_title="feat: pr sync",
|
||||
pr_body="Fixes #727",
|
||||
source_branch="feat/issue-727-pr-sync-status",
|
||||
remote="prgs",
|
||||
host=None,
|
||||
org="Scaled-Tech-Consulting",
|
||||
repo="Gitea-Tools",
|
||||
)
|
||||
self.assertTrue(result["proven"], result)
|
||||
self.assertEqual(result["matched_issue"], 727)
|
||||
self.assertEqual(result["matched_via"], "durable_lock")
|
||||
|
||||
def test_legacy_same_number_still_accepted(self):
|
||||
lock = _live_lock(
|
||||
issue_number=100,
|
||||
branch_name="fix/issue-100",
|
||||
worktree_path="/tmp/branches/issue-100",
|
||||
)
|
||||
issue_lock_store.bind_session_lock(lock)
|
||||
result = mcp._prove_author_ownership_for_pr(
|
||||
pr_number=100,
|
||||
pr_title="fix something",
|
||||
pr_body="Closes #100",
|
||||
source_branch="fix/issue-100",
|
||||
remote="prgs",
|
||||
host=None,
|
||||
org="Scaled-Tech-Consulting",
|
||||
repo="Gitea-Tools",
|
||||
worktree_path=lock["worktree_path"],
|
||||
)
|
||||
self.assertTrue(result["proven"], result)
|
||||
self.assertEqual(result["matched_issue"], 100)
|
||||
|
||||
def test_unrelated_issue_lock_rejected(self):
|
||||
"""Lock for issue #999 must not authorize PR #728 linked only to #727."""
|
||||
lock = _live_lock(
|
||||
issue_number=999,
|
||||
branch_name="feat/issue-999",
|
||||
worktree_path="/tmp/branches/issue-999",
|
||||
)
|
||||
issue_lock_store.bind_session_lock(lock)
|
||||
path = issue_lock_store.lock_file_path(
|
||||
remote="prgs",
|
||||
org="Scaled-Tech-Consulting",
|
||||
repo="Gitea-Tools",
|
||||
issue_number=999,
|
||||
lock_dir=self.lock_dir,
|
||||
)
|
||||
issue_lock_store.save_lock_file(path, lock)
|
||||
result = mcp._prove_author_ownership_for_pr(
|
||||
pr_number=728,
|
||||
pr_title="feat: pr sync",
|
||||
pr_body="Closes #727",
|
||||
source_branch="feat/issue-727-pr-sync-status",
|
||||
remote="prgs",
|
||||
host=None,
|
||||
org="Scaled-Tech-Consulting",
|
||||
repo="Gitea-Tools",
|
||||
worktree_path=lock["worktree_path"],
|
||||
)
|
||||
self.assertFalse(result["proven"], result)
|
||||
self.assertFalse(result["has_author_lock"])
|
||||
self.assertIsNone(result["matched_issue"])
|
||||
self.assertTrue(result["reasons"])
|
||||
|
||||
def test_branch_mismatch_on_session_lock_fail_closed(self):
|
||||
lock = _live_lock(
|
||||
issue_number=727,
|
||||
branch_name="feat/other-branch",
|
||||
)
|
||||
issue_lock_store.bind_session_lock(lock)
|
||||
result = mcp._prove_author_ownership_for_pr(
|
||||
pr_number=728,
|
||||
pr_title="feat: pr sync",
|
||||
pr_body="Closes #727",
|
||||
source_branch="feat/issue-727-pr-sync-status",
|
||||
remote="prgs",
|
||||
host=None,
|
||||
org="Scaled-Tech-Consulting",
|
||||
repo="Gitea-Tools",
|
||||
worktree_path=lock["worktree_path"],
|
||||
)
|
||||
self.assertFalse(result["proven"], result)
|
||||
self.assertTrue(any("branch" in r for r in result["reasons"]))
|
||||
|
||||
def test_no_lock_fail_closed(self):
|
||||
result = mcp._prove_author_ownership_for_pr(
|
||||
pr_number=728,
|
||||
pr_title="feat: pr sync",
|
||||
pr_body="Closes #727",
|
||||
source_branch="feat/issue-727-pr-sync-status",
|
||||
remote="prgs",
|
||||
host=None,
|
||||
org="Scaled-Tech-Consulting",
|
||||
repo="Gitea-Tools",
|
||||
)
|
||||
self.assertFalse(result["proven"], result)
|
||||
self.assertTrue(any("no live author issue lock" in r for r in result["reasons"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user