Skip to content

Commit 0771f83

Browse files
committed
test: cover runtime properties, signalr handlers, and session relay paths
1 parent a3c561c commit 0771f83

2 files changed

Lines changed: 526 additions & 0 deletions

File tree

tests/cli/test_runtime_more.py

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
"""Extra runtime tests targeting properties, handlers, and shallow paths."""
2+
3+
from unittest.mock import AsyncMock, MagicMock, patch
4+
5+
import pytest
6+
7+
from uipath_mcp._cli._runtime._context import UiPathServerType
8+
from uipath_mcp._cli._runtime._exception import UiPathMcpRuntimeError
9+
from uipath_mcp._cli._runtime._runtime import UiPathMcpRuntime
10+
11+
12+
def _make_runtime(server: MagicMock | None = None, **extra) -> UiPathMcpRuntime:
13+
server = server or MagicMock(
14+
name="server",
15+
is_streamable_http=False,
16+
args=[],
17+
command="cmd",
18+
env={},
19+
url=None,
20+
)
21+
server.name = "svc"
22+
with patch("uipath_mcp._cli._runtime._runtime.UiPath"):
23+
rt = UiPathMcpRuntime(
24+
server=server,
25+
runtime_id="rid",
26+
entrypoint="ep",
27+
**extra,
28+
)
29+
rt._uipath = MagicMock()
30+
return rt
31+
32+
33+
@pytest.fixture
34+
def runtime() -> UiPathMcpRuntime:
35+
return _make_runtime()
36+
37+
38+
@pytest.mark.asyncio
39+
async def test_get_schema(runtime):
40+
schema = await runtime.get_schema()
41+
assert schema.file_path == "ep"
42+
assert schema.type == "mcpserver"
43+
44+
45+
def test_slug_defaults_to_server_name(runtime):
46+
assert runtime.slug == "svc"
47+
48+
49+
def test_slug_uses_server_slug_when_set():
50+
rt = _make_runtime(server_slug="explicit-slug")
51+
assert rt.slug == "explicit-slug"
52+
53+
54+
def test_sandboxed_property(runtime):
55+
runtime._job_id = None
56+
assert runtime.sandboxed is False
57+
runtime._job_id = "job-1"
58+
assert runtime.sandboxed is True
59+
60+
61+
def test_packaged_property(runtime):
62+
runtime._process_key = None
63+
assert runtime.packaged is False
64+
runtime._process_key = "00000000-0000-0000-0000-000000000000"
65+
assert runtime.packaged is False
66+
runtime._process_key = "11111111-2222-3333-4444-555555555555"
67+
assert runtime.packaged is True
68+
69+
70+
def test_server_type_selfhosted(runtime):
71+
runtime._job_id = None
72+
runtime._process_key = None
73+
assert runtime.server_type is UiPathServerType.SelfHosted
74+
75+
76+
def test_server_type_command(runtime):
77+
runtime._job_id = "j"
78+
runtime._process_key = None
79+
assert runtime.server_type is UiPathServerType.Command
80+
81+
82+
def test_server_type_coded(runtime):
83+
runtime._job_id = "j"
84+
runtime._process_key = "11111111-2222-3333-4444-555555555555"
85+
assert runtime.server_type is UiPathServerType.Coded
86+
87+
88+
@pytest.mark.asyncio
89+
async def test_validate_auth_missing_url(runtime):
90+
fake_cfg = MagicMock()
91+
fake_cfg.base_url = None
92+
with patch("uipath_mcp._cli._runtime._runtime.UiPathConfig", fake_cfg):
93+
with pytest.raises(UiPathMcpRuntimeError):
94+
runtime._validate_auth()
95+
96+
97+
@pytest.mark.asyncio
98+
async def test_validate_auth_missing_tenant_or_org(runtime):
99+
fake_cfg = MagicMock()
100+
fake_cfg.base_url = "https://x"
101+
runtime._tenant_id = None
102+
runtime._org_id = None
103+
with patch("uipath_mcp._cli._runtime._runtime.UiPathConfig", fake_cfg):
104+
with pytest.raises(UiPathMcpRuntimeError):
105+
runtime._validate_auth()
106+
107+
108+
@pytest.mark.asyncio
109+
async def test_validate_auth_ok(runtime):
110+
fake_cfg = MagicMock()
111+
fake_cfg.base_url = "https://x"
112+
runtime._tenant_id = "t"
113+
runtime._org_id = "o"
114+
with patch("uipath_mcp._cli._runtime._runtime.UiPathConfig", fake_cfg):
115+
runtime._validate_auth() # should not raise
116+
117+
118+
@pytest.mark.asyncio
119+
async def test_handle_signalr_error_open_close(runtime):
120+
await runtime._handle_signalr_error("e")
121+
await runtime._handle_signalr_open()
122+
await runtime._handle_signalr_close()
123+
124+
125+
@pytest.mark.asyncio
126+
async def test_handle_signalr_session_closed_invalid_args(runtime):
127+
await runtime._handle_signalr_session_closed([])
128+
129+
130+
@pytest.mark.asyncio
131+
async def test_handle_signalr_session_closed_unknown_session(runtime):
132+
runtime._job_id = None
133+
await runtime._handle_signalr_session_closed(["unknown"])
134+
135+
136+
@pytest.mark.asyncio
137+
async def test_handle_signalr_session_closed_with_session(runtime):
138+
sess = MagicMock()
139+
sess.stop = AsyncMock()
140+
sess.output = "out"
141+
runtime._session_servers["s1"] = sess
142+
runtime._job_id = "j" # sandboxed
143+
await runtime._handle_signalr_session_closed(["s1"])
144+
sess.stop.assert_awaited_once()
145+
assert runtime._session_output == "out"
146+
assert runtime._cancel_event.is_set()
147+
148+
149+
@pytest.mark.asyncio
150+
async def test_handle_signalr_message_invalid_args(runtime):
151+
await runtime._handle_signalr_message([])
152+
153+
154+
@pytest.mark.asyncio
155+
async def test_handle_signalr_message_existing_session(runtime):
156+
sess = MagicMock()
157+
sess.on_message_received = AsyncMock()
158+
runtime._session_servers["s1"] = sess
159+
await runtime._handle_signalr_message(["s1", "req1"])
160+
sess.on_message_received.assert_awaited_once_with("req1")
161+
162+
163+
@pytest.mark.asyncio
164+
async def test_handle_signalr_message_new_session(runtime):
165+
runtime._server.is_streamable_http = False
166+
fake_sess = MagicMock()
167+
fake_sess.start = AsyncMock()
168+
fake_sess.on_message_received = AsyncMock()
169+
with patch(
170+
"uipath_mcp._cli._runtime._runtime.StdioSessionServer", return_value=fake_sess
171+
):
172+
await runtime._handle_signalr_message(["sNew", "r"])
173+
fake_sess.start.assert_awaited_once()
174+
fake_sess.on_message_received.assert_awaited_once()
175+
176+
177+
@pytest.mark.asyncio
178+
async def test_handle_signalr_message_new_http_session(runtime):
179+
runtime._server.is_streamable_http = True
180+
fake_sess = MagicMock()
181+
fake_sess.start = AsyncMock()
182+
fake_sess.on_message_received = AsyncMock()
183+
with patch(
184+
"uipath_mcp._cli._runtime._runtime.StreamableHttpSessionServer",
185+
return_value=fake_sess,
186+
):
187+
await runtime._handle_signalr_message(["sNew", "r"])
188+
fake_sess.start.assert_awaited_once()
189+
190+
191+
@pytest.mark.asyncio
192+
async def test_on_runtime_abort_logs_when_non_202(runtime):
193+
response = MagicMock(status_code=500, text="oops")
194+
runtime._uipath.api_client.request_async = AsyncMock(return_value=response)
195+
await runtime._on_runtime_abort()
196+
197+
198+
@pytest.mark.asyncio
199+
async def test_on_runtime_abort_success(runtime):
200+
response = MagicMock(status_code=202)
201+
runtime._uipath.api_client.request_async = AsyncMock(return_value=response)
202+
await runtime._on_runtime_abort()
203+
204+
205+
@pytest.mark.asyncio
206+
async def test_on_runtime_abort_swallows_exception(runtime):
207+
runtime._uipath.api_client.request_async = AsyncMock(side_effect=RuntimeError("x"))
208+
await runtime._on_runtime_abort()
209+
210+
211+
@pytest.mark.asyncio
212+
async def test_on_session_start_error_success(runtime):
213+
response = MagicMock(status_code=202)
214+
runtime._uipath.api_client.request_async = AsyncMock(return_value=response)
215+
await runtime._on_session_start_error("s1")
216+
217+
218+
@pytest.mark.asyncio
219+
async def test_on_session_start_error_non_202(runtime):
220+
response = MagicMock(status_code=400, text="bad")
221+
runtime._uipath.api_client.request_async = AsyncMock(return_value=response)
222+
await runtime._on_session_start_error("s1")
223+
224+
225+
@pytest.mark.asyncio
226+
async def test_on_session_start_error_exception(runtime):
227+
runtime._uipath.api_client.request_async = AsyncMock(side_effect=RuntimeError("x"))
228+
await runtime._on_session_start_error("s1")
229+
230+
231+
@pytest.mark.asyncio
232+
async def test_dispose_calls_cleanup(runtime):
233+
with patch.object(runtime, "_cleanup", new=AsyncMock()) as cu:
234+
await runtime.dispose()
235+
cu.assert_awaited_once()
236+
237+
238+
@pytest.mark.asyncio
239+
async def test_cleanup_idempotent(runtime):
240+
runtime._cleanup_done = True
241+
await runtime._cleanup()
242+
243+
244+
@pytest.mark.asyncio
245+
async def test_cleanup_runs(runtime):
246+
runtime._token_refresher = MagicMock(stop=AsyncMock())
247+
with patch.object(runtime, "_on_runtime_abort", new=AsyncMock()):
248+
await runtime._cleanup()
249+
assert runtime._cleanup_done is True
250+
251+
252+
@pytest.mark.asyncio
253+
async def test_execute_calls_run_server(runtime):
254+
with patch.object(runtime, "_run_server", new=AsyncMock(return_value="R")) as rs:
255+
result = await runtime.execute()
256+
assert result == "R"
257+
rs.assert_awaited_once()
258+
259+
260+
@pytest.mark.asyncio
261+
async def test_stream_yields_result(runtime):
262+
with patch.object(runtime, "_run_server", new=AsyncMock(return_value="R")):
263+
results = [r async for r in runtime.stream()]
264+
assert results == ["R"]
265+
266+
267+
@pytest.mark.asyncio
268+
async def test_stop_http_server_process_no_process(runtime):
269+
await runtime._stop_http_server_process()
270+
271+
272+
@pytest.mark.asyncio
273+
async def test_monitor_http_server_process_no_process(runtime):
274+
await runtime._monitor_http_server_process()
275+
276+
277+
@pytest.mark.asyncio
278+
async def test_drain_http_stderr_no_process(runtime):
279+
await runtime._drain_http_stderr()

0 commit comments

Comments
 (0)