| """ |
| LanceDB Advanced Features Integration Tests |
| |
| Tests advanced LanceDB handler features: |
| - Knowledge graph operations (add_edge, query_edges, graph_traversal) |
| - Batch operations (large-scale document insertion, vector operations) |
| - S3/R2 storage configuration |
| - Advanced embedding features (hybrid search, reranking, multi-provider) |
| |
| Coverage Target: 75%+ on advanced functionality (lines 900-1397) |
| """ |
|
|
| import sys |
| from unittest.mock import Mock, MagicMock, patch, AsyncMock, call |
| import pytest |
| from datetime import datetime |
| from typing import Dict, Any, List, Optional |
|
|
| |
| sys.modules['lancedb'] = MagicMock() |
| sys.modules['lancedb.pydantic'] = MagicMock() |
| sys.modules['lancedb.query'] = MagicMock() |
|
|
| |
| sys.modules['boto3'] = MagicMock() |
| sys.modules['botocore'] = MagicMock() |
| sys.modules['botocore.exceptions'] = MagicMock() |
|
|
| |
| sys.modules['s3fs'] = MagicMock() |
|
|
| |
| import numpy as np |
| |
| if 'numpy' not in sys.modules: |
| sys.modules['numpy'] = MagicMock() |
|
|
| |
| import core.lancedb_handler as lancedb_module |
| lancedb_module.PANDAS_AVAILABLE = True |
|
|
| |
| from core.lancedb_handler import ( |
| LanceDBHandler, |
| ChatHistoryManager, |
| get_lancedb_handler, |
| get_chat_history_manager, |
| create_memory_schema |
| ) |
|
|
|
|
| |
| |
| |
|
|
| @pytest.fixture |
| def clear_handlers(): |
| """Clear singleton handlers before each test""" |
| import core.lancedb_handler |
| core.lancedb_handler._workspace_handlers = {} |
| yield |
| core.lancedb_handler._workspace_handlers = {} |
|
|
|
|
| @pytest.fixture |
| def mock_knowledge_graph_table(): |
| """Mock knowledge graph table with edge schema""" |
| table = MagicMock() |
| table.schema = MagicMock() |
| table.schema.names = ["id", "from_id", "to_id", "type", "description", "vector", "metadata", "created_at"] |
|
|
| |
| def mock_search(query_vector=None): |
| result_df = MagicMock() |
| result_df.empty = False |
| result_df.iterrows = MagicMock(return_value=[ |
| (0, { |
| "id": "edge-1", |
| "from_id": "entity-1", |
| "to_id": "entity-2", |
| "type": "related_to", |
| "description": "Entity 1 is related to Entity 2", |
| "metadata": '{"source": "test", "weight": 0.8}' |
| }), |
| (1, { |
| "id": "edge-2", |
| "from_id": "entity-2", |
| "to_id": "entity-3", |
| "type": "depends_on", |
| "description": "Entity 2 depends on Entity 3", |
| "metadata": '{"source": "test", "weight": 0.9}' |
| }) |
| ]) |
| result_df.to_list.return_value = ["edge-1", "edge-2"] |
| return result_df |
|
|
| table.search = mock_search |
| table.add = MagicMock() |
| table.create = MagicMock() |
| table.to_pandas = MagicMock(return_value=table.search().to_pandas()) |
| return table |
|
|
|
|
| @pytest.fixture |
| def mock_s3_client(): |
| """Mock boto3 S3 client for storage operations""" |
| with patch('boto3.client') as mock_client: |
| client = MagicMock() |
| mock_client.return_value = client |
| yield client |
|
|
|
|
| @pytest.fixture |
| def mock_s3fs(): |
| """Mock s3fs filesystem for S3 path operations""" |
| with patch('s3fs.S3FileSystem') as mock_s3fs_class: |
| fs = MagicMock() |
| mock_s3fs_class.return_value = fs |
| yield fs |
|
|
|
|
| @pytest.fixture |
| def handler_with_s3_config(monkeypatch, clear_handlers): |
| """Handler initialized with S3/R2 configuration""" |
| |
| monkeypatch.setenv("AWS_ENDPOINT_URL", "https://example.r2.cloudflarestorage.com") |
| monkeypatch.setenv("R2_ACCESS_KEY_ID", "test_r2_key") |
| monkeypatch.setenv("R2_SECRET_ACCESS_KEY", "test_r2_secret") |
| monkeypatch.setenv("AWS_REGION", "auto") |
|
|
| |
| with patch('lancedb.connect') as mock_connect: |
| mock_db = MagicMock() |
| mock_db.table_names.return_value = [] |
| mock_db.open_table.return_value = None |
| mock_connect.return_value = mock_db |
|
|
| handler = LanceDBHandler( |
| db_path="s3://atom-memory-bucket/lancedb", |
| workspace_id="test_workspace" |
| ) |
| handler._ensure_db() |
| yield handler |
|
|
|
|
| @pytest.fixture |
| def large_batch_documents(): |
| """Fixture with 100+ documents for batch testing""" |
| documents = [] |
| for i in range(100): |
| documents.append({ |
| "text": f"Test document {i} with some content for semantic search", |
| "source": f"source-{i % 5}", |
| "metadata": { |
| "batch_id": "test-batch-1", |
| "index": i, |
| "category": f"category-{i % 3}" |
| } |
| }) |
| return documents |
|
|
|
|
| @pytest.fixture |
| def mock_r2_credentials(monkeypatch): |
| """Mock R2 credentials""" |
| monkeypatch.setenv("R2_ACCESS_KEY_ID", "test_r2_access_key") |
| monkeypatch.setenv("R2_SECRET_ACCESS_KEY", "test_r2_secret_key") |
| monkeypatch.setenv("AWS_ENDPOINT_URL", "https://example.r2.cloudflarestorage.com") |
|
|
|
|
| @pytest.fixture |
| def mock_aws_credentials(monkeypatch): |
| """Mock AWS credentials""" |
| monkeypatch.setenv("AWS_ACCESS_KEY_ID", "test_aws_access_key") |
| monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "test_aws_secret_key") |
| monkeypatch.setenv("AWS_REGION", "us-east-1") |
|
|
|
|
| @pytest.fixture |
| def s3_storage_options(): |
| """Complete storage_options dict for S3 connection""" |
| return { |
| "endpoint": "https://example.r2.cloudflarestorage.com", |
| "aws_access_key_id": "test_r2_access_key", |
| "aws_secret_access_key": "test_r2_secret_key", |
| "region": "auto" |
| } |
|
|
|
|
| @pytest.fixture |
| def handler_with_mock_db(clear_handlers): |
| """Handler with mocked database connection""" |
| with patch('lancedb.connect') as mock_connect: |
| mock_db = MagicMock() |
| mock_db.table_names.return_value = ["documents", "episodes"] |
|
|
| |
| mock_table = MagicMock() |
| mock_table.add = MagicMock() |
| mock_table.create = MagicMock() |
| mock_db.open_table.return_value = mock_table |
| mock_db.create_table.return_value = mock_table |
|
|
| |
| mock_result_df = MagicMock() |
| mock_result_df.empty = False |
| mock_result_df.iterrows = MagicMock(return_value=[ |
| (0, {"id": "doc-1", "text": "Test", "_distance": 0.1}) |
| ]) |
| mock_table.search.return_value.limit.return_value.to_pandas.return_value = mock_result_df |
|
|
| mock_connect.return_value = mock_db |
|
|
| handler = LanceDBHandler(db_path="./test_data", workspace_id="test") |
| handler._ensure_db() |
| yield handler, mock_db, mock_table |
|
|
|
|
| |
| |
| |
|
|
| class TestKnowledgeGraphOperations: |
| """Test knowledge graph edge operations""" |
|
|
| def test_query_knowledge_graph_basic(self, handler_with_mock_db): |
| """Test basic knowledge graph query""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| |
| |
| results = handler.query_knowledge_graph("agent workflow relationship") |
|
|
| assert isinstance(results, list) |
| |
| assert len(results) >= 0 |
|
|
| def test_query_knowledge_graph_with_user_filter(self, handler_with_mock_db): |
| """Test knowledge graph query with user filter""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| results = handler.query_knowledge_graph( |
| "test query", |
| user_id="user-123", |
| limit=10 |
| ) |
|
|
| assert isinstance(results, list) |
| |
| |
|
|
| def test_query_knowledge_graph_limit_parameter(self, handler_with_mock_db): |
| """Test knowledge graph query respects limit parameter""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| results = handler.query_knowledge_graph("test", limit=5) |
|
|
| |
| assert isinstance(results, list) |
|
|
| def test_knowledge_graph_table_creation_on_first_query(self, handler_with_mock_db): |
| """Test knowledge graph table created if not exists""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| mock_db.table_names.return_value = ["documents"] |
| mock_db.open_table.return_value = None |
|
|
| |
| results = handler.query_knowledge_graph("test") |
|
|
| assert isinstance(results, list) |
|
|
| def test_knowledge_graph_empty_results(self, handler_with_mock_db): |
| """Test knowledge graph query with no matching results""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| mock_result_df = MagicMock() |
| mock_result_df.empty = True |
| mock_result_df.iterrows = MagicMock(return_value=[]) |
| mock_table.search.return_value.limit.return_value.to_pandas.return_value = mock_result_df |
|
|
| results = handler.query_knowledge_graph("nonexistent query") |
|
|
| assert isinstance(results, list) |
| assert len(results) == 0 |
|
|
| def test_knowledge_graph_search_with_distance_scoring(self, handler_with_mock_db): |
| """Test knowledge graph results include distance/similarity scores""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| mock_result_df = MagicMock() |
| mock_result_df.empty = False |
| mock_result_df.iterrows = MagicMock(return_value=[ |
| (0, {"id": "edge-1", "text": "Related edge", "_distance": 0.1}), |
| (1, {"id": "edge-2", "text": "Less related", "_distance": 0.5}) |
| ]) |
| mock_table.search.return_value.limit.return_value.to_pandas.return_value = mock_result_df |
|
|
| results = handler.query_knowledge_graph("test") |
|
|
| assert isinstance(results, list) |
|
|
| def test_knowledge_graph_metadata_parsing(self, handler_with_mock_db): |
| """Test knowledge graph metadata is properly parsed""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| import json |
|
|
| mock_result_df = MagicMock() |
| mock_result_df.empty = False |
| mock_result_df.iterrows = MagicMock(return_value=[ |
| (0, { |
| "id": "edge-1", |
| "text": "Test edge", |
| "metadata": json.dumps({"weight": 0.8, "confidence": 0.9}), |
| "_distance": 0.1 |
| }) |
| ]) |
| mock_table.search.return_value.limit.return_value.to_pandas.return_value = mock_result_df |
|
|
| results = handler.query_knowledge_graph("test") |
|
|
| assert isinstance(results, list) |
|
|
| def test_query_knowledge_graph_error_handling(self, handler_with_mock_db): |
| """Test knowledge graph query handles errors gracefully""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| mock_table.search.side_effect = Exception("Database connection error") |
|
|
| results = handler.query_knowledge_graph("test") |
|
|
| |
| assert isinstance(results, list) |
|
|
| def test_knowledge_graph_with_special_characters(self, handler_with_mock_db): |
| """Test knowledge graph query with special characters in query""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| mock_result_df = MagicMock() |
| mock_result_df.empty = True |
| mock_result_df.iterrows = MagicMock(return_value=[]) |
| mock_table.search.return_value.limit.return_value.to_pandas.return_value = mock_result_df |
|
|
| |
| results = handler.query_knowledge_graph("test & (query OR query) | special: chars") |
|
|
| assert isinstance(results, list) |
|
|
| def test_knowledge_graph_query_without_db_initialization(self, clear_handlers): |
| """Test knowledge graph query when DB not initialized""" |
| handler = LanceDBHandler(db_path="./test", workspace_id="test") |
| |
|
|
| |
| results = handler.query_knowledge_graph("test") |
|
|
| assert isinstance(results, list) |
| assert len(results) == 0 |
|
|
|
|
| |
| |
| |
|
|
| class TestBatchOperations: |
| """Test large-scale batch operations""" |
|
|
| def test_add_large_batch_success(self, handler_with_mock_db, large_batch_documents): |
| """Test adding 100+ documents in single batch""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| mock_table.add.return_value = None |
|
|
| count = handler.add_documents_batch("documents", large_batch_documents) |
|
|
| |
| assert mock_table.add.called |
| assert count >= 0 |
|
|
| def test_batch_performance_under_5_seconds(self, handler_with_mock_db, large_batch_documents): |
| """Test large batch completes in under 5 seconds""" |
| import time |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| mock_table.add.return_value = None |
|
|
| start_time = time.time() |
| handler.add_documents_batch("documents", large_batch_documents) |
| duration = time.time() - start_time |
|
|
| |
| assert duration < 5.0 |
|
|
| def test_batch_returns_accurate_count(self, handler_with_mock_db): |
| """Test batch returns count of successfully added documents""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| documents = [ |
| {"text": f"Document {i}", "source": "test"} |
| for i in range(10) |
| ] |
|
|
| mock_table.add.return_value = None |
|
|
| count = handler.add_documents_batch("documents", documents) |
|
|
| assert isinstance(count, int) |
| assert count >= 0 |
|
|
| def test_batch_handles_partial_failures(self, handler_with_mock_db): |
| """Test batch continues on individual document failures""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| documents = [ |
| {"text": f"Document {i}", "source": "test"} |
| for i in range(5) |
| ] |
|
|
| |
| call_count = [0] |
| def side_effect(docs): |
| call_count[0] += 1 |
| if call_count[0] > 1: |
| raise Exception("Simulated failure") |
| return None |
|
|
| mock_table.add.side_effect = side_effect |
|
|
| |
| count = handler.add_documents_batch("documents", documents) |
|
|
| assert isinstance(count, int) |
|
|
| def test_batch_with_mixed_metadata(self, handler_with_mock_db): |
| """Test batch handles documents with varying metadata fields""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| documents = [ |
| {"text": "Doc 1", "source": "test", "metadata": {"key1": "value1"}}, |
| {"text": "Doc 2", "source": "test", "metadata": {"key2": "value2"}}, |
| {"text": "Doc 3", "source": "test", "metadata": {"key1": "value1", "key2": "value2"}}, |
| ] |
|
|
| mock_table.add.return_value = None |
|
|
| count = handler.add_documents_batch("documents", documents) |
|
|
| assert mock_table.add.called |
|
|
| def test_batch_creates_table_if_not_exists(self, handler_with_mock_db): |
| """Test batch creates table if it doesn't exist""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| mock_db.table_names.return_value = [] |
| mock_db.open_table.return_value = None |
| mock_db.create_table.return_value = mock_table |
|
|
| documents = [{"text": "Test", "source": "test"}] |
|
|
| handler.add_documents_batch("documents", documents) |
|
|
| |
| assert mock_db.create_table.called |
|
|
| def test_batch_with_empty_document_list(self, handler_with_mock_db): |
| """Test batch handles empty document list""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| count = handler.add_documents_batch("documents", []) |
|
|
| assert isinstance(count, int) |
| |
| if count == 0: |
| mock_table.add.assert_not_called() |
|
|
| def test_batch_database_not_initialized(self, clear_handlers): |
| """Test batch operation when database not initialized""" |
| handler = LanceDBHandler(db_path="./test", workspace_id="test") |
| |
|
|
| documents = [{"text": "Test", "source": "test"}] |
|
|
| count = handler.add_documents_batch("documents", documents) |
|
|
| |
| assert isinstance(count, int) |
|
|
|
|
| |
| |
| |
|
|
| class TestS3Storage: |
| """Test S3/R2 storage configuration and connection""" |
|
|
| def test_s3_endpoint_from_env(self, monkeypatch, clear_handlers): |
| """Test AWS_ENDPOINT_URL used in storage_options""" |
| monkeypatch.setenv("AWS_ENDPOINT_URL", "https://custom.r2.endpoint.com") |
|
|
| with patch('lancedb.connect') as mock_connect: |
| mock_db = MagicMock() |
| mock_db.table_names.return_value = [] |
| mock_connect.return_value = mock_db |
|
|
| handler = LanceDBHandler( |
| db_path="s3://bucket/path", |
| workspace_id="test" |
| ) |
| handler._ensure_db() |
|
|
| |
| assert mock_connect.called |
|
|
| def test_r2_keys_from_env(self, monkeypatch, clear_handlers): |
| """Test R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY used""" |
| monkeypatch.setenv("R2_ACCESS_KEY_ID", "r2_key_id") |
| monkeypatch.setenv("R2_SECRET_ACCESS_KEY", "r2_secret") |
|
|
| with patch('lancedb.connect') as mock_connect: |
| mock_db = MagicMock() |
| mock_db.table_names.return_value = [] |
| mock_connect.return_value = mock_db |
|
|
| handler = LanceDBHandler( |
| db_path="s3://bucket/path", |
| workspace_id="test" |
| ) |
| handler._ensure_db() |
|
|
| |
| assert mock_connect.called |
|
|
| def test_aws_keys_fallback(self, monkeypatch, clear_handlers): |
| """Test falls back to AWS_ACCESS_KEY_ID when R2 keys missing""" |
| |
| monkeypatch.delenv("R2_ACCESS_KEY_ID", raising=False) |
| monkeypatch.delenv("R2_SECRET_ACCESS_KEY", raising=False) |
| monkeypatch.setenv("AWS_ACCESS_KEY_ID", "aws_key_id") |
| monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "aws_secret") |
|
|
| with patch('lancedb.connect') as mock_connect: |
| mock_db = MagicMock() |
| mock_db.table_names.return_value = [] |
| mock_connect.return_value = mock_db |
|
|
| handler = LanceDBHandler( |
| db_path="s3://bucket/path", |
| workspace_id="test" |
| ) |
| handler._ensure_db() |
|
|
| |
| assert mock_connect.called |
|
|
| def test_region_configuration(self, monkeypatch, clear_handlers): |
| """Test AWS_REGION or 'auto' used in storage_options""" |
| monkeypatch.setenv("AWS_REGION", "us-west-2") |
|
|
| with patch('lancedb.connect') as mock_connect: |
| mock_db = MagicMock() |
| mock_db.table_names.return_value = [] |
| mock_connect.return_value = mock_db |
|
|
| handler = LanceDBHandler( |
| db_path="s3://bucket/path", |
| workspace_id="test" |
| ) |
| handler._ensure_db() |
|
|
| |
| assert mock_connect.called |
|
|
| def test_connect_to_s3_database(self, monkeypatch, clear_handlers): |
| """Test lancedb.connect() called with s3:// URI and storage_options""" |
| monkeypatch.setenv("AWS_ENDPOINT_URL", "https://r2.example.com") |
| monkeypatch.setenv("R2_ACCESS_KEY_ID", "key") |
| monkeypatch.setenv("R2_SECRET_ACCESS_KEY", "secret") |
|
|
| with patch('lancedb.connect') as mock_connect: |
| mock_db = MagicMock() |
| mock_db.table_names.return_value = [] |
| mock_connect.return_value = mock_db |
|
|
| handler = LanceDBHandler( |
| db_path="s3://atom-bucket/lancedb", |
| workspace_id="test" |
| ) |
| handler._ensure_db() |
|
|
| |
| mock_connect.assert_called_once() |
| call_args = mock_connect.call_args |
| assert "s3://" in str(call_args) |
|
|
| def test_s3_connection_persists(self, handler_with_s3_config): |
| """Test connection reused across operations""" |
| |
| assert handler_with_s3_config.db is not None |
|
|
| |
| handler_with_s3_config._ensure_db() |
| assert handler_with_s3_config.db is not None |
|
|
| def test_s3_connection_failure_handled(self, monkeypatch, clear_handlers): |
| """Test connection failures return error status""" |
| monkeypatch.setenv("AWS_ENDPOINT_URL", "https://invalid.example.com") |
|
|
| with patch('lancedb.connect') as mock_connect: |
| |
| mock_connect.side_effect = Exception("Connection failed") |
|
|
| handler = LanceDBHandler( |
| db_path="s3://bucket/path", |
| workspace_id="test" |
| ) |
|
|
| |
| handler._ensure_db() |
| assert handler.db is None |
|
|
|
|
| |
| |
| |
|
|
| class TestAdvancedEmbedding: |
| """Test advanced embedding features (dual vector, hybrid search)""" |
|
|
| def test_add_embedding_to_vector_column(self, handler_with_mock_db): |
| """Test add_embedding with 1024-dim vector column""" |
| import asyncio |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| vector_1024 = [0.1] * 1024 |
|
|
| async def test_add(): |
| result = await handler.add_embedding( |
| table_name="episodes", |
| episode_id="episode-1", |
| vector=vector_1024, |
| vector_column="vector" |
| ) |
| return result |
|
|
| result = asyncio.run(test_add()) |
|
|
| |
| |
| assert isinstance(result, bool) |
|
|
| def test_add_embedding_to_fastembed_column(self, handler_with_mock_db): |
| """Test add_embedding with 384-dim vector_fastembed column""" |
| import asyncio |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| vector_384 = [0.2] * 384 |
|
|
| async def test_add(): |
| result = await handler.add_embedding( |
| table_name="episodes", |
| episode_id="episode-2", |
| vector=vector_384, |
| vector_column="vector_fastembed" |
| ) |
| return result |
|
|
| result = asyncio.run(test_add()) |
|
|
| |
| assert isinstance(result, bool) |
|
|
| def test_add_embedding_dimension_mismatch_raises_error(self, handler_with_mock_db): |
| """Test add_embedding raises ValueError for wrong dimension""" |
| import asyncio |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| wrong_vector = [0.1] * 500 |
|
|
| async def test_add(): |
| with pytest.raises(ValueError, match="Dimension mismatch"): |
| await handler.add_embedding( |
| table_name="test_table", |
| episode_id="episode-1", |
| vector=wrong_vector, |
| vector_column="vector" |
| ) |
|
|
| asyncio.run(test_add()) |
|
|
| def test_add_embedding_unknown_column_raises_error(self, handler_with_mock_db): |
| """Test add_embedding raises ValueError for unknown vector column""" |
| import asyncio |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| vector_1024 = [0.1] * 1024 |
|
|
| async def test_add(): |
| with pytest.raises(ValueError, match="Unknown vector column"): |
| await handler.add_embedding( |
| table_name="test_table", |
| episode_id="episode-1", |
| vector=vector_1024, |
| vector_column="unknown_column" |
| ) |
|
|
| asyncio.run(test_add()) |
|
|
| def test_similarity_search_on_vector_column(self, handler_with_mock_db): |
| """Test similarity_search on specific vector column""" |
| import asyncio |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| mock_result_df = MagicMock() |
| mock_result_df.empty = False |
| mock_result_df.iterrows = MagicMock(return_value=[ |
| (0, {"id": "episode-1", "_distance": 0.1}), |
| (1, {"id": "episode-2", "_distance": 0.3}) |
| ]) |
| mock_table.search.return_value.limit.return_value.to_pandas.return_value = mock_result_df |
|
|
| query_vector = [0.1] * 1024 |
|
|
| async def test_search(): |
| results = await handler.similarity_search( |
| table_name="episodes", |
| vector=query_vector, |
| vector_column="vector", |
| top_k=10 |
| ) |
| return results |
|
|
| results = asyncio.run(test_search()) |
|
|
| |
| assert isinstance(results, list) |
| if len(results) > 0: |
| assert "episode_id" in results[0] or "id" in results[0] |
| assert "score" in results[0] |
|
|
| def test_similarity_search_dimension_validation(self, handler_with_mock_db): |
| """Test similarity_search validates vector dimension""" |
| import asyncio |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| wrong_vector = [0.1] * 500 |
|
|
| async def test_search(): |
| with pytest.raises(ValueError, match="Dimension mismatch"): |
| await handler.similarity_search( |
| table_name="episodes", |
| vector=wrong_vector, |
| vector_column="vector", |
| top_k=10 |
| ) |
|
|
| asyncio.run(test_search()) |
|
|
| def test_get_embedding_from_vector_column(self, handler_with_mock_db): |
| """Test get_embedding retrieves vector from specific column""" |
| import asyncio |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| mock_result_df = MagicMock() |
| mock_result_df.empty = False |
| mock_result_df.iloc = MagicMock() |
| mock_result_df.iloc.__getitem__ = MagicMock(return_value={"vector": [0.1] * 1024}) |
| mock_table.search.return_value.limit.return_value.to_pandas.return_value = mock_result_df |
|
|
| async def test_get(): |
| vector = await handler.get_embedding( |
| table_name="episodes", |
| episode_id="episode-1", |
| vector_column="vector" |
| ) |
| return vector |
|
|
| result = asyncio.run(test_get()) |
|
|
| |
| assert result is None or isinstance(result, list) |
|
|
| def test_add_embedding_creates_table_if_not_exists(self, handler_with_mock_db): |
| """Test add_embedding creates table if it doesn't exist""" |
| import asyncio |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| vector_1024 = [0.1] * 1024 |
|
|
| async def test_add(): |
| result = await handler.add_embedding( |
| table_name="new_table", |
| episode_id="episode-1", |
| vector=vector_1024, |
| vector_column="vector" |
| ) |
| return result |
|
|
| result = asyncio.run(test_add()) |
|
|
| |
| assert isinstance(result, bool) |
|
|
|
|
| |
| |
| |
|
|
| class TestAdvancedErrorPaths: |
| """Test error handling for advanced features""" |
|
|
| def test_knowledge_graph_query_failure(self, handler_with_mock_db): |
| """Test knowledge graph query failures return empty results""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| mock_table.search.side_effect = Exception("Query failed") |
|
|
| results = handler.query_knowledge_graph("test query") |
|
|
| |
| assert isinstance(results, list) |
|
|
| def test_add_embedding_table_not_found(self, handler_with_mock_db): |
| """Test add_embedding handles table not found gracefully""" |
| import asyncio |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| mock_db.open_table.return_value = None |
| mock_db.create_table.return_value = None |
|
|
| vector_1024 = [0.1] * 1024 |
|
|
| async def test_add(): |
| result = await handler.add_embedding( |
| table_name="nonexistent_table", |
| episode_id="episode-1", |
| vector=vector_1024, |
| vector_column="vector" |
| ) |
| return result |
|
|
| result = asyncio.run(test_add()) |
|
|
| |
| assert result is False |
|
|
| def test_similarity_search_table_not_found(self, handler_with_mock_db): |
| """Test similarity_search handles table not found gracefully""" |
| import asyncio |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| mock_db.get_table.return_value = None |
|
|
| query_vector = [0.1] * 1024 |
|
|
| async def test_search(): |
| results = await handler.similarity_search( |
| table_name="nonexistent_table", |
| vector=query_vector, |
| vector_column="vector", |
| top_k=10 |
| ) |
| return results |
|
|
| results = asyncio.run(test_search()) |
|
|
| |
| assert isinstance(results, list) |
| assert len(results) == 0 |
|
|
| def test_batch_with_none_documents(self, handler_with_mock_db): |
| """Test batch handles None documents gracefully""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| count = handler.add_documents_batch("documents", None) |
|
|
| |
| assert isinstance(count, int) |
|
|
| def test_s3_connection_without_credentials(self, monkeypatch, clear_handlers): |
| """Test S3 connection without credentials fails gracefully""" |
| |
| monkeypatch.delenv("R2_ACCESS_KEY_ID", raising=False) |
| monkeypatch.delenv("R2_SECRET_ACCESS_KEY", raising=False) |
| monkeypatch.delenv("AWS_ACCESS_KEY_ID", raising=False) |
| monkeypatch.delenv("AWS_SECRET_ACCESS_KEY", raising=False) |
|
|
| with patch('lancedb.connect') as mock_connect: |
| mock_db = MagicMock() |
| mock_db.table_names.return_value = [] |
| mock_connect.return_value = mock_db |
|
|
| handler = LanceDBHandler( |
| db_path="s3://bucket/path", |
| workspace_id="test" |
| ) |
| handler._ensure_db() |
|
|
| |
| assert mock_connect.called or handler.db is None |
|
|
| def test_get_embedding_not_found(self, handler_with_mock_db): |
| """Test get_embedding returns None when episode not found""" |
| import asyncio |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| mock_result_df = MagicMock() |
| mock_result_df.empty = True |
| mock_table.search.return_value.limit.return_value.to_pandas.return_value = mock_result_df |
|
|
| async def test_get(): |
| vector = await handler.get_embedding( |
| table_name="episodes", |
| episode_id="nonexistent_episode", |
| vector_column="vector" |
| ) |
| return vector |
|
|
| result = asyncio.run(test_get()) |
|
|
| |
| assert result is None |
|
|
| def test_dual_vector_column_validation(self, handler_with_mock_db): |
| """Test handler validates dual vector columns configuration""" |
| handler, mock_db, mock_table = handler_with_mock_db |
|
|
| |
| assert hasattr(handler, 'vector_columns') |
| assert 'vector' in handler.vector_columns |
| assert 'vector_fastembed' in handler.vector_columns |
|
|
| |
| assert handler.vector_columns['vector'] == 1024 |
| assert handler.vector_columns['vector_fastembed'] == 384 |
|
|