Merge branch 'master' into fix/issue-685-side-effect-free-resolver
Resolve gitea_resolve_task_capability conflict by keeping both #685 runtime_reconnect_required blocker and #702 stale_binding_recovery annotation. Keep resolve-path stale-binding assessment report-only (auto_recover=False) so capability resolution never mutates env or session state. Preserve #725 merger-lease and #728 PR-sync code from master. Regression: resolve uses report-only binding assessment.
This commit is contained in:
@@ -322,5 +322,109 @@ class TestIssue685DocstringReadOnlyContract(unittest.TestCase):
|
||||
self.assertNotIn("os._exit", lower)
|
||||
|
||||
|
||||
class TestIssue685MergeCoexistenceWithMasterAnnotations(unittest.TestCase):
|
||||
"""After merging master: #685 reconnect blocker + #702 report-only binding."""
|
||||
|
||||
def setUp(self):
|
||||
mcp_server._process_boot_head_sha = None
|
||||
|
||||
def tearDown(self):
|
||||
mcp_server._process_boot_head_sha = None
|
||||
if hasattr(mcp_server, "capability_stop_terminal"):
|
||||
mcp_server.capability_stop_terminal.clear()
|
||||
|
||||
def test_resolve_uses_report_only_stale_binding_assessment(self):
|
||||
"""Capability resolve must call stale-binding assess with auto_recover=False."""
|
||||
allowed = [
|
||||
"gitea.read",
|
||||
"gitea.issue.create",
|
||||
"gitea.issue.comment",
|
||||
"gitea.branch.push",
|
||||
"gitea.pr.create",
|
||||
"gitea.pr.comment",
|
||||
"gitea.pr.review",
|
||||
"gitea.pr.merge",
|
||||
"gitea.repo.commit",
|
||||
]
|
||||
profile = {
|
||||
"profile_name": "prgs-author",
|
||||
"role": "author",
|
||||
"allowed_operations": allowed,
|
||||
"forbidden_operations": [],
|
||||
}
|
||||
config = {
|
||||
"profiles": {
|
||||
"prgs-author": {
|
||||
"role": "author",
|
||||
"allowed_operations": allowed,
|
||||
"forbidden_operations": [],
|
||||
}
|
||||
}
|
||||
}
|
||||
mock_getpid, mock_exists, mock_getmtime, mock_run = _stale_self_ps_mocks(
|
||||
"prgs-author"
|
||||
)
|
||||
fake_binding = {
|
||||
"classification": "missing_path",
|
||||
"active_worktree": "/tmp/gone",
|
||||
"reasons": ["path missing"],
|
||||
}
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"GITEA_FORCE_MCP_RUNTIME_CHECK": "1",
|
||||
"GITEA_MCP_PROFILE": "prgs-author",
|
||||
},
|
||||
clear=False,
|
||||
), patch.object(mcp_server, "get_profile", return_value=profile), patch.object(
|
||||
mcp_server.gitea_config, "load_config", return_value=config
|
||||
), patch.object(
|
||||
mcp_server, "_authenticated_username", return_value="test-user"
|
||||
), patch.object(
|
||||
mcp_server, "_ensure_matching_profile", return_value=None
|
||||
), patch.object(
|
||||
mcp_server, "record_preflight_check", return_value=None
|
||||
), patch.object(
|
||||
mcp_server, "record_mutation_authority", return_value=None
|
||||
), patch.object(
|
||||
mcp_server, "init_review_decision_lock", return_value=None
|
||||
), patch.object(
|
||||
mcp_server,
|
||||
"_assess_stale_active_binding",
|
||||
return_value=fake_binding,
|
||||
) as mock_assess, patch(
|
||||
"subprocess.run", mock_run
|
||||
), patch(
|
||||
"os.path.getmtime", mock_getmtime
|
||||
), patch(
|
||||
"os.path.exists", mock_exists
|
||||
), patch(
|
||||
"os.getpid", mock_getpid
|
||||
), patch(
|
||||
"os.utime"
|
||||
) as mock_utime, patch(
|
||||
"threading.Thread"
|
||||
) as mock_thread, patch(
|
||||
"os._exit"
|
||||
) as mock_exit:
|
||||
result = mcp_server.gitea_resolve_task_capability(
|
||||
task="create_issue", remote="prgs"
|
||||
)
|
||||
|
||||
mock_assess.assert_called()
|
||||
# Every call must be report-only (no env clear / session-state write).
|
||||
for call in mock_assess.call_args_list:
|
||||
self.assertFalse(call.kwargs.get("auto_recover", True))
|
||||
self.assertEqual(
|
||||
result.get("blocker_kind"), "runtime_reconnect_required", result
|
||||
)
|
||||
self.assertEqual(result.get("stale_binding_recovery"), fake_binding, result)
|
||||
self.assertIs(result.get("mutation_performed"), False, result)
|
||||
mock_utime.assert_not_called()
|
||||
mock_thread.assert_not_called()
|
||||
mock_exit.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user