"""The LangGraph ``research_subtopic`` subagent pool must propagate the lead thread's search context (which carries the user's DB password) into its stdlib ``ThreadPoolExecutor`` workers. Without it, a subagent that re-creates an engine / registers the user's document collections runs ``get_user_db_session`` with no password and fails to open the per-user ENCRYPTED database — surfacing as "Unknown search engine 'collection_…'". Sibling strategies close this gap with ``@preserve_research_context``; this strategy captures the context on the lead thread and re-sets it per pool task. The test drives the REAL ``_make_research_subtopic_tool`` pool wiring; only the leaf subagent internals (``_make_web_search_tool``, ``build_fetch_tool``, ``create_agent``) are stubbed, so the assertion is purely about what context a worker thread observes. """ from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest import local_deep_research.advanced_search_system.strategies.langgraph_agent_strategy as strat # noqa: E501 from local_deep_research.utilities.thread_context import ( clear_search_context, get_search_context, set_search_context, ) @pytest.fixture(autouse=True) def _clean_search_context(): # Never let a context leak into or out of these tests. clear_search_context() yield clear_search_context() def _invoke_one_subtopic(seen): """Build the real research_subtopic tool with stubbed subagent internals, then invoke it once. ``seen`` collects the search context observed inside the pool worker (via the stubbed web-search tool factory).""" def fake_web_search_tool(*_a, **_k): # Runs INSIDE the pool worker — record the context it can see. seen.append(get_search_context()) return SimpleNamespace(name="web_search") agent = MagicMock() agent.invoke.return_value = {"messages": [SimpleNamespace(content="ok")]} with ( patch.object(strat, "_make_web_search_tool", fake_web_search_tool), patch.object(strat, "build_fetch_tool", lambda *_a, **_k: None), patch("langchain.agents.create_agent", return_value=agent), ): tool = strat._make_research_subtopic_tool( search_engine_name="collection_abc", model=MagicMock(), settings_snapshot={"search.tool": "collection_abc"}, collector=MagicMock(), max_sub_iterations=1, ) tool.invoke({"subtopics": ["what is X?"]}) class TestSubagentSearchContextPropagation: def test_password_context_reaches_pool_worker(self): # Lead thread has the user's DB password in the search context... set_search_context({"user_password": "secret", "research_id": "r1"}) seen = [] _invoke_one_subtopic(seen) # ...and the pool worker sees it (propagated), not None. This fails # against the pre-fix code where the ContextVar was never carried in. assert seen, "subagent web_search tool was never built" assert seen[0] is not None assert seen[0].get("user_password") == "secret" def test_no_context_does_not_crash(self): # No lead-thread context (programmatic/no-encryption): the pool worker # simply sees None and the run proceeds without error. seen = [] _invoke_one_subtopic(seen) assert seen and seen[0] is None