Skip to content

Commit 37129a6

Browse files
authored
Merge pull request #973 from galofilip/add-manage-packages-tests
test: add unit tests for manage_packages tool and CLI
2 parents 740cfcd + 2dd83df commit 37129a6

1 file changed

Lines changed: 303 additions & 0 deletions

File tree

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
"""Tests for manage_packages tool and CLI commands."""
2+
3+
import asyncio
4+
import pytest
5+
from unittest.mock import patch, MagicMock, AsyncMock
6+
from click.testing import CliRunner
7+
8+
from cli.commands.packages import packages
9+
from cli.utils.config import CLIConfig
10+
from services.tools.manage_packages import ALL_ACTIONS
11+
12+
13+
# =============================================================================
14+
# Fixtures
15+
# =============================================================================
16+
17+
@pytest.fixture
18+
def runner():
19+
"""Return a Click CLI test runner."""
20+
return CliRunner()
21+
22+
23+
@pytest.fixture
24+
def mock_config():
25+
"""Return a default CLIConfig for testing."""
26+
return CLIConfig(
27+
host="127.0.0.1",
28+
port=8080,
29+
timeout=30,
30+
format="text",
31+
unity_instance=None,
32+
)
33+
34+
35+
@pytest.fixture
36+
def mock_success():
37+
"""Return a generic success response."""
38+
return {"success": True, "message": "OK", "data": {}}
39+
40+
41+
@pytest.fixture
42+
def cli_runner(runner, mock_config, mock_success):
43+
"""Invoke a packages CLI command with run_command mocked out.
44+
45+
Usage::
46+
47+
def test_something(cli_runner):
48+
result, mock_run = cli_runner(["list"])
49+
assert result.exit_code == 0
50+
params = mock_run.call_args.args[1]
51+
assert params["action"] == "list_packages"
52+
"""
53+
def _invoke(args):
54+
with patch("cli.commands.packages.get_config", return_value=mock_config):
55+
with patch("cli.commands.packages.run_command", return_value=mock_success) as mock_run:
56+
result = runner.invoke(packages, args)
57+
return result, mock_run
58+
return _invoke
59+
60+
61+
# =============================================================================
62+
# Action Lists
63+
# =============================================================================
64+
65+
class TestActionLists:
66+
"""Verify action list completeness and consistency."""
67+
68+
def test_all_actions_is_not_empty(self):
69+
"""ALL_ACTIONS must contain at least one entry."""
70+
assert len(ALL_ACTIONS) > 0
71+
72+
def test_no_duplicate_actions(self):
73+
"""ALL_ACTIONS must not contain duplicate entries."""
74+
assert len(ALL_ACTIONS) == len(set(ALL_ACTIONS))
75+
76+
def test_expected_query_actions_present(self):
77+
"""Query actions must all be present in ALL_ACTIONS."""
78+
expected = {"list_packages", "search_packages", "get_package_info", "ping", "status"}
79+
assert expected.issubset(set(ALL_ACTIONS))
80+
81+
def test_expected_install_remove_actions_present(self):
82+
"""Install/remove actions must all be present in ALL_ACTIONS."""
83+
expected = {"add_package", "remove_package", "embed_package", "resolve_packages"}
84+
assert expected.issubset(set(ALL_ACTIONS))
85+
86+
def test_expected_registry_actions_present(self):
87+
"""Registry actions must all be present in ALL_ACTIONS."""
88+
expected = {"add_registry", "remove_registry", "list_registries"}
89+
assert expected.issubset(set(ALL_ACTIONS))
90+
91+
92+
# =============================================================================
93+
# Tool Validation (Python-side, no Unity)
94+
# =============================================================================
95+
96+
class TestManagePackagesToolValidation:
97+
"""Test action validation in the manage_packages tool function."""
98+
99+
def test_unknown_action_returns_error(self):
100+
"""An unrecognised action must return success=False with an error message."""
101+
from services.tools.manage_packages import manage_packages
102+
103+
ctx = MagicMock()
104+
ctx.get_state = AsyncMock(return_value=None)
105+
106+
result = asyncio.run(manage_packages(ctx, action="invalid_action"))
107+
assert result["success"] is False
108+
assert "Unknown action" in result["message"]
109+
110+
def test_unknown_action_lists_valid_actions(self):
111+
"""The error message for an unknown action must list valid actions."""
112+
from services.tools.manage_packages import manage_packages
113+
114+
ctx = MagicMock()
115+
ctx.get_state = AsyncMock(return_value=None)
116+
117+
result = asyncio.run(manage_packages(ctx, action="bogus"))
118+
assert result["success"] is False
119+
assert "Valid actions" in result["message"]
120+
121+
def test_unknown_action_does_not_call_unity(self):
122+
"""An unknown action must be rejected before any Unity call is made."""
123+
from services.tools.manage_packages import manage_packages
124+
125+
ctx = MagicMock()
126+
ctx.get_state = AsyncMock(return_value=None)
127+
128+
with patch(
129+
"services.tools.manage_packages._send_packages_command",
130+
new_callable=AsyncMock,
131+
) as mock_send:
132+
asyncio.run(manage_packages(ctx, action="bogus"))
133+
mock_send.assert_not_called()
134+
135+
def test_action_matching_is_case_insensitive(self):
136+
"""Actions must be accepted regardless of capitalisation and normalised to lowercase."""
137+
from services.tools.manage_packages import manage_packages
138+
139+
ctx = MagicMock()
140+
ctx.get_state = AsyncMock(return_value=None)
141+
142+
with patch(
143+
"services.tools.manage_packages._send_packages_command",
144+
new_callable=AsyncMock,
145+
) as mock_send:
146+
mock_send.return_value = {"success": True, "message": "OK"}
147+
result = asyncio.run(manage_packages(ctx, action="LIST_PACKAGES"))
148+
149+
assert result["success"] is True
150+
sent_params = mock_send.call_args.args[1]
151+
assert sent_params["action"] == "list_packages"
152+
153+
154+
# =============================================================================
155+
# CLI Command Parameter Building
156+
# =============================================================================
157+
158+
class TestPackagesQueryCLICommands:
159+
"""Verify query CLI commands build correct parameter dicts."""
160+
161+
def test_list_builds_correct_params(self, cli_runner):
162+
"""packages list must send action=list_packages."""
163+
result, mock_run = cli_runner(["list"])
164+
assert result.exit_code == 0
165+
mock_run.assert_called_once()
166+
params = mock_run.call_args.args[1]
167+
assert params["action"] == "list_packages"
168+
169+
def test_search_builds_correct_params(self, cli_runner):
170+
"""packages search <query> must send action=search_packages and the query."""
171+
result, mock_run = cli_runner(["search", "input"])
172+
assert result.exit_code == 0
173+
params = mock_run.call_args.args[1]
174+
assert params["action"] == "search_packages"
175+
assert params["query"] == "input"
176+
177+
def test_info_builds_correct_params(self, cli_runner):
178+
"""packages info <package> must send action=get_package_info and the package name."""
179+
result, mock_run = cli_runner(["info", "com.unity.inputsystem"])
180+
assert result.exit_code == 0
181+
params = mock_run.call_args.args[1]
182+
assert params["action"] == "get_package_info"
183+
assert params["package"] == "com.unity.inputsystem"
184+
185+
def test_ping_builds_correct_params(self, cli_runner):
186+
"""packages ping must send action=ping."""
187+
result, mock_run = cli_runner(["ping"])
188+
assert result.exit_code == 0
189+
params = mock_run.call_args.args[1]
190+
assert params["action"] == "ping"
191+
192+
def test_status_without_job_id(self, cli_runner):
193+
"""packages status with no args must send action=status and omit job_id."""
194+
result, mock_run = cli_runner(["status"])
195+
assert result.exit_code == 0
196+
params = mock_run.call_args.args[1]
197+
assert params["action"] == "status"
198+
assert "job_id" not in params
199+
200+
def test_status_with_job_id(self, cli_runner):
201+
"""packages status <job_id> must include the job_id in params."""
202+
result, mock_run = cli_runner(["status", "abc123"])
203+
assert result.exit_code == 0
204+
params = mock_run.call_args.args[1]
205+
assert params["action"] == "status"
206+
assert params["job_id"] == "abc123"
207+
208+
209+
class TestPackagesInstallRemoveCLICommands:
210+
"""Verify install/remove CLI commands build correct parameter dicts."""
211+
212+
def test_add_builds_correct_params(self, cli_runner):
213+
"""packages add <package> must send action=add_package and the package name."""
214+
result, mock_run = cli_runner(["add", "com.unity.inputsystem"])
215+
assert result.exit_code == 0
216+
params = mock_run.call_args.args[1]
217+
assert params["action"] == "add_package"
218+
assert params["package"] == "com.unity.inputsystem"
219+
220+
def test_add_with_version_builds_correct_params(self, cli_runner):
221+
"""packages add <package@version> must preserve the version specifier."""
222+
result, mock_run = cli_runner(["add", "com.unity.inputsystem@1.8.0"])
223+
assert result.exit_code == 0
224+
params = mock_run.call_args.args[1]
225+
assert params["action"] == "add_package"
226+
assert params["package"] == "com.unity.inputsystem@1.8.0"
227+
228+
def test_remove_builds_correct_params(self, cli_runner):
229+
"""packages remove <package> must send action=remove_package without force."""
230+
result, mock_run = cli_runner(["remove", "com.unity.inputsystem"])
231+
assert result.exit_code == 0
232+
params = mock_run.call_args.args[1]
233+
assert params["action"] == "remove_package"
234+
assert params["package"] == "com.unity.inputsystem"
235+
assert "force" not in params
236+
237+
def test_remove_with_force_builds_correct_params(self, cli_runner):
238+
"""packages remove --force must include force=True in params."""
239+
result, mock_run = cli_runner(["remove", "com.unity.inputsystem", "--force"])
240+
assert result.exit_code == 0
241+
params = mock_run.call_args.args[1]
242+
assert params["action"] == "remove_package"
243+
assert params["force"] is True
244+
245+
def test_embed_builds_correct_params(self, cli_runner):
246+
"""packages embed <package> must send action=embed_package and the package name."""
247+
result, mock_run = cli_runner(["embed", "com.unity.timeline"])
248+
assert result.exit_code == 0
249+
params = mock_run.call_args.args[1]
250+
assert params["action"] == "embed_package"
251+
assert params["package"] == "com.unity.timeline"
252+
253+
def test_resolve_builds_correct_params(self, cli_runner):
254+
"""packages resolve must send action=resolve_packages."""
255+
result, mock_run = cli_runner(["resolve"])
256+
assert result.exit_code == 0
257+
params = mock_run.call_args.args[1]
258+
assert params["action"] == "resolve_packages"
259+
260+
261+
class TestRegistryCLICommands:
262+
"""Verify registry CLI commands build correct parameter dicts."""
263+
264+
def test_list_registries_builds_correct_params(self, cli_runner):
265+
"""packages list-registries must send action=list_registries."""
266+
result, mock_run = cli_runner(["list-registries"])
267+
assert result.exit_code == 0
268+
params = mock_run.call_args.args[1]
269+
assert params["action"] == "list_registries"
270+
271+
def test_add_registry_builds_correct_params(self, cli_runner):
272+
"""packages add-registry must send name, url, and all scopes."""
273+
result, mock_run = cli_runner([
274+
"add-registry", "OpenUPM",
275+
"--url", "https://package.openupm.com",
276+
"--scope", "com.cysharp",
277+
"--scope", "com.neuecc",
278+
])
279+
assert result.exit_code == 0
280+
params = mock_run.call_args.args[1]
281+
assert params["action"] == "add_registry"
282+
assert params["name"] == "OpenUPM"
283+
assert params["url"] == "https://package.openupm.com"
284+
assert params["scopes"] == ["com.cysharp", "com.neuecc"]
285+
286+
def test_add_registry_with_single_scope(self, cli_runner):
287+
"""packages add-registry with one --scope must produce a single-element scopes list."""
288+
result, mock_run = cli_runner([
289+
"add-registry", "MyReg",
290+
"--url", "https://registry.example.com",
291+
"--scope", "com.example",
292+
])
293+
assert result.exit_code == 0
294+
params = mock_run.call_args.args[1]
295+
assert params["scopes"] == ["com.example"]
296+
297+
def test_remove_registry_builds_correct_params(self, cli_runner):
298+
"""packages remove-registry <name> must send action=remove_registry and the name."""
299+
result, mock_run = cli_runner(["remove-registry", "OpenUPM"])
300+
assert result.exit_code == 0
301+
params = mock_run.call_args.args[1]
302+
assert params["action"] == "remove_registry"
303+
assert params["name"] == "OpenUPM"

0 commit comments

Comments
 (0)