Files
wehub-resource-sync c56bef871b
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
Docker image release / Build base image (push) Has been cancelled
Sync docs with Docusaurus / sync (push) Has been cancelled
Tests / Check if changed (push) Has been cancelled
Tests / format (push) Has been cancelled
Tests / check-imports (push) Has been cancelled
Tests / Unit / macos-latest (push) Has been cancelled
Tests / Unit / ubuntu-latest (push) Has been cancelled
Tests / Unit / windows-latest (push) Has been cancelled
Tests / mypy (push) Has been cancelled
Tests / Integration / ubuntu-latest (push) Has been cancelled
Tests / Integration / macos-latest (push) Has been cancelled
Tests / Integration / windows-latest (push) Has been cancelled
Tests / notify-slack-on-failure (push) Has been cancelled
Tests / Mark tests as completed (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:28 +08:00

662 lines
29 KiB
Python

# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import os
from typing import Any
from unittest.mock import ANY, AsyncMock, Mock
import pytest
from haystack import Document, component
from haystack.components.embedders import OpenAIDocumentEmbedder, OpenAITextEmbedder
from haystack.components.retrievers import (
InMemoryBM25Retriever,
InMemoryEmbeddingRetriever,
MultiRetriever,
TextEmbeddingRetriever,
)
from haystack.components.writers import DocumentWriter
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.document_stores.types import DuplicatePolicy
from haystack.utils.experimental import ExperimentalWarning
pytestmark = pytest.mark.filterwarnings("ignore::haystack.utils.experimental.ExperimentalWarning")
@component
class MockRetriever:
def __init__(self, documents: list[Document] | None = None):
self.documents = documents or []
@component.output_types(documents=list[Document])
def run(self, query: str, filters: dict[str, Any] | None = None, top_k: int | None = None):
return {"documents": self.documents}
@component
class FailingRetriever:
@component.output_types(documents=list[Document])
def run(self, query: str, filters: dict[str, Any] | None = None, top_k: int | None = None):
raise RuntimeError("connection error")
@pytest.fixture
def sample_documents():
return [
Document(
content="Renewable energy is energy that is collected from renewable resources.",
meta={"category": "renewable"},
id="doc1",
),
Document(
content="Solar energy is a type of green energy that is harnessed from the sun.",
meta={"category": "solar"},
id="doc2",
),
Document(
content="Wind energy is another type of green energy that is generated by wind turbines.",
meta={"category": "wind"},
id="doc3",
),
Document(
content="Geothermal energy is heat that comes from the sub-surface of the earth.",
meta={"category": "geothermal"},
id="doc4",
),
Document(
content="Fossil fuels, such as coal, oil, and natural gas, are non-renewable energy sources.",
meta={"category": "fossil"},
id="doc5",
),
]
@pytest.fixture
def document_store_with_embeddings(sample_documents):
"""Create a document store populated with embedded documents."""
document_store = InMemoryDocumentStore()
doc_embedder = OpenAIDocumentEmbedder()
doc_writer = DocumentWriter(document_store=document_store, policy=DuplicatePolicy.SKIP)
embedded_docs = doc_embedder.run(sample_documents)["documents"]
doc_writer.run(documents=embedded_docs)
return document_store
@pytest.fixture
def bm25_retriever(document_store_with_embeddings):
return InMemoryBM25Retriever(document_store=document_store_with_embeddings)
@pytest.fixture
def embedding_retriever(document_store_with_embeddings):
return TextEmbeddingRetriever(
retriever=InMemoryEmbeddingRetriever(document_store=document_store_with_embeddings),
text_embedder=OpenAITextEmbedder(),
)
class TestMultiRetriever:
def test_init_default_parameters(self):
retrievers = {"mock": MockRetriever()}
retriever = MultiRetriever(retrievers=retrievers)
assert retriever.retrievers == retrievers
assert retriever.filters is None
assert retriever.top_k_per_retriever is None
assert retriever.top_k is None
assert retriever.max_workers == 4
assert retriever.join_mode == "reciprocal_rank_fusion"
def test_init_custom_parameters(self):
retrievers = {"mock": MockRetriever()}
retriever = MultiRetriever(
retrievers=retrievers, filters={"field": "meta.category"}, top_k=5, max_workers=2, join_mode="concatenate"
)
assert retriever.retrievers == retrievers
assert retriever.filters == {"field": "meta.category"}
assert retriever.top_k == 5
assert retriever.max_workers == 2
assert retriever.join_mode == "concatenate"
def test_run_rrf_assigns_scores_and_sorts(self, sample_documents):
docs_a = [sample_documents[0], sample_documents[1], sample_documents[2]]
docs_b = [sample_documents[2], sample_documents[0], sample_documents[3]]
retriever = MultiRetriever(
retrievers={"a": MockRetriever(docs_a), "b": MockRetriever(docs_b)}, join_mode="reciprocal_rank_fusion"
)
result = retriever.run(query="energy")
assert all(doc.score is not None for doc in result["documents"])
scores = [doc.score for doc in result["documents"]]
assert scores == sorted(scores, reverse=True)
# doc1 ranked 1st in a and 2nd in b, doc3 ranked 3rd in a and 1st in b — doc1 should beat doc3
ids = [doc.id for doc in result["documents"]]
assert ids.index("doc1") < ids.index("doc3")
def test_run_with_empty_document_store(self):
retriever = MultiRetriever(retrievers={"mock": MockRetriever()})
result = retriever.run(query="green energy")
assert "documents" in result
assert result["documents"] == []
def test_run_combines_results_from_multiple_retrievers(self, sample_documents):
retriever = MultiRetriever(
retrievers={
"a": MockRetriever(documents=[sample_documents[0]]),
"b": MockRetriever(documents=[sample_documents[1]]),
},
max_workers=2,
)
result = retriever.run(query="energy")
assert len(result["documents"]) == 2
assert {doc.id for doc in result["documents"]} == {"doc1", "doc2"}
def test_run_deduplicates_results(self, sample_documents):
retriever = MultiRetriever(
retrievers={
"c": MockRetriever(documents=[sample_documents[0], sample_documents[1]]),
"d": MockRetriever(documents=[sample_documents[0]]),
},
max_workers=2,
)
result = retriever.run(query="energy")
assert len(result["documents"]) == 2
ids = [doc.id for doc in result["documents"]]
assert ids.count("doc1") == 1
def test_run_resolves_filters_and_top_k_per_retriever(self):
received: dict = {}
@component
class CapturingRetriever:
@component.output_types(documents=list[Document])
def run(self, query: str, filters: dict[str, Any] | None = None, top_k: int | None = None):
received["filters"] = filters
received["top_k"] = top_k
return {"documents": []}
retriever = MultiRetriever(
retrievers={"capturing": CapturingRetriever()}, filters={"field": "meta.category"}, top_k_per_retriever=5
)
# Should use init-time values when not overridden (top_k_per_retriever is forwarded as the retriever's top_k)
retriever.run(query="energy")
assert received["filters"] == {"field": "meta.category"}
assert received["top_k"] == 5
# Should prefer run-time values when provided
retriever.run(query="energy", filters={"field": "meta.other"}, top_k_per_retriever=2)
assert received["filters"] == {"field": "meta.other"}
assert received["top_k"] == 2
def test_run_forwards_top_k_per_retriever_not_overall_top_k(self):
received: dict = {}
@component
class CapturingRetriever:
@component.output_types(documents=list[Document])
def run(self, query: str, filters: dict[str, Any] | None = None, top_k: int | None = None):
received["top_k"] = top_k
return {"documents": []}
retriever = MultiRetriever(retrievers={"capturing": CapturingRetriever()})
# top_k_per_retriever is forwarded to each retriever as its top_k
retriever.run(query="energy", top_k_per_retriever=3)
assert received["top_k"] == 3
# the overall top_k is applied at merge-time only, not forwarded to retrievers
received.clear()
retriever.run(query="energy", top_k=5)
assert received.get("top_k") is None
def test_run_top_k_truncates_merged_results(self, sample_documents):
retriever = MultiRetriever(
retrievers={
"a": MockRetriever(documents=sample_documents[:3]),
"b": MockRetriever(documents=sample_documents[2:5]),
},
max_workers=2,
)
result = retriever.run(query="energy", top_k=2)
assert len(result["documents"]) == 2
scores = [doc.score for doc in result["documents"]]
assert all(score is not None for score in scores)
assert scores == sorted(scores, reverse=True)
def test_run_top_k_forces_rrf_in_concatenate_mode(self, sample_documents):
# In concatenate mode there is no global ranking, so setting top_k falls back to RRF to truncate consistently
retriever = MultiRetriever(
retrievers={
"a": MockRetriever(documents=sample_documents[:3]),
"b": MockRetriever(documents=sample_documents[1:4]),
},
join_mode="concatenate",
max_workers=2,
)
result = retriever.run(query="energy", top_k=2)
assert len(result["documents"]) == 2
assert all(doc.score is not None for doc in result["documents"])
def test_run_with_active_retrievers(self, sample_documents):
retriever = MultiRetriever(
retrievers={"a": MockRetriever([sample_documents[0]]), "b": MockRetriever([sample_documents[1]])}
)
# Only run retriever "a"
result = retriever.run(query="energy", active_retrievers=["a"])
assert len(result["documents"]) == 1
assert result["documents"][0].id == "doc1"
def test_run_with_unknown_active_retriever_raises(self):
retriever = MultiRetriever(retrievers={"mock": MockRetriever()})
with pytest.raises(ValueError, match="Unknown retriever name"):
retriever.run(query="energy", active_retrievers=["nonexistent"])
def test_run_retriever_failure_raises_with_name(self):
retriever = MultiRetriever(retrievers={"failing": FailingRetriever()})
with pytest.raises(RuntimeError, match="Retriever 'failing' failed"):
retriever.run(query="energy")
def test_to_dict(self):
retriever = MultiRetriever(
retrievers={"bm25": InMemoryBM25Retriever(document_store=InMemoryDocumentStore())},
filters=None,
top_k_per_retriever=3,
top_k=5,
max_workers=2,
)
result = retriever.to_dict()
assert result == {
"type": "haystack.components.retrievers.multi_retriever.MultiRetriever",
"init_parameters": {
"retrievers": {
"bm25": {
"type": "haystack.components.retrievers.in_memory.bm25_retriever.InMemoryBM25Retriever",
"init_parameters": {
"document_store": {
"type": "haystack.document_stores.in_memory.document_store.InMemoryDocumentStore",
"init_parameters": {
"bm25_tokenization_regex": "(?u)\\b\\w+\\b",
"bm25_algorithm": "BM25L",
"bm25_parameters": {},
"embedding_similarity_function": "dot_product",
"index": ANY,
"shared": True,
"return_embedding": True,
},
},
"filters": None,
"top_k": 10,
"scale_score": False,
"filter_policy": "replace",
},
}
},
"filters": None,
"top_k_per_retriever": 3,
"top_k": 5,
"max_workers": 2,
"join_mode": "reciprocal_rank_fusion",
},
}
def test_from_dict(self):
data = {
"type": "haystack.components.retrievers.multi_retriever.MultiRetriever",
"init_parameters": {
"retrievers": {
"bm25": {
"type": "haystack.components.retrievers.in_memory.bm25_retriever.InMemoryBM25Retriever",
"init_parameters": {
"document_store": {
"type": "haystack.document_stores.in_memory.document_store.InMemoryDocumentStore",
"init_parameters": {
"bm25_tokenization_regex": "(?u)\\b\\w\\w+\\b",
"bm25_algorithm": "BM25L",
"bm25_parameters": {},
"embedding_similarity_function": "dot_product",
"index": "4bb5369d-779f-487b-9c16-3c40f503438b",
"shared": True,
"return_embedding": True,
},
},
"filters": None,
"top_k": 10,
"scale_score": False,
"filter_policy": "replace",
},
}
},
"filters": None,
"top_k_per_retriever": 3,
"top_k": 5,
"max_workers": 2,
"join_mode": "concatenate",
},
}
result = MultiRetriever.from_dict(data)
assert isinstance(result, MultiRetriever)
assert len(result.retrievers) == 1
assert "bm25" in result.retrievers
assert isinstance(result.retrievers["bm25"], InMemoryBM25Retriever)
assert result.top_k_per_retriever == 3
assert result.top_k == 5
assert result.max_workers == 2
assert result.join_mode == "concatenate"
def test_from_dict_with_no_retrievers(self):
data = {
"type": "haystack.components.retrievers.multi_retriever.MultiRetriever",
"init_parameters": {"retrievers": {}, "filters": None, "top_k": 10, "max_workers": 4},
}
result = MultiRetriever.from_dict(data)
assert isinstance(result, MultiRetriever)
assert result.retrievers == {}
def test_from_dict_with_unknown_retriever_type_raises(self):
data = {
"type": "haystack.components.retrievers.multi_retriever.MultiRetriever",
"init_parameters": {
"retrievers": {
"bad": {"type": "haystack.components.retrievers.NonExistentRetriever", "init_parameters": {}}
},
"filters": None,
"top_k": 10,
"max_workers": 4,
},
}
with pytest.raises(ImportError, match="Could not import class"):
MultiRetriever.from_dict(data)
@pytest.mark.skipif(os.environ.get("OPENAI_API_KEY", "") == "", reason="OPENAI_API_KEY is not set")
@pytest.mark.integration
def test_run_with_filters(self, bm25_retriever, embedding_retriever):
retriever = MultiRetriever(retrievers={"bm25": bm25_retriever, "embedding": embedding_retriever})
result = retriever.run(query="energy", filters={"field": "meta.category", "operator": "==", "value": "solar"})
assert len(result["documents"]) == 1
assert result["documents"][0].meta["category"] == "solar"
@pytest.mark.skipif(os.environ.get("OPENAI_API_KEY", "") == "", reason="OPENAI_API_KEY is not set")
@pytest.mark.integration
def test_run_with_top_k(self, bm25_retriever, embedding_retriever):
retriever = MultiRetriever(retrievers={"bm25": bm25_retriever, "embedding": embedding_retriever})
result = retriever.run(query="energy", top_k=2)
assert len(result["documents"]) == 2
@pytest.mark.skipif(os.environ.get("OPENAI_API_KEY", "") == "", reason="OPENAI_API_KEY is not set")
@pytest.mark.integration
def test_run_with_active_retrievers_integration(self, bm25_retriever, embedding_retriever):
retriever = MultiRetriever(retrievers={"bm25": bm25_retriever, "embedding": embedding_retriever})
result_bm25_active = retriever.run(query="energy", active_retrievers=["bm25"])
result_bm25 = bm25_retriever.run(query="energy")
# Scores differ because MultiRetriever applies join_mode processing (e.g. RRF) even for a single retriever.
assert [doc.id for doc in result_bm25_active["documents"]] == [doc.id for doc in result_bm25["documents"]]
class TestMultiRetrieverAsync:
@pytest.mark.asyncio
async def test_run_async_with_empty_results(self):
retriever = MultiRetriever(retrievers={"mock": MockRetriever()})
result = await retriever.run_async(query="green energy")
assert "documents" in result
assert result["documents"] == []
@pytest.mark.asyncio
async def test_run_async_combines_results_from_multiple_retrievers(self, sample_documents):
retriever = MultiRetriever(
retrievers={
"a": MockRetriever(documents=[sample_documents[0]]),
"b": MockRetriever(documents=[sample_documents[1]]),
}
)
result = await retriever.run_async(query="energy")
assert len(result["documents"]) == 2
assert {doc.id for doc in result["documents"]} == {"doc1", "doc2"}
@pytest.mark.asyncio
async def test_run_async_deduplicates_results(self, sample_documents):
retriever = MultiRetriever(
retrievers={
"c": MockRetriever(documents=[sample_documents[0], sample_documents[1]]),
"d": MockRetriever(documents=[sample_documents[0]]),
}
)
result = await retriever.run_async(query="energy")
assert len(result["documents"]) == 2
assert [doc.id for doc in result["documents"]].count("doc1") == 1
@pytest.mark.asyncio
async def test_run_async_rrf_assigns_scores_and_sorts(self, sample_documents):
docs_a = [sample_documents[0], sample_documents[1], sample_documents[2]]
docs_b = [sample_documents[2], sample_documents[0], sample_documents[3]]
retriever = MultiRetriever(
retrievers={"a": MockRetriever(docs_a), "b": MockRetriever(docs_b)}, join_mode="reciprocal_rank_fusion"
)
result = await retriever.run_async(query="energy")
assert all(doc.score is not None for doc in result["documents"])
scores = [doc.score for doc in result["documents"]]
assert scores == sorted(scores, reverse=True)
ids = [doc.id for doc in result["documents"]]
assert ids.index("doc1") < ids.index("doc3")
@pytest.mark.asyncio
async def test_run_async_resolves_filters_and_top_k_per_retriever(self):
received: dict = {}
@component
class CapturingRetriever:
@component.output_types(documents=list[Document])
def run(self, query: str, filters: dict[str, Any] | None = None, top_k: int | None = None):
received["filters"] = filters
received["top_k"] = top_k
return {"documents": []}
retriever = MultiRetriever(
retrievers={"capturing": CapturingRetriever()}, filters={"field": "meta.category"}, top_k_per_retriever=5
)
# top_k_per_retriever is forwarded as the retriever's top_k
await retriever.run_async(query="energy")
assert received["filters"] == {"field": "meta.category"}
assert received["top_k"] == 5
await retriever.run_async(query="energy", filters={"field": "meta.other"}, top_k_per_retriever=2)
assert received["filters"] == {"field": "meta.other"}
assert received["top_k"] == 2
@pytest.mark.asyncio
async def test_run_async_forwards_top_k_per_retriever_not_overall_top_k(self):
received: dict = {}
@component
class CapturingRetriever:
@component.output_types(documents=list[Document])
def run(self, query: str, filters: dict[str, Any] | None = None, top_k: int | None = None):
received["top_k"] = top_k
return {"documents": []}
retriever = MultiRetriever(retrievers={"capturing": CapturingRetriever()})
# top_k_per_retriever is forwarded to each retriever as its top_k
await retriever.run_async(query="energy", top_k_per_retriever=3)
assert received["top_k"] == 3
# the overall top_k is applied at merge-time only, not forwarded to retrievers
received.clear()
await retriever.run_async(query="energy", top_k=5)
assert received.get("top_k") is None
@pytest.mark.asyncio
async def test_run_async_top_k_truncates_merged_results(self, sample_documents):
retriever = MultiRetriever(
retrievers={
"a": MockRetriever(documents=sample_documents[:3]),
"b": MockRetriever(documents=sample_documents[2:5]),
}
)
result = await retriever.run_async(query="energy", top_k=2)
assert len(result["documents"]) == 2
scores = [doc.score for doc in result["documents"]]
assert all(score is not None for score in scores)
assert scores == sorted(scores, reverse=True)
@pytest.mark.asyncio
async def test_run_async_top_k_forces_rrf_in_concatenate_mode(self, sample_documents):
# In concatenate mode there is no global ranking, so setting top_k falls back to RRF to truncate consistently
retriever = MultiRetriever(
retrievers={
"a": MockRetriever(documents=sample_documents[:3]),
"b": MockRetriever(documents=sample_documents[1:4]),
},
join_mode="concatenate",
)
result = await retriever.run_async(query="energy", top_k=2)
assert len(result["documents"]) == 2
assert all(doc.score is not None for doc in result["documents"])
@pytest.mark.asyncio
async def test_run_async_with_active_retrievers(self, sample_documents):
retriever = MultiRetriever(
retrievers={"a": MockRetriever([sample_documents[0]]), "b": MockRetriever([sample_documents[1]])}
)
result = await retriever.run_async(query="energy", active_retrievers=["a"])
assert len(result["documents"]) == 1
assert result["documents"][0].id == "doc1"
@pytest.mark.asyncio
async def test_run_async_with_unknown_active_retriever_raises(self):
retriever = MultiRetriever(retrievers={"mock": MockRetriever()})
with pytest.raises(ValueError, match="Unknown retriever name"):
await retriever.run_async(query="energy", active_retrievers=["nonexistent"])
@pytest.mark.asyncio
async def test_run_async_retriever_failure_raises_with_name(self):
retriever = MultiRetriever(retrievers={"failing": FailingRetriever()})
with pytest.raises(RuntimeError, match="Retriever 'failing' failed"):
await retriever.run_async(query="energy")
@pytest.mark.asyncio
async def test_run_async_uses_run_async_on_retriever_if_available(self):
@component
class AsyncCapableRetriever:
def __init__(self):
self.used_async = False
@component.output_types(documents=list[Document])
def run(self, query: str, filters: dict[str, Any] | None = None, top_k: int | None = None):
return {"documents": []}
@component.output_types(documents=list[Document])
async def run_async(self, query: str, filters: dict[str, Any] | None = None, top_k: int | None = None):
self.used_async = True
return {"documents": [Document(content="async result", id="async1")]}
inner = AsyncCapableRetriever()
retriever = MultiRetriever(retrievers={"async_capable": inner})
result = await retriever.run_async(query="energy")
assert inner.used_async is True
assert len(result["documents"]) == 1
assert result["documents"][0].id == "async1"
@pytest.mark.skipif(os.environ.get("OPENAI_API_KEY", "") == "", reason="OPENAI_API_KEY is not set")
@pytest.mark.integration
@pytest.mark.asyncio
async def test_run_async_with_filters(self, bm25_retriever, embedding_retriever):
retriever = MultiRetriever(retrievers={"bm25": bm25_retriever, "embedding": embedding_retriever})
result = await retriever.run_async(
query="energy", filters={"field": "meta.category", "operator": "==", "value": "solar"}
)
assert len(result["documents"]) == 1
assert result["documents"][0].meta["category"] == "solar"
@pytest.mark.skipif(os.environ.get("OPENAI_API_KEY", "") == "", reason="OPENAI_API_KEY is not set")
@pytest.mark.integration
@pytest.mark.asyncio
async def test_run_async_with_top_k(self, bm25_retriever, embedding_retriever):
retriever = MultiRetriever(retrievers={"bm25": bm25_retriever, "embedding": embedding_retriever})
result = await retriever.run_async(query="energy", top_k=2)
assert len(result["documents"]) == 2
@pytest.mark.skipif(os.environ.get("OPENAI_API_KEY", "") == "", reason="OPENAI_API_KEY is not set")
@pytest.mark.integration
@pytest.mark.asyncio
async def test_run_async_with_active_retrievers_integration(self, bm25_retriever, embedding_retriever):
retriever = MultiRetriever(retrievers={"bm25": bm25_retriever, "embedding": embedding_retriever})
result_bm25_active = await retriever.run_async(query="energy", active_retrievers=["bm25"])
result_bm25 = await bm25_retriever.run_async(query="energy")
# Scores differ because MultiRetriever applies join_mode processing (e.g. RRF) even for a single retriever.
assert [doc.id for doc in result_bm25_active["documents"]] == [doc.id for doc in result_bm25["documents"]]
class TestMultiRetrieverExperimental:
@pytest.mark.filterwarnings("always::haystack.utils.experimental.ExperimentalWarning")
def test_emits_experimental_warning_on_init(self):
with pytest.warns(ExperimentalWarning, match="MultiRetriever.*experimental"):
MultiRetriever(retrievers={"mock": MockRetriever()})
@pytest.mark.filterwarnings("always::haystack.utils.experimental.ExperimentalWarning")
def test_experimental_attribute_is_set(self):
assert getattr(MultiRetriever, "__experimental__", False) is True
class TestComponentLifecycle:
def test_warm_up_delegates_to_all_retrievers(self):
a = Mock(spec=["run", "warm_up"])
b = Mock(spec=["run", "warm_up"])
retriever = MultiRetriever(retrievers={"a": a, "b": b})
retriever.warm_up()
a.warm_up.assert_called_once()
b.warm_up.assert_called_once()
async def test_warm_up_async_delegates_to_all_retrievers(self):
a = Mock(spec=["run", "warm_up_async"])
a.warm_up_async = AsyncMock()
b = Mock(spec=["run", "warm_up_async"])
b.warm_up_async = AsyncMock()
retriever = MultiRetriever(retrievers={"a": a, "b": b})
await retriever.warm_up_async()
a.warm_up_async.assert_awaited_once()
b.warm_up_async.assert_awaited_once()
async def test_warm_up_async_falls_back_to_sync_warm_up(self):
a = Mock(spec=["run", "warm_up"])
b = Mock(spec=["run", "warm_up"])
retriever = MultiRetriever(retrievers={"a": a, "b": b})
await retriever.warm_up_async()
a.warm_up.assert_called_once()
b.warm_up.assert_called_once()
def test_close_delegates_to_all_retrievers(self):
a = Mock(spec=["run", "close"])
b = Mock(spec=["run", "close"])
retriever = MultiRetriever(retrievers={"a": a, "b": b})
retriever.close()
a.close.assert_called_once()
b.close.assert_called_once()
async def test_close_async_delegates_to_all_retrievers(self):
a = Mock(spec=["run", "close_async"])
a.close_async = AsyncMock()
b = Mock(spec=["run", "close_async"])
b.close_async = AsyncMock()
retriever = MultiRetriever(retrievers={"a": a, "b": b})
await retriever.close_async()
a.close_async.assert_awaited_once()
b.close_async.assert_awaited_once()
async def test_close_async_falls_back_to_sync_close(self):
a = Mock(spec=["run", "close"])
b = Mock(spec=["run", "close"])
retriever = MultiRetriever(retrievers={"a": a, "b": b})
await retriever.close_async()
a.close.assert_called_once()
b.close.assert_called_once()
async def test_lifecycle_is_safe_when_retrievers_lack_methods(self):
a = Mock(spec=["run"])
b = Mock(spec=["run"])
retriever = MultiRetriever(retrievers={"a": a, "b": b})
retriever.warm_up()
await retriever.warm_up_async()
retriever.close()
await retriever.close_async()