|
| 1 | +import os |
| 2 | +import tempfile |
1 | 3 | from argparse import ArgumentParser |
2 | | -from unittest.mock import AsyncMock, MagicMock, patch |
| 4 | +from unittest.mock import AsyncMock, MagicMock, mock_open, patch |
3 | 5 |
|
4 | 6 | import pytest |
5 | 7 | from mcp import McpError |
|
11 | 13 | mcp_server, |
12 | 14 | parse_cli_args, |
13 | 15 | query_tool, |
| 16 | + vectorise_files, |
14 | 17 | ) |
15 | 18 |
|
16 | 19 |
|
@@ -168,6 +171,125 @@ async def test_query_tool_no_collection(): |
168 | 171 | ) |
169 | 172 |
|
170 | 173 |
|
| 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 | + |
171 | 293 | @pytest.mark.asyncio |
172 | 294 | async def test_mcp_server(): |
173 | 295 | with ( |
|
0 commit comments