Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,352 @@
from typing import Any
from unittest.mock import MagicMock, Mock, patch

import pytest
from lfx.components.pinecone import PineconeVectorStoreComponent
from lfx.schema.data import Data

from tests.base import ComponentTestBaseWithoutClient, VersionComponentMapping


@pytest.mark.api_key_required
class TestPineconeVectorStoreComponent(ComponentTestBaseWithoutClient):
@pytest.fixture
def component_class(self) -> type[Any]:
"""Return the component class to test."""
return PineconeVectorStoreComponent

@pytest.fixture
def default_kwargs(self) -> dict[str, Any]:
"""Return the default kwargs for the component."""
from lfx.components.openai.openai import OpenAIEmbeddingsComponent

from tests.api_keys import get_openai_api_key

try:
api_key = get_openai_api_key()
except ValueError:
pytest.skip("OPENAI_API_KEY is not set")

return {
"embedding": OpenAIEmbeddingsComponent(openai_api_key=api_key).build_embeddings(),
"index_name": "test-index",
"namespace": "test-namespace",
"pinecone_api_key": "test-pinecone-key",
"text_key": "text",
}

@pytest.fixture
def file_names_mapping(self) -> list[VersionComponentMapping]:
"""Return the file names mapping for different versions."""
return []

def test_search_documents_with_namespace(
self, component_class: type[PineconeVectorStoreComponent], default_kwargs: dict[str, Any]
) -> None:
"""Test that search_documents properly passes namespace parameter.

This test verifies the fix for issue #10512 where namespace wasn't being
properly passed to Pinecone queries, resulting in zero results.
"""
component: PineconeVectorStoreComponent = component_class().set(**default_kwargs)

# Mock the Pinecone client and index
mock_pinecone = MagicMock()
mock_index = MagicMock()
mock_pinecone.Index.return_value = mock_index

# Mock query results
mock_match = Mock()
mock_match.metadata = {"text": "test result", "source": "test"}
mock_results = Mock()
mock_results.matches = [mock_match]
mock_index.query.return_value = mock_results

with (
patch("lfx.components.pinecone.pinecone.Pinecone", return_value=mock_pinecone),
patch("lfx.components.pinecone.pinecone.Float32Embeddings") as mock_float32,
):
mock_embeddings_instance = MagicMock()
mock_embeddings_instance.embed_query.return_value = [0.1] * 3072
mock_float32.return_value = mock_embeddings_instance

component.set(search_query="test query")
results = component.search_documents()

# Verify Pinecone was called correctly
mock_pinecone.Index.assert_called_once_with("test-index")
mock_index.query.assert_called_once()

# Verify namespace was passed
call_kwargs = mock_index.query.call_args[1]
assert "namespace" in call_kwargs
assert call_kwargs["namespace"] == "test-namespace"
assert call_kwargs["top_k"] == 4
assert call_kwargs["include_metadata"] is True

# Verify results are returned
assert len(results) == 1
assert isinstance(results[0], Data)
assert results[0].text == "test result"

def test_search_documents_without_namespace(
self, component_class: type[PineconeVectorStoreComponent], default_kwargs: dict[str, Any]
) -> None:
"""Test that search_documents works without namespace."""
# Remove namespace from kwargs
default_kwargs.pop("namespace", None)
component: PineconeVectorStoreComponent = component_class().set(**default_kwargs)

# Mock the Pinecone client and index
mock_pinecone = MagicMock()
mock_index = MagicMock()
mock_pinecone.Index.return_value = mock_index

# Mock query results
mock_match = Mock()
mock_match.metadata = {"text": "test result"}
mock_results = Mock()
mock_results.matches = [mock_match]
mock_index.query.return_value = mock_results

with (
patch("lfx.components.pinecone.pinecone.Pinecone", return_value=mock_pinecone),
patch("lfx.components.pinecone.pinecone.Float32Embeddings") as mock_float32,
):
mock_embeddings_instance = MagicMock()
mock_embeddings_instance.embed_query.return_value = [0.1] * 3072
mock_float32.return_value = mock_embeddings_instance

component.set(search_query="test query")
results = component.search_documents()

# Verify namespace was NOT passed when not set
call_kwargs = mock_index.query.call_args[1]
assert "namespace" not in call_kwargs

# Verify results are still returned
assert len(results) == 1

def test_search_documents_empty_query(
self, component_class: type[PineconeVectorStoreComponent], default_kwargs: dict[str, Any]
) -> None:
"""Test that empty query returns empty results."""
component: PineconeVectorStoreComponent = component_class().set(**default_kwargs)
component.set(search_query="")
results = component.search_documents()
assert results == []

