feat: harden issue lock store against concurrent session clobbering (Closes #438)

Replace read-modify-write on the global /tmp/gitea_issue_lock.json pointer with
atomic per-issue locks under GITEA_ISSUE_LOCK_DIR using flock serialization,
PID/session metadata, fail-closed stale recovery, and pre-mutation freshness
re-checks in gitea_create_pr. Queue inventory now surfaces live_issue_locks.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
2026-07-07 16:25:30 -04:00
co-authored by Claude Opus 4.8
parent 89a7d4dbfc
commit 992d055998
7 changed files with 829 additions and 50 deletions
+28 -3
View File
@@ -3042,13 +3042,38 @@ class TestIssueLocking(unittest.TestCase):
"""Test issue locking and PR gating constraints."""
def setUp(self):
self._env_patcher = patch.dict(os.environ, ISSUE_WRITE_ENV, clear=True)
import shutil
import tempfile
self._lock_dir = tempfile.mkdtemp(prefix="mcp-issue-lock-")
self._session_lock = os.path.join(self._lock_dir, "session.json")
env = {
**ISSUE_WRITE_ENV,
"GITEA_ISSUE_LOCK_DIR": self._lock_dir,
"GITEA_ISSUE_LOCK_FILE": self._session_lock,
}
self._env_patcher = patch.dict(os.environ, env, clear=True)
self._env_patcher.start()
self._patchers = [
patch("mcp_server.ISSUE_LOCK_FILE", self._session_lock),
patch("mcp_server.issue_lock_store.DEFAULT_LOCK_DIR", self._lock_dir),
patch("mcp_server.issue_lock_store.LEGACY_LOCK_FILE", self._session_lock),
patch("tests.test_mcp_server.ISSUE_LOCK_FILE", self._session_lock),
patch(
"mcp_server.issue_lock_worktree.read_worktree_git_state",
return_value=_clean_master_git_state_for_lock(),
),
]
for patcher in self._patchers:
patcher.start()
def tearDown(self):
import shutil
for patcher in reversed(getattr(self, "_patchers", [])):
patcher.stop()
self._env_patcher.stop()
if os.path.exists(ISSUE_LOCK_FILE):
os.remove(ISSUE_LOCK_FILE)
shutil.rmtree(getattr(self, "_lock_dir", ""), ignore_errors=True)
@patch(
"mcp_server.issue_lock_worktree.read_worktree_git_state",