11import pytest
22import sys
33import os
4+ import types
45from pathlib import Path
56from 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
32121crewai_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):
74164sys .modules ["litellm" ] = create_mock_module ("litellm" )
75165sys .modules ["litellm.types" ] = create_mock_module ("litellm.types" )
76166sys .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+ )
77190sys .modules ["instructor" ] = create_mock_module ("instructor" )
78191sys .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