Merge branch 'master' into feat/issue-523-reconciler-cleanup
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
"""Tests for the master-parity staleness gate (#420).
|
||||
|
||||
Covers the pure assessment logic, the mutation-block helper, the env escape
|
||||
hatches, and the server-side wiring: reads are never blocked by staleness, a
|
||||
mutation is refused when the on-disk master has advanced, and the read-only
|
||||
gitea_assess_master_parity tool reports the stale state.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent))
|
||||
|
||||
import master_parity_gate as mp # noqa: E402
|
||||
|
||||
|
||||
SHA_A = "a" * 40
|
||||
SHA_B = "b" * 40
|
||||
|
||||
|
||||
class TestAssessMasterParity(unittest.TestCase):
|
||||
def test_in_parity_when_heads_match(self):
|
||||
res = mp.assess_master_parity({"startup_head": SHA_A}, SHA_A)
|
||||
self.assertTrue(res["in_parity"])
|
||||
self.assertFalse(res["stale"])
|
||||
self.assertFalse(res["restart_required"])
|
||||
self.assertTrue(res["determinable"])
|
||||
|
||||
def test_stale_when_master_advanced(self):
|
||||
res = mp.assess_master_parity({"startup_head": SHA_A}, SHA_B)
|
||||
self.assertFalse(res["in_parity"])
|
||||
self.assertTrue(res["stale"])
|
||||
self.assertTrue(res["restart_required"])
|
||||
self.assertTrue(res["determinable"])
|
||||
self.assertTrue(any("restart" in r for r in res["reasons"]))
|
||||
|
||||
def test_missing_startup_head_not_determinable(self):
|
||||
res = mp.assess_master_parity({"startup_head": None}, SHA_B)
|
||||
self.assertFalse(res["determinable"])
|
||||
self.assertFalse(res["stale"])
|
||||
self.assertTrue(res["in_parity"])
|
||||
|
||||
def test_missing_current_head_not_determinable(self):
|
||||
res = mp.assess_master_parity({"startup_head": SHA_A}, None)
|
||||
self.assertFalse(res["determinable"])
|
||||
self.assertFalse(res["stale"])
|
||||
self.assertTrue(res["in_parity"])
|
||||
|
||||
def test_none_startup_baseline(self):
|
||||
res = mp.assess_master_parity(None, SHA_A)
|
||||
self.assertFalse(res["determinable"])
|
||||
self.assertFalse(res["stale"])
|
||||
|
||||
|
||||
class TestBlockReasonsAndReport(unittest.TestCase):
|
||||
def test_stale_produces_block_reasons(self):
|
||||
res = mp.assess_master_parity({"startup_head": SHA_A}, SHA_B)
|
||||
self.assertTrue(mp.parity_block_reasons(res))
|
||||
|
||||
def test_in_parity_no_block_reasons(self):
|
||||
res = mp.assess_master_parity({"startup_head": SHA_A}, SHA_A)
|
||||
self.assertEqual(mp.parity_block_reasons(res), [])
|
||||
|
||||
def test_disable_env_suppresses_block(self):
|
||||
res = mp.assess_master_parity({"startup_head": SHA_A}, SHA_B)
|
||||
with patch.dict(os.environ, {mp.ENV_DISABLE: "1"}):
|
||||
self.assertTrue(mp.gate_disabled())
|
||||
self.assertEqual(mp.parity_block_reasons(res), [])
|
||||
|
||||
def test_report_shape_when_stale(self):
|
||||
res = mp.assess_master_parity({"startup_head": SHA_A}, SHA_B)
|
||||
report = mp.parity_report(res)
|
||||
self.assertEqual(report["kind"], "server_stale")
|
||||
self.assertTrue(report["restart_required"])
|
||||
self.assertEqual(report["startup_head"], SHA_A)
|
||||
self.assertEqual(report["current_head"], SHA_B)
|
||||
self.assertTrue(report["recovery"])
|
||||
|
||||
|
||||
class TestReadGitHead(unittest.TestCase):
|
||||
def test_test_override_takes_precedence(self):
|
||||
with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_B}):
|
||||
self.assertEqual(mp.read_git_head("/nonexistent"), SHA_B)
|
||||
|
||||
def test_blank_override_is_none(self):
|
||||
with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: " "}):
|
||||
self.assertIsNone(mp.read_git_head("/nonexistent"))
|
||||
|
||||
def test_empty_root_is_none(self):
|
||||
# No override set; empty root cannot resolve a HEAD.
|
||||
env = {k: v for k, v in os.environ.items()
|
||||
if k != mp.ENV_TEST_CURRENT_HEAD}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
self.assertIsNone(mp.read_git_head(""))
|
||||
|
||||
|
||||
class TestServerWiring(unittest.TestCase):
|
||||
"""Integration with the gate choke point in the server namespace."""
|
||||
|
||||
def setUp(self):
|
||||
import mcp_server
|
||||
self.srv = mcp_server
|
||||
# Pin a known startup baseline so the current-HEAD override can diverge.
|
||||
self._saved = self.srv._STARTUP_PARITY
|
||||
self.srv._STARTUP_PARITY = {"root": self.srv.PROJECT_ROOT,
|
||||
"startup_head": SHA_A}
|
||||
|
||||
def tearDown(self):
|
||||
self.srv._STARTUP_PARITY = self._saved
|
||||
|
||||
def test_reads_not_blocked_when_stale(self):
|
||||
with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_B}):
|
||||
self.assertEqual(self.srv._master_parity_block("gitea.read"), [])
|
||||
|
||||
def test_mutation_blocked_when_stale(self):
|
||||
with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_B}):
|
||||
reasons = self.srv._master_parity_block("gitea.pr.create")
|
||||
self.assertTrue(reasons)
|
||||
# The profile-operation choke point surfaces the same block.
|
||||
self.assertTrue(self.srv._profile_operation_gate("gitea.pr.create"))
|
||||
|
||||
def test_mutation_allowed_when_in_parity(self):
|
||||
with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_A}):
|
||||
self.assertEqual(
|
||||
self.srv._master_parity_block("gitea.pr.create"), [])
|
||||
|
||||
def test_disable_env_lets_mutation_through(self):
|
||||
with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_B,
|
||||
mp.ENV_DISABLE: "1"}):
|
||||
self.assertEqual(
|
||||
self.srv._master_parity_block("gitea.pr.create"), [])
|
||||
|
||||
def test_assess_tool_reports_stale(self):
|
||||
with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_B}):
|
||||
out = self.srv.gitea_assess_master_parity(remote="prgs")
|
||||
self.assertTrue(out["stale"])
|
||||
self.assertTrue(out["restart_required"])
|
||||
self.assertEqual(out["startup_head"], SHA_A)
|
||||
self.assertEqual(out["current_head"], SHA_B)
|
||||
self.assertIn("report", out)
|
||||
|
||||
def test_assess_tool_reports_in_parity(self):
|
||||
with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_A}):
|
||||
out = self.srv.gitea_assess_master_parity(remote="prgs")
|
||||
self.assertFalse(out["stale"])
|
||||
self.assertTrue(out["in_parity"])
|
||||
self.assertNotIn("report", out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -105,6 +105,19 @@ class TestMcpMenuScript(unittest.TestCase):
|
||||
self.assertIn("./mcp-menu.sh", docs_text)
|
||||
self.assertIn("placeholder", docs_text.lower())
|
||||
|
||||
def test_reviewer_skip_stale_request_changes_prompt_discoverable(self):
|
||||
# #482: the skip-already-reviewed-stale-REQUEST_CHANGES reviewer prompt
|
||||
# must be reachable from the reviewer menu and documented.
|
||||
label = "Skip already-reviewed stale REQUEST_CHANGES PR and hand off to author"
|
||||
reviewer_fn = self._extract_function("show_reviewer_prompts")
|
||||
self.assertIn(label, reviewer_fn, "new reviewer prompt label must appear in the reviewer menu")
|
||||
# The prompt must instruct: no duplicate terminal review mutation for a
|
||||
# non-stale REQUEST_CHANGES head, and must carry the author handoff.
|
||||
self.assertIn("do NOT submit another terminal review mutation", reviewer_fn)
|
||||
self.assertIn("author-ready fix prompt", reviewer_fn)
|
||||
docs_text = DOCS.read_text(encoding="utf-8")
|
||||
self.assertIn("REQUEST_CHANGES", docs_text)
|
||||
|
||||
def _extract_function(self, name: str) -> str:
|
||||
marker = f"{name}() {{"
|
||||
start = self.content.index(marker)
|
||||
|
||||
@@ -265,5 +265,38 @@ class TestCleanupReportVerifier(unittest.TestCase):
|
||||
self.assertEqual(direct["proven"], wrapped["proven"])
|
||||
|
||||
|
||||
class TestCleanupEmptyQueue(unittest.TestCase):
|
||||
def test_empty_queue_report_passes(self):
|
||||
report = _clean_report(
|
||||
selected="Selected PR: none",
|
||||
decision="Review decision: comment",
|
||||
next="Next suggested PR: approvals (queue empty)",
|
||||
pagination="PR inventory pagination proof: inventory_complete=true, total_count 0",
|
||||
)
|
||||
report += "\npr_inventory_trust_gate.status: trusted_empty"
|
||||
result = assess_pr_queue_cleanup_report(report)
|
||||
self.assertTrue(result["proven"], result["reasons"])
|
||||
|
||||
def test_empty_queue_without_evidence_fails(self):
|
||||
report = _clean_report(
|
||||
selected="Selected PR: none",
|
||||
)
|
||||
result = assess_pr_queue_cleanup_report(report)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(
|
||||
any("no valid empty-queue claim" in r for r in result["reasons"]),
|
||||
result["reasons"]
|
||||
)
|
||||
|
||||
def test_contradictory_selected_pr_fails(self):
|
||||
report = _clean_report() + "\nSelected PR: none"
|
||||
result = assess_pr_queue_cleanup_report(report)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(
|
||||
any("contains both a selected PR and a claim of none selected" in r for r in result["reasons"]),
|
||||
result["reasons"]
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user