Remediates reviewer findings F1, F2, and F3 from review 483 on PR #770. F1 (blocking): _current_runtime_mode_report derived the runtime-gate alignment input as realpath(workspace_root) == process_project_root, redefining workspace_roots_aligned. Its established meaning is ctx["roots_aligned"] (canonical_repo_root == process_project_root) — a repository-level question. Because the global worktree rule requires all task work to live in a branches/ worktree, the old derivation reported aligned False for exactly the sessions that are configured correctly, raising unsafe_process_root_workspace_alignment and refusing every non-read operation once an author, reviewer, or merger namespace held a worktree binding. The gate is now fed ctx["roots_aligned"]. F2 (blocking): _current_runtime_mode_report cached its first result into _STARTUP_RUNTIME_MODE, which was initialised to None rather than captured at import, and the read-only refresh=True path seeded it too. Session-scoped fields (active_task_workspace and its derived alignment) and mutable fields (dirty_files) were frozen for the process lifetime, so the acceptance criterion 7 dirty-runtime blocker stopped applying after the snapshot and one session's binding decided alignment for every later session. Immutable process facts (process root, branch, head, checkout-ness) are now captured at import as _STARTUP_RUNTIME_FACTS, matching the #420 parity baseline. Dirty state, workspace binding, and alignment are recomputed on every call. A new stable_control_runtime.observe_dirty_files() splits out the one runtime fact that legitimately changes during a process lifetime; observe_runtime() delegates to it so the parsing lives in one place. refresh is retained for the read-only reporting path but no longer selects a cache, so a read-only call can neither seed nor weaken a later mutation decision. F3 (major): no test exercised the real derivation — every server-wiring test asserting a healthy runtime permits mutations patched _current_runtime_mode_report with a fixture whose workspace_roots_aligned was True. New TestServerWiringRealDerivation patches only the derivation's inputs and lets the real function build the report: - a clean stable control checkout plus a correctly bound branches/ worktree permits an otherwise authorized mutation; - misaligned process/canonical roots fail closed; - newly dirty task state is detected after an earlier clean read; - a read-only refresh cannot freeze a permissive mutation result; - an unresolvable binding reports unknown alignment, never alignment proof. Acceptance criteria 6, 8, 10, and 11 are unchanged and untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -468,5 +468,136 @@ class TestServerWiring(unittest.TestCase):
|
||||
self.assertEqual(report["mcp_process_root"], self.srv.PROJECT_ROOT)
|
||||
|
||||
|
||||
class TestServerWiringRealDerivation(unittest.TestCase):
|
||||
"""Drive the *real* report derivation, not a pre-built fixture (#615 F3).
|
||||
|
||||
Every other server-wiring test patches ``_current_runtime_mode_report`` with
|
||||
a fixture, so the derivation the daemon actually runs was never executed by
|
||||
the suite. These tests patch only its *inputs* -- the import-time facts, the
|
||||
dirty-file read, and the resolved namespace binding -- and let the real
|
||||
function build the report.
|
||||
"""
|
||||
|
||||
TASK_WORKTREE = STABLE_ROOT + "/branches/issue-615-runtime-mode-enforcement"
|
||||
|
||||
def setUp(self):
|
||||
import gitea_mcp_server as srv # imported lazily: heavy module
|
||||
|
||||
self.srv = srv
|
||||
|
||||
def _stable_facts(self):
|
||||
"""Immutable facts of a promoted stable-control runtime."""
|
||||
return {
|
||||
"checkout_branch": "master",
|
||||
"runtime_head": SHA_A,
|
||||
"is_git_checkout": True,
|
||||
"dirty_files": [],
|
||||
}
|
||||
|
||||
def _binding(self, *, roots_aligned=True, workspace=None):
|
||||
"""A resolved namespace binding, as the server's resolver returns it."""
|
||||
return {
|
||||
"workspace_path": workspace or self.TASK_WORKTREE,
|
||||
"canonical_repo_root": STABLE_ROOT,
|
||||
"process_project_root": STABLE_ROOT,
|
||||
"roots_aligned": roots_aligned,
|
||||
}
|
||||
|
||||
def _real_derivation(self, *, dirty=None, roots_aligned=True, workspace=None):
|
||||
"""Context managers that patch only the inputs, never the derivation."""
|
||||
return (
|
||||
patch.dict(os.environ, {"GITEA_TEST_FORCE_PRODUCTION_GUARDS": "1"}),
|
||||
patch.object(self.srv, "PROJECT_ROOT", STABLE_ROOT),
|
||||
patch.object(self.srv, "_STARTUP_RUNTIME_FACTS", self._stable_facts()),
|
||||
patch.object(
|
||||
self.srv,
|
||||
"_resolve_namespace_mutation_context",
|
||||
return_value=self._binding(
|
||||
roots_aligned=roots_aligned, workspace=workspace),
|
||||
),
|
||||
patch.object(scr, "observe_dirty_files", return_value=list(dirty or [])),
|
||||
)
|
||||
|
||||
def test_clean_stable_checkout_with_bound_task_worktree_permits_mutation(self):
|
||||
# The sanctioned configuration: a clean control checkout on master plus a
|
||||
# correctly bound branches/ worktree. Before the F1 fix this failed, because
|
||||
# alignment was path equality between the task workspace and the process
|
||||
# root, which a branches/ worktree can never satisfy.
|
||||
env, root, facts, ctx, dirty = self._real_derivation()
|
||||
with env, root, facts, ctx, dirty:
|
||||
report = self.srv._current_runtime_mode_report()
|
||||
self.assertEqual(report["runtime_mode"], scr.RUNTIME_MODE_STABLE)
|
||||
self.assertEqual(report["active_task_workspace"], self.TASK_WORKTREE)
|
||||
self.assertTrue(report["workspace_roots_aligned"])
|
||||
self.assertTrue(
|
||||
report["real_mutations_allowed"], report["mutation_block_reasons"])
|
||||
self.assertEqual(self.srv._runtime_mode_block("gitea.pr.create"), [])
|
||||
|
||||
def test_misaligned_process_and_canonical_roots_fail_closed(self):
|
||||
# Alignment keeps its repository-level meaning: the namespace targeting a
|
||||
# different repository than the process is installed in is the unsafe case.
|
||||
env, root, facts, ctx, dirty = self._real_derivation(roots_aligned=False)
|
||||
with env, root, facts, ctx, dirty:
|
||||
report = self.srv._current_runtime_mode_report()
|
||||
self.assertFalse(report["workspace_roots_aligned"])
|
||||
self.assertFalse(report["real_mutations_allowed"])
|
||||
reasons = self.srv._runtime_mode_block("gitea.pr.create")
|
||||
self.assertTrue(reasons)
|
||||
self.assertTrue(any("alignment" in reason for reason in reasons))
|
||||
|
||||
def test_newly_dirty_task_state_is_detected_after_an_earlier_clean_read(self):
|
||||
# A clean read must not license every later mutation: the acceptance
|
||||
# criterion 7 dirty blocker has to keep applying for the process lifetime.
|
||||
env, root, facts, ctx, dirty = self._real_derivation(dirty=[])
|
||||
with env, root, facts, ctx, dirty:
|
||||
self.assertTrue(
|
||||
self.srv._current_runtime_mode_report()["real_mutations_allowed"])
|
||||
self.assertEqual(self.srv._runtime_mode_block("gitea.pr.create"), [])
|
||||
|
||||
env, root, facts, ctx, dirty = self._real_derivation(
|
||||
dirty=["gitea_mcp_server.py"])
|
||||
with env, root, facts, ctx, dirty:
|
||||
report = self.srv._current_runtime_mode_report()
|
||||
self.assertEqual(report["dirty_files"], ["gitea_mcp_server.py"])
|
||||
self.assertFalse(report["real_mutations_allowed"])
|
||||
self.assertTrue(self.srv._runtime_mode_block("gitea.pr.create"))
|
||||
|
||||
def test_read_only_refresh_cannot_freeze_a_permissive_mutation_result(self):
|
||||
# gitea_get_runtime_context() calls with refresh=True. That read-only call
|
||||
# must not seed a cache that a later mutation gate would then trust.
|
||||
env, root, facts, ctx, dirty = self._real_derivation(dirty=[])
|
||||
with env, root, facts, ctx, dirty:
|
||||
self.assertTrue(
|
||||
self.srv._current_runtime_mode_report(refresh=True)[
|
||||
"real_mutations_allowed"]
|
||||
)
|
||||
|
||||
env, root, facts, ctx, dirty = self._real_derivation(
|
||||
dirty=["stable_control_runtime.py"])
|
||||
with env, root, facts, ctx, dirty:
|
||||
self.assertFalse(
|
||||
self.srv._current_runtime_mode_report()["real_mutations_allowed"])
|
||||
self.assertTrue(self.srv._runtime_mode_block("gitea.pr.create"))
|
||||
|
||||
def test_unresolvable_binding_reports_unknown_alignment_never_alignment_proof(self):
|
||||
# An unresolvable binding must report alignment as unknown (None), never
|
||||
# as True. Only *definite* misalignment blocks: a session with no task
|
||||
# binding resolved is the ordinary case, and failing it closed would
|
||||
# reintroduce exactly the F1 breakage this change removes.
|
||||
env, root, facts, _, dirty = self._real_derivation()
|
||||
broken = patch.object(
|
||||
self.srv,
|
||||
"_resolve_namespace_mutation_context",
|
||||
side_effect=RuntimeError("no binding"),
|
||||
)
|
||||
with env, root, facts, broken, dirty:
|
||||
report = self.srv._current_runtime_mode_report()
|
||||
self.assertIsNone(report["workspace_roots_aligned"])
|
||||
self.assertNotIn(
|
||||
scr.BLOCKER_UNSAFE_ALIGNMENT,
|
||||
scr.assess_runtime_mutation_gate(report)["blocker_kinds"],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user