"""Per-collection "Available to the research agent" flag (agent_enabled). A collection a user marked as NOT available to the research agent must be excluded from the LangGraph agent's specialized tool list — its name never reaches ``create_agent()`` — while agent-enabled collections survive. This is a usability switch, independent of egress scope: it filters even a collection that egress would otherwise allow. Drives the REAL ``LangGraphAgentStrategy._build_tools`` loop; only the leaf tool factories and engine discovery are mocked, so the assertion is the filter decision (which collection names survive into the built tool list). """ from __future__ import annotations from contextlib import contextmanager 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.security import clear_active_context @pytest.fixture(autouse=True) def _clear_ctx(): clear_active_context() yield clear_active_context() @contextmanager def _patched_factories(): spec = MagicMock( side_effect=lambda name, *a, **k: SimpleNamespace(name=name) ) with ( patch.object( strat, "_make_web_search_tool", lambda *a, **k: SimpleNamespace(name="web_search"), ), patch.object(strat, "build_fetch_tool", lambda *a, **k: None), patch.object(strat, "_make_specialized_search_tool", spec), ): yield spec def _build(available): # BOTH scope: a collection is allowed by egress regardless of is_public, so # the ONLY differentiator left is the agent_enabled flag. snapshot = {"policy.egress_scope": "both", "search.fetch.mode": "disabled"} reg = MagicMock() reg.get_metadata.return_value = None with _patched_factories() as spec: with ( patch( "local_deep_research.web_search_engines.search_engines_config" ".get_available_engines", return_value=available, ), patch( "local_deep_research.web_search_engines.retriever_registry" ".retriever_registry", reg, ), ): strategy = strat.LangGraphAgentStrategy( model=MagicMock(), search=MagicMock(), citation_handler=object(), include_sub_research=False, settings_snapshot=snapshot, ) tools = strategy._build_tools("overall query") return {t.name for t in tools}, spec def _coll(is_public=True, agent_enabled=True): cfg = { "is_public": is_public, "is_local": not is_public, "is_retriever": False, "description": "a collection", "strengths": [], } if agent_enabled is not None: cfg["agent_enabled"] = agent_enabled return cfg def test_agent_disabled_collection_excluded_from_tools(): names, spec = _build( { "collection_enabled": _coll(agent_enabled=True), "collection_disabled": _coll(agent_enabled=False), } ) assert "collection_enabled" in names assert "collection_disabled" not in names # The disabled collection's tool factory was never even called (the loop # `continue`s before construction). built = {c.args[0] for c in spec.call_args_list} assert "collection_disabled" not in built assert "collection_enabled" in built def test_missing_flag_defaults_to_available(): names, _ = _build({"collection_x": _coll(agent_enabled=None)}) assert "collection_x" in names def test_agent_flag_independent_of_egress(): # A PUBLIC collection that egress would allow under BOTH is still excluded # when agent_enabled is False — proving the switch is orthogonal to scope. names, _ = _build( {"collection_pub_disabled": _coll(is_public=True, agent_enabled=False)} ) assert "collection_pub_disabled" not in names