"""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()