Skip to content

Commit 6c4f934

Browse files
committed
test: pytest coverage for manage_unity_hub
Add 7 pytest cases (Server/tests/integration/test_manage_unity_hub.py) covering: - list_installed_editors: tool dispatch + Hub args + parser integration - parse_installed_editors: direct parser test against realistic Hub output - set_install_path: confirmation gate (confirm omitted -> no subprocess) - install_editor: confirm=True invokes Hub with version + _INSTALL_TIMEOUT - install_modules: confirm=True repeats --module per item - detect_hub_path: UNITY_HUB_PATH env override - run_hub_command: hub_not_found error when detection fails Mocks run_hub_command and detect_hub_path; no real Hub required.
1 parent 7ea7e2f commit 6c4f934

1 file changed

Lines changed: 178 additions & 0 deletions

File tree

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
"""
2+
Tests for the manage_unity_hub tool and its underlying service.
3+
4+
Covers MCP-tool dispatch, the confirmation gate for state-changing actions,
5+
service-layer parsing, and Unity Hub auto-detection (UNITY_HUB_PATH override
6+
and hub-not-found error).
7+
8+
Tests do NOT require a real Unity Hub installation - the subprocess and
9+
detection layers are mocked.
10+
"""
11+
import pytest
12+
13+
import services.tools.manage_unity_hub as manage_hub_mod
14+
import services.unity_hub as unity_hub_mod
15+
16+
17+
@pytest.mark.asyncio
18+
async def test_list_installed_editors_invokes_hub_and_parses_output(monkeypatch):
19+
"""list_installed_editors calls Hub with the right args and returns parsed editors."""
20+
captured = {}
21+
22+
async def fake_run(args, timeout=30, hub_path=None):
23+
captured["args"] = args
24+
captured["timeout"] = timeout
25+
return {
26+
"success": True,
27+
"hub_path": "/Applications/Unity Hub.app/Contents/MacOS/Unity Hub",
28+
"raw_output": (
29+
"6000.3.10f1 (Apple silicon) installed at /Applications/Unity/Hub/Editor/6000.3.10f1\n"
30+
"2022.3.0f1 , installed at /Applications/Unity/Hub/Editor/2022.3.0f1"
31+
),
32+
"stderr": None,
33+
}
34+
35+
monkeypatch.setattr(manage_hub_mod, "run_hub_command", fake_run)
36+
37+
resp = await manage_hub_mod.manage_unity_hub(action="list_installed_editors")
38+
39+
assert resp["success"] is True
40+
assert resp["action"] == "list_installed_editors"
41+
assert captured["args"] == ["editors", "--installed"]
42+
assert resp["data"] == [
43+
{"version": "6000.3.10f1", "path": "/Applications/Unity/Hub/Editor/6000.3.10f1"},
44+
{"version": "2022.3.0f1", "path": "/Applications/Unity/Hub/Editor/2022.3.0f1"},
45+
]
46+
47+
48+
def test_parse_installed_editors_handles_realistic_output():
49+
"""parse_installed_editors handles both 'installed at' and comma-separated formats."""
50+
raw = (
51+
"6000.3.10f1 (Apple silicon) installed at /Applications/Unity/Hub/Editor/6000.3.10f1\n"
52+
"2022.3.0f1 , installed at /Applications/Unity/Hub/Editor/2022.3.0f1\n"
53+
"\n" # blank line should be skipped
54+
)
55+
56+
result = unity_hub_mod.parse_installed_editors(raw)
57+
58+
assert result == [
59+
{"version": "6000.3.10f1", "path": "/Applications/Unity/Hub/Editor/6000.3.10f1"},
60+
{"version": "2022.3.0f1", "path": "/Applications/Unity/Hub/Editor/2022.3.0f1"},
61+
]
62+
63+
64+
@pytest.mark.asyncio
65+
async def test_set_install_path_confirm_false_does_not_invoke_hub(monkeypatch):
66+
"""State-changing action without confirm=True must not invoke the Hub subprocess."""
67+
invoked = {"called": False}
68+
69+
async def fake_run(*args, **kwargs):
70+
invoked["called"] = True
71+
return {"success": True, "hub_path": "/dummy", "raw_output": ""}
72+
73+
monkeypatch.setattr(manage_hub_mod, "run_hub_command", fake_run)
74+
monkeypatch.setattr(
75+
manage_hub_mod, "detect_hub_path", lambda: "/dummy/Unity Hub"
76+
)
77+
78+
resp = await manage_hub_mod.manage_unity_hub(
79+
action="set_install_path",
80+
path="/Applications/Unity/Hub/Editor",
81+
)
82+
83+
assert resp["success"] is False
84+
assert resp.get("confirmation_required") is True
85+
assert resp.get("hint")
86+
assert invoked["called"] is False
87+
88+
89+
@pytest.mark.asyncio
90+
async def test_install_editor_confirm_true_invokes_hub_with_install_timeout(monkeypatch):
91+
"""install_editor with confirm=True invokes Hub install with version arg and install timeout."""
92+
captured = {}
93+
94+
async def fake_run(args, timeout=30, hub_path=None):
95+
captured["args"] = args
96+
captured["timeout"] = timeout
97+
return {
98+
"success": True,
99+
"hub_path": "/dummy/Unity Hub",
100+
"raw_output": "Installation started",
101+
"stderr": None,
102+
}
103+
104+
monkeypatch.setattr(manage_hub_mod, "run_hub_command", fake_run)
105+
monkeypatch.setattr(
106+
manage_hub_mod, "detect_hub_path", lambda: "/dummy/Unity Hub"
107+
)
108+
109+
resp = await manage_hub_mod.manage_unity_hub(
110+
action="install_editor",
111+
version="6000.3.10f1",
112+
confirm=True,
113+
)
114+
115+
assert resp["success"] is True
116+
assert resp["action"] == "install_editor"
117+
assert captured["args"] == ["install", "--version", "6000.3.10f1"]
118+
assert captured["timeout"] == unity_hub_mod._INSTALL_TIMEOUT
119+
120+
121+
@pytest.mark.asyncio
122+
async def test_install_modules_confirm_true_passes_each_module(monkeypatch):
123+
"""install_modules with confirm=True forwards each module via repeated --module flags."""
124+
captured = {}
125+
126+
async def fake_run(args, timeout=30, hub_path=None):
127+
captured["args"] = args
128+
captured["timeout"] = timeout
129+
return {
130+
"success": True,
131+
"hub_path": "/dummy/Unity Hub",
132+
"raw_output": "Modules installation started",
133+
"stderr": None,
134+
}
135+
136+
monkeypatch.setattr(manage_hub_mod, "run_hub_command", fake_run)
137+
monkeypatch.setattr(
138+
manage_hub_mod, "detect_hub_path", lambda: "/dummy/Unity Hub"
139+
)
140+
141+
resp = await manage_hub_mod.manage_unity_hub(
142+
action="install_modules",
143+
version="6000.3.10f1",
144+
modules=["android", "ios"],
145+
confirm=True,
146+
)
147+
148+
assert resp["success"] is True
149+
assert resp["action"] == "install_modules"
150+
assert captured["args"] == [
151+
"install-modules",
152+
"--version", "6000.3.10f1",
153+
"--module", "android",
154+
"--module", "ios",
155+
]
156+
assert captured["timeout"] == unity_hub_mod._INSTALL_TIMEOUT
157+
158+
159+
def test_detect_hub_path_uses_unity_hub_path_env_override(monkeypatch, tmp_path):
160+
"""UNITY_HUB_PATH env var takes precedence when it points at an existing file."""
161+
fake_hub = tmp_path / "Unity Hub"
162+
fake_hub.write_text("#!/bin/sh\nexit 0\n")
163+
164+
monkeypatch.setenv("UNITY_HUB_PATH", str(fake_hub))
165+
166+
assert unity_hub_mod.detect_hub_path() == str(fake_hub)
167+
168+
169+
@pytest.mark.asyncio
170+
async def test_run_hub_command_returns_hub_not_found_when_detection_fails(monkeypatch):
171+
"""run_hub_command surfaces a structured hub_not_found error when detection returns None."""
172+
monkeypatch.setattr(unity_hub_mod, "detect_hub_path", lambda: None)
173+
174+
result = await unity_hub_mod.run_hub_command(["editors", "--installed"])
175+
176+
assert result["success"] is False
177+
assert result["error"]["type"] == "hub_not_found"
178+
assert "UNITY_HUB_PATH" in result["error"]["message"]

0 commit comments

Comments
 (0)