Skip to content

Commit 62dedf0

Browse files
committed
fix test
1 parent 256f7b3 commit 62dedf0

File tree

1 file changed

+301
-23
lines changed

1 file changed

+301
-23
lines changed

tests/test_action_handlers.py

Lines changed: 301 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from unittest.mock import AsyncMock, patch, MagicMock, create_autospec, PropertyMock
66
from typing import Tuple, Optional, Dict, Any
77
import inspect
8+
import time
9+
from unittest.mock import mock_open
10+
import base64
811

912
# Add the source directory to the path
1013
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
@@ -28,6 +31,9 @@ def mock_socket():
2831
handle_remote_macos_mouse_double_click,
2932
handle_remote_macos_mouse_move,
3033
handle_remote_macos_send_keys,
34+
handle_remote_macos_get_screen_update,
35+
handle_remote_macos_open_application,
36+
handle_remote_macos_mouse_drag_n_drop,
3137
)
3238

3339
# Patch paths - the key insight is that we need to patch where the object is USED, not where it's defined
@@ -36,6 +42,13 @@ def mock_socket():
3642
VNC_CLIENT_PATH = 'src.action_handlers.VNCClient' # Need to patch where it's used
3743
CAPTURE_VNC_SCREEN_PATH = 'src.action_handlers.capture_vnc_screen' # Need to patch where it's used
3844

45+
# Constants for testing
46+
TEST_HOST = 'test-host'
47+
TEST_PORT = 5900
48+
TEST_USERNAME = 'test-user'
49+
TEST_PASSWORD = 'test-password'
50+
MESSAGE_TYPE_SCREEN_UPDATE = "screen_update"
51+
3952
# Check which functions are async
4053
IS_GET_SCREEN_ASYNC = inspect.iscoroutinefunction(handle_remote_macos_get_screen)
4154
IS_MOUSE_SCROLL_ASYNC = inspect.iscoroutinefunction(handle_remote_macos_mouse_scroll)
@@ -47,12 +60,6 @@ def mock_socket():
4760
# Use actual MCP types for validation
4861
import mcp.types as types
4962

50-
# Constants for testing (must match what's in conftest.py)
51-
TEST_HOST = 'test-host'
52-
TEST_PORT = 5900
53-
TEST_USERNAME = 'test-user'
54-
TEST_PASSWORD = 'test-password'
55-
5663
@pytest.fixture
5764
def mock_env_vars():
5865
"""Mock environment variables for testing."""
@@ -77,24 +84,39 @@ async def test_handle_remote_macos_get_screen_success(mock_capture_vnc_screen, m
7784
(1366, 768) # dimensions
7885
)
7986

80-
# Act
81-
if IS_GET_SCREEN_ASYNC:
82-
result = await handle_remote_macos_get_screen({})
83-
else:
84-
result = handle_remote_macos_get_screen({})
87+
# Create mock LiveKit client
88+
mock_livekit_client = MagicMock()
89+
90+
# Configure mock LiveKit client to return screen captures
91+
screen_capture = {
92+
"timestamp": time.time(),
93+
"screen_path": "test_screen.png",
94+
"message": {
95+
"content": {
96+
"dimensions": {
97+
"width": 1366,
98+
"height": 768
99+
}
100+
}
101+
}
102+
}
103+
mock_livekit_client.get_screen_captures.return_value = [screen_capture]
104+
105+
# Mock file operations
106+
with patch("builtins.open", mock_open(read_data=b'test_image_data')), \
107+
patch("os.path.exists", return_value=True):
108+
109+
# Act
110+
if IS_GET_SCREEN_ASYNC:
111+
result = await handle_remote_macos_get_screen({}, mock_livekit_client)
112+
else:
113+
result = handle_remote_macos_get_screen({}, mock_livekit_client)
85114

