import os import unittest from unittest.mock import patch, MagicMock from datetime import datetime import gitea_mcp_server class TestMcpStaleRuntime(unittest.TestCase): @patch("subprocess.run") @patch("os.path.getmtime") @patch("os.path.exists") @patch("os.getpid") @patch.dict("os.environ", {"GITEA_FORCE_MCP_RUNTIME_CHECK": "1"}) def test_stale_and_missing_runtimes(self, mock_getpid, mock_exists, mock_getmtime, mock_run): # Setup mocks mock_getpid.return_value = 12345 mock_exists.return_value = True # Code modification time: Jul 8 2026, 14:00:00 code_time = datetime(2026, 7, 8, 14, 0, 0) mock_getmtime.return_value = code_time.timestamp() # Mock ps -ax output # PID 12345 is self (started at 13:00:00 - stale) # PID 54321 is prgs-author (started at 15:00:00 - fresh) # prgs-reviewer is missing ps_output = ( " PID LSTART COMMAND\n" "12345 Wed Jul 8 13:00:00 2026 /path/to/python mcp_server.py\n" "54321 Wed Jul 8 15:00:00 2026 /path/to/python mcp_server.py\n" ) mock_run_ps = MagicMock() mock_run_ps.stdout = ps_output # Mock env output for ps eww mock_run_env12345 = MagicMock() mock_run_env12345.stdout = "GITEA_MCP_PROFILE=prgs-reconciler" mock_run_env54321 = MagicMock() mock_run_env54321.stdout = "GITEA_MCP_PROFILE=prgs-author" def side_effect(args, **kwargs): if args[0] == "ps" and "eww" in args: pid = args[2] if pid == "12345": return mock_run_env12345 elif pid == "54321": return mock_run_env54321 elif args[0] == "ps": return mock_run_ps raise ValueError(f"Unexpected subprocess run args: {args}") mock_run.side_effect = side_effect # Test 1: required reviewer role matching prgs-reviewer (which is missing) reasons = gitea_mcp_server._check_mcp_runtimes_diagnostics("review_pr", ["prgs-reviewer"]) self.assertTrue(any("stale-runtime: The active Gitea MCP server process is stale" in r for r in reasons)) self.assertTrue(any("stale-runtime: None of the matching profiles for task" in r for r in reasons)) # Test 2: required author role matching prgs-author (which is fresh) reasons_author = gitea_mcp_server._check_mcp_runtimes_diagnostics("create_issue", ["prgs-author"]) # Still contains self stale error self.assertTrue(any("stale-runtime: The active Gitea MCP server process is stale" in r for r in reasons_author)) # But does NOT contain missing author profile error self.assertFalse(any("None of the matching profiles for task" in r for r in reasons_author)) @patch("subprocess.run") @patch("os.path.getmtime") @patch("os.path.exists") @patch("os.getpid") @patch.dict("os.environ", {"GITEA_FORCE_MCP_RUNTIME_CHECK": "1"}) def test_git_head_change_triggers_staleness(self, mock_getpid, mock_exists, mock_getmtime, mock_run): # Set boot SHA to FAKE1 and current to FAKE2 gitea_mcp_server._process_boot_head_sha = "FAKE1" mock_getpid.return_value = 12345 mock_exists.return_value = True # Code time is in the past (fresh process chronologically) code_time = datetime(2026, 7, 8, 11, 0, 0) mock_getmtime.return_value = code_time.timestamp() ps_output = ( " PID LSTART COMMAND\n" "12345 Wed Jul 8 12:00:00 2026 /path/to/python mcp_server.py\n" ) mock_run_ps = MagicMock() mock_run_ps.stdout = ps_output mock_run_env = MagicMock() mock_run_env.stdout = "GITEA_MCP_PROFILE=prgs-author" mock_run_git = MagicMock() mock_run_git.stdout = "FAKE2" # different SHA def side_effect(args, **kwargs): if args[0] == "ps" and "eww" in args: return mock_run_env elif args[0] == "ps": return mock_run_ps elif args[0] == "git" and "rev-parse" in args: return mock_run_git raise ValueError(f"Unexpected: {args}") mock_run.side_effect = side_effect # Stale even though process started AFTER code mtime, because git HEAD changed reasons = gitea_mcp_server._check_mcp_runtimes_diagnostics("create_issue", ["prgs-author"]) self.assertTrue(any("stale-runtime: The active Gitea MCP server process is stale" in r for r in reasons)) @patch("threading.Thread") @patch("os.utime") @patch("os.path.exists") @patch.dict("os.environ", {"MCP_CONFIG_PATH": "/tmp/mcp_config.json"}) def test_auto_restart_trigger_touches_and_spawns(self, mock_exists, mock_utime, mock_thread): mock_exists.return_value = True gitea_mcp_server._restart_triggered = False # Ensure we are not skipped in test mode for testing purposes with patch("gitea_mcp_server._preflight_in_test_mode", return_value=False): gitea_mcp_server._trigger_mcp_auto_restart() mock_utime.assert_called_once_with("/tmp/mcp_config.json", None) mock_thread.assert_called_once() self.assertTrue(gitea_mcp_server._restart_triggered) if __name__ == "__main__": unittest.main()