Remediate PR #788 review F1/F2: suite-wide hermetic mode prevents git ls-remote from running in tests so feature worktrees no longer flip legacy runtime-context assertions to live_stale, and unit tests stay offline. Module flag survives patch.dict(clear=True); env override still wins. Co-Authored-By: Grok 4.5 <[email protected]>
422 lines
18 KiB
Python
422 lines
18 KiB
Python
"""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 TestLiveRemoteParity(unittest.TestCase):
|
|
"""#610: parity must account for the live remote master, not just local.
|
|
|
|
The daemon can be stale relative to the live remote target while the local
|
|
checkout HEAD still matches the daemon's startup commit, so local parity
|
|
reports green even though a mutation would run against outdated code.
|
|
"""
|
|
|
|
SHA_C = "c" * 40
|
|
|
|
def test_distinguishes_three_shas(self):
|
|
res = mp.assess_master_parity(
|
|
{"startup_head": SHA_A}, SHA_A, live_remote_head=SHA_B)
|
|
self.assertEqual(res["daemon_start_head"], SHA_A)
|
|
self.assertEqual(res["local_head"], SHA_A)
|
|
self.assertEqual(res["live_remote_head"], SHA_B)
|
|
|
|
def test_mutation_safe_only_when_all_three_match(self):
|
|
res = mp.assess_master_parity(
|
|
{"startup_head": SHA_A}, SHA_A, live_remote_head=SHA_A)
|
|
self.assertTrue(res["mutation_safe"])
|
|
self.assertTrue(res["live_known"])
|
|
self.assertFalse(res["live_stale"])
|
|
|
|
def test_live_stale_when_remote_advanced_past_daemon(self):
|
|
# Local checkout still matches the daemon start (local parity green),
|
|
# but the live remote master has advanced -> daemon is live-stale.
|
|
res = mp.assess_master_parity(
|
|
{"startup_head": SHA_A}, SHA_A, live_remote_head=SHA_B)
|
|
self.assertTrue(res["in_parity"]) # local parity still green
|
|
self.assertTrue(res["live_stale"])
|
|
self.assertFalse(res["mutation_safe"])
|
|
self.assertTrue(any("live" in r.lower() for r in res["reasons"]))
|
|
|
|
def test_live_unknown_is_not_mutation_safe_but_not_stale(self):
|
|
# Non-goal: unfetchable live remote must not be treated as stale for
|
|
# read-only, but a mutation-safe claim fails closed.
|
|
res = mp.assess_master_parity(
|
|
{"startup_head": SHA_A}, SHA_A, live_remote_head=None)
|
|
self.assertFalse(res["live_known"])
|
|
self.assertFalse(res["mutation_safe"])
|
|
self.assertFalse(res["live_stale"])
|
|
self.assertTrue(res["in_parity"])
|
|
|
|
def test_default_live_remote_preserves_legacy_shape(self):
|
|
# Callers that do not supply a live head keep the pre-#610 behavior:
|
|
# in-parity, not live-stale, no live-derived block.
|
|
res = mp.assess_master_parity({"startup_head": SHA_A}, SHA_A)
|
|
self.assertFalse(res["live_stale"])
|
|
self.assertEqual(mp.parity_block_reasons(res), [])
|
|
|
|
|
|
class TestLiveStaleBlockAndReport(unittest.TestCase):
|
|
"""#610: live-staleness must block mutations and surface a typed blocker."""
|
|
|
|
def test_live_stale_produces_block_reasons(self):
|
|
res = mp.assess_master_parity(
|
|
{"startup_head": SHA_A}, SHA_A, live_remote_head=SHA_B)
|
|
self.assertTrue(mp.parity_block_reasons(res))
|
|
|
|
def test_disable_env_suppresses_live_stale_block(self):
|
|
res = mp.assess_master_parity(
|
|
{"startup_head": SHA_A}, SHA_A, live_remote_head=SHA_B)
|
|
with patch.dict(os.environ, {mp.ENV_DISABLE: "1"}):
|
|
self.assertEqual(mp.parity_block_reasons(res), [])
|
|
|
|
def test_resolver_disagreement_returns_typed_blocker(self):
|
|
# Parity says local-green, resolver says restart required -> disagreement
|
|
# is a typed, fail-closed blocker naming the resolver as authoritative.
|
|
res = mp.assess_master_parity({"startup_head": SHA_A}, SHA_A)
|
|
blocker = mp.parity_resolver_disagreement(res, resolver_restart_required=True)
|
|
self.assertIsNotNone(blocker)
|
|
self.assertEqual(blocker["kind"], "parity_resolver_disagreement")
|
|
self.assertTrue(blocker["restart_required"])
|
|
self.assertTrue(blocker["resolver_authoritative"])
|
|
|
|
def test_no_disagreement_when_resolver_agrees(self):
|
|
res = mp.assess_master_parity({"startup_head": SHA_A}, SHA_A)
|
|
self.assertIsNone(
|
|
mp.parity_resolver_disagreement(res, resolver_restart_required=False))
|
|
|
|
def test_live_stale_report_names_live_remote(self):
|
|
res = mp.assess_master_parity(
|
|
{"startup_head": SHA_A}, SHA_A, live_remote_head=SHA_B)
|
|
report = mp.parity_report(res)
|
|
self.assertEqual(report["live_remote_head"], SHA_B)
|
|
self.assertTrue(report["restart_required"])
|
|
|
|
|
|
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 TestReadRemoteMasterHead(unittest.TestCase):
|
|
"""#610: live remote master head reader (env-overridable, fails to None)."""
|
|
|
|
def test_test_override_takes_precedence(self):
|
|
with patch.dict(os.environ, {mp.ENV_TEST_LIVE_REMOTE_HEAD: SHA_B}):
|
|
self.assertEqual(mp.read_remote_master_head("/nonexistent"), SHA_B)
|
|
|
|
def test_blank_override_is_none(self):
|
|
with patch.dict(os.environ, {mp.ENV_TEST_LIVE_REMOTE_HEAD: " "}):
|
|
self.assertIsNone(mp.read_remote_master_head("/nonexistent"))
|
|
|
|
def test_unfetchable_remote_is_none(self):
|
|
# No override; a bogus root/remote must fail closed to None, never raise.
|
|
env = {k: v for k, v in os.environ.items()
|
|
if k != mp.ENV_TEST_LIVE_REMOTE_HEAD}
|
|
with patch.dict(os.environ, env, clear=True):
|
|
self.assertIsNone(
|
|
mp.read_remote_master_head("/nonexistent", remote="nope"))
|
|
|
|
|
|
class TestRemoteHeadCache(unittest.TestCase):
|
|
"""#610: live remote reads are cached with a TTL to stay off the network.
|
|
|
|
The parity gate runs on every mutation and every runtime-context read, so an
|
|
unbounded ``git ls-remote`` per call would be a latency/flakiness regression.
|
|
"""
|
|
|
|
def setUp(self):
|
|
# These cases intentionally exercise the subprocess/cache path, so they
|
|
# opt out of suite-wide hermetic mode (PR #788 F1).
|
|
self._saved_hermetic = mp.hermetic_test_mode()
|
|
mp.set_hermetic_test_mode(False)
|
|
mp._clear_remote_head_cache()
|
|
env = {
|
|
k: v for k, v in os.environ.items()
|
|
if k not in (mp.ENV_TEST_LIVE_REMOTE_HEAD,
|
|
mp.ENV_TEST_ALLOW_LIVE_REMOTE_PROBE,
|
|
"PYTEST_CURRENT_TEST")
|
|
}
|
|
# Allow the probe path under hermetic defenses while still mocking
|
|
# subprocess so no real network call runs.
|
|
env[mp.ENV_TEST_ALLOW_LIVE_REMOTE_PROBE] = "1"
|
|
self._env = patch.dict(os.environ, env, clear=True)
|
|
self._env.start()
|
|
self.addCleanup(self._env.stop)
|
|
self.addCleanup(mp._clear_remote_head_cache)
|
|
self.addCleanup(
|
|
lambda: mp.set_hermetic_test_mode(self._saved_hermetic)
|
|
)
|
|
|
|
def _fake_run(self, sha):
|
|
class _R:
|
|
returncode = 0
|
|
stdout = f"{sha}\trefs/heads/master\n"
|
|
calls = {"n": 0}
|
|
|
|
def run(*args, **kwargs):
|
|
calls["n"] += 1
|
|
return _R()
|
|
return run, calls
|
|
|
|
def test_second_call_within_ttl_uses_cache(self):
|
|
run, calls = self._fake_run(SHA_B)
|
|
with patch.object(mp.subprocess, "run", run):
|
|
a = mp.read_remote_master_head("/repo", remote="prgs", ttl=100)
|
|
b = mp.read_remote_master_head("/repo", remote="prgs", ttl=100)
|
|
self.assertEqual(a, SHA_B)
|
|
self.assertEqual(b, SHA_B)
|
|
self.assertEqual(calls["n"], 1)
|
|
|
|
def test_zero_ttl_bypasses_cache(self):
|
|
run, calls = self._fake_run(SHA_B)
|
|
with patch.object(mp.subprocess, "run", run):
|
|
mp.read_remote_master_head("/repo", remote="prgs", ttl=0)
|
|
mp.read_remote_master_head("/repo", remote="prgs", ttl=0)
|
|
self.assertEqual(calls["n"], 2)
|
|
|
|
def test_env_override_never_touches_subprocess(self):
|
|
run, calls = self._fake_run(SHA_B)
|
|
with patch.dict(os.environ, {mp.ENV_TEST_LIVE_REMOTE_HEAD: SHA_A}):
|
|
with patch.object(mp.subprocess, "run", run):
|
|
self.assertEqual(
|
|
mp.read_remote_master_head("/repo", remote="prgs"), SHA_A)
|
|
self.assertEqual(calls["n"], 0)
|
|
|
|
|
|
class TestHermeticLiveRemoteReads(unittest.TestCase):
|
|
"""#610 / PR #788 F1/F2: suite hermetic mode never hits the network."""
|
|
|
|
def setUp(self):
|
|
self._saved = mp.hermetic_test_mode()
|
|
mp.set_hermetic_test_mode(True)
|
|
mp._clear_remote_head_cache()
|
|
self.addCleanup(lambda: mp.set_hermetic_test_mode(self._saved))
|
|
self.addCleanup(mp._clear_remote_head_cache)
|
|
|
|
def test_hermetic_mode_returns_none_without_subprocess(self):
|
|
run_calls = {"n": 0}
|
|
|
|
def boom(*args, **kwargs):
|
|
run_calls["n"] += 1
|
|
raise AssertionError("ls-remote must not run under hermetic mode")
|
|
|
|
env = {
|
|
k: v for k, v in os.environ.items()
|
|
if k not in (mp.ENV_TEST_LIVE_REMOTE_HEAD,
|
|
mp.ENV_TEST_ALLOW_LIVE_REMOTE_PROBE)
|
|
}
|
|
with patch.dict(os.environ, env, clear=True):
|
|
with patch.object(mp.subprocess, "run", boom):
|
|
self.assertIsNone(
|
|
mp.read_remote_master_head("/repo", remote="prgs")
|
|
)
|
|
self.assertEqual(run_calls["n"], 0)
|
|
|
|
def test_hermetic_mode_survives_clear_true_env(self):
|
|
"""Module flag, not env pin: clear=True cannot re-enable the probe."""
|
|
run_calls = {"n": 0}
|
|
|
|
def boom(*args, **kwargs):
|
|
run_calls["n"] += 1
|
|
raise AssertionError("ls-remote must not run after clear=True")
|
|
|
|
with patch.dict(os.environ, {}, clear=True):
|
|
with patch.object(mp.subprocess, "run", boom):
|
|
self.assertIsNone(mp.read_remote_master_head("/repo"))
|
|
self.assertEqual(run_calls["n"], 0)
|
|
|
|
def test_explicit_override_still_wins_under_hermetic(self):
|
|
run_calls = {"n": 0}
|
|
|
|
def boom(*args, **kwargs):
|
|
run_calls["n"] += 1
|
|
raise AssertionError("override must bypass subprocess")
|
|
|
|
with patch.dict(os.environ, {mp.ENV_TEST_LIVE_REMOTE_HEAD: SHA_B}):
|
|
with patch.object(mp.subprocess, "run", boom):
|
|
self.assertEqual(
|
|
mp.read_remote_master_head("/repo"), SHA_B
|
|
)
|
|
self.assertEqual(run_calls["n"], 0)
|
|
|
|
|
|
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}
|
|
# Keep the live-remote read hermetic (no real ls-remote network call):
|
|
# default the live master to the daemon start so parity is fully green
|
|
# unless a test overrides the live head explicitly (#610).
|
|
self._live_patch = patch.dict(
|
|
os.environ, {mp.ENV_TEST_LIVE_REMOTE_HEAD: SHA_A})
|
|
self._live_patch.start()
|
|
self.addCleanup(self._live_patch.stop)
|
|
|
|
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)
|
|
|
|
# --- #610: live-remote wiring -------------------------------------------
|
|
|
|
def test_live_stale_blocks_mutation_though_local_green(self):
|
|
# Local checkout matches the daemon start (local parity green) but the
|
|
# live remote master has advanced -> mutations must fail closed.
|
|
with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_A,
|
|
mp.ENV_TEST_LIVE_REMOTE_HEAD: SHA_B}):
|
|
self.assertEqual(self.srv._master_parity_block("gitea.read"), [])
|
|
self.assertTrue(
|
|
self.srv._master_parity_block("gitea.pr.create"))
|
|
|
|
def test_assess_tool_exposes_three_distinct_shas(self):
|
|
with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_A,
|
|
mp.ENV_TEST_LIVE_REMOTE_HEAD: SHA_B}):
|
|
out = self.srv.gitea_assess_master_parity(remote="prgs")
|
|
self.assertEqual(out["daemon_start_head"], SHA_A)
|
|
self.assertEqual(out["local_head"], SHA_A)
|
|
self.assertEqual(out["live_remote_head"], SHA_B)
|
|
self.assertTrue(out["live_stale"])
|
|
self.assertFalse(out["mutation_safe"])
|
|
self.assertIn("report", out)
|
|
|
|
def test_assess_tool_mutation_safe_when_all_three_match(self):
|
|
with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_A,
|
|
mp.ENV_TEST_LIVE_REMOTE_HEAD: SHA_A}):
|
|
out = self.srv.gitea_assess_master_parity(remote="prgs")
|
|
self.assertTrue(out["mutation_safe"])
|
|
self.assertFalse(out["live_stale"])
|
|
self.assertNotIn("report", out)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|