86115
# Assert
87116
assert len(result) == 2
88117
assert result[0].type == "image"
89118
assert result[0].mimeType == "image/png"
90119
assert result[1].text == "Image dimensions: 1366x768"
91-
mock_capture_vnc_screen.assert_called_once_with(
92-
host=TEST_HOST,
93-
port=TEST_PORT,
94-
password=TEST_PASSWORD,
95-
username=TEST_USERNAME,
96-
encryption='prefer_on'
97-
)
98120

99121
@pytest.mark.asyncio
100122
@patch(CAPTURE_VNC_SCREEN_PATH, new_callable=AsyncMock)
@@ -108,17 +130,23 @@ async def test_handle_remote_macos_get_screen_failure(mock_capture_vnc_screen, m
108130
None # dimensions
109131
)
110132

133+
# Create mock LiveKit client
134+
mock_livekit_client = MagicMock()
135+
136+
# Configure mock LiveKit client to return an empty list of screen captures
137+
mock_livekit_client.get_screen_captures.return_value = []
138+
mock_livekit_client.get_message_history.return_value = []
139+
111140
# Act
112141
if IS_GET_SCREEN_ASYNC:
113-
result = await handle_remote_macos_get_screen({})
142+
result = await handle_remote_macos_get_screen({}, mock_livekit_client)
114143
else:
115-
result = handle_remote_macos_get_screen({})
144+
result = handle_remote_macos_get_screen({}, mock_livekit_client)
116145

117146
# Assert
118147
assert len(result) == 1
119148
assert result[0].type == "text"
120-
assert "Connection failed" in result[0].text
121-
mock_capture_vnc_screen.assert_called_once()
149+
assert "No screen images found in message history" in result[0].text
122150

