Skip to content

Commit 2a45669

Browse files
jfrancoacursoragent
andcommitted
Honor bool/Optional return contracts from weaviate-python-client
Several methods of the weaviate-python-client are not the usual "raise on failure" style. Instead they accept multiple HTTP status codes as "ok" and return: * bool -- False means a logical no-op (404 not found, 409 already in target state, ...) * Optional[X] -- None means "not found" (404) The CLI was discarding those returns and silently reporting success on no-ops -- e.g. `delete user --user_name=ghost` printed "User 'ghost' deleted successfully." with exit code 0. Surface the no-op as a clear error in the affected managers: * UserManager - delete_user : raise on False (404) - update_user : raise on False from activate()/deactivate() (409 already in target state) - get_user : raise on None (404) when looking up by name * AliasManager - delete_alias : raise on False (404) - update_alias : raise on False (404) - get_alias : fix return type hint to Optional[AliasReturn] * DataManager - __delete_data : raise on False from data.delete_by_id() (404) in the uuid path The CLI commands already wrap manager calls in `try/except Exception` and exit 1 with `Error: ...`, so no command-layer changes were needed beyond simplifying delete_user_cli (already on this branch). Add regression coverage on two layers: * Manager unit tests for each new bool=False / None path (test_user_manager.py, test_alias_manager.py, test_data_manager.py) * End-to-end CliRunner tests in test/unittests/test_cli_bool_contract.py that pin the contract: a no-op exits non-zero with a clear message and never prints a success line. If a future contributor swallows a bool again, these tests fail loudly. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c9af10b commit 2a45669

7 files changed

Lines changed: 463 additions & 14 deletions

