|
9 | 9 | import os |
10 | 10 | import sys |
11 | 11 | from typing import List, Dict, Any |
| 12 | +from datetime import datetime, timedelta |
12 | 13 |
|
13 | 14 | # Add the project root to the path |
14 | 15 | current_dir = os.path.dirname(os.path.abspath(__file__)) |
15 | 16 | project_root = os.path.abspath(os.path.join(current_dir, "../../..")) |
16 | 17 | sys.path.insert(0, project_root) |
17 | 18 |
|
18 | 19 | # Import the class under test |
19 | | -from sdk.nexent.vector_database.elasticsearch_core import ElasticSearchCore |
| 20 | +from sdk.nexent.vector_database.elasticsearch_core import ElasticSearchCore, BulkOperation |
20 | 21 | from elasticsearch import exceptions |
21 | 22 |
|
22 | 23 |
|
@@ -257,6 +258,231 @@ def test_delete_documents_exception(self, vdb_core): |
257 | 258 | assert result == 0 |
258 | 259 | vdb_core.client.delete_by_query.assert_called_once() |
259 | 260 |
|
| 261 | + def test_create_index_request_error_existing(self, vdb_core): |
| 262 | + """Ensure RequestError with resource already exists still succeeds.""" |
| 263 | + vdb_core.client = MagicMock() |
| 264 | + vdb_core.client.indices.exists.return_value = False |
| 265 | + meta = MagicMock(status=400) |
| 266 | + vdb_core.client.indices.create.side_effect = exceptions.RequestError( |
| 267 | + "resource_already_exists_exception", meta, {"error": {"reason": "exists"}} |
| 268 | + ) |
| 269 | + vdb_core._ensure_index_ready = MagicMock(return_value=True) |
| 270 | + |
| 271 | + assert vdb_core.create_index("test_index") is True |
| 272 | + vdb_core._ensure_index_ready.assert_called_once_with("test_index") |
| 273 | + |
| 274 | + def test_create_index_request_error_failure(self, vdb_core): |
| 275 | + """Ensure create_index returns False for non recoverable RequestError.""" |
| 276 | + vdb_core.client = MagicMock() |
| 277 | + vdb_core.client.indices.exists.return_value = False |
| 278 | + meta = MagicMock(status=400) |
| 279 | + vdb_core.client.indices.create.side_effect = exceptions.RequestError( |
| 280 | + "validation_exception", meta, {"error": {"reason": "bad"}} |
| 281 | + ) |
| 282 | + |
| 283 | + assert vdb_core.create_index("test_index") is False |
| 284 | + |
| 285 | + def test_create_index_general_exception(self, vdb_core): |
| 286 | + """Ensure unexpected exception from create_index returns False.""" |
| 287 | + vdb_core.client = MagicMock() |
| 288 | + vdb_core.client.indices.exists.return_value = False |
| 289 | + vdb_core.client.indices.create.side_effect = Exception("boom") |
| 290 | + |
| 291 | + assert vdb_core.create_index("test_index") is False |
| 292 | + |
| 293 | + def test_force_refresh_with_retry_zero_attempts(self, vdb_core): |
| 294 | + """Ensure guard clause without attempts returns False.""" |
| 295 | + vdb_core.client = MagicMock() |
| 296 | + result = vdb_core._force_refresh_with_retry("idx", max_retries=0) |
| 297 | + assert result is False |
| 298 | + |
| 299 | + def test_bulk_operation_context_preexisting_operation(self, vdb_core): |
| 300 | + """Ensure context skips apply/restore when operations remain.""" |
| 301 | + existing = BulkOperation( |
| 302 | + index_name="test_index", |
| 303 | + operation_id="existing", |
| 304 | + start_time=datetime.utcnow(), |
| 305 | + expected_duration=timedelta(seconds=30), |
| 306 | + ) |
| 307 | + vdb_core._bulk_operations = {"test_index": [existing]} |
| 308 | + |
| 309 | + with patch.object(vdb_core, "_apply_bulk_settings") as mock_apply, \ |
| 310 | + patch.object(vdb_core, "_restore_normal_settings") as mock_restore: |
| 311 | + |
| 312 | + with vdb_core.bulk_operation_context("test_index") as op_id: |
| 313 | + assert op_id != existing.operation_id |
| 314 | + |
| 315 | + mock_apply.assert_not_called() |
| 316 | + mock_restore.assert_not_called() |
| 317 | + assert vdb_core._bulk_operations["test_index"] == [existing] |
| 318 | + |
| 319 | + def test_get_user_indices_exception(self, vdb_core): |
| 320 | + """Ensure get_user_indices returns empty list on failure.""" |
| 321 | + vdb_core.client = MagicMock() |
| 322 | + vdb_core.client.indices.get_alias.side_effect = Exception("failure") |
| 323 | + |
| 324 | + assert vdb_core.get_user_indices() == [] |
| 325 | + |
| 326 | + def test_check_index_exists(self, vdb_core): |
| 327 | + """Ensure check_index_exists delegates to client.""" |
| 328 | + vdb_core.client = MagicMock() |
| 329 | + vdb_core.client.indices.exists.return_value = True |
| 330 | + |
| 331 | + assert vdb_core.check_index_exists("idx") is True |
| 332 | + vdb_core.client.indices.exists.assert_called_once_with(index="idx") |
| 333 | + |
| 334 | + def test_small_batch_insert_sets_embedding_model_name(self, vdb_core): |
| 335 | + """_small_batch_insert should attach embedding model name.""" |
| 336 | + vdb_core.client = MagicMock() |
| 337 | + vdb_core.client.bulk.return_value = {"errors": False, "items": []} |
| 338 | + vdb_core._preprocess_documents = MagicMock(return_value=[{"content": "body"}]) |
| 339 | + vdb_core._handle_bulk_errors = MagicMock() |
| 340 | + |
| 341 | + mock_embedding_model = MagicMock() |
| 342 | + mock_embedding_model.get_embeddings.return_value = [[0.1, 0.2]] |
| 343 | + mock_embedding_model.embedding_model_name = "demo-model" |
| 344 | + |
| 345 | + vdb_core._small_batch_insert("idx", [{"content": "body"}], "content", mock_embedding_model) |
| 346 | + operations = vdb_core.client.bulk.call_args.kwargs["operations"] |
| 347 | + inserted_doc = operations[1] |
| 348 | + assert inserted_doc["embedding_model_name"] == "demo-model" |
| 349 | + |
| 350 | + def test_large_batch_insert_sets_default_embedding_model_name(self, vdb_core): |
| 351 | + """_large_batch_insert should fall back to 'unknown' when attr missing.""" |
| 352 | + vdb_core.client = MagicMock() |
| 353 | + vdb_core.client.bulk.return_value = {"errors": False, "items": []} |
| 354 | + vdb_core._preprocess_documents = MagicMock(return_value=[{"content": "body"}]) |
| 355 | + vdb_core._handle_bulk_errors = MagicMock() |
| 356 | + |
| 357 | + class SimpleEmbedding: |
| 358 | + def get_embeddings(self, texts): |
| 359 | + return [[0.1 for _ in texts]] |
| 360 | + |
| 361 | + embedding_model = SimpleEmbedding() |
| 362 | + |
| 363 | + vdb_core._large_batch_insert("idx", [{"content": "body"}], 10, "content", embedding_model) |
| 364 | + operations = vdb_core.client.bulk.call_args.kwargs["operations"] |
| 365 | + inserted_doc = operations[1] |
| 366 | + assert inserted_doc["embedding_model_name"] == "unknown" |
| 367 | + |
| 368 | + def test_large_batch_insert_bulk_exception(self, vdb_core): |
| 369 | + """Ensure bulk exceptions are handled and indexing continues.""" |
| 370 | + vdb_core.client = MagicMock() |
| 371 | + vdb_core.client.bulk.side_effect = Exception("bulk error") |
| 372 | + vdb_core._preprocess_documents = MagicMock(return_value=[{"content": "body"}]) |
| 373 | + |
| 374 | + mock_embedding_model = MagicMock() |
| 375 | + mock_embedding_model.get_embeddings.return_value = [[0.1]] |
| 376 | + |
| 377 | + result = vdb_core._large_batch_insert("idx", [{"content": "body"}], 1, "content", mock_embedding_model) |
| 378 | + assert result == 0 |
| 379 | + |
| 380 | + def test_large_batch_insert_preprocess_exception(self, vdb_core): |
| 381 | + """Ensure outer exception handler returns zero on preprocess failure.""" |
| 382 | + vdb_core._preprocess_documents = MagicMock(side_effect=Exception("fail")) |
| 383 | + |
| 384 | + mock_embedding_model = MagicMock() |
| 385 | + result = vdb_core._large_batch_insert("idx", [{"content": "body"}], 10, "content", mock_embedding_model) |
| 386 | + assert result == 0 |
| 387 | + |
| 388 | + def test_count_documents_success(self, vdb_core): |
| 389 | + """Ensure count_documents returns ES count.""" |
| 390 | + vdb_core.client = MagicMock() |
| 391 | + vdb_core.client.count.return_value = {"count": 42} |
| 392 | + |
| 393 | + assert vdb_core.count_documents("idx") == 42 |
| 394 | + |
| 395 | + def test_count_documents_exception(self, vdb_core): |
| 396 | + """Ensure count_documents returns zero on error.""" |
| 397 | + vdb_core.client = MagicMock() |
| 398 | + vdb_core.client.count.side_effect = Exception("fail") |
| 399 | + |
| 400 | + assert vdb_core.count_documents("idx") == 0 |
| 401 | + |
| 402 | + def test_search_and_multi_search_passthrough(self, vdb_core): |
| 403 | + """Ensure search helpers delegate to the client.""" |
| 404 | + vdb_core.client = MagicMock() |
| 405 | + vdb_core.client.search.return_value = {"hits": {}} |
| 406 | + vdb_core.client.msearch.return_value = {"responses": []} |
| 407 | + |
| 408 | + assert vdb_core.search("idx", {"query": {"match_all": {}}}) == {"hits": {}} |
| 409 | + assert vdb_core.multi_search([{"query": {"match_all": {}}}], "idx") == {"responses": []} |
| 410 | + |
| 411 | + def test_exec_query_formats_results(self, vdb_core): |
| 412 | + """Ensure exec_query strips metadata and exposes scores.""" |
| 413 | + vdb_core.client = MagicMock() |
| 414 | + vdb_core.client.search.return_value = { |
| 415 | + "hits": { |
| 416 | + "hits": [ |
| 417 | + { |
| 418 | + "_score": 1.23, |
| 419 | + "_index": "idx", |
| 420 | + "_source": {"id": "doc1", "content": "body"}, |
| 421 | + } |
| 422 | + ] |
| 423 | + } |
| 424 | + } |
| 425 | + |
| 426 | + results = vdb_core.exec_query("idx", {"query": {}}) |
| 427 | + assert results == [ |
| 428 | + {"score": 1.23, "document": {"id": "doc1", "content": "body"}, "index": "idx"} |
| 429 | + ] |
| 430 | + |
| 431 | + def test_hybrid_search_missing_fields_logged_for_accurate(self, vdb_core): |
| 432 | + """Ensure hybrid_search tolerates missing accurate fields.""" |
| 433 | + mock_embedding_model = MagicMock() |
| 434 | + with patch.object(vdb_core, "accurate_search", return_value=[{"score": 1.0}]), \ |
| 435 | + patch.object(vdb_core, "semantic_search", return_value=[]): |
| 436 | + assert vdb_core.hybrid_search(["idx"], "query", mock_embedding_model) == [] |
| 437 | + |
| 438 | + def test_hybrid_search_missing_fields_logged_for_semantic(self, vdb_core): |
| 439 | + """Ensure hybrid_search tolerates missing semantic fields.""" |
| 440 | + mock_embedding_model = MagicMock() |
| 441 | + with patch.object(vdb_core, "accurate_search", return_value=[]), \ |
| 442 | + patch.object(vdb_core, "semantic_search", return_value=[{"score": 0.5}]): |
| 443 | + assert vdb_core.hybrid_search(["idx"], "query", mock_embedding_model) == [] |
| 444 | + |
| 445 | + def test_hybrid_search_faulty_combined_results(self, vdb_core): |
| 446 | + """Inject faulty combined result to hit KeyError handling in final loop.""" |
| 447 | + mock_embedding_model = MagicMock() |
| 448 | + accurate_payload = [ |
| 449 | + {"score": 1.0, "document": {"id": "doc1"}, "index": "idx"} |
| 450 | + ] |
| 451 | + |
| 452 | + with patch.object(vdb_core, "accurate_search", return_value=accurate_payload), \ |
| 453 | + patch.object(vdb_core, "semantic_search", return_value=[]): |
| 454 | + |
| 455 | + injected = {"done": False} |
| 456 | + |
| 457 | + def tracer(frame, event, arg): |
| 458 | + if ( |
| 459 | + frame.f_code.co_name == "hybrid_search" |
| 460 | + and event == "line" |
| 461 | + and frame.f_lineno == 788 |
| 462 | + and not injected["done"] |
| 463 | + ): |
| 464 | + frame.f_locals["combined_results"]["faulty"] = { |
| 465 | + "accurate_score": 0, |
| 466 | + "semantic_score": 0, |
| 467 | + } |
| 468 | + injected["done"] = True |
| 469 | + return tracer |
| 470 | + |
| 471 | + sys.settrace(tracer) |
| 472 | + try: |
| 473 | + results = vdb_core.hybrid_search(["idx"], "query", mock_embedding_model) |
| 474 | + finally: |
| 475 | + sys.settrace(None) |
| 476 | + |
| 477 | + assert len(results) == 1 |
| 478 | + |
| 479 | + def test_get_documents_detail_exception(self, vdb_core): |
| 480 | + """Ensure get_documents_detail returns empty list on failure.""" |
| 481 | + vdb_core.client = MagicMock() |
| 482 | + vdb_core.client.search.side_effect = Exception("fail") |
| 483 | + |
| 484 | + assert vdb_core.get_documents_detail("idx") == [] |
| 485 | + |
260 | 486 | def test_get_indices_detail_success(self, vdb_core): |
261 | 487 | """Test get_indices_detail successful case""" |
262 | 488 | vdb_core.client = MagicMock() |
|
0 commit comments