123151
@pytest.mark.asyncio
124152
async def test_handle_remote_macos_mouse_scroll(mock_env_vars):
@@ -377,4 +405,254 @@ async def test_handle_connection_error(mock_env_vars):
377405
assert "Connection failed" in result[0].text
378406
mock_instance.connect.assert_called_once()
379407
# Note: close() is not called when connection fails because we return early
380-
# This is correct behavior based on the implementation
408+
# This is correct behavior based on the implementation
409+
410+
@pytest.mark.asyncio
411+
async def test_handle_remote_macos_get_screen_update():
412+
"""Test get screen update function."""
413+
# Create mock LiveKit client
414+
mock_livekit_client = MagicMock()
415+
416+
# Set up mock data
417+
message_history = [
418+
{
419+
"timestamp": 1000,
420+
"direction": "incoming",
421+
"participant": "test-participant",
422+
"message": {
423+
"type": MESSAGE_TYPE_SCREEN_UPDATE,
424+
"content": {"test": "data"}
425+
}
426+
}
427+
]
428+
mock_livekit_client.get_message_history.return_value = message_history
429+
430+
# Create audit log entry
431+
action_entry = {
432+
"timestamp": 1001,
433+
"iso_time": "2023-01-01T00:00:01",
434+
"action": "test_action",
435+
"arguments": {"arg1": "value1"}
436+
}
437+
438+
# Mock the global ACTION_AUDIT_LOG
439+
with patch("src.action_handlers.ACTION_AUDIT_LOG", [action_entry]), \
440+
patch("src.action_handlers.LAST_ACTION_TIMESTAMP", 1001):
441+
442+
# Call the function
443+
result = handle_remote_macos_get_screen_update({"max_entries": 5}, mock_livekit_client)
444+
445+
# Assertions
446+
assert len(result) == 1
447+
assert result[0].type == "text"
448+
assert "Consolidated action and screen update history" in result[0].text
449+
assert "test_action" in result[0].text
450+
assert "test-participant" in result[0].text
451+
assert "IMPORTANT: The last action has not yet resulted in a screen update" in result[0].text
452+
453+
@pytest.mark.asyncio
454+
async def test_handle_remote_macos_get_screen_update_no_client():
455+
"""Test get screen update function with no LiveKit client."""
456+
# Call the function without a LiveKit client
457+
result = handle_remote_macos_get_screen_update({"max_entries": 5})
458+
459+
# Assertions
460+
assert len(result) == 1
461+
assert result[0].type == "text"
462+
assert "Error: LiveKit client is not available" in result[0].text
463+
464+
@pytest.mark.asyncio
465+
async def test_handle_remote_macos_open_application(mock_env_vars):
466+
"""Test opening an application on remote macOS."""
467+
with patch(VNC_CLIENT_PATH) as MockVNCClass:
468+
# Setup mock VNC instance
469+
mock_instance = MagicMock()
470+
MockVNCClass.return_value = mock_instance
471+
472+
# Configure mock behavior
473+
mock_instance.connect.return_value = (True, None)
474+
mock_instance.send_key_event.return_value = True
475+
mock_instance.send_text.return_value = True
476+
477+
# Mock the time module
478+
with patch('src.action_handlers.time') as mock_time:
479+
# Configure mock to maintain its own time value
480+
mock_time_value = 100.0
481+
def time_side_effect():
482+
nonlocal mock_time_value
483+
mock_time_value += 0.25
484+
return mock_time_value
485+
486+
mock_time.time.side_effect = time_side_effect
487+
mock_time.sleep.return_value = None # No-op for sleep
488+
489+
# Act
490+
result = handle_remote_macos_open_application({
491+
"identifier": "TestApp"
492+
})
493+
494+
# Assert
495+
assert len(result) == 1
496+
assert result[0].type == "text"
497+
assert "Launched application: TestApp" in result[0].text
498+
assert "Processing time:" in result[0].text
499+
500+
# Verify interaction with VNC client
501+
mock_instance.connect.assert_called_once()
502+
assert mock_instance.send_key_event.call_count >= 4 # Command+Space press/release and Enter press/release
503+
mock_instance.send_text.assert_called_once_with("TestApp")
504+
mock_instance.close.assert_called_once()
505+
506+
# Verify time.sleep was called twice (once for Spotlight to open, once for app lookup)
507+
assert mock_time.sleep.call_count == 2
508+
509+
@pytest.mark.asyncio
510+
async def test_handle_remote_macos_open_application_connection_error(mock_env_vars):
511+
"""Test handling connection error when opening an application."""
512+
with patch(VNC_CLIENT_PATH) as MockVNCClass:
513+
# Setup mock VNC instance
514+
mock_instance = MagicMock()
515+
MockVNCClass.return_value = mock_instance
516+
517+
# Configure mock to simulate connection failure
518+
mock_instance.connect.return_value = (False, "Connection failed")
519+
520+
# Act
521+
result = handle_remote_macos_open_application({
522+
"identifier": "TestApp"
523+
})
524+
525+
# Assert
526+
assert len(result) == 1
527+
assert result[0].type == "text"
528+
assert "Failed to connect" in result[0].text
529+
assert "Connection failed" in result[0].text
530+
531+
# Verify VNC client was not used after connection failure
532+
mock_instance.connect.assert_called_once()
533+
mock_instance.send_key_event.assert_not_called()
534+
mock_instance.send_text.assert_not_called()
535+
mock_instance.close.assert_not_called()
536+
537+
@pytest.mark.asyncio
538+
async def test_handle_remote_macos_mouse_drag_n_drop(mock_env_vars):
539+
"""Test mouse drag and drop operation."""
540+
with patch(VNC_CLIENT_PATH) as MockVNCClass:
541+
# Setup mock VNC instance
542+
mock_instance = MagicMock()
543+
MockVNCClass.return_value = mock_instance
544+
545+
# Configure mock behavior
546+
mock_instance.connect.return_value = (True, None)
547+
mock_instance.width = 1920
548+
mock_instance.height = 1080
549+
mock_instance.send_pointer_event.return_value = True
550+
551+
# Mock time.sleep to prevent actual waiting in test
552+
with patch('src.action_handlers.time.sleep') as mock_sleep:
553+
# Act
554+
result = handle_remote_macos_mouse_drag_n_drop({
555+
"start_x": 100,
556+
"start_y": 200,
557+
"end_x": 300,
558+
"end_y": 400,
559+
"source_width": 1366,
560+
"source_height": 768,
561+
"button": 1,
562+
"steps": 5,
563+
"delay_ms": 10
564+
})
565+
566+
# Assert
567+
assert len(result) == 1
568+
assert result[0].type == "text"
569+
assert "Mouse drag (button 1) completed" in result[0].text
570+
assert "From source (100, 200) to (300, 400)" in result[0].text
571+
572+
# Verify interaction with VNC client
573+
mock_instance.connect.assert_called_once()
574+
575+
# Should have these calls:
576+
# 1. Initial move to start position
577+
# 2. Press button at start position
578+
# 3. Five steps of movement (pointer events during drag)
579+
# 4. Release button at end position
580+
assert mock_instance.send_pointer_event.call_count >= 7
581+
582+
# Verify sleep was called for each step
583+
assert mock_sleep.call_count == 5
584+
585+
# Verify connection was closed
586+
mock_instance.close.assert_called_once()
587+
588+
@pytest.mark.asyncio
589+
async def test_handle_remote_macos_mouse_drag_n_drop_connection_error(mock_env_vars):
590+
"""Test handling connection error in drag and drop operation."""
591+
with patch(VNC_CLIENT_PATH) as MockVNCClass:
592+
# Setup mock VNC instance
593+
mock_instance = MagicMock()
594+
MockVNCClass.return_value = mock_instance
595+
596+
# Configure mock to simulate connection failure
597+
mock_instance.connect.return_value = (False, "Connection failed")
598+
599+
# Act
600+
result = handle_remote_macos_mouse_drag_n_drop({
601+
"start_x": 100,
602+
"start_y": 200,
603+
"end_x": 300,
604+
"end_y": 400
605+
})
606+
607+
# Assert
608+
assert len(result) == 1
609+
assert result[0].type == "text"
610+
assert "Failed to connect" in result[0].text
611+
assert "Connection failed" in result[0].text
612+
613+
# Verify VNC client was not used after connection failure
614+
mock_instance.connect.assert_called_once()
615+
mock_instance.send_pointer_event.assert_not_called()
616+
mock_instance.close.assert_not_called()
617+
618+
@pytest.mark.asyncio
619+
async def test_handle_remote_macos_mouse_drag_n_drop_step_failure(mock_env_vars):
620+
"""Test handling failure during drag operation."""
621+
with patch(VNC_CLIENT_PATH) as MockVNCClass:
622+
# Setup mock VNC instance
623+
mock_instance = MagicMock()
624+
MockVNCClass.return_value = mock_instance
625+
626+
# Configure mock behavior
627+
mock_instance.connect.return_value = (True, None)
628+
mock_instance.width = 1920
629+
mock_instance.height = 1080
630+
631+
# Configure send_pointer_event to fail on the third step
632+
call_count = 0
633+
def side_effect(*args, **kwargs):
634+
nonlocal call_count
635+
call_count += 1
636+
# Fail on the third step (after initial move and button press)
637+
return call_count <= 2
638+
639+
mock_instance.send_pointer_event.side_effect = side_effect
640+
641+
# Act
642+
result = handle_remote_macos_mouse_drag_n_drop({
643+
"start_x": 100,
644+
"start_y": 200,
645+
"end_x": 300,
646+
"end_y": 400,
647+
"steps": 5
648+
})
649+
650+
# Assert
651+
assert len(result) == 1
652+
assert result[0].type == "text"
653+
assert "Failed during drag at step 1" in result[0].text
654+
655+
# Verify interaction with VNC client
656+
mock_instance.connect.assert_called_once()
657+
assert mock_instance.send_pointer_event.call_count == 3 # Initial, button press, first step (fails)
658+
mock_instance.close.assert_called_once()

0 commit comments

Comments
 (0)