fix(mcp): role-exclusive capability invariants and structured fail-closed submit (Closes #723)
Defect A - invariants: - task_capability_map.ROLE_EXCLUSIVE_TASKS is now the single shared definition of role-exclusive tasks; the resolver's inline copy is gone (AC1/AC5 drift surface removed). - tests: every role-exclusive task must exist in the capability map; formal-review tasks stay role-exclusive; canonical merger satisfies merge_pr; canonical reconciler satisfies branch-cleanup tasks (extends the #722 break-glass invariant suite). Defect B - unactionable internal_error: - AC3: gitea_resolve_task_capability records the capability purity baseline first but stamps _preflight_resolved_role/_task only for an ALLOWED resolve; a denied resolve clears any stale stamp (_clear_resolved_capability_stamp) so later mutation preflights key off the real profile role. - AC4: _evaluate_pr_review_submission converts _verify_role_mutation_workspace failures (role binding, stale runtime) into result reasons with blocker_kind=workspace_role_binding instead of letting RuntimeError escape as a generic internal_error. - AC5: _build_runtime_task_capabilities applies the resolver's role-exclusive filter when given the active role kind and labels each entry role_filtered/permission_only; matching_configured_profiles honors declared profile roles for role-exclusive tasks. Validation: focused suites 22 passed (+48 subtests); adjacent resolver/ runtime/review suites 72 passed; full suite 2929 passed, 6 skipped, 221 subtests (single pre-existing Starlette warning, #682). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -19,7 +19,12 @@ import unittest
|
||||
|
||||
import gitea_config
|
||||
from role_session_router import MERGER_TASKS, REVIEWER_TASKS
|
||||
from task_capability_map import required_permission, required_role
|
||||
from task_capability_map import (
|
||||
ROLE_EXCLUSIVE_TASKS,
|
||||
TASK_CAPABILITY_MAP,
|
||||
required_permission,
|
||||
required_role,
|
||||
)
|
||||
|
||||
# Canonical role-profile permission shape. Mirrors the configured
|
||||
# author/reviewer/merger/reconciler profiles (profiles.json v2 role split):
|
||||
@@ -201,5 +206,44 @@ class TestMergerBoundary(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestRoleExclusiveSetIntegrity(unittest.TestCase):
|
||||
"""#723 AC1/AC5: the shared role-exclusive set stays coherent."""
|
||||
|
||||
def test_every_role_exclusive_task_exists_in_capability_map(self):
|
||||
for task in sorted(ROLE_EXCLUSIVE_TASKS):
|
||||
with self.subTest(task=task):
|
||||
self.assertIn(
|
||||
task,
|
||||
TASK_CAPABILITY_MAP,
|
||||
f"role-exclusive task {task!r} missing from the "
|
||||
f"capability map — required_role() would raise and the "
|
||||
f"resolver would 500 instead of failing closed",
|
||||
)
|
||||
|
||||
def test_formal_review_tasks_are_role_exclusive(self):
|
||||
for task in FORMAL_REVIEW_TASKS:
|
||||
with self.subTest(task=task):
|
||||
self.assertIn(task, ROLE_EXCLUSIVE_TASKS)
|
||||
|
||||
def test_merge_pr_is_role_exclusive_and_merger_satisfiable(self):
|
||||
"""AC1 merger equivalent: merging must stay possible for a canonical
|
||||
merger profile (permission AND role together)."""
|
||||
self.assertIn("merge_pr", ROLE_EXCLUSIVE_TASKS)
|
||||
self.assertTrue(
|
||||
_profile_satisfies("merger", "merge_pr"),
|
||||
"no canonical merger profile satisfies merge_pr — merging would "
|
||||
"be impossible for every configured profile",
|
||||
)
|
||||
|
||||
def test_reconciler_cleanup_tasks_stay_reconciler_satisfiable(self):
|
||||
for task in ("cleanup_merged_pr_branch", "reconciliation_cleanup"):
|
||||
with self.subTest(task=task):
|
||||
self.assertIn(task, ROLE_EXCLUSIVE_TASKS)
|
||||
self.assertTrue(
|
||||
_profile_satisfies("reconciler", task),
|
||||
f"no canonical reconciler profile satisfies {task!r}",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user