File tree

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
"""
2+
End-to-end CLI regression tests for the bool / Optional contract.
3+
4+
Several methods of the weaviate-python-client are *not* the usual "raise on
5+
failure" style. Instead, they accept multiple HTTP status codes as "ok" and
6+
return:
7+
8+
* ``bool`` -- ``False`` means a logical no-op (404 not found, 409
9+
already in target state, ...)
10+
* ``Optional[X]`` -- ``None`` means "not found" (404)
11+
12+
The CLI must surface those as a non-zero exit code with a clear ``Error:``
13+
message -- never silently report success. These tests lock in that contract
14+
end-to-end (CliRunner -> command -> manager -> mocked client method) so the
15+
bug class cannot creep back in.
16+
17+
If you add a new caller of one of these client APIs, add a test here.
18+
"""
19+
20+
import pytest
21+
from click.testing import CliRunner
22+
from unittest.mock import MagicMock, patch
23+
24+
from cli import main
25+
26+
27+
@pytest.fixture
28+
def cli_runner() -> CliRunner:
29+
return CliRunner()
30+
31+
32+
@pytest.fixture
33+
def fake_client() -> MagicMock:
34+
"""A MagicMock standing in for a real WeaviateClient."""
35+
return MagicMock()
36+
37+
38+
def _invoke(cli_runner: CliRunner, fake_client: MagicMock, command_module: str, args):
39+
"""Invoke ``main`` with ``args`` while patching
40+
``get_client_from_context`` in the targeted command module so the CLI gets
41+
our fake client instead of opening a real connection."""
42+
target = f"{command_module}.get_client_from_context"
43+
with patch(target, return_value=fake_client):
44+
return cli_runner.invoke(main, args)
45+
46+
47+
# ---------------------------------------------------------------------------
48+
# delete user -- client.users.db.delete() returns False on 404
49+
# ---------------------------------------------------------------------------
50+
51+
52+
def test_cli_delete_user_not_found_exits_nonzero(cli_runner, fake_client):
53+
fake_client.users.db.delete.return_value = False
54+
55+
result = _invoke(
56+
cli_runner,
57+
fake_client,
58+
"weaviate_cli.commands.delete",
59+
["delete", "user", "--user_name", "ghost"],
60+
)
61+
62+
assert result.exit_code == 1, result.output
63+
assert "User 'ghost' not found." in result.output
64+
assert "deleted successfully" not in result.output
65+
66+
67+
def test_cli_delete_user_success(cli_runner, fake_client):
68+
fake_client.users.db.delete.return_value = True
69+
70+
result = _invoke(
71+
cli_runner,
72+
fake_client,
73+
"weaviate_cli.commands.delete",
74+
["delete", "user", "--user_name", "alice"],
75+
)
76+
77+
assert result.exit_code == 0, result.output
78+
assert "alice" in result.output
79+
assert "deleted successfully" in result.output
80+
81+
82+
# ---------------------------------------------------------------------------
83+
# update user --activate / --deactivate -- bool=False on 409
84+
# ---------------------------------------------------------------------------
85+
86+
87+
def test_cli_update_user_activate_already_active_exits_nonzero(cli_runner, fake_client):
88+
fake_client.users.db.activate.return_value = False
89+
90+
result = _invoke(
91+
cli_runner,
92+
fake_client,
93+
"weaviate_cli.commands.update",
94+
["update", "user", "--user_name", "alice", "--activate"],
95+
)
96+
97+
assert result.exit_code == 1, result.output
98+
assert "already active" in result.output
99+
assert "activated successfully" not in result.output
100+
101+
102+
def test_cli_update_user_activate_success(cli_runner, fake_client):
103+
fake_client.users.db.activate.return_value = True
104+
105+
result = _invoke(
106+
cli_runner,
107+
fake_client,
108+
"weaviate_cli.commands.update",
109+
["update", "user", "--user_name", "alice", "--activate"],
110+
)
111+
112+
assert result.exit_code == 0, result.output
113+
assert "activated successfully" in result.output
114+
115+
116+
def test_cli_update_user_deactivate_already_deactivated_exits_nonzero(
117+
cli_runner, fake_client
118+
):
119+
fake_client.users.db.deactivate.return_value = False
120+
121+
result = _invoke(
122+
cli_runner,
123+
fake_client,
124+
"weaviate_cli.commands.update",
125+
["update", "user", "--user_name", "alice", "--deactivate"],
126+
)
127+
128+
assert result.exit_code == 1, result.output
129+
assert "already deactivated" in result.output
130+
assert "deactivated successfully" not in result.output
131+
132+
133+
def test_cli_update_user_deactivate_success(cli_runner, fake_client):
134+
fake_client.users.db.deactivate.return_value = True
135+
136+
result = _invoke(
137+
cli_runner,
138+
fake_client,
139+
"weaviate_cli.commands.update",
140+
["update", "user", "--user_name", "alice", "--deactivate"],
141+
)
142+
143+
assert result.exit_code == 0, result.output
144+
assert "deactivated successfully" in result.output
145+
146+
147+
# ---------------------------------------------------------------------------
148+
# get user -- client.users.db.get() returns None on 404
149+
# ---------------------------------------------------------------------------
150+
151+
152+
def test_cli_get_user_not_found_exits_nonzero(cli_runner, fake_client):
153+
fake_client.users.db.get.return_value = None
154+
155+
result = _invoke(
156+
cli_runner,
157+
fake_client,
158+
"weaviate_cli.commands.get",
159+
["get", "user", "--user_name", "ghost"],
160+
)
161+
162+
assert result.exit_code == 1, result.output
163+
assert "User 'ghost' not found." in result.output
164+
165+
166+
# ---------------------------------------------------------------------------
167+
# delete alias -- client.alias.delete() returns False on 404
168+
# ---------------------------------------------------------------------------
169+
170+
171+
def test_cli_delete_alias_not_found_exits_nonzero(cli_runner, fake_client):
172+
fake_client.alias.delete.return_value = False
173+
174+
result = _invoke(
175+
cli_runner,
176+
fake_client,
177+
"weaviate_cli.commands.delete",
178+
["delete", "alias", "ghost_alias"],
179+
)
180+
181+
assert result.exit_code == 1, result.output
182+
assert "Alias 'ghost_alias' not found." in result.output
183+
assert "deleted successfully" not in result.output
184+
185+
186+
def test_cli_delete_alias_success(cli_runner, fake_client):
187+
fake_client.alias.delete.return_value = True
188+
189+
result = _invoke(
190+
cli_runner,
191+
fake_client,
192+
"weaviate_cli.commands.delete",
193+
["delete", "alias", "my_alias"],
194+
)
195+
196+
assert result.exit_code == 0, result.output
197+
assert "deleted successfully" in result.output
198+
199+
200+
# ---------------------------------------------------------------------------
201+
# update alias -- client.alias.update() returns False on 404
202+
# ---------------------------------------------------------------------------
203+
204+
205+
def test_cli_update_alias_not_found_exits_nonzero(cli_runner, fake_client):
206+
fake_client.alias.update.return_value = False
207+
208+
result = _invoke(
209+
cli_runner,
210+
fake_client,
211+
"weaviate_cli.commands.update",
212+
["update", "alias", "ghost", "MyCollection"],
213+
)
214+
215+
assert result.exit_code == 1, result.output
216+
assert "Alias 'ghost' not found." in result.output
217+
assert "updated successfully" not in result.output
218+
219+
220+
def test_cli_update_alias_success(cli_runner, fake_client):
221+
fake_client.alias.update.return_value = True
222+
223+
result = _invoke(
224+
cli_runner,
225+
fake_client,
226+
"weaviate_cli.commands.update",
227+
["update", "alias", "my_alias", "MyCollection"],
228+
)
229+
230+
assert result.exit_code == 0, result.output
231+
assert "updated successfully" in result.output
232+
233+
234+
# ---------------------------------------------------------------------------
235+
# delete data --uuid -- collection.data.delete_by_id() returns False on 404
236+
# ---------------------------------------------------------------------------
237+
238+
239+
def test_cli_delete_data_by_uuid_not_found_exits_nonzero(cli_runner, fake_client):
240+
# Pretend the collection exists and is not multi-tenant.
241+
fake_client.collections.exists.return_value = True
242+
mock_collection = MagicMock()
243+
mock_collection.name = "TestCollection"
244+
mock_collection.config.get.return_value.multi_tenancy_config.enabled = False
245+
mock_collection.with_consistency_level.return_value.data.delete_by_id.return_value = (
246+
False
247+
)
248+
fake_client.collections.get.return_value = mock_collection
249+
250+
result = _invoke(
251+
cli_runner,
252+
fake_client,
253+
"weaviate_cli.commands.delete",
254+
[
255+
"delete",
256+
"data",
257+
"--collection",
258+
"TestCollection",
259+
"--uuid",
260+
"00000000-0000-0000-0000-000000000000",
261+
],
262+
)
263+
264+
assert result.exit_code == 1, result.output
265+
assert "00000000-0000-0000-0000-000000000000" in result.output
266+
assert "not found" in result.output
267+
268+
269+
# ---------------------------------------------------------------------------
270+
# get alias -- client.alias.get() returns None on 404 (CLI handles it
271+
# explicitly, but pin the contract).
272+
# ---------------------------------------------------------------------------
273+
274+
275+
def test_cli_get_alias_not_found_reports_not_found(cli_runner, fake_client):
276+
fake_client.alias.get.return_value = None
277+
278+
result = _invoke(
279+
cli_runner,
280+
fake_client,
281+
"weaviate_cli.commands.get",
282+
["get", "alias", "--alias_name", "ghost"],
283+
)
284+
285+
# The get command currently treats a missing alias as a soft miss (exit 0
286+
# with an explanatory line). The important guard is that we never silently
287+
# render a None alias as if it existed.
288+
assert result.exit_code == 0, result.output
289+
assert "Alias 'ghost' not found." in result.output