def test_search_documents_with_custom_text_key(
self, component_class: type[PineconeVectorStoreComponent], default_kwargs: dict[str, Any]
) -> None:
"""Test that custom text_key is properly used to extract content."""
default_kwargs["text_key"] = "chunk_text"
component: PineconeVectorStoreComponent = component_class().set(**default_kwargs)

# Mock the Pinecone client and index
mock_pinecone = MagicMock()
mock_index = MagicMock()
mock_pinecone.Index.return_value = mock_index

# Mock query results with custom text key
mock_match = Mock()
mock_match.metadata = {"chunk_text": "custom text content", "source": "test"}
mock_results = Mock()
mock_results.matches = [mock_match]
mock_index.query.return_value = mock_results

# Mock Float32Embeddings
with (
patch("lfx.components.pinecone.pinecone.Pinecone", return_value=mock_pinecone),
patch("lfx.components.pinecone.pinecone.Float32Embeddings") as mock_float32,
):
mock_embeddings_instance = MagicMock()
mock_embeddings_instance.embed_query.return_value = [0.1] * 3072
mock_float32.return_value = mock_embeddings_instance

component.set(search_query="test query")
results = component.search_documents()

# Verify the custom text_key was used
assert len(results) == 1
assert results[0].text == "custom text content"

def test_search_documents_pinecone_api_error(
self, component_class: type[PineconeVectorStoreComponent], default_kwargs: dict[str, Any]
) -> None:
"""Test that Pinecone API errors are properly handled."""
component: PineconeVectorStoreComponent = component_class().set(**default_kwargs)

# Mock the Pinecone client to raise an error
mock_pinecone = MagicMock()
mock_index = MagicMock()
mock_index.query.side_effect = Exception("Pinecone API error")
mock_pinecone.Index.return_value = mock_index

with (
patch("lfx.components.pinecone.pinecone.Pinecone", return_value=mock_pinecone),
patch("lfx.components.pinecone.pinecone.Float32Embeddings") as mock_float32,
):
mock_embeddings_instance = MagicMock()
mock_embeddings_instance.embed_query.return_value = [0.1] * 3072
mock_float32.return_value = mock_embeddings_instance

component.set(search_query="test query")

# Verify error is raised
with pytest.raises(ValueError, match="Error searching documents"):
component.search_documents()

def test_search_documents_embedding_error(
self, component_class: type[PineconeVectorStoreComponent], default_kwargs: dict[str, Any]
) -> None:
"""Test that embedding errors are properly handled."""
component: PineconeVectorStoreComponent = component_class().set(**default_kwargs)

# Mock Float32Embeddings to raise an error
with patch("lfx.components.pinecone.pinecone.Float32Embeddings") as mock_float32:
mock_embeddings_instance = MagicMock()
mock_embeddings_instance.embed_query.side_effect = Exception("Embedding error")
mock_float32.return_value = mock_embeddings_instance

component.set(search_query="test query")

# Verify error is raised
with pytest.raises(ValueError, match="Error searching documents"):
component.search_documents()

def test_float32_embeddings_wrapper_usage(
self, component_class: type[PineconeVectorStoreComponent], default_kwargs: dict[str, Any]
) -> None:
"""Test that Float32Embeddings wrapper is correctly instantiated and used."""
component: PineconeVectorStoreComponent = component_class().set(**default_kwargs)

# Mock the Pinecone client and index
mock_pinecone = MagicMock()
mock_index = MagicMock()
mock_pinecone.Index.return_value = mock_index

# Mock query results
mock_match = Mock()
mock_match.metadata = {"text": "test result"}
mock_results = Mock()
mock_results.matches = [mock_match]
mock_index.query.return_value = mock_results

with (
patch("lfx.components.pinecone.pinecone.Pinecone", return_value=mock_pinecone),
patch("lfx.components.pinecone.pinecone.Float32Embeddings") as mock_float32,
):
mock_embeddings_instance = MagicMock()
mock_embeddings_instance.embed_query.return_value = [0.1] * 3072
mock_float32.return_value = mock_embeddings_instance

component.set(search_query="test query")
component.search_documents()

# Verify Float32Embeddings was instantiated with the correct embeddings
mock_float32.assert_called_once_with(default_kwargs["embedding"])

# Verify embed_query was called with the search query
mock_embeddings_instance.embed_query.assert_called_once_with("test query")

def test_pinecone_client_initialization(
self, component_class: type[PineconeVectorStoreComponent], default_kwargs: dict[str, Any]
) -> None:
"""Test that Pinecone client is initialized with correct parameters."""
component: PineconeVectorStoreComponent = component_class().set(**default_kwargs)

