ssh-licco 2.5.2__tar.gz → 2.5.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ssh_licco-2.5.2/ssh_licco.egg-info → ssh_licco-2.5.3}/PKG-INFO +1 -1
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/pyproject.toml +2 -1
- {ssh_licco-2.5.2 → ssh_licco-2.5.3/ssh_licco.egg-info}/PKG-INFO +1 -1
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_licco.egg-info/SOURCES.txt +2 -1
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/__init__.py +2 -2
- ssh_licco-2.5.3/tests/test_watchdog.py +181 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/LICENSE +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/README.md +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/setup.cfg +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_licco.egg-info/dependency_links.txt +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_licco.egg-info/entry_points.txt +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_licco.egg-info/requires.txt +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_licco.egg-info/top_level.txt +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/__main__.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/audit_logger.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/batch_executor.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/cli.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/clients/__init__.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/clients/additional_clients.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/clients/factory.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/clients/interface.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/clients/paramiko_client.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/config_manager.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/connection_config.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/connection_pool.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/exceptions.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/executor.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/handlers/__init__.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/handlers/connect.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/handlers/context.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/handlers/docker.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/handlers/execute.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/handlers/file_transfer.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/handlers/host.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/handlers/key.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/handlers/process.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/handlers/schemas.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/handlers/session.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/handlers/utils.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/key_manager.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/logging_config.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/security.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/server.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/service.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/session_manager.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/tunnel.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/ssh_mcp/watchdog.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/tests/test_config_manager.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/tests/test_key_manager.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/tests/test_security.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/tests/test_server.py +0 -0
- {ssh_licco-2.5.2 → ssh_licco-2.5.3}/tests/test_session_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ssh-licco"
|
|
3
|
-
version = "2.5.
|
|
3
|
+
version = "2.5.3"
|
|
4
4
|
description = "SSH Model Context Protocol Server - Enable SSH functionality for AI models"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -59,4 +59,5 @@ warn_unused_configs = true
|
|
|
59
59
|
disallow_untyped_defs = false
|
|
60
60
|
[tool.pytest.ini_options]
|
|
61
61
|
asyncio_mode = "auto"
|
|
62
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
62
63
|
testpaths = ["tests"]
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import threading
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from ssh_mcp.watchdog import (
|
|
11
|
+
GlobalExceptionHandler,
|
|
12
|
+
Watchdog,
|
|
13
|
+
WatchdogEvent,
|
|
14
|
+
WatchdogStatus,
|
|
15
|
+
get_watchdog,
|
|
16
|
+
setup_global_exception_handler,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestWatchdogStatus:
|
|
21
|
+
def test_values(self):
|
|
22
|
+
assert WatchdogStatus.RUNNING.value == "running"
|
|
23
|
+
assert WatchdogStatus.STOPPED.value == "stopped"
|
|
24
|
+
assert WatchdogStatus.PAUSED.value == "paused"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestWatchdogEvent:
|
|
28
|
+
def test_creation(self):
|
|
29
|
+
event = WatchdogEvent(
|
|
30
|
+
timestamp=datetime.now(),
|
|
31
|
+
event_type="TEST",
|
|
32
|
+
message="test event",
|
|
33
|
+
)
|
|
34
|
+
assert event.event_type == "TEST"
|
|
35
|
+
assert event.message == "test event"
|
|
36
|
+
assert event.details == {}
|
|
37
|
+
|
|
38
|
+
def test_with_details(self):
|
|
39
|
+
event = WatchdogEvent(
|
|
40
|
+
timestamp=datetime.now(),
|
|
41
|
+
event_type="TEST",
|
|
42
|
+
message="test",
|
|
43
|
+
details={"key": "value"},
|
|
44
|
+
)
|
|
45
|
+
assert event.details["key"] == "value"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestWatchdog:
|
|
49
|
+
def setup_method(self):
|
|
50
|
+
Watchdog._instance = None
|
|
51
|
+
|
|
52
|
+
def _fresh_watchdog(self) -> Watchdog:
|
|
53
|
+
Watchdog._instance = None
|
|
54
|
+
return Watchdog()
|
|
55
|
+
|
|
56
|
+
def test_singleton(self):
|
|
57
|
+
w1 = Watchdog()
|
|
58
|
+
w2 = Watchdog()
|
|
59
|
+
assert w1 is w2
|
|
60
|
+
|
|
61
|
+
def test_initial_status_stopped(self):
|
|
62
|
+
w = self._fresh_watchdog()
|
|
63
|
+
assert w.status == WatchdogStatus.STOPPED
|
|
64
|
+
|
|
65
|
+
def test_register_task(self):
|
|
66
|
+
w = self._fresh_watchdog()
|
|
67
|
+
w.register_task("task-1", "Test Task", timeout=60)
|
|
68
|
+
tasks = w.get_tasks()
|
|
69
|
+
assert len(tasks) == 1
|
|
70
|
+
assert tasks[0].task_id == "task-1"
|
|
71
|
+
|
|
72
|
+
def test_unregister_task(self):
|
|
73
|
+
w = self._fresh_watchdog()
|
|
74
|
+
w.register_task("task-1", "Test Task")
|
|
75
|
+
w.unregister_task("task-1")
|
|
76
|
+
assert len(w.get_tasks()) == 0
|
|
77
|
+
|
|
78
|
+
def test_update_heartbeat(self):
|
|
79
|
+
w = self._fresh_watchdog()
|
|
80
|
+
w.register_task("task-1", "Test Task")
|
|
81
|
+
old_hb = w.get_tasks()[0].last_heartbeat
|
|
82
|
+
w.update_task_heartbeat("task-1")
|
|
83
|
+
new_hb = w.get_tasks()[0].last_heartbeat
|
|
84
|
+
assert new_hb >= old_hb
|
|
85
|
+
|
|
86
|
+
def test_update_heartbeat_nonexistent(self):
|
|
87
|
+
w = self._fresh_watchdog()
|
|
88
|
+
w.update_task_heartbeat("nonexistent")
|
|
89
|
+
|
|
90
|
+
def test_update_progress(self):
|
|
91
|
+
w = self._fresh_watchdog()
|
|
92
|
+
w.register_task("task-1", "Test Task")
|
|
93
|
+
w.update_task_progress("task-1", 50)
|
|
94
|
+
assert w.get_tasks()[0].progress == 50
|
|
95
|
+
|
|
96
|
+
def test_capture_exception(self):
|
|
97
|
+
w = self._fresh_watchdog()
|
|
98
|
+
w.capture_exception(ValueError("test error"), context="test")
|
|
99
|
+
events = w.get_events()
|
|
100
|
+
assert len(events) >= 1
|
|
101
|
+
assert events[0].event_type == "SYSTEM_ERROR"
|
|
102
|
+
|
|
103
|
+
def test_get_events_limit(self):
|
|
104
|
+
w = self._fresh_watchdog()
|
|
105
|
+
for i in range(5):
|
|
106
|
+
w.capture_exception(ValueError(f"error {i}"))
|
|
107
|
+
events = w.get_events(limit=3)
|
|
108
|
+
assert len(events) <= 3
|
|
109
|
+
|
|
110
|
+
def test_add_recovery_callback(self):
|
|
111
|
+
w = self._fresh_watchdog()
|
|
112
|
+
callback = MagicMock()
|
|
113
|
+
w.add_recovery_callback(callback)
|
|
114
|
+
assert callback in w._recovery_callbacks
|
|
115
|
+
|
|
116
|
+
def test_start_sets_running(self):
|
|
117
|
+
w = self._fresh_watchdog()
|
|
118
|
+
with patch.object(w, "_monitor_loop", MagicMock()):
|
|
119
|
+
with patch("asyncio.create_task", return_value=MagicMock()):
|
|
120
|
+
w.start()
|
|
121
|
+
assert w.status == WatchdogStatus.RUNNING
|
|
122
|
+
w.stop()
|
|
123
|
+
|
|
124
|
+
def test_stop_sets_stopped(self):
|
|
125
|
+
w = self._fresh_watchdog()
|
|
126
|
+
with patch.object(w, "_monitor_loop", MagicMock()):
|
|
127
|
+
with patch("asyncio.create_task", return_value=MagicMock()):
|
|
128
|
+
w.start()
|
|
129
|
+
w.stop()
|
|
130
|
+
assert w.status == WatchdogStatus.STOPPED
|
|
131
|
+
|
|
132
|
+
def test_start_already_running(self):
|
|
133
|
+
w = self._fresh_watchdog()
|
|
134
|
+
with patch.object(w, "_monitor_loop", MagicMock()):
|
|
135
|
+
with patch("asyncio.create_task", return_value=MagicMock()):
|
|
136
|
+
w.start()
|
|
137
|
+
with patch.object(w, "_monitor_loop", MagicMock()):
|
|
138
|
+
with patch("asyncio.create_task", return_value=MagicMock()):
|
|
139
|
+
w.start()
|
|
140
|
+
assert w.status == WatchdogStatus.RUNNING
|
|
141
|
+
w.stop()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TestGlobalExceptionHandler:
|
|
145
|
+
def test_enable(self):
|
|
146
|
+
handler = GlobalExceptionHandler()
|
|
147
|
+
mock_loop = MagicMock()
|
|
148
|
+
mock_loop.set_exception_handler = MagicMock()
|
|
149
|
+
mock_loop.get_exception_handler = MagicMock(return_value=None)
|
|
150
|
+
with patch("asyncio.get_event_loop", return_value=mock_loop):
|
|
151
|
+
handler.enable()
|
|
152
|
+
assert handler._enabled is True
|
|
153
|
+
|
|
154
|
+
def test_disable(self):
|
|
155
|
+
handler = GlobalExceptionHandler()
|
|
156
|
+
mock_loop = MagicMock()
|
|
157
|
+
mock_loop.set_exception_handler = MagicMock()
|
|
158
|
+
mock_loop.get_exception_handler = MagicMock(return_value=None)
|
|
159
|
+
mock_loop.set_exception_handler = MagicMock()
|
|
160
|
+
with patch("asyncio.get_event_loop", return_value=mock_loop):
|
|
161
|
+
handler.enable()
|
|
162
|
+
handler.disable()
|
|
163
|
+
assert handler._enabled is False
|
|
164
|
+
|
|
165
|
+
def test_disable_not_enabled(self):
|
|
166
|
+
handler = GlobalExceptionHandler()
|
|
167
|
+
handler.disable()
|
|
168
|
+
assert handler._enabled is False
|
|
169
|
+
|
|
170
|
+
def test_enable_already_enabled(self):
|
|
171
|
+
handler = GlobalExceptionHandler()
|
|
172
|
+
handler._enabled = True
|
|
173
|
+
handler.enable()
|
|
174
|
+
assert handler._enabled is True
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class TestGetWatchdog:
|
|
178
|
+
def test_returns_watchdog(self):
|
|
179
|
+
Watchdog._instance = None
|
|
180
|
+
w = get_watchdog()
|
|
181
|
+
assert isinstance(w, Watchdog)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|