|
88 | 88 | ) |
89 | 89 |
|
90 | 90 |
|
91 | | -def _ensure_optional_deps_stubbed() -> None: |
92 | | - """Stub missing heavy optional deps so the chat agent can import.""" |
| 91 | +def _ensure_optional_deps_stubbed() -> List[str]: |
| 92 | + """Stub missing heavy optional deps so the chat agent can import. |
| 93 | +
|
| 94 | + Returns the names this call newly stubbed, so the caller can remove them |
| 95 | + again — a leaked ``MagicMock`` makes ``import faiss`` *succeed* for later |
| 96 | + tests that probe for the real dep, turning their graceful-skip into a hard |
| 97 | + failure. Modules already present (real or stubbed by someone else) are left |
| 98 | + untouched and not returned. |
| 99 | + """ |
| 100 | + stubbed: List[str] = [] |
93 | 101 | for mod in _OPTIONAL_DEPS: |
94 | 102 | if mod in sys.modules: |
95 | 103 | continue |
96 | 104 | if importlib.util.find_spec(mod) is not None: |
97 | 105 | continue |
98 | 106 | sys.modules[mod] = MagicMock() |
| 107 | + stubbed.append(mod) |
| 108 | + return stubbed |
99 | 109 |
|
100 | 110 |
|
101 | 111 | def get_tokenizer() -> Optional[Any]: |
@@ -156,50 +166,60 @@ def build_doc_agent_skeleton( |
156 | 166 |
|
157 | 167 | The freshly registered tools are snapshotted into the instance via |
158 | 168 | ``_instance_tools``; the global registry is restored on the way out. |
159 | | - """ |
160 | | - _ensure_optional_deps_stubbed() |
161 | | - |
162 | | - from gaia.agents.chat.agent import ChatAgent, ChatAgentConfig |
163 | 169 |
|
164 | | - cfg = ChatAgentConfig( |
165 | | - rag_documents=[], |
166 | | - streaming=False, |
167 | | - silent_mode=True, |
168 | | - prompt_profile=profile, |
169 | | - ) |
| 170 | + Cleans up after itself: any ``MagicMock`` dep stubs this call adds are |
| 171 | + removed from ``sys.modules`` on exit. Without that, a leaked ``faiss``/ |
| 172 | + ``pypdf`` mock makes ``import faiss`` *succeed* for later tests that probe |
| 173 | + for the real dep — turning their graceful-skip into a hard failure. (Only |
| 174 | + the stubs are removed; gaia modules stay cached, since the consumers that |
| 175 | + matter import these deps lazily, so dropping the stub is enough.) |
| 176 | + """ |
| 177 | + stubbed = _ensure_optional_deps_stubbed() |
| 178 | + try: |
| 179 | + from gaia.agents.chat.agent import ChatAgent, ChatAgentConfig |
170 | 180 |
|
171 | | - with _isolated_registry() as tools_mod: |
172 | | - stack = contextlib.ExitStack() |
173 | | - stack.enter_context( |
174 | | - patch("gaia.agents.base.agent.Agent.__init__", return_value=None) |
| 181 | + cfg = ChatAgentConfig( |
| 182 | + rag_documents=[], |
| 183 | + streaming=False, |
| 184 | + silent_mode=True, |
| 185 | + prompt_profile=profile, |
175 | 186 | ) |
176 | | - if deterministic: |
177 | | - # No npx -> search_documentation skipped; scrub PERPLEXITY_API_KEY |
178 | | - # -> search_web skipped. clear=True + filtered copy removes the key |
179 | | - # for the duration and restores the full environment afterwards. |
180 | | - stack.enter_context(patch("shutil.which", return_value=None)) |
181 | | - scrubbed = { |
182 | | - k: v for k, v in os.environ.items() if k != "PERPLEXITY_API_KEY" |
183 | | - } |
184 | | - stack.enter_context(patch.dict(os.environ, scrubbed, clear=True)) |
185 | | - |
186 | | - with stack: |
187 | | - agent = ChatAgent.__new__(ChatAgent) |
188 | | - agent.config = cfg |
189 | | - agent._instance_tools = None |
190 | | - agent.model_id = "Gemma-4-E4B-it-GGUF" |
191 | | - agent._memory_store = MagicMock() # non-None -> memory tools register |
192 | | - agent.rag = MagicMock() |
193 | | - agent.console = MagicMock() |
194 | | - # Satisfy ChatAgent.__del__ so GC of the skeleton stays quiet. |
195 | | - agent.observers = [] |
196 | | - agent._web_client = None |
197 | | - agent._fs_index = None |
198 | | - agent._scratchpad = None |
199 | | - agent._register_tools() |
200 | | - agent._instance_tools = dict(tools_mod._TOOL_REGISTRY) |
201 | | - |
202 | | - return agent |
| 187 | + |
| 188 | + with _isolated_registry() as tools_mod: |
| 189 | + stack = contextlib.ExitStack() |
| 190 | + stack.enter_context( |
| 191 | + patch("gaia.agents.base.agent.Agent.__init__", return_value=None) |
| 192 | + ) |
| 193 | + if deterministic: |
| 194 | + # No npx -> search_documentation skipped; scrub PERPLEXITY_API_KEY |
| 195 | + # -> search_web skipped. clear=True + filtered copy removes the key |
| 196 | + # for the duration and restores the full environment afterwards. |
| 197 | + stack.enter_context(patch("shutil.which", return_value=None)) |
| 198 | + scrubbed = { |
| 199 | + k: v for k, v in os.environ.items() if k != "PERPLEXITY_API_KEY" |
| 200 | + } |
| 201 | + stack.enter_context(patch.dict(os.environ, scrubbed, clear=True)) |
| 202 | + |
| 203 | + with stack: |
| 204 | + agent = ChatAgent.__new__(ChatAgent) |
| 205 | + agent.config = cfg |
| 206 | + agent._instance_tools = None |
| 207 | + agent.model_id = "Gemma-4-E4B-it-GGUF" |
| 208 | + agent._memory_store = MagicMock() # non-None -> memory tools register |
| 209 | + agent.rag = MagicMock() |
| 210 | + agent.console = MagicMock() |
| 211 | + # Satisfy ChatAgent.__del__ so GC of the skeleton stays quiet. |
| 212 | + agent.observers = [] |
| 213 | + agent._web_client = None |
| 214 | + agent._fs_index = None |
| 215 | + agent._scratchpad = None |
| 216 | + agent._register_tools() |
| 217 | + agent._instance_tools = dict(tools_mod._TOOL_REGISTRY) |
| 218 | + |
| 219 | + return agent |
| 220 | + finally: |
| 221 | + for mod in stubbed: |
| 222 | + sys.modules.pop(mod, None) |
203 | 223 |
|
204 | 224 |
|
205 | 225 | def _render_paths( |
|
0 commit comments