Merge branch 'master' into feat/issue-610-live-remote-parity

This commit is contained in:
2026-07-22 04:30:44 -05:00
9 changed files with 1847 additions and 27 deletions
@@ -91,6 +91,132 @@ def test_compound_command_detects_the_kill_half():
assert result["contamination"] is True
# ── #787: background separator and subshell forms reach the classifier ───────
def test_background_separator_kill_is_contamination():
result = guard.classify_recovery_command("sleep 1 & pkill -f mcp_server.py")
assert result["process_kill"] is True
assert result["contamination"] is True
assert result["reason_class"] == guard.REASON_MANUAL_DAEMON_KILL
assert result["ambiguous"] is False
def test_subshell_wrapped_kill_is_contamination():
result = guard.classify_recovery_command("(pkill -f mcp_server.py)")
assert result["process_kill"] is True
assert result["contamination"] is True
assert result["reason_class"] == guard.REASON_MANUAL_DAEMON_KILL
assert result["ambiguous"] is False
def test_further_background_and_subshell_forms_are_contamination():
for command in (
"pkill -f mcp_server.py &",
"( sudo pkill -f mcp_server.py )",
"((pkill -f gitea_mcp_server))",
"sleep 1 & killall mcp_server",
"(ps aux | grep mcp_server) & pkill -f mcp_server.py",
):
result = guard.classify_recovery_command(command)
assert result["contamination"] is True, command
assert result["reason_class"] == guard.REASON_MANUAL_DAEMON_KILL, command
def test_logical_operators_are_not_split_into_single_characters():
# ``&&``/``||`` must still be consumed whole by the separator scan.
assert guard._split_segments("a && b || c") == ["a", "b", "c"]
assert guard._split_segments("a & b") == ["a", "b"]
assert guard._split_segments("(a)") == ["a"]
assert guard._split_segments("a; b\nc | d") == ["a", "b", "c", "d"]
# ── #789 F1: separators only separate outside quoted or escaped text ─────────
# The three commands the PR #789 review measured as regressions at head
# 6b58f04: each merely *mentions* the canonical kill string inside quotes.
F1_QUOTED_COMMANDS = (
'git commit -m "block sleep 1 & pkill -f mcp_server.py as recovery"',
'echo "docs: sleep 1 & pkill -f mcp_server.py is now detected"',
'grep -rn "sleep 1 & pkill -f mcp_server.py" docs/',
)
def test_quoted_ampersand_examples_from_review_f1_are_not_kills():
for command in F1_QUOTED_COMMANDS:
result = guard.classify_recovery_command(command)
assert result["process_kill"] is False, command
assert result["contamination"] is False, command
assert result["reason_class"] is None, command
def test_ampersand_inside_double_quotes_is_not_a_separator():
assert guard._split_segments('echo "a & b"') == ['echo "a & b"']
result = guard.classify_recovery_command(
'echo "restart it: sleep 1 & pkill -f mcp_server.py"'
)
assert result["process_kill"] is False
assert result["contamination"] is False
def test_ampersand_inside_single_quotes_is_not_a_separator():
assert guard._split_segments("echo 'a & b'") == ["echo 'a & b'"]
result = guard.classify_recovery_command(
"git commit -m 'sleep 1 & pkill -f mcp_server.py stays quoted'"
)
assert result["process_kill"] is False
assert result["contamination"] is False
def test_backslash_escaped_ampersand_is_not_a_separator():
command = r"echo a \& pkill -f mcp_server.py"
assert guard._split_segments(command) == [command]
result = guard.classify_recovery_command(command)
assert result["process_kill"] is False
assert result["contamination"] is False
def test_backslash_does_not_escape_inside_single_quotes():
# POSIX: a backslash is literal inside single quotes, so the closing quote
# still closes and the following ``&`` is a genuinely active separator.
command = r"echo 'a\' & pkill -f mcp_server.py"
assert guard._split_segments(command) == [r"echo 'a\'", "pkill -f mcp_server.py"]
assert guard.classify_recovery_command(command)["contamination"] is True
def test_quote_awareness_also_retires_the_pre_existing_semicolon_and_pipe_cases():
# ``;`` and ``|`` misclassified quoted text before #787 as well. The fix is
# the quote-unawareness, not the ``&`` instance the issue happens to name.
for command in (
'git commit -m "fix; pkill -f mcp_server.py"',
'git commit -m "fix | pkill -f mcp_server.py"',
):
result = guard.classify_recovery_command(command)
assert result["process_kill"] is False, command
assert result["contamination"] is False, command
# ── #789 F3: subshell stripping and redirection stay syntactically honest ────
def test_command_substitution_is_not_mangled_by_subshell_stripping():
# Only a wrapper this call opened may be unwrapped; a ``)`` closing ``$(``
# must survive intact.
assert guard._strip_subshell("kill $(pgrep -f myapp)") == "kill $(pgrep -f myapp)"
result = guard.classify_recovery_command("kill $(pgrep -f myapp)")
assert result["contamination"] is False
assert result["ambiguous"] is True
def test_redirection_is_not_treated_as_a_background_separator():
assert guard._split_segments("a 2>&1") == ["a 2>&1"]
assert guard._split_segments("a &> log") == ["a &> log"]
assert guard._split_segments("pkill -f mcp_server.py 2>&1") == [
"pkill -f mcp_server.py 2>&1"
]
result = guard.classify_recovery_command("pkill -f mcp_server.py 2>&1")
assert result["contamination"] is True
assert result["reason_class"] == guard.REASON_MANUAL_DAEMON_KILL
# ── no false positives ───────────────────────────────────────────────────────
def test_read_only_inspection_is_not_a_kill():
@@ -112,6 +238,22 @@ def test_unrelated_pkill_target_is_not_contamination():
assert result["ambiguous"] is False
def test_user_scoped_pkill_of_unrelated_app_is_not_contamination():
# ``-u`` consumes ``mcpuser``; the surviving operand names no daemon (#787).
result = guard.classify_recovery_command("pkill -u mcpuser -f myapp")
assert result["process_kill"] is True
assert result["contamination"] is False
assert result["ambiguous"] is False
def test_commit_message_quoting_the_kill_string_is_not_a_kill():
result = guard.classify_recovery_command(
'git commit -m "block pkill -f mcp_server.py as workflow recovery"'
)
assert result["process_kill"] is False
assert result["contamination"] is False
def test_bare_kill_of_unknown_pid_is_ambiguous_not_contamination():
result = guard.classify_recovery_command("kill 31337")
assert result["contamination"] is False
@@ -333,6 +475,43 @@ def test_record_tool_marks_manual_daemon_kill():
assert "mcp_server.py" in loaded["command_summary"]
def test_record_tool_marks_background_separator_kill():
_clear_marker()
res = srv.gitea_record_daemon_process_kill_attempt(
command="sleep 1 & pkill -f mcp_server.py", remote="prgs"
)
assert res["contaminated"] is True
assert res["marked"] is True
assert res["marker"]["reason_class"] == guard.REASON_MANUAL_DAEMON_KILL
loaded = srv._load_runtime_recovery_marker("prgs")
assert loaded is not None
assert "mcp_server.py" in loaded["command_summary"]
def test_record_tool_marks_subshell_wrapped_kill():
_clear_marker()
res = srv.gitea_record_daemon_process_kill_attempt(
command="(pkill -f mcp_server.py)", remote="prgs"
)
assert res["contaminated"] is True
assert res["marked"] is True
assert res["marker"]["reason_class"] == guard.REASON_MANUAL_DAEMON_KILL
loaded = srv._load_runtime_recovery_marker("prgs")
assert loaded is not None
assert "mcp_server.py" in loaded["command_summary"]
def test_record_tool_does_not_mark_a_quoted_mention_of_the_kill_string():
# The marker is what fails review/merge/close closed and only a reconciler
# may clear it, so a quoted mention must never create one (PR #789 F1).
for command in F1_QUOTED_COMMANDS:
_clear_marker()
res = srv.gitea_record_daemon_process_kill_attempt(command=command, remote="prgs")
assert res["contaminated"] is False, command
assert res["marked"] is False, command
assert srv._load_runtime_recovery_marker("prgs") is None, command
def test_record_tool_marks_broad_sweep():
_clear_marker()
res = srv.gitea_record_daemon_process_kill_attempt(
@@ -0,0 +1,447 @@
"""Exact-owner renewal of an expired author issue lease (#760).
Covers the renewal disposition that lets the exact recorded owner re-acquire
its own lock after the wall-clock lease expires — including while the recording
MCP daemon PID is still alive — plus every rejection condition that must keep
failing closed, and the pre-existing dead-PID and live-foreign dispositions
that must remain untouched.
"""
import inspect
import os
import subprocess
import sys
import tempfile
import unittest
from datetime import datetime, timedelta, timezone
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent))
import issue_lock_renewal # noqa: E402
import issue_lock_store # noqa: E402
ISSUE = 5150
BRANCH = f"fix/issue-{ISSUE}-demo"
WORKTREE = "/scratch/wt-5150"
HEAD = "c" * 40
OTHER_SHA = "d" * 40
IDENTITY = "example-user"
PROFILE = "example-author"
REMOTE = "prgs"
ORG = "ExampleOrg"
REPO = "ExampleRepo"
def dead_pid() -> int:
"""A PID that has certainly exited (spawned, then reaped)."""
proc = subprocess.Popen([sys.executable, "-c", "pass"])
proc.wait()
return proc.pid
def past_ts(hours: int = 1) -> str:
return (
(datetime.now(timezone.utc) - timedelta(hours=hours))
.isoformat()
.replace("+00:00", "Z")
)
def future_ts(hours: int = 4) -> str:
return (
(datetime.now(timezone.utc) + timedelta(hours=hours))
.isoformat()
.replace("+00:00", "Z")
)
def make_lock(*, expires_at: str | None = None, pid: int | None = None, **overrides):
"""An expired lock owned by a still-alive daemon PID — the #760 condition."""
lock = {
"issue_number": ISSUE,
"branch_name": BRANCH,
"worktree_path": WORKTREE,
"remote": REMOTE,
"org": ORG,
"repo": REPO,
# os.getpid() is unambiguously alive: the whole point of #760 is that
# daemon liveness is not evidence of an active author task.
"session_pid": os.getpid() if pid is None else pid,
"lock_generation": 3,
"work_lease": {
"operation_type": issue_lock_store.AUTHOR_ISSUE_WORK_LEASE,
"issue_number": ISSUE,
"branch": BRANCH,
"worktree_path": WORKTREE,
"claimant": {"username": IDENTITY, "profile": PROFILE},
"created_at": past_ts(5),
"expires_at": expires_at or past_ts(),
},
}
lease_overrides = overrides.pop("work_lease", None)
if lease_overrides:
lock["work_lease"].update(lease_overrides)
lock.update(overrides)
return lock
def assess(lock=None, **overrides):
"""Run the assessor with all-passing evidence unless overridden."""
kwargs = {
"issue_number": ISSUE,
"branch_name": BRANCH,
"worktree_path": WORKTREE,
"remote": REMOTE,
"org": ORG,
"repo": REPO,
"identity": IDENTITY,
"profile": PROFILE,
"current_branch": BRANCH,
"porcelain_status": "",
"worktree_exists": True,
"head_sha": HEAD,
"remote_head_sha": HEAD,
"pr_head_sha": None,
"pr_number": None,
"competing_live_locks": [],
"candidate_branches": [BRANCH],
"current_pid": 4242,
}
kwargs.update(overrides)
return issue_lock_renewal.assess_exact_owner_lease_renewal(
make_lock() if lock is None else lock, **kwargs
)
class ExactOwnerRenewalGranted(unittest.TestCase):
"""AC1/AC3-AC7: the positive path."""
def test_expired_lease_alive_pid_exact_owner_is_renewable(self):
result = assess()
self.assertEqual(result["outcome"], issue_lock_renewal.RENEWAL_SANCTIONED)
self.assertTrue(result["renewal_sanctioned"])
self.assertTrue(result["is_candidate"])
def test_renewal_holds_when_owning_pr_head_matches(self):
result = assess(pr_number=999, pr_head_sha=HEAD)
self.assertTrue(result["renewal_sanctioned"])
def test_evidence_records_both_sides_of_the_transition(self):
result = assess()
evidence = result["evidence"]
self.assertEqual(evidence["prior_pid"], os.getpid())
self.assertTrue(evidence["prior_pid_alive"])
self.assertEqual(evidence["replacement_pid"], 4242)
self.assertTrue(evidence["prior_expires_at"])
class ExactOwnerRenewalRefused(unittest.TestCase):
"""AC3-AC8: every near-match must fail closed, one reason at a time."""
def _refused(self, **overrides):
result = assess(**overrides)
self.assertEqual(result["outcome"], issue_lock_renewal.REFUSED)
self.assertFalse(result["renewal_sanctioned"])
self.assertTrue(result["reasons"])
return result
def test_different_branch_refused(self):
result = self._refused(branch_name=f"fix/issue-{ISSUE}-other")
self.assertTrue(any("branch" in r for r in result["reasons"]))
def test_different_worktree_refused(self):
result = self._refused(worktree_path="/scratch/somewhere-else")
self.assertTrue(any("worktree" in r for r in result["reasons"]))
def test_different_claimant_refused(self):
result = self._refused(identity="someone-else")
self.assertTrue(any("claimant" in r for r in result["reasons"]))
def test_different_profile_refused(self):
result = self._refused(profile="other-author")
self.assertTrue(any("profile" in r for r in result["reasons"]))
def test_different_remote_org_or_repo_refused(self):
self._refused(remote="dadeschools")
self._refused(org="OtherOrg")
self._refused(repo="OtherRepo")
def test_dirty_worktree_refused(self):
result = self._refused(porcelain_status=" M gitea_mcp_server.py\n")
self.assertTrue(any("uncommitted" in r for r in result["reasons"]))
def test_missing_worktree_refused(self):
result = self._refused(worktree_exists=False)
self.assertTrue(any("does not exist" in r for r in result["reasons"]))
def test_worktree_on_wrong_branch_refused(self):
self._refused(current_branch="master")
def test_local_and_remote_head_mismatch_refused(self):
result = self._refused(remote_head_sha=OTHER_SHA)
self.assertTrue(
any("does not equal remote head" in r for r in result["reasons"])
)
def test_unpublished_branch_refused(self):
result = self._refused(remote_head_sha=None)
self.assertTrue(any("remote branch head" in r for r in result["reasons"]))
def test_pr_head_mismatch_refused(self):
result = self._refused(pr_number=999, pr_head_sha=OTHER_SHA)
self.assertTrue(any("does not equal local" in r for r in result["reasons"]))
def test_unobservable_pr_head_refused(self):
self._refused(pr_number=999, pr_head_sha=None)
def test_competing_live_lock_on_same_issue_refused(self):
result = self._refused(
competing_live_locks=[
{"issue_number": ISSUE, "branch_name": BRANCH, "pid": 777}
]
)
self.assertTrue(any("live lock" in r for r in result["reasons"]))
def test_competing_live_lock_holding_the_branch_refused(self):
self._refused(
competing_live_locks=[
{"issue_number": 111, "branch_name": BRANCH, "worktree_path": ""}
]
)
def test_competing_branch_claim_refused(self):
result = self._refused(candidate_branches=[BRANCH, f"feat/issue-{ISSUE}-rival"])
self.assertTrue(any("issue marker" in r for r in result["reasons"]))
def test_malformed_durable_lock_refused(self):
lock = make_lock()
lock["worktree_path"] = ""
result = assess(lock)
self.assertEqual(result["outcome"], issue_lock_renewal.REFUSED)
def test_lock_without_recorded_claimant_refused(self):
lock = make_lock()
lock["work_lease"]["claimant"] = {}
result = assess(lock)
self.assertEqual(result["outcome"], issue_lock_renewal.REFUSED)
class NotARenewalCandidate(unittest.TestCase):
"""AC12 and scope: situations renewal must decline to judge at all."""
def test_live_foreign_lease_is_never_a_candidate(self):
lock = make_lock(expires_at=future_ts())
result = assess(lock, identity="someone-else")
self.assertEqual(result["outcome"], issue_lock_renewal.NO_CANDIDATE)
self.assertFalse(result["renewal_sanctioned"])
def test_unexpired_lease_is_never_a_candidate(self):
lock = make_lock(expires_at=future_ts())
result = assess(lock)
self.assertEqual(result["outcome"], issue_lock_renewal.NO_CANDIDATE)
def test_dead_pid_under_unexpired_lease_stays_with_753(self):
"""The opposite trigger; #760 must not re-own it."""
lock = make_lock(expires_at=future_ts(), pid=dead_pid())
result = assess(lock)
self.assertEqual(result["outcome"], issue_lock_renewal.NO_CANDIDATE)
def test_absent_lock_is_not_a_candidate(self):
result = assess({})
self.assertEqual(result["outcome"], issue_lock_renewal.NO_CANDIDATE)
def test_different_issue_is_not_a_candidate(self):
lock = make_lock()
lock["issue_number"] = ISSUE + 1
result = assess(lock)
self.assertEqual(result["outcome"], issue_lock_renewal.NO_CANDIDATE)
def test_different_operation_type_is_not_a_candidate(self):
lock = make_lock()
lock["work_lease"]["operation_type"] = "review_pr_work"
result = assess(lock)
self.assertEqual(result["outcome"], issue_lock_renewal.NO_CANDIDATE)
class DaemonPidIsNotTaskLiveness(unittest.TestCase):
"""AC16: a live recorded PID is never, by itself, authorization."""
def test_alive_pid_alone_does_not_authorize_renewal(self):
# Every ownership fact except the live PID is wrong.
result = assess(identity="someone-else", branch_name="fix/issue-1-nope")
self.assertEqual(result["outcome"], issue_lock_renewal.REFUSED)
self.assertTrue(result["evidence"]["prior_pid_alive"])
def test_renewal_does_not_require_a_dead_pid(self):
result = assess()
self.assertTrue(result["evidence"]["prior_pid_alive"])
self.assertTrue(result["renewal_sanctioned"])
def test_dead_pid_does_not_block_an_otherwise_exact_owner(self):
lock = make_lock(pid=dead_pid())
result = assess(lock)
self.assertTrue(result["renewal_sanctioned"])
class ConflictGateOrdering(unittest.TestCase):
"""AC2: the same-owner allowance is reachable on an expired lease.
These cases need a worktree that genuinely exists on disk. The #601 reclaim
affordance already permits takeover when the recorded worktree is missing,
so a fictional path would satisfy the gate for the wrong reason and never
exercise the ordering defect this issue is about.
"""
@classmethod
def setUpClass(cls):
cls._tmp = tempfile.TemporaryDirectory()
cls.worktree = cls._tmp.name
@classmethod
def tearDownClass(cls):
cls._tmp.cleanup()
def present_lock(self, **overrides):
return make_lock(worktree_path=self.worktree, **overrides)
def test_expired_same_owner_is_allowed_when_renewal_is_sanctioned(self):
block = issue_lock_store.assess_same_issue_lease_conflict(
self.present_lock(),
issue_number=ISSUE,
branch_name=BRANCH,
worktree_path=self.worktree,
renewal_sanctioned=True,
)
self.assertIsNone(block)
def test_expired_same_owner_still_blocks_without_the_waiver(self):
"""Regression for the ordering defect: no waiver, no change in behavior.
Live PID and a present worktree, so the #601 reclaim affordance refuses;
before #760 this was the permanent dead end for an exact owner.
"""
lock = self.present_lock()
self.assertFalse(
issue_lock_store.assess_expired_lock_reclaim(lock)["reclaim_allowed"]
)
block = issue_lock_store.assess_same_issue_lease_conflict(
lock,
issue_number=ISSUE,
branch_name=BRANCH,
worktree_path=self.worktree,
)
self.assertIsNotNone(block)
self.assertIn("Recovery review is required", block)
def test_waiver_does_not_unlock_a_different_owner(self):
"""AC11: the waiver is scoped by same_owner, not merely by its own flag."""
block = issue_lock_store.assess_same_issue_lease_conflict(
self.present_lock(),
issue_number=ISSUE,
branch_name=f"fix/issue-{ISSUE}-someone-else",
worktree_path=self.worktree,
renewal_sanctioned=True,
)
self.assertIsNotNone(block)
self.assertIn("Recovery review is required", block)
def test_live_lease_disposition_is_unchanged(self):
"""AC12: a live foreign lease still blocks, waiver or not."""
block = issue_lock_store.assess_same_issue_lease_conflict(
self.present_lock(expires_at=future_ts()),
issue_number=ISSUE,
branch_name=f"fix/issue-{ISSUE}-someone-else",
worktree_path="/scratch/other",
renewal_sanctioned=True,
)
self.assertIsNotNone(block)
self.assertIn("already has an active", block)
def test_dead_pid_reclaim_path_is_unchanged(self):
"""AC11: expired + dead PID still reclaims through the #601 affordance."""
lock = self.present_lock(pid=dead_pid())
reclaim = issue_lock_store.assess_expired_lock_reclaim(lock)
self.assertTrue(reclaim["reclaim_allowed"])
block = issue_lock_store.assess_same_issue_lease_conflict(
lock,
issue_number=ISSUE,
branch_name=BRANCH,
worktree_path=self.worktree,
)
self.assertIsNone(block)
class RenewalRecordAndDownstream(unittest.TestCase):
"""AC9/AC10: durable audit trail, and a renewed lock that actually works."""
def test_record_captures_prior_and_replacement_state(self):
assessment = assess()
record = issue_lock_renewal.build_renewal_record(
assessment,
renewed_at="2026-01-01T00:00:00Z",
new_expires_at="2026-01-01T04:00:00Z",
)
self.assertTrue(record["renewed"])
self.assertEqual(record["prior_pid"], os.getpid())
self.assertEqual(record["new_expires_at"], "2026-01-01T04:00:00Z")
self.assertEqual(record["renewed_at"], "2026-01-01T00:00:00Z")
self.assertEqual(record["identity"], IDENTITY)
self.assertEqual(record["profile"], PROFILE)
self.assertTrue(record["prior_expires_at"])
self.assertTrue(record["proof"])
def test_renewed_lock_satisfies_verify_lock_for_mutation(self):
renewed = make_lock(expires_at=future_ts())
renewed["session_pid"] = os.getpid()
renewed["lease_renewal"] = {"renewed": True}
verdict = issue_lock_store.verify_lock_for_mutation(
renewed,
issue_number=ISSUE,
branch_name=BRANCH,
)
self.assertTrue(verdict["proven"])
self.assertFalse(verdict["block"])
def test_refusal_message_names_the_missing_evidence(self):
assessment = assess(porcelain_status=" M gitea_mcp_server.py\n")
message = issue_lock_renewal.format_renewal_refusal(assessment)
self.assertIn("refused", message)
self.assertIn("uncommitted", message)
class NoCallerControlledRenewalFlag(unittest.TestCase):
"""AC14: renewal eligibility is never declarable by a caller."""
def test_lock_issue_tool_exposes_no_renewal_parameter(self):
import gitea_mcp_server
target = gitea_mcp_server.gitea_lock_issue
target = getattr(target, "fn", getattr(target, "__wrapped__", target))
params = set(inspect.signature(target).parameters)
for forbidden in ("renewal_sanctioned", "renew", "allow_renewal", "is_owner"):
self.assertNotIn(forbidden, params)
def test_store_defaults_to_no_waiver(self):
params = inspect.signature(
issue_lock_store.assess_same_issue_lease_conflict
).parameters
self.assertIs(params["renewal_sanctioned"].default, False)
bind_params = inspect.signature(issue_lock_store.bind_session_lock).parameters
self.assertIs(bind_params["renewal_sanctioned"].default, False)
class NoIssueNumberSpecialCasing(unittest.TestCase):
"""AC17: no repository issue or PR number is special-cased."""
def test_module_contains_no_hardcoded_issue_special_cases(self):
source = inspect.getsource(issue_lock_renewal)
code = "\n".join(
line for line in source.splitlines() if not line.strip().startswith("#")
)
for literal in ("757", "759", "760"):
self.assertNotIn(f"== {literal}", code)
self.assertNotIn(f"issue_number == {literal}", code)
if __name__ == "__main__":
unittest.main()
+342
View File
@@ -0,0 +1,342 @@
"""MCP-level exact-owner lease renewal through ``gitea_lock_issue`` (#760).
The unit suite in ``test_issue_760_exact_owner_lease_renewal`` proves the
renewal *disposition*. It cannot prove the disposition survives the rest of the
tool, and it did not: the waiver was computed and then discarded before
``assess_issue_lock_worktree``, so every real renewal still failed on
base-equivalence. A branch being renewed always carries committed work, so it is
never base-equivalent by construction — exactly the argument #753 already makes
for recovery.
These tests drive the public tool end to end against a real git repository and a
real durable lock file, composing every gate in the production order.
"""
from __future__ import annotations
import os
import subprocess
import sys
import tempfile
import unittest
from datetime import datetime, timedelta, timezone
from unittest.mock import patch
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from mutation_profile_fixture import shared_mutation_env # noqa: E402
import issue_lock_provenance # noqa: E402
import issue_lock_store # noqa: E402
import mcp_server # noqa: E402
ISSUE = 9760
BRANCH = f"fix/issue-{ISSUE}-renewal-mcp"
IDENTITY = "example-user"
PROFILE = "test-author-prgs"
ORG = "Scaled-Tech-Consulting"
REPO = "Gitea-Tools"
def _past_ts(hours: int = 1) -> str:
return (
(datetime.now(timezone.utc) - timedelta(hours=hours))
.isoformat()
.replace("+00:00", "Z")
)
class _RenewalMcpBase(unittest.TestCase):
"""Real git repo + durable expired lock owned by a live PID.
The recorded PID is ``os.getpid()`` — unambiguously alive. That is the whole
point of #760: the PID belongs to the long-lived MCP daemon, so its liveness
says nothing about whether the authoring task still holds the work.
"""
def setUp(self):
self.lock_dir = tempfile.TemporaryDirectory()
self.addCleanup(self.lock_dir.cleanup)
self.repo = tempfile.mkdtemp(prefix="issue760-mcp-")
self.addCleanup(lambda: subprocess.run(["rm", "-rf", self.repo], check=False))
self._init_worktree()
self.remotes = patch.dict(
mcp_server.REMOTES,
{"prgs": {"host": "gitea.prgs.cc", "org": ORG, "repo": REPO}},
)
self.remotes.start()
self.addCleanup(patch.stopall)
mcp_server._IDENTITY_CACHE.clear()
def _git(self, *args):
return subprocess.run(
["git", "-C", self.repo, *args],
capture_output=True,
text=True,
check=True,
)
def _init_worktree(self):
self._git("init", "-q", "-b", "master")
self._git("config", "user.email", "[email protected]")
self._git("config", "user.name", "Test")
with open(os.path.join(self.repo, "seed.txt"), "w") as fh:
fh.write("seed\n")
self._git("add", "seed.txt")
self._git("commit", "-q", "-m", "seed")
self.base_sha = self._git("rev-parse", "HEAD").stdout.strip()
# The branch carries committed work, so it is NOT base-equivalent.
self._git("checkout", "-q", "-b", BRANCH)
with open(os.path.join(self.repo, "work.txt"), "w") as fh:
fh.write("author work\n")
self._git("add", "work.txt")
self._git("commit", "-q", "-m", "author work")
self.head_sha = self._git("rev-parse", "HEAD").stdout.strip()
self.worktree = os.path.realpath(self.repo)
def write_expired_lock(self, **overrides):
path = issue_lock_store.lock_file_path(
remote="prgs",
org=ORG,
repo=REPO,
issue_number=ISSUE,
lock_dir=self.lock_dir.name,
)
claimant = {"username": IDENTITY, "profile": PROFILE}
pid = overrides.pop("session_pid", os.getpid())
overrides.pop("pid", None)
lease_overrides = overrides.pop("work_lease", {})
data = {
"issue_number": ISSUE,
"branch_name": BRANCH,
"remote": "prgs",
"org": ORG,
"repo": REPO,
"worktree_path": self.worktree,
"session_pid": pid,
"pid": pid,
"lock_generation": 3,
"work_lease": {
"operation_type": issue_lock_store.AUTHOR_ISSUE_WORK_LEASE,
"issue_number": ISSUE,
"pr_number": None,
"branch": BRANCH,
"worktree_path": self.worktree,
"claimant": claimant,
"created_at": _past_ts(5),
"last_heartbeat_at": _past_ts(5),
"expires_at": _past_ts(), # already expired
},
"lock_provenance": issue_lock_provenance.build_sanctioned_lock_provenance(
tool="gitea_lock_issue",
claimant=claimant,
),
}
data["work_lease"].update(lease_overrides)
data.update(overrides)
data["session_pid"] = pid
data["pid"] = pid
data["lock_file_path"] = path
issue_lock_store.save_lock_file(path, data)
return path
def _tool_env(self):
env = shared_mutation_env(
PROFILE,
include_example_repo=True,
GITEA_ISSUE_LOCK_DIR=self.lock_dir.name,
)
env["GITEA_ISSUE_LOCK_DIR"] = self.lock_dir.name
return env
def _git_state(self, *, porcelain="", branch=BRANCH, head=None):
return {
"current_branch": branch,
"porcelain_status": porcelain,
# The decisive fact: a branch carrying work is never base-equivalent.
"base_equivalent": False,
"head_sha": head or self.head_sha,
"inspected_git_root": self.worktree,
"base_branch": "master",
}
def run_lock_issue(
self,
*,
branch_entries=None,
open_prs=None,
git_state=None,
identity=IDENTITY,
profile=PROFILE,
):
"""Drive the public tool for the published exact-owner renewal shape."""
if branch_entries is None:
branch_entries = [{"name": BRANCH, "commit": {"id": self.head_sha}}]
if open_prs is None:
open_prs = [{"number": 4242, "head": {"ref": BRANCH, "sha": self.head_sha}}]
if git_state is None:
git_state = self._git_state()
env = self._tool_env()
with patch(
"mcp_server.api_get_all", return_value=list(branch_entries)
), patch(
"mcp_server._list_open_pulls", return_value=list(open_prs)
), patch(
"mcp_server.get_auth_header", return_value="token x"
), patch(
"mcp_server._work_lease_claimant",
return_value={"username": identity, "profile": profile},
), patch(
"mcp_server.issue_lock_worktree.read_worktree_git_state",
return_value=git_state,
), patch(
"mcp_server.issue_duplicate_context_fetcher",
side_effect=lambda h, o, r, auth, issue_number: (
list(open_prs),
[b.get("name") for b in branch_entries if isinstance(b, dict)],
{"status": "not_claimed"},
),
), patch.dict(os.environ, env, clear=True):
os.environ["GITEA_ISSUE_LOCK_DIR"] = self.lock_dir.name
return mcp_server.gitea_lock_issue(
issue_number=ISSUE,
branch_name=BRANCH,
remote="prgs",
worktree_path=self.worktree,
)
class TestRenewalReachableThroughTool(_RenewalMcpBase):
"""F1: the sanctioned renewal must survive every downstream gate."""
def test_expired_lease_live_pid_exact_owner_renews_through_the_tool(self):
prior = issue_lock_store.read_lock_file(self.write_expired_lock())
self.assertTrue(issue_lock_store.is_lease_expired(prior))
self.assertTrue(issue_lock_store.is_process_alive(prior["session_pid"]))
result = self.run_lock_issue()
self.assertTrue(result["success"], result)
self.assertEqual(result["issue_number"], ISSUE)
self.assertEqual(result["branch_name"], BRANCH)
# The renewal is reported natively, so no lock-file inspection is needed.
self.assertIn("lease_renewal", result)
self.assertTrue(result["lease_renewal"]["renewed"])
self.assertIn("Renewed the expired", result["message"])
def test_renewed_lock_records_prior_and_replacement_evidence(self):
prior = issue_lock_store.read_lock_file(self.write_expired_lock())
prior_expiry = prior["work_lease"]["expires_at"]
prior_generation = issue_lock_store.lock_generation(prior)
result = self.run_lock_issue()
written = issue_lock_store.read_lock_file(result["lock_file_path"])
renewal = written["lease_renewal"]
self.assertTrue(renewal["renewed"])
self.assertEqual(renewal["prior_pid"], prior["session_pid"])
self.assertTrue(renewal["prior_pid_alive"])
self.assertEqual(renewal["prior_expires_at"], prior_expiry)
self.assertEqual(renewal["identity"], IDENTITY)
self.assertEqual(renewal["profile"], PROFILE)
self.assertEqual(renewal["head_sha"], self.head_sha)
self.assertTrue(renewal["proof"])
# New expiry is a fresh absolute stamp, later than the one it replaced.
self.assertEqual(renewal["new_expires_at"], written["work_lease"]["expires_at"])
self.assertGreater(renewal["new_expires_at"], prior_expiry)
# Compare-and-swap advanced the generation exactly once.
self.assertEqual(
issue_lock_store.lock_generation(written), prior_generation + 1
)
def test_renewed_lock_is_live_and_satisfies_mutation_ownership(self):
self.write_expired_lock()
result = self.run_lock_issue()
written = issue_lock_store.read_lock_file(result["lock_file_path"])
self.assertTrue(issue_lock_store.assess_lock_freshness(written)["live"])
verdict = issue_lock_store.verify_lock_for_mutation(
written,
issue_number=ISSUE,
branch_name=BRANCH,
worktree_path=self.worktree,
)
self.assertTrue(verdict["proven"], verdict)
self.assertFalse(verdict["block"])
def test_recovery_record_is_not_written_for_a_live_owner_renewal(self):
"""#753 recovery must not be claimed when the recorded PID is alive."""
self.write_expired_lock()
result = self.run_lock_issue()
written = issue_lock_store.read_lock_file(result["lock_file_path"])
self.assertNotIn("dead_session_recovery", written)
class TestRenewalWaiverIsNarrow(_RenewalMcpBase):
"""The waiver relaxes base-equivalence and nothing else."""
def test_dirty_worktree_still_blocks_a_would_be_renewal(self):
"""Cleanliness is never waived; the renewal assessor refuses first.
A dirty worktree makes the renewal refuse, so no waiver is issued and
the lease-conflict gate fails closed ahead of the worktree gate. The
refusal names the uncommitted files, so the owner still learns why.
"""
self.write_expired_lock()
with self.assertRaises(Exception) as ctx:
self.run_lock_issue(
git_state=self._git_state(porcelain=" M gitea_mcp_server.py\n")
)
message = str(ctx.exception)
self.assertIn("Recovery review is required before takeover", message)
self.assertIn("worktree has uncommitted tracked changes", message)
self.assertIn("gitea_mcp_server.py", message)
def test_foreign_claimant_cannot_use_the_waiver(self):
"""A near-match owner gets no renewal and no base-equivalence waiver."""
self.write_expired_lock()
with self.assertRaises(Exception) as ctx:
self.run_lock_issue(identity="someone-else")
message = str(ctx.exception)
self.assertIn("Recovery review is required before takeover", message)
# The refusal names the missing ownership evidence (#760 diagnostics).
self.assertIn("does not match active identity", message)
def test_foreign_profile_cannot_use_the_waiver(self):
self.write_expired_lock()
with self.assertRaises(Exception) as ctx:
self.run_lock_issue(profile="other-author")
self.assertIn(
"Recovery review is required before takeover", str(ctx.exception)
)
def test_unpublished_branch_cannot_use_the_waiver(self):
"""No remote head to agree with, so exact-owner renewal is refused."""
self.write_expired_lock()
with self.assertRaises(Exception) as ctx:
self.run_lock_issue(branch_entries=[], open_prs=[])
self.assertIn(
"Recovery review is required before takeover", str(ctx.exception)
)
def test_pr_head_mismatch_cannot_use_the_waiver(self):
self.write_expired_lock()
other = "9" * 40
with self.assertRaises(Exception) as ctx:
self.run_lock_issue(
open_prs=[{"number": 4242, "head": {"ref": BRANCH, "sha": other}}]
)
self.assertIn(
"Recovery review is required before takeover", str(ctx.exception)
)
def test_non_base_equivalent_branch_still_blocks_without_any_waiver(self):
"""No durable lock at all: the ordinary base-equivalence rule applies."""
with self.assertRaises(Exception) as ctx:
self.run_lock_issue()
self.assertIn("must be base-equivalent", str(ctx.exception))
if __name__ == "__main__":
unittest.main()
@@ -871,9 +871,13 @@ class TestAc6McpUnpublishedClaimRecovery(_UnpublishedMcpBase):
save_calls: list[dict] = []
real_save = mcp_server._save_issue_lock
def tracking_save(data, *, expected_generation=None):
def tracking_save(data, *, expected_generation=None, renewal_sanctioned=False):
save_calls.append({"expected_generation": expected_generation, "data": dict(data)})
return real_save(data, expected_generation=expected_generation)
return real_save(
data,
expected_generation=expected_generation,
renewal_sanctioned=renewal_sanctioned,
)
with patch("mcp_server._save_issue_lock", side_effect=tracking_save):
result = self.run_lock_issue()
@@ -926,18 +930,33 @@ class TestAc6McpUnpublishedClaimRecovery(_UnpublishedMcpBase):
real_bind = issue_lock_store.bind_session_lock
bind_calls: list[int | None] = []
def racing_bind(data, lock_dir=None, expected_generation=None):
# #760 added the renewal waiver keyword; the double forwards it verbatim
# so this race still exercises the real compare-and-swap.
def racing_bind(
data, lock_dir=None, expected_generation=None, renewal_sanctioned=False
):
bind_calls.append(expected_generation)
if expected_generation is None:
return real_bind(data, lock_dir=lock_dir, expected_generation=None)
return real_bind(
data,
lock_dir=lock_dir,
expected_generation=None,
renewal_sanctioned=renewal_sanctioned,
)
# First concurrent writer wins.
if len([c for c in bind_calls if c is not None]) == 1:
return real_bind(
data, lock_dir=lock_dir, expected_generation=expected_generation
data,
lock_dir=lock_dir,
expected_generation=expected_generation,
renewal_sanctioned=renewal_sanctioned,
)
# Second concurrent writer still holds the pre-race generation.
return real_bind(
data, lock_dir=lock_dir, expected_generation=expected_generation
data,
lock_dir=lock_dir,
expected_generation=expected_generation,
renewal_sanctioned=renewal_sanctioned,
)
# First recovery succeeds and advances generation.