Share review-workflow-load and review-decision-lock state across MCP daemon process pools via a user-private durable store keyed by profile identity (not host-global /tmp), with TTL and fail-closed profile checks.
208 lines
7.2 KiB
Python
208 lines
7.2 KiB
Python
"""Tests for durable MCP session state shared across daemon processes (#559)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
sys_path_root = str(Path(__file__).resolve().parent.parent)
|
|
import sys
|
|
|
|
if sys_path_root not in sys.path:
|
|
sys.path.insert(0, sys_path_root)
|
|
|
|
import mcp_session_state
|
|
import review_workflow_load
|
|
import mcp_server
|
|
|
|
|
|
class TestMcpSessionStateStore(unittest.TestCase):
|
|
def setUp(self):
|
|
self._tmpdir = tempfile.TemporaryDirectory()
|
|
self.state_dir = self._tmpdir.name
|
|
self._env = patch.dict(
|
|
os.environ,
|
|
{
|
|
mcp_session_state.STATE_DIR_ENV: self.state_dir,
|
|
mcp_session_state.SESSION_PROFILE_LOCK_ENV: "prgs-reviewer",
|
|
"GITEA_MCP_PROFILE": "prgs-reviewer",
|
|
},
|
|
clear=False,
|
|
)
|
|
self._env.start()
|
|
|
|
def tearDown(self):
|
|
self._env.stop()
|
|
self._tmpdir.cleanup()
|
|
|
|
def test_round_trip_same_identity(self):
|
|
saved = mcp_session_state.save_state(
|
|
kind=mcp_session_state.KIND_WORKFLOW_LOAD,
|
|
payload={"loaded": True, "workflow_hash": "abc123def456"},
|
|
remote="prgs",
|
|
org="Scaled-Tech-Consulting",
|
|
repo="Gitea-Tools",
|
|
)
|
|
self.assertIsNotNone(saved)
|
|
self.assertEqual(saved["workflow_hash"], "abc123def456")
|
|
self.assertIn("recorded_at", saved)
|
|
|
|
loaded = mcp_session_state.load_state(
|
|
kind=mcp_session_state.KIND_WORKFLOW_LOAD,
|
|
remote="prgs",
|
|
org="Scaled-Tech-Consulting",
|
|
repo="Gitea-Tools",
|
|
)
|
|
self.assertIsNotNone(loaded)
|
|
self.assertEqual(loaded["workflow_hash"], "abc123def456")
|
|
self.assertEqual(loaded["profile_identity"], "prgs-reviewer")
|
|
|
|
def test_profile_mismatch_returns_none(self):
|
|
mcp_session_state.save_state(
|
|
kind=mcp_session_state.KIND_DECISION_LOCK,
|
|
payload={"final_review_decision_ready": True, "remote": "prgs"},
|
|
remote="prgs",
|
|
)
|
|
with patch.dict(
|
|
os.environ,
|
|
{mcp_session_state.SESSION_PROFILE_LOCK_ENV: "prgs-author"},
|
|
clear=False,
|
|
):
|
|
loaded = mcp_session_state.load_state(
|
|
kind=mcp_session_state.KIND_DECISION_LOCK,
|
|
remote="prgs",
|
|
)
|
|
self.assertIsNone(loaded)
|
|
|
|
def test_clear_removes_file(self):
|
|
mcp_session_state.save_state(
|
|
kind=mcp_session_state.KIND_WORKFLOW_LOAD,
|
|
payload={"loaded": True},
|
|
remote="prgs",
|
|
)
|
|
path = mcp_session_state.state_file_path(
|
|
kind=mcp_session_state.KIND_WORKFLOW_LOAD,
|
|
remote="prgs",
|
|
profile_identity="prgs-reviewer",
|
|
state_dir=self.state_dir,
|
|
)
|
|
self.assertTrue(os.path.exists(path))
|
|
mcp_session_state.clear_state(
|
|
kind=mcp_session_state.KIND_WORKFLOW_LOAD,
|
|
remote="prgs",
|
|
profile_identity="prgs-reviewer",
|
|
)
|
|
self.assertFalse(os.path.exists(path))
|
|
|
|
def test_files_are_private_mode(self):
|
|
mcp_session_state.save_state(
|
|
kind=mcp_session_state.KIND_WORKFLOW_LOAD,
|
|
payload={"loaded": True},
|
|
remote="prgs",
|
|
)
|
|
path = mcp_session_state.state_file_path(
|
|
kind=mcp_session_state.KIND_WORKFLOW_LOAD,
|
|
remote="prgs",
|
|
profile_identity="prgs-reviewer",
|
|
state_dir=self.state_dir,
|
|
)
|
|
mode = os.stat(path).st_mode & 0o777
|
|
self.assertEqual(mode, 0o600)
|
|
|
|
|
|
class TestWorkflowLoadCrossProcess(unittest.TestCase):
|
|
def setUp(self):
|
|
self._tmpdir = tempfile.TemporaryDirectory()
|
|
self._env = patch.dict(
|
|
os.environ,
|
|
{
|
|
mcp_session_state.STATE_DIR_ENV: self._tmpdir.name,
|
|
mcp_session_state.SESSION_PROFILE_LOCK_ENV: "prgs-reviewer",
|
|
"GITEA_MCP_PROFILE": "prgs-reviewer",
|
|
"GITEA_MCP_REMOTE": "prgs",
|
|
},
|
|
clear=False,
|
|
)
|
|
self._env.start()
|
|
review_workflow_load.clear_review_workflow_load()
|
|
|
|
def tearDown(self):
|
|
review_workflow_load.clear_review_workflow_load()
|
|
self._env.stop()
|
|
self._tmpdir.cleanup()
|
|
|
|
def test_different_pid_same_profile_accepted(self):
|
|
root = str(Path(__file__).resolve().parent.parent)
|
|
recorded = review_workflow_load.record_review_workflow_load(root)
|
|
self.assertTrue(recorded["loaded"])
|
|
|
|
# Simulate another daemon process: clear memory, keep durable file,
|
|
# and change apparent PID identity only (profile remains the same).
|
|
review_workflow_load._REVIEW_WORKFLOW_LOAD = None
|
|
status = review_workflow_load.workflow_load_status(root)
|
|
self.assertTrue(status["workflow_load_proof_present"])
|
|
self.assertTrue(
|
|
status["workflow_load_valid"],
|
|
msg=status.get("reasons"),
|
|
)
|
|
|
|
def test_profile_mismatch_blocks(self):
|
|
root = str(Path(__file__).resolve().parent.parent)
|
|
review_workflow_load.record_review_workflow_load(root)
|
|
review_workflow_load._REVIEW_WORKFLOW_LOAD = None
|
|
with patch.dict(
|
|
os.environ,
|
|
{mcp_session_state.SESSION_PROFILE_LOCK_ENV: "prgs-author"},
|
|
clear=False,
|
|
):
|
|
# Durable load uses active identity; mismatched profile key misses.
|
|
status = review_workflow_load.workflow_load_status(root)
|
|
self.assertFalse(status["workflow_load_proof_present"])
|
|
|
|
|
|
class TestDecisionLockCrossProcess(unittest.TestCase):
|
|
def setUp(self):
|
|
self._tmpdir = tempfile.TemporaryDirectory()
|
|
self._env = patch.dict(
|
|
os.environ,
|
|
{
|
|
mcp_session_state.STATE_DIR_ENV: self._tmpdir.name,
|
|
mcp_session_state.SESSION_PROFILE_LOCK_ENV: "prgs-reviewer",
|
|
},
|
|
clear=False,
|
|
)
|
|
self._env.start()
|
|
mcp_server._save_review_decision_lock(None)
|
|
review_workflow_load.clear_review_workflow_load()
|
|
|
|
def tearDown(self):
|
|
mcp_server._save_review_decision_lock(None)
|
|
review_workflow_load.clear_review_workflow_load()
|
|
self._env.stop()
|
|
self._tmpdir.cleanup()
|
|
|
|
def test_decision_lock_survives_memory_clear(self):
|
|
with patch.object(mcp_server, "get_profile", return_value={
|
|
"profile_name": "prgs-reviewer",
|
|
}):
|
|
mcp_server.init_review_decision_lock("prgs", "review_pr", force=True)
|
|
lock = mcp_server._load_review_decision_lock()
|
|
self.assertIsNotNone(lock)
|
|
self.assertEqual(lock["remote"], "prgs")
|
|
self.assertFalse(lock["final_review_decision_ready"])
|
|
|
|
# New process: memory empty, durable state remains.
|
|
mcp_server._REVIEW_DECISION_LOCK = None
|
|
restored = mcp_server._load_review_decision_lock()
|
|
self.assertIsNotNone(restored)
|
|
self.assertEqual(restored["remote"], "prgs")
|
|
reasons = mcp_server._review_decision_session_reasons(restored)
|
|
self.assertEqual(reasons, [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|