5
5
from unittest .mock import AsyncMock , patch , MagicMock , create_autospec , PropertyMock
6
6
from typing import Tuple , Optional , Dict , Any
7
7
import inspect
8
+ import time
9
+ from unittest .mock import mock_open
10
+ import base64
8
11
9
12
# Add the source directory to the path
10
13
sys .path .insert (0 , os .path .abspath (os .path .join (os .path .dirname (__file__ ), '..' )))
@@ -28,6 +31,9 @@ def mock_socket():
28
31
handle_remote_macos_mouse_double_click ,
29
32
handle_remote_macos_mouse_move ,
30
33
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 ,
31
37
)
32
38
33
39
# 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():
36
42
VNC_CLIENT_PATH = 'src.action_handlers.VNCClient' # Need to patch where it's used
37
43
CAPTURE_VNC_SCREEN_PATH = 'src.action_handlers.capture_vnc_screen' # Need to patch where it's used
38
44
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
+
39
52
# Check which functions are async
40
53
IS_GET_SCREEN_ASYNC = inspect .iscoroutinefunction (handle_remote_macos_get_screen )
41
54
IS_MOUSE_SCROLL_ASYNC = inspect .iscoroutinefunction (handle_remote_macos_mouse_scroll )
@@ -47,12 +60,6 @@ def mock_socket():
47
60
# Use actual MCP types for validation
48
61
import mcp .types as types
49
62
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
-
56
63
@pytest .fixture
57
64
def mock_env_vars ():
58
65
"""Mock environment variables for testing."""
@@ -77,24 +84,39 @@ async def test_handle_remote_macos_get_screen_success(mock_capture_vnc_screen, m
77
84
(1366 , 768 ) # dimensions
78
85
)
79
86
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 )
85
114
86
115
# Assert
87
116
assert len (result ) == 2
88
117
assert result [0 ].type == "image"
89
118
assert result [0 ].mimeType == "image/png"
90
119
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
- )
98
120
99
121
@pytest .mark .asyncio
100
122
@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
108
130
None # dimensions
109
131
)
110
132
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
+
111
140
# Act
112
141
if IS_GET_SCREEN_ASYNC :
113
- result = await handle_remote_macos_get_screen ({})
142
+ result = await handle_remote_macos_get_screen ({}, mock_livekit_client )
114
143
else :
115
- result = handle_remote_macos_get_screen ({})
144
+ result = handle_remote_macos_get_screen ({}, mock_livekit_client )
116
145
117
146
# Assert
118
147
assert len (result ) == 1
119
148
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
122
150
123
151
@pytest .mark .asyncio
124
152
async def test_handle_remote_macos_mouse_scroll (mock_env_vars ):
@@ -377,4 +405,254 @@ async def test_handle_connection_error(mock_env_vars):
377
405
assert "Connection failed" in result [0 ].text
378
406
mock_instance .connect .assert_called_once ()
379
407
# 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