Files
Gitea-Tools/tests/test_pr_ownership_issue_pr_mismatch.py
T
sysadmin c1c61e9b15 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.
2026-07-17 13:57:22 -04:00

197 lines
6.8 KiB
Python

"""#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()