Skip to content

Commit 28a1012

Browse files
author
Zhe Yu
committed
tests(cli): Fix query and vectorise tool edge cases
1 parent 14cbd49 commit 28a1012

4 files changed

Lines changed: 160 additions & 5 deletions

File tree

src/vectorcode/mcp_main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ async def vectorise_files(paths: list[str], project_root: str) -> dict[str, int]
119119
message=f"{e.__class__.__name__}: Failed to create the collection at {project_root}.",
120120
)
121121
)
122-
if collection is None:
122+
if collection is None: # pragma: nocover
123123
raise McpError(
124124
ErrorData(
125125
code=1,

src/vectorcode/subcommands/query/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
import logging
33
import os
4-
from typing import cast
4+
from typing import Any, cast
55

66
from chromadb import GetResult, Where
77
from chromadb.api.models.AsyncCollection import AsyncCollection
@@ -49,12 +49,15 @@ async def get_query_result_files(
4949
try:
5050
if len(configs.query_exclude):
5151
logger.info(f"Excluding {len(configs.query_exclude)} files from the query.")
52-
filter: dict[str, dict] = {"path": {"$nin": configs.query_exclude}}
52+
filter: dict[str, Any] = {"path": {"$nin": configs.query_exclude}}
5353
else:
5454
filter = {}
5555
num_query = configs.n_result
5656
if QueryInclude.chunk in configs.include:
57-
filter["start"] = {"$gte": 0}
57+
if filter:
58+
filter = {"$and": [filter.copy(), {"$gte": 0}]}
59+
else:
60+
filter["start"] = {"$gte": 0}
5861
else:
5962
num_query = await collection.count()
6063
if configs.query_multiplier > 0:

tests/subcommands/query/test_query.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,36 @@ async def test_get_query_result_files_with_query_exclude(mock_collection, mock_c
205205
assert kwargs["where"] == {"path": {"$nin": ["/excluded/path.py"]}}
206206

207207

208+
@pytest.mark.asyncio
209+
async def test_get_query_result_chunks_with_query_exclude(mock_collection, mock_config):
210+
# Setup query_exclude
211+
mock_config.query_exclude = ["/excluded/path.py"]
212+
mock_config.include = [QueryInclude.chunk, QueryInclude.path]
213+
214+
with (
215+
patch("vectorcode.subcommands.query.expand_path") as mock_expand_path,
216+
patch("vectorcode.subcommands.query.expand_globs") as mock_expand_globs,
217+
patch("vectorcode.subcommands.query.reranker.NaiveReranker") as MockReranker,
218+
patch("os.path.isfile", return_value=True), # Add this line to mock isfile
219+
):
220+
mock_expand_globs.return_value = ["/excluded/path.py"]
221+
mock_expand_path.return_value = "/excluded/path.py"
222+
223+
mock_reranker_instance = MagicMock()
224+
mock_reranker_instance.rerank = AsyncMock(return_value=["file1.py", "file2.py"])
225+
MockReranker.return_value = mock_reranker_instance
226+
227+
# Call the function
228+
await get_query_result_files(mock_collection, mock_config)
229+
230+
# Check that query was called with the right parameters including the where clause
231+
mock_collection.query.assert_called_once()
232+
_, kwargs = mock_collection.query.call_args
233+
assert kwargs["where"] == {
234+
"$and": [{"path": {"$nin": ["/excluded/path.py"]}}, {"$gte": 0}]
235+
}
236+
237+
208238
@pytest.mark.asyncio
209239
async def test_get_query_reranker_initialisation_error(mock_collection, mock_config):
210240
# Configure to use CrossEncoder reranker

tests/test_mcp.py

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import os
2+
import tempfile
13
from argparse import ArgumentParser
2-
from unittest.mock import AsyncMock, MagicMock, patch
4+
from unittest.mock import AsyncMock, MagicMock, mock_open, patch
35

46
import pytest
57
from mcp import McpError
@@ -11,6 +13,7 @@
1113
mcp_server,
1214
parse_cli_args,
1315
query_tool,
16+
vectorise_files,
1417
)
1518

1619

@@ -168,6 +171,125 @@ async def test_query_tool_no_collection():
168171
)
169172

170173

174+
@pytest.mark.asyncio
175+
async def test_vectorise_tool_invalid_project_root():
176+
with (
177+
patch("os.path.isdir", return_value=False),
178+
):
179+
with pytest.raises(McpError):
180+
await vectorise_files(paths=["foo.bar"], project_root=".")
181+
182+
183+
@pytest.mark.asyncio
184+
async def test_vectorise_files_success():
185+
with tempfile.TemporaryDirectory() as temp_dir:
186+
file_path = f"{temp_dir}/test_file.py"
187+
with open(file_path, "w") as f:
188+
f.write("def func(): pass")
189+
190+
with (
191+
patch("os.path.isdir", return_value=True),
192+
patch("vectorcode.mcp_main.get_project_config") as mock_get_project_config,
193+
patch("vectorcode.mcp_main.get_client") as mock_get_client,
194+
patch("vectorcode.mcp_main.get_collection") as mock_get_collection,
195+
patch("vectorcode.subcommands.vectorise.chunked_add"),
196+
patch(
197+
"vectorcode.subcommands.vectorise.hash_file", return_value="test_hash"
198+
),
199+
):
200+
mock_config = Config(project_root=temp_dir)
201+
mock_get_project_config.return_value = mock_config
202+
mock_client = AsyncMock()
203+
mock_get_client.return_value = mock_client
204+
mock_collection = AsyncMock()
205+
mock_collection.get.return_value = {"ids": [], "metadatas": []}
206+
mock_get_collection.return_value = mock_collection
207+
mock_client.get_max_batch_size.return_value = 100
208+
209+
result = await vectorise_files(paths=[file_path], project_root=temp_dir)
210+
211+
assert result["add"] == 1
212+
mock_get_project_config.assert_called_once_with(temp_dir)
213+
mock_get_client.assert_called_once_with(mock_config)
214+
mock_get_collection.assert_called_once_with(mock_client, mock_config, True)
215+
216+
217+
@pytest.mark.asyncio
218+
async def test_vectorise_files_collection_access_failure():
219+
with (
220+
patch("os.path.isdir", return_value=True),
221+
patch("vectorcode.mcp_main.get_project_config"),
222+
patch("vectorcode.mcp_main.get_client", side_effect=Exception("Client error")),
223+
patch("vectorcode.mcp_main.get_collection"),
224+
):
225+
with pytest.raises(McpError) as exc_info:
226+
await vectorise_files(paths=["file.py"], project_root="/valid/path")
227+
228+
assert exc_info.value.error.code == 1
229+
assert (
230+
"Failed to create the collection at /valid/path"
231+
in exc_info.value.error.message
232+
)
233+
234+
235+
@pytest.mark.asyncio
236+
async def test_vectorise_files_with_exclude_spec():
237+
with tempfile.TemporaryDirectory() as temp_dir:
238+
file1 = f"{temp_dir}/file1.py"
239+
excluded_file = f"{temp_dir}/excluded.py"
240+
exclude_spec_file = f"{temp_dir}/.vectorcode/vectorcode.exclude"
241+
242+
os.makedirs(f"{temp_dir}/.vectorcode")
243+
with open(file1, "w") as f:
244+
f.write("content1")
245+
with open(excluded_file, "w") as f:
246+
f.write("content_excluded")
247+
248+
# Create mock file handles for specific file contents
249+
mock_exclude_file_handle = mock_open(read_data="excluded.py").return_value
250+
251+
def mock_open_side_effect(filename, *args, **kwargs):
252+
if filename == exclude_spec_file:
253+
return mock_exclude_file_handle
254+
# For other files that might be opened, return a generic mock
255+
return MagicMock()
256+
257+
with (
258+
patch("os.path.isdir", return_value=True),
259+
patch("vectorcode.mcp_main.get_project_config") as mock_get_project_config,
260+
patch("vectorcode.mcp_main.get_client") as mock_get_client,
261+
patch("vectorcode.mcp_main.get_collection") as mock_get_collection,
262+
patch("vectorcode.subcommands.vectorise.chunked_add") as mock_chunked_add,
263+
patch(
264+
"vectorcode.subcommands.vectorise.hash_file", return_value="test_hash"
265+
),
266+
# Patch builtins.open with the custom side effect
267+
patch("builtins.open", side_effect=mock_open_side_effect),
268+
# Patch os.path.isfile to control which files "exist"
269+
patch(
270+
"os.path.isfile",
271+
side_effect=lambda x: x in [file1, excluded_file, exclude_spec_file],
272+
),
273+
):
274+
mock_config = Config(project_root=temp_dir)
275+
mock_get_project_config.return_value = mock_config
276+
mock_client = AsyncMock()
277+
mock_get_client.return_value = mock_client
278+
mock_collection = AsyncMock()
279+
mock_collection.get.return_value = {"ids": [], "metadatas": []}
280+
mock_get_collection.return_value = mock_collection
281+
mock_client.get_max_batch_size.return_value = 100
282+
283+
result = await vectorise_files(
284+
paths=[file1, excluded_file], project_root=temp_dir
285+
)
286+
287+
assert result["add"] == 0
288+
assert mock_chunked_add.call_count == 0
289+
call_args = [call[0][0] for call in mock_chunked_add.call_args_list]
290+
assert excluded_file not in call_args
291+
292+
171293
@pytest.mark.asyncio
172294
async def test_mcp_server():
173295
with (

0 commit comments

Comments
 (0)