test/unittests/test_managers/test_alias_manager.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def test_update_alias_success(
5858
"""
5959
alias_name = "test_alias"
6060
collection_name = "NewTestCollection"
61+
mock_client.alias.update.return_value = True
6162

6263
alias_manager.update_alias(alias_name, collection_name)
6364

@@ -66,6 +67,22 @@ def test_update_alias_success(
6667
)
6768

6869

70+
def test_update_alias_not_found_raises(
71+
alias_manager: AliasManager, mock_client: MagicMock
72+
) -> None:
73+
"""The client returns False on 404 instead of raising."""
74+
alias_name = "missing_alias"
75+
mock_client.alias.update.return_value = False
76+
77+
with pytest.raises(Exception) as exc_info:
78+
alias_manager.update_alias(alias_name, "TargetCollection")
79+
80+
assert (
81+
f"Error updating alias '{alias_name}': Alias '{alias_name}' not found."
82+
in str(exc_info.value)
83+
)
84+
85+
6986
def test_update_alias_error(
7087
alias_manager: AliasManager, mock_client: MagicMock
7188
) -> None:
@@ -120,12 +137,29 @@ def test_delete_alias_success(
120137
Test successful alias deletion.
121138
"""
122139
alias_name = "test_alias"
140+
mock_client.alias.delete.return_value = True
123141

124142
alias_manager.delete_alias(alias_name)
125143

126144
mock_client.alias.delete.assert_called_once_with(alias_name=alias_name)
127145

128146

147+
def test_delete_alias_not_found_raises(
148+
alias_manager: AliasManager, mock_client: MagicMock
149+
) -> None:
150+
"""The client returns False on 404 instead of raising."""
151+
alias_name = "missing_alias"
152+
mock_client.alias.delete.return_value = False
153+
154+
with pytest.raises(Exception) as exc_info:
155+
alias_manager.delete_alias(alias_name)
156+
157+
assert (
158+
f"Error deleting alias '{alias_name}': Alias '{alias_name}' not found."
159+
in str(exc_info.value)
160+
)
161+
162+
129163
def test_delete_alias_error(
130164
alias_manager: AliasManager, mock_client: MagicMock
131165
) -> None:
@@ -219,6 +253,7 @@ def test_delete_alias_success_text(
219253
alias_manager: AliasManager, mock_client: MagicMock, capsys
220254
) -> None:
221255
"""Test delete_alias emits a text success message."""
256+
mock_client.alias.delete.return_value = True
222257
alias_manager.delete_alias("my_alias", json_output=False)
223258

224259
mock_client.alias.delete.assert_called_once_with(alias_name="my_alias")
@@ -231,6 +266,7 @@ def test_delete_alias_success_json(
231266
alias_manager: AliasManager, mock_client: MagicMock, capsys
232267
) -> None:
233268
"""Test delete_alias emits a JSON success message."""
269+
mock_client.alias.delete.return_value = True
234270
alias_manager.delete_alias("my_alias", json_output=True)
235271

236272
out = capsys.readouterr().out
@@ -283,6 +319,7 @@ def test_update_alias_json_output(
283319
alias_manager: AliasManager, mock_client: MagicMock, capsys
284320
) -> None:
285321
"""Test update_alias emits JSON when json_output=True."""
322+
mock_client.alias.update.return_value = True
286323
alias_manager.update_alias("my_alias", "NewCollection", json_output=True)
287324

288325
out = capsys.readouterr().out
@@ -295,6 +332,7 @@ def test_update_alias_text_output(
295332
alias_manager: AliasManager, mock_client: MagicMock, capsys
296333
) -> None:
297334
"""Test update_alias emits text when json_output=False."""
335+
mock_client.alias.update.return_value = True
298336
alias_manager.update_alias("my_alias", "NewCollection", json_output=False)
299337

300338
out = capsys.readouterr().out

0 commit comments

Comments
 (0)