Skip to content

Commit c1dcbb4

Browse files
committed
fix(tests): harden CrewAI/chromadb/litellm mocks and restore full pytest pass
1 parent a7e316c commit c1dcbb4

3 files changed

Lines changed: 352 additions & 22 deletions

File tree

.gitignore

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,11 @@ tests example/
184184

185185
applications
186186
vlm_test
187-
examples/vlm_piezo_test
187+
examples/vlm_piezo_test
188+
189+
# Test results
190+
db
191+
results
192+
elsevier_test.xml
193+
springer_test.xml
194+
wiley_test.pdf

tests/conftest.py

Lines changed: 325 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22
import sys
33
import os
4+
import types
45
from pathlib import Path
56
from unittest.mock import MagicMock, patch
67

@@ -28,20 +29,109 @@ def create_mock_module(name, **kwargs):
2829
return mock
2930

3031

32+
def create_real_module(name, **kwargs):
33+
"""Create a real module object (not MagicMock) with explicit attributes."""
34+
module = types.ModuleType(name)
35+
module.__package__ = name.rsplit(".", 1)[0] if "." in name else ""
36+
module.__path__ = [name]
37+
38+
for key, value in kwargs.items():
39+
setattr(module, key, value)
40+
41+
return module
42+
43+
44+
def _identity_decorator(*args, **kwargs):
45+
if len(args) == 1 and callable(args[0]) and not kwargs:
46+
return args[0]
47+
48+
def _decorator(func):
49+
return func
50+
51+
return _decorator
52+
53+
54+
def _event_decorator(*args, **kwargs):
55+
def _decorator(func):
56+
return func
57+
58+
return _decorator
59+
60+
61+
def _or_(*signals):
62+
return signals
63+
64+
65+
class _DummyFlow:
66+
def __class_getitem__(cls, _item):
67+
return cls
68+
69+
def __init__(self, *args, **kwargs):
70+
self.state = types.SimpleNamespace(
71+
is_materials_mentioned="",
72+
composition_extracted_data={},
73+
composition_formatted_data={},
74+
synthesis_extracted_data={},
75+
synthesis_formatted_data={},
76+
doi="",
77+
materials_data_identifier_query="",
78+
main_extraction_keyword="",
79+
composition_property_text_data="",
80+
synthesis_text_data="",
81+
is_extract_synthesis_data=True,
82+
vlm_model="gemini/gemini-3-flash-preview",
83+
related_figures_base_path="results/related_figures",
84+
llm=None,
85+
rag_config=None,
86+
output_log_folder=None,
87+
task_output_folder=None,
88+
is_log_json=False,
89+
verbose=True,
90+
expected_composition_property_example="",
91+
expected_variable_composition_property_example="",
92+
composition_property_extraction_agent_note="",
93+
composition_property_extraction_task_note="",
94+
composition_property_formatting_agent_note="",
95+
composition_property_formatting_task_note="",
96+
synthesis_extraction_agent_note="",
97+
synthesis_extraction_task_note="",
98+
synthesis_formatting_agent_note="",
99+
synthesis_formatting_task_note="",
100+
allowed_synthesis_methods="",
101+
allowed_characterization_techniques="",
102+
)
103+
104+
105+
class _DummyChromaCollection:
106+
def __init__(self, *args, **kwargs):
107+
pass
108+
109+
def query(self, *args, **kwargs):
110+
return {"ids": [[]], "metadatas": [[]], "documents": [[]], "distances": [[]]}
111+
112+
def upsert(self, *args, **kwargs):
113+
return None
114+
115+
116+
class _DummyCrewAILLM:
117+
pass
118+
119+
31120
# Mock crewai completely before any imports
32121
crewai_modules = {
33122
"crewai": {
34-
"LLM": MagicMock(),
123+
"LLM": _DummyCrewAILLM,
35124
"Agent": MagicMock(),
36125
"Task": MagicMock(),
37126
"Crew": MagicMock(),
38127
},
39128
"crewai.flow": {},
40129
"crewai.flow.flow": {
41-
"Flow": MagicMock(),
42-
"listen": MagicMock(),
43-
"start": MagicMock(),
44-
"router": MagicMock(),
130+
"Flow": _DummyFlow,
131+
"listen": _event_decorator,
132+
"start": _identity_decorator,
133+
"router": _event_decorator,
134+
"or_": _or_,
45135
},
46136
"crewai.project": {
47137
"CrewBase": MagicMock(),
@@ -53,7 +143,7 @@ def create_mock_module(name, **kwargs):
53143
"BaseTool": MagicMock(),
54144
},
55145
"crewai.agent": {},
56-
"crewai.llm": {"LLM": MagicMock()},
146+
"crewai.llm": {"LLM": _DummyCrewAILLM},
57147
"crewai.agents": {},
58148
"crewai.agents.crew_agent_executor": {"CrewAgentExecutor": MagicMock()},
59149
"crewai.agents.agent_builder": {},
@@ -74,13 +164,238 @@ def create_mock_module(name, **kwargs):
74164
sys.modules["litellm"] = create_mock_module("litellm")
75165
sys.modules["litellm.types"] = create_mock_module("litellm.types")
76166
sys.modules["litellm.types.utils"] = create_mock_module("litellm.types.utils")
167+
sys.modules["litellm.exceptions"] = create_real_module(
168+
"litellm.exceptions",
169+
ContextWindowExceededError=type("ContextWindowExceededError", (Exception,), {}),
170+
)
171+
sys.modules["litellm.utils"] = create_real_module(
172+
"litellm.utils",
173+
supports_response_schema=lambda *args, **kwargs: True,
174+
supports_function_calling=lambda *args, **kwargs: True,
175+
)
176+
sys.modules["litellm.litellm_core_utils"] = create_real_module(
177+
"litellm.litellm_core_utils"
178+
)
179+
sys.modules["litellm.litellm_core_utils.get_supported_openai_params"] = (
180+
create_real_module(
181+
"litellm.litellm_core_utils.get_supported_openai_params",
182+
get_supported_openai_params=lambda *args, **kwargs: ["stop"],
183+
)
184+
)
185+
sys.modules["litellm.integrations"] = create_real_module("litellm.integrations")
186+
sys.modules["litellm.integrations.custom_logger"] = create_real_module(
187+
"litellm.integrations.custom_logger",
188+
CustomLogger=type("CustomLogger", (), {}),
189+
)
77190
sys.modules["instructor"] = create_mock_module("instructor")
78191
sys.modules["crewai_tools"] = create_mock_module("crewai_tools")
79-
sys.modules["langchain_chroma"] = create_mock_module(
80-
"langchain_chroma", Chroma=MagicMock()
192+
sys.modules["langchain_chroma"] = create_real_module(
193+
"langchain_chroma", Chroma=type("Chroma", (), {})
194+
)
195+
196+
# Use lightweight real types/functions (not MagicMock) for chromadb stubs.
197+
# CrewAI/Pydantic dataclass parsing reads type annotations and fails on MagicMock.
198+
class _PydanticAnyTypeMixin:
199+
@classmethod
200+
def __get_pydantic_core_schema__(cls, _source_type, _handler):
201+
from pydantic_core import core_schema
202+
203+
return core_schema.any_schema()
204+
205+
206+
class _DummyPersistentClient(_PydanticAnyTypeMixin):
207+
def __init__(self, *args, **kwargs):
208+
self._args = args
209+
self._kwargs = kwargs
210+
211+
def get_or_create_collection(self, *args, **kwargs):
212+
return _DummyChromaCollection()
213+
214+
def reset(self):
215+
return None
216+
217+
def clear_system_cache(self):
218+
return None
219+
220+
221+
class _DummySettings(_PydanticAnyTypeMixin):
222+
def __init__(self, *args, **kwargs):
223+
self._kwargs = kwargs
224+
225+
226+
class _DummyAsyncClientAPI(_PydanticAnyTypeMixin):
227+
pass
228+
229+
230+
class _DummyClientAPI(_PydanticAnyTypeMixin):
231+
pass
232+
233+
234+
class _DummyCollectionConfigurationInterface(_PydanticAnyTypeMixin):
235+
pass
236+
237+
238+
class _DummyCollectionMetadata(dict, _PydanticAnyTypeMixin):
239+
pass
240+
241+
242+
class _DummyLoadable(_PydanticAnyTypeMixin):
243+
pass
244+
245+
246+
class _DummyWhere(dict, _PydanticAnyTypeMixin):
247+
pass
248+
249+
250+
class _DummyWhereDocument(dict, _PydanticAnyTypeMixin):
251+
pass
252+
253+
254+
class _DummyDataLoader(_PydanticAnyTypeMixin):
255+
def __class_getitem__(cls, _item):
256+
return cls
257+
258+
259+
class _DummyEmbeddingFunction(_PydanticAnyTypeMixin):
260+
def __class_getitem__(cls, _item):
261+
return cls
262+
263+
264+
class _DummyInclude(list, _PydanticAnyTypeMixin):
265+
pass
266+
267+
268+
class _DummyDocuments(list, _PydanticAnyTypeMixin):
269+
pass
270+
271+
272+
class _DummyEmbeddings(list, _PydanticAnyTypeMixin):
273+
pass
274+
275+
276+
class _DummyMetadata(dict, _PydanticAnyTypeMixin):
277+
pass
278+
279+
280+
class _DummyCollection(_PydanticAnyTypeMixin):
281+
pass
282+
283+
284+
class _DummyOpenAIEmbeddingFunction(_PydanticAnyTypeMixin):
285+
def __init__(self, *args, **kwargs):
286+
pass
287+
288+
289+
class _DummyEmbeddingCallable(_PydanticAnyTypeMixin):
290+
def __init__(self, *args, **kwargs):
291+
pass
292+
293+
294+
def _dummy_validate_embedding_function(*args, **kwargs):
295+
return None
296+
297+
298+
sys.modules["chromadb"] = create_real_module(
299+
"chromadb",
300+
PersistentClient=_DummyPersistentClient,
301+
Collection=_DummyCollection,
302+
Documents=_DummyDocuments,
303+
EmbeddingFunction=_DummyEmbeddingFunction,
304+
Embeddings=_DummyEmbeddings,
305+
Metadata=_DummyMetadata,
306+
)
307+
sys.modules["chromadb.config"] = create_real_module(
308+
"chromadb.config", Settings=_DummySettings
309+
)
310+
sys.modules["chromadb.api"] = create_real_module(
311+
"chromadb.api",
312+
AsyncClientAPI=_DummyAsyncClientAPI,
313+
ClientAPI=_DummyClientAPI,
314+
)
315+
sys.modules["chromadb.api.types"] = create_real_module(
316+
"chromadb.api.types",
317+
CollectionMetadata=_DummyCollectionMetadata,
318+
DataLoader=_DummyDataLoader,
319+
Documents=_DummyDocuments,
320+
EmbeddingFunction=_DummyEmbeddingFunction,
321+
Embeddings=_DummyEmbeddings,
322+
Include=_DummyInclude,
323+
Loadable=_DummyLoadable,
324+
OneOrMany=list,
325+
Where=_DummyWhere,
326+
WhereDocument=_DummyWhereDocument,
327+
validate_embedding_function=_dummy_validate_embedding_function,
328+
)
329+
sys.modules["chromadb.errors"] = create_real_module(
330+
"chromadb.errors",
331+
InvalidDimensionException=type("InvalidDimensionException", (Exception,), {}),
332+
)
333+
sys.modules["chromadb.api.configuration"] = create_real_module(
334+
"chromadb.api.configuration",
335+
CollectionConfigurationInterface=_DummyCollectionConfigurationInterface,
336+
)
337+
sys.modules["chromadb.utils"] = create_real_module("chromadb.utils")
338+
sys.modules["chromadb.utils.embedding_functions"] = create_real_module(
339+
"chromadb.utils.embedding_functions"
340+
)
341+
342+
343+
def _register_embedding_function_module(module_name, class_names):
344+
attrs = {class_name: _DummyEmbeddingCallable for class_name in class_names}
345+
sys.modules[module_name] = create_real_module(module_name, **attrs)
346+
347+
348+
_register_embedding_function_module(
349+
"chromadb.utils.embedding_functions.openai_embedding_function",
350+
["OpenAIEmbeddingFunction"],
351+
)
352+
_register_embedding_function_module(
353+
"chromadb.utils.embedding_functions.amazon_bedrock_embedding_function",
354+
["AmazonBedrockEmbeddingFunction"],
355+
)
356+
_register_embedding_function_module(
357+
"chromadb.utils.embedding_functions.cohere_embedding_function",
358+
["CohereEmbeddingFunction"],
359+
)
360+
_register_embedding_function_module(
361+
"chromadb.utils.embedding_functions.google_embedding_function",
362+
["GoogleGenerativeAiEmbeddingFunction", "GoogleVertexEmbeddingFunction"],
363+
)
364+
_register_embedding_function_module(
365+
"chromadb.utils.embedding_functions.huggingface_embedding_function",
366+
["HuggingFaceEmbeddingFunction", "HuggingFaceEmbeddingServer"],
367+
)
368+
_register_embedding_function_module(
369+
"chromadb.utils.embedding_functions.instructor_embedding_function",
370+
["InstructorEmbeddingFunction"],
371+
)
372+
_register_embedding_function_module(
373+
"chromadb.utils.embedding_functions.jina_embedding_function",
374+
["JinaEmbeddingFunction"],
375+
)
376+
_register_embedding_function_module(
377+
"chromadb.utils.embedding_functions.ollama_embedding_function",
378+
["OllamaEmbeddingFunction"],
379+
)
380+
_register_embedding_function_module(
381+
"chromadb.utils.embedding_functions.onnx_mini_lm_l6_v2",
382+
["ONNXMiniLM_L6_V2"],
383+
)
384+
_register_embedding_function_module(
385+
"chromadb.utils.embedding_functions.open_clip_embedding_function",
386+
["OpenCLIPEmbeddingFunction"],
387+
)
388+
_register_embedding_function_module(
389+
"chromadb.utils.embedding_functions.roboflow_embedding_function",
390+
["RoboflowEmbeddingFunction"],
391+
)
392+
_register_embedding_function_module(
393+
"chromadb.utils.embedding_functions.sentence_transformer_embedding_function",
394+
["SentenceTransformerEmbeddingFunction"],
81395
)
82-
sys.modules["chromadb"] = create_mock_module(
83-
"chromadb", PersistentClient=MagicMock()
396+
_register_embedding_function_module(
397+
"chromadb.utils.embedding_functions.text2vec_embedding_function",
398+
["Text2VecEmbeddingFunction"],
84399
)
85400

86401
# Mock aiohttp to avoid the ConnectionTimeoutError

0 commit comments

Comments
 (0)