# Mock the Pinecone client and index
mock_pinecone = MagicMock()
mock_index = MagicMock()
mock_pinecone.Index.return_value = mock_index

# Mock query results
mock_match = Mock()
mock_match.metadata = {"text": "test result"}
mock_results = Mock()
mock_results.matches = [mock_match]
mock_index.query.return_value = mock_results

with (
patch("lfx.components.pinecone.pinecone.Pinecone", return_value=mock_pinecone) as mock_pc_class,
patch("lfx.components.pinecone.pinecone.Float32Embeddings") as mock_float32,
):
mock_embeddings_instance = MagicMock()
mock_embeddings_instance.embed_query.return_value = [0.1] * 3072
mock_float32.return_value = mock_embeddings_instance

component.set(search_query="test query")
component.search_documents()

# Verify Pinecone was initialized with correct API key
mock_pc_class.assert_called_once_with(api_key="test-pinecone-key")

# Verify Index was called with correct index name
mock_pinecone.Index.assert_called_once_with("test-index")

def test_search_documents_with_missing_text_key_in_metadata(
self, component_class: type[PineconeVectorStoreComponent], default_kwargs: dict[str, Any]
) -> None:
"""Test that documents with missing text_key in metadata are handled gracefully."""
component: PineconeVectorStoreComponent = component_class().set(**default_kwargs)

# Mock the Pinecone client and index
mock_pinecone = MagicMock()
mock_index = MagicMock()
mock_pinecone.Index.return_value = mock_index

# Mock query results with metadata missing the text key
mock_match = Mock()
mock_match.metadata = {"source": "test"} # Missing "text" key
mock_results = Mock()
mock_results.matches = [mock_match]
mock_index.query.return_value = mock_results

with (
patch("lfx.components.pinecone.pinecone.Pinecone", return_value=mock_pinecone),
patch("lfx.components.pinecone.pinecone.Float32Embeddings") as mock_float32,
):
mock_embeddings_instance = MagicMock()
mock_embeddings_instance.embed_query.return_value = [0.1] * 3072
mock_float32.return_value = mock_embeddings_instance

component.set(search_query="test query")
results = component.search_documents()

# Verify results are returned with empty text
assert len(results) == 1
assert results[0].text == ""

def test_search_documents_with_none_metadata(
self, component_class: type[PineconeVectorStoreComponent], default_kwargs: dict[str, Any]
) -> None:
"""Test that documents with None metadata are handled gracefully."""
component: PineconeVectorStoreComponent = component_class().set(**default_kwargs)

# Mock the Pinecone client and index
mock_pinecone = MagicMock()
mock_index = MagicMock()
mock_pinecone.Index.return_value = mock_index

# Mock query results with None metadata
mock_match = Mock()
mock_match.metadata = None
mock_results = Mock()
mock_results.matches = [mock_match]
mock_index.query.return_value = mock_results

with (
patch("lfx.components.pinecone.pinecone.Pinecone", return_value=mock_pinecone),
patch("lfx.components.pinecone.pinecone.Float32Embeddings") as mock_float32,
):
mock_embeddings_instance = MagicMock()
mock_embeddings_instance.embed_query.return_value = [0.1] * 3072
mock_float32.return_value = mock_embeddings_instance

component.set(search_query="test query")
results = component.search_documents()

# Verify results are returned with empty text and metadata
assert len(results) == 1
Comment on lines +43 to +351
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Pinecone mocking target doesn’t match implementation import; tests may call the real client

In search_documents the implementation does a local import:

from pinecone import Pinecone
pc = Pinecone(api_key=self.pinecone_api_key)

This binds Pinecone as a local name from the external pinecone module. The tests, however, patch lfx.components.pinecone.pinecone.Pinecone, which is a different symbol and won’t intercept that local import. As a result, these tests can end up using the real Pinecone client (or failing with ImportError) instead of the mocked one.

To ensure the tests actually mock what’s used at runtime, you have two main options:

  • Option A (change tests, keep implementation as-is – simplest):

Update all contexts like:

patch("lfx.components.pinecone.pinecone.Pinecone", return_value=mock_pinecone)

to patch the external module instead:

-            patch("lfx.components.pinecone.pinecone.Pinecone", return_value=mock_pinecone),
+            patch("pinecone.Pinecone", return_value=mock_pinecone),

This way from pinecone import Pinecone pulls in the mocked class.

  • Option B (change implementation to use module-level import), then patch the component module’s symbol.

