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