"""Regression tests for the deployed backend research pipeline fixes.""" from __future__ import annotations import asyncio from uuid import uuid4 import httpx from unittest.mock import AsyncMock from nsct.config import AppSettings, AudioConfig, DatabaseConfig, LLMConfig, VisionConfig from nsct.orchestration.orchestrator import ResearchOrchestrator from nsct.orchestration.state import ResearchRunState from nsct.providers.llm import _LLMProviderImpl from nsct.providers.metrics import ProviderMetrics from nsct.providers.searxng import SearXNGProvider def _config(*, searxng_base_url: str | None = "http://searxng:8080") -> AppSettings: return AppSettings( llm=LLMConfig(base_url="http://llm.example/v1", model="test-model"), vision=VisionConfig(base_url="http://vision.example/v1", model="vision"), audio=AudioConfig(base_url="http://audio.example/v1"), postgres=DatabaseConfig(url="sqlite+aiosqlite://"), searxng_base_url=searxng_base_url, ) def test_llm_base_url_does_not_duplicate_existing_v1() -> None: provider = _LLMProviderImpl(_config(), ProviderMetrics()) provider._api_key = "test-key" client = provider._create_client() try: assert str(client.base_url) == "http://llm.example/v1/" finally: asyncio.run(client.close()) def test_searxng_provider_normalizes_json_results() -> None: async def run() -> None: provider = SearXNGProvider("http://searxng:8080") provider._client = httpx.AsyncClient( transport=httpx.MockTransport( lambda request: httpx.Response( 200, json={ "results": [ {"url": "https://example.org/a", "title": "A", "content": "Snippet"}, {"url": "mailto:invalid@example.org", "title": "Invalid"}, ] }, ) ) ) try: results = await provider.search("test") assert len(results) == 1 assert results[0].provider == "searxng" assert results[0].url == "https://example.org/a" assert results[0].snippet == "Snippet" assert provider._client is not None finally: await provider._client.aclose() asyncio.run(run()) def test_orchestrator_registers_searxng_provider() -> None: orchestrator = ResearchOrchestrator(_config(), uuid4(), "test") provider = orchestrator._get_multi_search().get_provider("searxng") assert isinstance(provider, SearXNGProvider) def test_pipeline_executes_extracting_between_fetching_and_analyzing() -> None: async def run() -> None: orchestrator = ResearchOrchestrator(_config(), uuid4(), "test") orchestrator._comparison_data = {} for step in ("planning", "searching", "fetching", "extracting", "analyzing", "comparing", "synthesizing"): async def handler(target: str = step) -> dict[str, object]: orchestrator._transition_to(target) return {"success": True, "data": {}} setattr(orchestrator, f"_step_{step}", AsyncMock(side_effect=handler)) result = await orchestrator.run() assert result["success"] is True assert orchestrator._step_extracting.await_count == 1 assert orchestrator.state is ResearchRunState.COMPLETED asyncio.run(run()) def test_no_search_provider_marks_run_failed_with_reason() -> None: async def run() -> None: orchestrator = ResearchOrchestrator(_config(searxng_base_url=None), uuid4(), "test") await orchestrator.start() orchestrator._step_planning = AsyncMock(return_value={"success": True, "data": {}}) result = await orchestrator.run() assert result["success"] is False assert result["failed_step"] == "searching" assert "No search provider" in result["error"] assert orchestrator.state is ResearchRunState.FAILED asyncio.run(run())