If you prefer to keep patching lfx.components.pinecone.pinecone.Pinecone, move the import to module scope with a guarded pattern and have search_documents reference the global Pinecone. That’s a broader behavioral change (and may affect optional-dependency semantics), so Option A is safer for this PR.

Also note that test_search_documents_embedding_error doesn’t patch Pinecone at all; if pinecone isn’t installed, the resulting ValueError("Error searching documents") would be due to an import error, not the embedding error. The assertion still passes, but the failure mode is different from what the test name suggests.


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Find PineconeVectorStoreComponent implementation file
find . -name "*.py" -type f | xargs grep -l "class PineconeVectorStoreComponent" | grep -v test | head -5

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Inspect the search_documents method in PineconeVectorStoreComponent
ast-grep --pattern 'def search_documents($$$) {
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check import statements at module level and inside search_documents
rg -n "from pinecone import|import pinecone" --type=py -g "!test*" | head -20

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Check for EOF newline in the test file (W292 - no newline at EOF)
tail -c 1 "src/backend/tests/unit/components/vectorstores/test_pinecone_vector_store_component.py" | od -A x -t x1z

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Count lines to confirm file structure
wc -l "src/backend/tests/unit/components/vectorstores/test_pinecone_vector_store_component.py"

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Show last 10 lines of test file to check EOF
tail -10 "src/backend/tests/unit/components/vectorstores/test_pinecone_vector_store_component.py"

Length of output: 447


🏁 Script executed:

#!/bin/bash
# Find all patch targets in the test file to understand mocking strategy
rg -n 'patch\("' "src/backend/tests/unit/components/vectorstores/test_pinecone_vector_store_component.py"

Length of output: 1620


🏁 Script executed:

#!/bin/bash
# Get the actual implementation of search_documents
ast-grep --pattern 'def search_documents($_) {
  $$$
}' | head -100

Length of output: 0


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# View the implementation file header and imports
head -20 "src/lfx/src/lfx/components/pinecone/pinecone.py"

Length of output: 817


🏁 Script executed:

#!/bin/bash
# View context around line 100 to see search_documents and import
sed -n '90,120p' "src/lfx/src/lfx/components/pinecone/pinecone.py"

Length of output: 1125


🏁 Script executed:

#!/bin/bash
# View the search_documents method more completely
sed -n '90,150p' "src/lfx/src/lfx/components/pinecone/pinecone.py"

Length of output: 2154


🏁 Script executed:

#!/bin/bash
# Check if there's a module-level Pinecone import
grep -n "^from pinecone\|^import pinecone" "src/lfx/src/lfx/components/pinecone/pinecone.py"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Show last line of test file with hex dump alternative to confirm no newline
tail -1 "src/backend/tests/unit/components/vectorstores/test_pinecone_vector_store_component.py" | wc -c

Length of output: 2


🏁 Script executed:

#!/bin/bash
# Check full context: what's the actual module path structure?
find . -path "*/lfx/components/pinecone/pinecone.py" -o -path "*/lfx/components/pinecone/__init__.py" | head -5

Length of output: 99


Pinecone mocking target doesn't match implementation import; tests may call the real client

In search_documents() (line 100), the implementation does a local import:

from pinecone import Pinecone
pc = Pinecone(api_key=self.pinecone_api_key)

This creates a local binding to the external pinecone module. The tests, however, patch lfx.components.pinecone.pinecone.Pinecone, which doesn't exist at module scope (Pinecone is never imported there). As a result, these tests won't intercept the local import and can end up using the real Pinecone client instead of the mocked one.

To fix, update all patch calls from:

patch("lfx.components.pinecone.pinecone.Pinecone", return_value=mock_pinecone)

to:

patch("pinecone.Pinecone", return_value=mock_pinecone)

This ensures from pinecone import Pinecone pulls in the mocked class.

Also, the test file is missing a trailing newline at EOF (W292).

🤖 Prompt for AI Agents
src/backend/tests/unit/components/vectorstores/test_pinecone_vector_store_component.py
lines 43-351: the tests patch "lfx.components.pinecone.pinecone.Pinecone" but
the implementation does a local import "from pinecone import Pinecone", so
update every patch(...) call that targets Pinecone to patch "pinecone.Pinecone"
(e.g. patch("pinecone.Pinecone", return_value=mock_pinecone)) so the local
import is mocked, and add a trailing newline at EOF to fix W292.

assert results[0].text == ""
2 changes: 1 addition & 1 deletion src/lfx/src/lfx/_assets/component_index.json

Large diffs are not rendered by default.

Loading
Loading