Skip to main content
Glama

MCP Claude Code

by SDGLBL
test_advanced_bash_session.py20.9 kB
"""Advanced tests for BashSession state management and interactive features. This module tests the sophisticated BashSession functionality including: - Advanced state management with prev_status tracking - Interactive process handling with is_input and blocking - Command conflict prevention - Timeout handling (no-change vs hard timeout) - Session persistence and cleanup """ import tempfile import os import pytest import shutil from unittest.mock import MagicMock, patch from mcp_claude_code.tools.shell.base import ( BashCommandStatus, CommandResult, ) from mcp_claude_code.tools.shell.bash_session import ( BashSession, split_bash_commands, escape_bash_special_chars, ) class TestBashSessionAdvancedStateManagement: """Test advanced BashSession state management features.""" @pytest.fixture def temp_work_dir(self): """Create a temporary working directory.""" with tempfile.TemporaryDirectory() as temp_dir: yield temp_dir def test_bash_session_initialization_with_id(self, temp_work_dir): """Test BashSession initialization with required id parameter.""" session = BashSession( id="test_session_123", work_dir=temp_work_dir, no_change_timeout_seconds=10 ) assert session.id == "test_session_123" assert session.work_dir == temp_work_dir assert session.NO_CHANGE_TIMEOUT_SECONDS == 10 assert session._initialized is False assert session.prev_status is None assert session.prev_output == "" assert session._closed is False @pytest.mark.skipif(not shutil.which("tmux"), reason="tmux not available") def test_bash_session_state_tracking(self, temp_work_dir): """Test BashSession state tracking with prev_status and prev_output.""" session = BashSession( id="state_test", work_dir=temp_work_dir, no_change_timeout_seconds=5 ) try: session.initialize() # Execute first command session.execute("echo 'first command'") # Check that state is tracked assert session.prev_status == BashCommandStatus.COMPLETED assert session.prev_output == "" # Should be reset after completion # Execute second command session.execute("echo 'second command'") # State should be updated assert session.prev_status == BashCommandStatus.COMPLETED finally: session.close() def test_bash_session_prev_status_initialization(self, temp_work_dir): """Test that prev_status starts as None.""" session = BashSession(id="prev_status_test", work_dir=temp_work_dir) assert session.prev_status is None assert session.prev_output == "" def test_bash_session_working_directory_tracking(self, temp_work_dir): """Test that working directory is properly tracked.""" session = BashSession(id="cwd_test", work_dir=temp_work_dir) # Should initialize with the provided work_dir assert session._cwd == os.path.abspath(temp_work_dir) assert session.cwd == os.path.abspath(temp_work_dir) @pytest.mark.skipif(not shutil.which("tmux"), reason="tmux not available") def test_bash_session_simple_ps1_fallback(self, temp_work_dir): """Test that BashSession uses simple PS1 for compatibility.""" session = BashSession( id="ps1_test", work_dir=temp_work_dir, no_change_timeout_seconds=5 ) try: # Check that PS1 is simple for compatibility assert session.PS1 == "$ " session.initialize() # Should still work with simple PS1 result = session.execute("echo 'ps1 test'") assert result.return_code in [0, -1] # May timeout on some systems finally: session.close() class TestBashSessionInteractiveProcessHandling: """Test BashSession interactive process handling.""" @pytest.fixture def temp_work_dir(self): """Create a temporary working directory.""" with tempfile.TemporaryDirectory() as temp_dir: yield temp_dir @pytest.mark.skipif(not shutil.which("tmux"), reason="tmux not available") def test_execute_with_is_input_parameter(self, temp_work_dir): """Test execute method with is_input parameter.""" session = BashSession( id="input_test", work_dir=temp_work_dir, no_change_timeout_seconds=5 ) try: session.initialize() # Execute with is_input=False (default) result1 = session.execute("echo 'normal command'", is_input=False) assert result1.command == "echo 'normal command'" # Execute with is_input=True result2 = session.execute("test input", is_input=True) assert result2.command == "test input" finally: session.close() @pytest.mark.skipif(not shutil.which("tmux"), reason="tmux not available") def test_execute_with_blocking_parameter(self, temp_work_dir): """Test execute method with blocking parameter.""" session = BashSession( id="blocking_test", work_dir=temp_work_dir, no_change_timeout_seconds=2, # Short timeout for testing ) try: session.initialize() # Execute with blocking=False (should timeout quickly) result1 = session.execute("sleep 0.1", blocking=False) # May complete or timeout depending on timing assert result1.status in [ BashCommandStatus.COMPLETED, BashCommandStatus.NO_CHANGE_TIMEOUT, ] # Execute with blocking=True (should ignore no-change timeout) result2 = session.execute( "echo 'blocking test'", blocking=True, timeout=3.0 ) # Should complete normally without no-change timeout assert result2.command == "echo 'blocking test'" finally: session.close() @pytest.mark.skipif(not shutil.which("tmux"), reason="tmux not available") def test_execute_command_validation_for_running_process(self, temp_work_dir): """Test command validation when previous process is still running.""" session = BashSession( id="validation_test", work_dir=temp_work_dir, no_change_timeout_seconds=2 ) try: session.initialize() # First, test the validation logic for empty commands result_empty = session.execute("", is_input=False) # Should get an error about no previous running command assert not result_empty.is_success assert ( "ERROR: No previous running command to retrieve logs from" in result_empty.error_message ) # Test validation for input when no command is running result_input = session.execute("", is_input=True) assert not result_input.is_success assert ( "ERROR: No previous running command to interact with" in result_input.error_message ) finally: session.close() def test_multiple_bash_commands_rejection(self, temp_work_dir): """Test that multiple bash commands are rejected.""" session = BashSession(id="multi_cmd_test", work_dir=temp_work_dir) try: # Mock split_bash_commands to return multiple commands with patch( "mcp_claude_code.tools.shell.bash_session.split_bash_commands" ) as mock_split: mock_split.return_value = ["echo 'first'", "echo 'second'"] result = session.execute("echo 'first'; echo 'second'") assert not result.is_success assert ( "Cannot execute multiple commands at once" in result.error_message ) assert result.status == BashCommandStatus.COMPLETED finally: session.close() def test_special_key_detection(self, temp_work_dir): """Test special key detection.""" session = BashSession(id="special_key_test", work_dir=temp_work_dir) # Test the _is_special_key method assert session._is_special_key("C-c") is True assert session._is_special_key("C-z") is True assert session._is_special_key("C-d") is True assert session._is_special_key("echo test") is False assert session._is_special_key("") is False class TestBashSessionTimeoutHandling: """Test BashSession timeout handling.""" @pytest.fixture def temp_work_dir(self): """Create a temporary working directory.""" with tempfile.TemporaryDirectory() as temp_dir: yield temp_dir @pytest.mark.skipif(not shutil.which("tmux"), reason="tmux not available") def test_no_change_timeout(self, temp_work_dir): """Test no-change timeout functionality.""" session = BashSession( id="no_change_timeout_test", work_dir=temp_work_dir, no_change_timeout_seconds=1, # Very short timeout ) try: session.initialize() # Execute a command that might trigger no-change timeout # Note: This test is timing-dependent and may be flaky result = session.execute("sleep 2", blocking=False, timeout=10.0) # Should either complete or hit no-change timeout assert result.status in [ BashCommandStatus.COMPLETED, BashCommandStatus.NO_CHANGE_TIMEOUT, BashCommandStatus.HARD_TIMEOUT, ] if result.status == BashCommandStatus.NO_CHANGE_TIMEOUT: assert "no new output after 1 seconds" in (result.error_message or "") finally: session.close() @pytest.mark.skipif(not shutil.which("tmux"), reason="tmux not available") def test_hard_timeout(self, temp_work_dir): """Test hard timeout functionality.""" session = BashSession( id="hard_timeout_test", work_dir=temp_work_dir, no_change_timeout_seconds=10, # Longer than hard timeout ) try: session.initialize() # Execute with a short hard timeout result = session.execute("sleep 5", blocking=False, timeout=1.0) # Should hit hard timeout if result.status == BashCommandStatus.HARD_TIMEOUT: assert "timed out after 1.0 seconds" in (result.error_message or "") assert result.return_code == -1 finally: session.close() def test_timeout_result_format(self, temp_work_dir): """Test that timeout results are properly formatted.""" BashSession(id="timeout_format_test", work_dir=temp_work_dir) # Test with mocked timeout scenarios # Mock _handle_nochange_timeout_command mock_result = CommandResult( return_code=-1, stdout="partial output", status=BashCommandStatus.NO_CHANGE_TIMEOUT, error_message="Command timed out after 30 seconds with no output changes", ) assert mock_result.status == BashCommandStatus.NO_CHANGE_TIMEOUT assert mock_result.error_message is not None assert "timed out" in mock_result.error_message class TestBashSessionCommandConflictPrevention: """Test BashSession command conflict prevention.""" @pytest.fixture def temp_work_dir(self): """Create a temporary working directory.""" with tempfile.TemporaryDirectory() as temp_dir: yield temp_dir def test_command_conflict_detection_logic(self, temp_work_dir): """Test the logic for detecting command conflicts.""" session = BashSession(id="conflict_test", work_dir=temp_work_dir) # Test the state checking logic # When prev_status is not in the "running" states, should allow commands session.prev_status = BashCommandStatus.COMPLETED # This would be checked in the execute method # When prev_status is in running states, should detect conflict running_states = { BashCommandStatus.NO_CHANGE_TIMEOUT, BashCommandStatus.HARD_TIMEOUT, } for status in running_states: session.prev_status = status # The execute method would check this condition def test_command_conflict_with_mocked_session(self, temp_work_dir): """Test command conflict prevention with mocked session state.""" session = BashSession(id="mock_conflict_test", work_dir=temp_work_dir) # Mock the session to appear initialized session._initialized = True session.prev_status = BashCommandStatus.NO_CHANGE_TIMEOUT # Mock pane and other tmux components mock_pane = MagicMock() mock_pane.cmd.return_value.stdout = ["line1", "line2"] session.pane = mock_pane # Mock the _get_pane_content to return content that doesn't end with PS1 with patch.object(session, "_get_pane_content") as mock_get_content: mock_get_content.return_value = "some output without prompt" # This should detect a conflict (command when previous is still running) # The actual conflict detection happens in the execute method # Test the condition that would be checked # Basic conflict detection without PS1 checking assert session.prev_status == BashCommandStatus.NO_CHANGE_TIMEOUT class TestBashSessionOutputProcessing: """Test BashSession advanced output processing.""" @pytest.fixture def temp_work_dir(self): """Create a temporary working directory.""" with tempfile.TemporaryDirectory() as temp_dir: yield temp_dir def test_fallback_completion_detection(self, temp_work_dir): """Test fallback completion detection when prompt patterns are used.""" session = BashSession(id="fallback_test", work_dir=temp_work_dir) # Mock the pane and other necessary components mock_pane = MagicMock() session.pane = mock_pane session._initialized = True # Mock pane.send_keys for exit code checking mock_pane.send_keys = MagicMock() # Mock _get_pane_content to simulate completion detection with patch.object(session, "_get_pane_content") as mock_get_content: # First call returns the main output, second call includes exit code mock_get_content.side_effect = [ "$ echo test\ntest output\n$ ", "$ echo test\ntest output\n$ echo EXIT_CODE:0\nEXIT_CODE:0\n$ ", ] result = session._fallback_completion_detection( "echo test", "$ echo test\ntest output\n$ " ) assert result.command == "echo test" assert result.status == BashCommandStatus.COMPLETED assert result.return_code == 0 def test_get_command_output_with_previous_output(self, temp_work_dir): """Test _get_command_output method with previous output tracking.""" session = BashSession(id="output_test", work_dir=temp_work_dir) # Test with no previous output session.prev_output = "" result1 = session._get_command_output("echo test", "test output") assert "test output" in result1 assert session.prev_output == "test output" # Test with previous output (should be removed) session.prev_output = "previous content" result2 = session._get_command_output( "echo test2", "previous content\nnew output", "prefix" ) assert "new output" in result2 # Should include prefix when continuing from previous output assert "prefix" in result2 def test_ready_for_next_command(self, temp_work_dir): """Test _ready_for_next_command method.""" session = BashSession(id="ready_test", work_dir=temp_work_dir) # Mock the _clear_screen method with patch.object(session, "_clear_screen") as mock_clear: session._ready_for_next_command() mock_clear.assert_called_once() class TestBashSessionUtilityFunctions: """Test BashSession utility functions.""" def test_split_bash_commands_single(self): """Test split_bash_commands with single command.""" result = split_bash_commands("echo test") assert len(result) == 1 assert result[0] == "echo test" def test_split_bash_commands_empty(self): """Test split_bash_commands with empty input.""" result = split_bash_commands("") assert result == [""] def test_split_bash_commands_multiple(self): """Test split_bash_commands with multiple commands.""" result = split_bash_commands("echo first; echo second") # The actual implementation may vary based on bashlex parsing assert len(result) >= 1 def test_split_bash_commands_parse_error(self): """Test split_bash_commands with unparseable input.""" # Test with invalid bash syntax result = split_bash_commands("echo 'unclosed quote") # Should fallback to returning original command assert result == ["echo 'unclosed quote"] def test_escape_bash_special_chars_empty(self): """Test escape_bash_special_chars with empty input.""" result = escape_bash_special_chars("") assert result == "" def test_escape_bash_special_chars_normal(self): """Test escape_bash_special_chars with normal input.""" result = escape_bash_special_chars("echo test") assert result == "echo test" def test_escape_bash_special_chars_with_special(self): """Test escape_bash_special_chars with special characters.""" # Test with characters that need escaping result = escape_bash_special_chars("echo test\\;more") # Should escape the special characters assert "\\\\" in result # The escaped backslash def test_escape_bash_special_chars_parse_error(self): """Test escape_bash_special_chars with unparseable input.""" result = escape_bash_special_chars("echo 'unclosed") # Should return original on parse error assert result == "echo 'unclosed" def test_remove_command_prefix(self): """Test _remove_command_prefix function.""" from mcp_claude_code.tools.shell.bash_session import _remove_command_prefix result = _remove_command_prefix("$ echo test\ntest output", "echo test") assert "test output" in result result2 = _remove_command_prefix(" echo test\noutput", "echo test") assert "output" in result2 class TestBashSessionCleanup: """Test BashSession cleanup functionality.""" @pytest.fixture def temp_work_dir(self): """Create a temporary working directory.""" with tempfile.TemporaryDirectory() as temp_dir: yield temp_dir def test_bash_session_close(self, temp_work_dir): """Test BashSession close method.""" session = BashSession(id="close_test", work_dir=temp_work_dir) # Mock session components mock_session = MagicMock() session.session = mock_session session._closed = False # Call close session.close() # Should call kill_session and set _closed mock_session.kill_session.assert_called_once() assert session._closed is True # Second call should not call kill_session again mock_session.reset_mock() session.close() mock_session.kill_session.assert_not_called() def test_bash_session_del_cleanup(self, temp_work_dir): """Test BashSession __del__ cleanup.""" session = BashSession(id="del_test", work_dir=temp_work_dir) # Mock the close method with patch.object(session, "close") as mock_close: # Trigger __del__ session.__del__() mock_close.assert_called_once() def test_bash_session_close_exception_handling(self, temp_work_dir): """Test BashSession close handles exceptions gracefully.""" session = BashSession(id="exception_test", work_dir=temp_work_dir) # Mock session to raise exception mock_session = MagicMock() mock_session.kill_session.side_effect = Exception("Test exception") session.session = mock_session session._closed = False # Should not raise exception session.close() assert session._closed is True if __name__ == "__main__": pytest.main([__file__, "-v"])

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/SDGLBL/mcp-claude-code'

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