Merge master into fix/issue-714-session-context-immutability
Resolve gitea_mcp_server.py conflicts after master advanced with #724 (side-effect-free resolver), #725 (merger lease), #728 (PR-sync), and #702 stale-binding recovery: - Keep #714 fail-closed session immutability: no auto profile switch (_ensure_matching_profile / _try_auto_switch_for_operation). - Retain master #709 _authenticated_actor identity helper. - Preserve resolve-path auto_recover=False (#685) and report-only stale_binding_recovery coexistence with runtime_reconnect_required. - Cross-host / cross-repository substitution still fails closed without mutating the pinned session context. - Regression coverage for merge coexistence, dual-module identity-cache isolation in conftest, and structured unknown_task fail-closed. Closes #714 conflict remediation path for PR #715.
This commit is contained in:
@@ -274,11 +274,17 @@ class TestIssue714SessionContextImmutability(unittest.TestCase):
|
||||
self.assertIn("reason", res)
|
||||
self.assertIn("exact_safe_next_action", res)
|
||||
self.assertIn("activate_profile", res["exact_safe_next_action"])
|
||||
# Unknown task still fail closed
|
||||
with self.assertRaises(ValueError):
|
||||
mcp_server.gitea_resolve_task_capability(
|
||||
task="reopen_issue", remote="dadeschools"
|
||||
)
|
||||
# Unknown task still fail closed (structured denial after #723;
|
||||
# never raises into internal_error and never substitutes profile).
|
||||
unknown = mcp_server.gitea_resolve_task_capability(
|
||||
task="reopen_issue", remote="dadeschools"
|
||||
)
|
||||
self.assertFalse(unknown.get("allowed_in_current_session"))
|
||||
self.assertTrue(unknown.get("stop_required"))
|
||||
self.assertEqual(unknown.get("reason_code"), "unknown_task")
|
||||
self.assertEqual(unknown.get("mutation_performed"), False)
|
||||
self.assertEqual(unknown.get("active_profile"), "mdcps-reviewer")
|
||||
self.assertNotEqual(unknown.get("active_profile"), "prgs-author")
|
||||
|
||||
def test_identity_mismatch_blocks_mutation(self):
|
||||
"""Profile expects 913443 but authenticated as jcwalker3."""
|
||||
@@ -604,5 +610,136 @@ class TestSessionContextTestBoundaryIsolation(unittest.TestCase):
|
||||
session_ctx._reset_session_context_for_testing()
|
||||
|
||||
|
||||
class TestIssue714MergeCoexistenceWithMaster(unittest.TestCase):
|
||||
"""Conflict-resolution regressions after merging advanced master into #715.
|
||||
|
||||
Preserves:
|
||||
- #714 immutable session / no auto profile substitution
|
||||
- #685 side-effect-free resolve (auto_recover=False)
|
||||
- #709 actor identity helper from master
|
||||
"""
|
||||
|
||||
def test_auto_switch_helpers_remain_fail_closed(self):
|
||||
self.assertFalse(mcp_server._try_auto_switch_for_operation("gitea.pr.review"))
|
||||
self.assertFalse(
|
||||
mcp_server._try_auto_switch_for_operation(
|
||||
"gitea.issue.comment", host="gitea.prgs.cc"
|
||||
)
|
||||
)
|
||||
# Active profile is prgs-author; cannot match mdcps review permission
|
||||
# and must not switch.
|
||||
with patch.object(
|
||||
mcp_server,
|
||||
"get_profile",
|
||||
return_value={
|
||||
"profile_name": "prgs-author",
|
||||
"allowed_operations": ["gitea.read", "gitea.issue.create"],
|
||||
"forbidden_operations": ["gitea.pr.approve"],
|
||||
"base_url": "https://gitea.prgs.cc",
|
||||
"context": "prgs",
|
||||
},
|
||||
):
|
||||
matched = mcp_server._ensure_matching_profile(
|
||||
"gitea.pr.approve", "reviewer", remote="prgs"
|
||||
)
|
||||
self.assertIsNone(matched)
|
||||
self.assertNotEqual(
|
||||
gitea_config.selected_profile_name() or "prgs-author",
|
||||
"mdcps-reviewer",
|
||||
)
|
||||
|
||||
def test_authenticated_actor_helper_survives_merge(self):
|
||||
"""Master #709 F7 actor identity coexists with #714 immutability."""
|
||||
self.assertTrue(hasattr(mcp_server, "_authenticated_actor"))
|
||||
with patch(
|
||||
"mcp_server.get_auth_header", return_value={"Authorization": "token x"}
|
||||
), patch(
|
||||
"mcp_server.api_request",
|
||||
return_value={"id": 42, "login": "sysadmin"},
|
||||
):
|
||||
mcp_server._ACTOR_IDENTITY_CACHE.clear()
|
||||
actor = mcp_server._authenticated_actor("gitea.prgs.cc")
|
||||
self.assertEqual(actor.get("user_id"), 42)
|
||||
self.assertEqual(actor.get("login"), "sysadmin")
|
||||
# Cached; second call does not re-request
|
||||
with patch("mcp_server.api_request") as mock_api:
|
||||
actor2 = mcp_server._authenticated_actor("gitea.prgs.cc")
|
||||
mock_api.assert_not_called()
|
||||
self.assertEqual(actor2.get("user_id"), 42)
|
||||
|
||||
def test_resolve_still_report_only_after_master_merge(self):
|
||||
"""#685: resolve path must not pass auto_recover=True after conflict merge."""
|
||||
allowed = [
|
||||
"gitea.read",
|
||||
"gitea.issue.create",
|
||||
"gitea.issue.comment",
|
||||
"gitea.pr.create",
|
||||
"gitea.pr.comment",
|
||||
"gitea.branch.create",
|
||||
"gitea.branch.push",
|
||||
"gitea.repo.commit",
|
||||
]
|
||||
profile = {
|
||||
"profile_name": "prgs-author",
|
||||
"role": "author",
|
||||
"allowed_operations": allowed,
|
||||
"forbidden_operations": [],
|
||||
"base_url": "https://gitea.prgs.cc",
|
||||
"context": "prgs",
|
||||
}
|
||||
config = {
|
||||
"contexts": {
|
||||
"prgs": {
|
||||
"enabled": True,
|
||||
"gitea": {"enabled": True, "base_url": "https://gitea.prgs.cc"},
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"prgs-author": {
|
||||
"role": "author",
|
||||
"allowed_operations": allowed,
|
||||
"forbidden_operations": [],
|
||||
"base_url": "https://gitea.prgs.cc",
|
||||
"context": "prgs",
|
||||
}
|
||||
},
|
||||
}
|
||||
fake_binding = {
|
||||
"classification": "unbound",
|
||||
"active_worktree": None,
|
||||
"reasons": [],
|
||||
}
|
||||
with patch.dict(
|
||||
os.environ, {"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="jcwalker3"
|
||||
), patch.object(
|
||||
mcp_server,
|
||||
"_assess_stale_active_binding",
|
||||
return_value=fake_binding,
|
||||
) as mock_assess, 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
|
||||
):
|
||||
result = mcp_server.gitea_resolve_task_capability(
|
||||
task="create_issue", remote="prgs"
|
||||
)
|
||||
mock_assess.assert_called()
|
||||
for call in mock_assess.call_args_list:
|
||||
self.assertFalse(call.kwargs.get("auto_recover", True))
|
||||
self.assertEqual(result.get("mutation_performed"), False)
|
||||
# Session context must not have been rewritten by resolve
|
||||
# (report-only; any prior binding stays; unbound stays unbound unless
|
||||
# seed-on-read path is intentional — resolve must not activate peers).
|
||||
self.assertNotEqual(result.get("active_profile"), "mdcps-reviewer")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user