Skip to main content
Glama
test_signal_manager.py11.4 kB
"""SignalManager 模块测试。 测试信号管理器的基本功能: - 信号处理策略 - 配置支持 - 双击退出 """ from __future__ import annotations import asyncio import os import sys from unittest import mock import pytest from cli_agent_mcp.config import SigintMode from cli_agent_mcp.orchestrator import RequestRegistry from cli_agent_mcp.signal_manager import SignalManager class TestSigintMode: """SigintMode 枚举测试。""" def test_from_string_valid(self): """有效字符串解析。""" assert SigintMode.from_string("cancel") == SigintMode.CANCEL assert SigintMode.from_string("exit") == SigintMode.EXIT assert SigintMode.from_string("cancel_then_exit") == SigintMode.CANCEL_THEN_EXIT def test_from_string_case_insensitive(self): """大小写不敏感。""" assert SigintMode.from_string("CANCEL") == SigintMode.CANCEL assert SigintMode.from_string("Exit") == SigintMode.EXIT assert SigintMode.from_string("Cancel_Then_Exit") == SigintMode.CANCEL_THEN_EXIT def test_from_string_invalid(self): """无效字符串返回默认值 CANCEL。""" assert SigintMode.from_string("invalid") == SigintMode.CANCEL assert SigintMode.from_string("") == SigintMode.CANCEL class TestSignalManagerInit: """SignalManager 初始化测试。""" def test_init_with_defaults(self): """使用默认配置初始化。""" registry = RequestRegistry() with mock.patch.dict(os.environ, {}, clear=False): # 确保没有相关环境变量 os.environ.pop("CAM_SIGINT_MODE", None) os.environ.pop("CAM_SIGINT_DOUBLE_TAP_WINDOW", None) # 重新加载配置 from cli_agent_mcp.config import reload_config reload_config() manager = SignalManager(registry) assert manager.registry is registry assert manager.sigint_mode == SigintMode.CANCEL assert manager.double_tap_window == 1.0 def test_init_with_custom_values(self): """使用自定义值初始化。""" registry = RequestRegistry() manager = SignalManager( registry, sigint_mode=SigintMode.EXIT, double_tap_window=2.0, ) assert manager.sigint_mode == SigintMode.EXIT assert manager.double_tap_window == 2.0 class TestSignalManagerSigintCancel: """SignalManager SIGINT CANCEL 模式测试。""" def test_sigint_with_active_requests_cancels_all(self): """有活动请求时 SIGINT 取消所有请求。""" registry = RequestRegistry() task = mock.MagicMock(spec=asyncio.Task) task.done.return_value = False registry.register("req-1", "codex", task) manager = SignalManager( registry, sigint_mode=SigintMode.CANCEL, ) # 模拟 SIGINT manager._handle_sigint() # 验证请求被取消 task.cancel.assert_called_once() # 验证没有请求关闭 assert manager.is_shutdown_requested is False def test_sigint_without_active_requests_shuts_down(self): """没有活动请求时 SIGINT 请求关闭。""" registry = RequestRegistry() manager = SignalManager( registry, sigint_mode=SigintMode.CANCEL, ) manager._shutdown_event = asyncio.Event() manager._loop = mock.MagicMock() # 模拟 SIGINT manager._handle_sigint() # 验证请求关闭 assert manager.is_shutdown_requested is True class TestSignalManagerSigintExit: """SignalManager SIGINT EXIT 模式测试。""" def test_sigint_always_shuts_down(self): """EXIT 模式下 SIGINT 始终请求关闭。""" registry = RequestRegistry() task = mock.MagicMock(spec=asyncio.Task) task.done.return_value = False registry.register("req-1", "codex", task) manager = SignalManager( registry, sigint_mode=SigintMode.EXIT, ) manager._shutdown_event = asyncio.Event() manager._loop = mock.MagicMock() # 模拟 SIGINT manager._handle_sigint() # 验证请求关闭(即使有活动请求) assert manager.is_shutdown_requested is True # 验证请求没有被取消 task.cancel.assert_not_called() class TestSignalManagerSigintCancelThenExit: """SignalManager SIGINT CANCEL_THEN_EXIT 模式测试。""" def test_sigint_first_cancels_second_exits(self): """CANCEL_THEN_EXIT 模式:第一次取消,第二次退出。""" registry = RequestRegistry() task = mock.MagicMock(spec=asyncio.Task) task.done.return_value = False registry.register("req-1", "codex", task) manager = SignalManager( registry, sigint_mode=SigintMode.CANCEL_THEN_EXIT, double_tap_window=1.0, ) manager._shutdown_event = asyncio.Event() manager._loop = mock.MagicMock() # 第一次 SIGINT manager._handle_sigint() # 验证请求被取消 task.cancel.assert_called_once() # 验证标记为已请求关闭但没有触发实际关闭 assert manager._shutdown_requested is True def test_sigint_without_active_requests_shuts_down(self): """CANCEL_THEN_EXIT 模式:没有活动请求时直接关闭。""" registry = RequestRegistry() manager = SignalManager( registry, sigint_mode=SigintMode.CANCEL_THEN_EXIT, ) manager._shutdown_event = asyncio.Event() manager._loop = mock.MagicMock() # 模拟 SIGINT manager._handle_sigint() # 验证请求关闭 assert manager.is_shutdown_requested is True class TestSignalManagerDoubleTap: """SignalManager 双击退出测试。""" def test_double_tap_forces_exit(self): """双击 SIGINT 设置强制退出标志。 注意:新实现不再直接调用 sys.exit(130),而是设置 is_force_exit 标志, 让主循环在清理完成后再退出。这确保子进程被正确清理。 """ registry = RequestRegistry() manager = SignalManager( registry, sigint_mode=SigintMode.CANCEL_THEN_EXIT, double_tap_window=1.0, ) manager._shutdown_event = asyncio.Event() manager._loop = mock.MagicMock() # 设置状态:已请求关闭 manager._shutdown_requested = True # 第一次 SIGINT manager._handle_sigint() # 第二次 SIGINT(在窗口内)- 应该设置强制退出标志 manager._handle_sigint() # 验证强制退出标志被设置 assert manager.is_force_exit is True # 验证 shutdown event 被触发 manager._loop.call_soon_threadsafe.assert_called() class TestSignalManagerSigterm: """SignalManager SIGTERM 测试。""" def test_sigterm_cancels_all_and_shuts_down(self): """SIGTERM 取消所有请求并关闭。""" registry = RequestRegistry() task = mock.MagicMock(spec=asyncio.Task) task.done.return_value = False registry.register("req-1", "codex", task) manager = SignalManager(registry) manager._shutdown_event = asyncio.Event() manager._loop = mock.MagicMock() # 模拟 SIGTERM manager._handle_sigterm() # 验证请求被取消 task.cancel.assert_called_once() # 验证请求关闭 assert manager.is_shutdown_requested is True class TestSignalManagerCallbacks: """SignalManager 回调测试。""" def test_on_shutdown_callback(self): """关闭时调用回调。""" registry = RequestRegistry() callback = mock.MagicMock() manager = SignalManager( registry, sigint_mode=SigintMode.EXIT, on_shutdown=callback, ) manager._shutdown_event = asyncio.Event() manager._loop = mock.MagicMock() # 模拟 SIGINT(EXIT 模式直接关闭) manager._handle_sigint() # 验证回调被调用 callback.assert_called_once() class TestSignalManagerGracefulShutdown: """SignalManager 程序化关闭测试。""" def test_request_graceful_shutdown(self): """程序化请求优雅退出。""" registry = RequestRegistry() task = mock.MagicMock(spec=asyncio.Task) task.done.return_value = False registry.register("req-1", "codex", task) manager = SignalManager(registry) manager._shutdown_event = asyncio.Event() manager._loop = mock.MagicMock() # 程序化请求关闭 manager.request_graceful_shutdown() # 验证请求被取消 task.cancel.assert_called_once() # 验证请求关闭 assert manager.is_shutdown_requested is True @pytest.mark.skipif(sys.platform == "win32", reason="POSIX signal handling") class TestSignalManagerStartStop: """SignalManager 启动/停止测试(仅 POSIX)。""" @pytest.mark.asyncio async def test_start_and_stop(self): """启动和停止信号管理器。""" registry = RequestRegistry() manager = SignalManager(registry) # 启动 await manager.start() assert manager._running is True assert manager._loop is not None # 停止 await manager.stop() assert manager._running is False class TestConfigParsing: """配置解析测试。""" def test_parse_sigint_mode_from_env(self): """从环境变量解析 SIGINT 模式。""" from cli_agent_mcp.config import load_config with mock.patch.dict(os.environ, {"CAM_SIGINT_MODE": "exit"}): config = load_config() assert config.sigint_mode == SigintMode.EXIT with mock.patch.dict(os.environ, {"CAM_SIGINT_MODE": "cancel_then_exit"}): config = load_config() assert config.sigint_mode == SigintMode.CANCEL_THEN_EXIT def test_parse_double_tap_window_from_env(self): """从环境变量解析双击窗口时间。""" from cli_agent_mcp.config import load_config with mock.patch.dict(os.environ, {"CAM_SIGINT_DOUBLE_TAP_WINDOW": "2.5"}): config = load_config() assert config.sigint_double_tap_window == 2.5 def test_parse_double_tap_window_clamped(self): """双击窗口时间被限制在范围内。""" from cli_agent_mcp.config import load_config # 太小 with mock.patch.dict(os.environ, {"CAM_SIGINT_DOUBLE_TAP_WINDOW": "0.01"}): config = load_config() assert config.sigint_double_tap_window == 0.1 # 太大 with mock.patch.dict(os.environ, {"CAM_SIGINT_DOUBLE_TAP_WINDOW": "100"}): config = load_config() assert config.sigint_double_tap_window == 10.0 def test_parse_double_tap_window_invalid(self): """无效值返回默认值。""" from cli_agent_mcp.config import load_config with mock.patch.dict(os.environ, {"CAM_SIGINT_DOUBLE_TAP_WINDOW": "invalid"}): config = load_config() assert config.sigint_double_tap_window == 1.0

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/shiharuharu/cli-agent-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server