"""Tests for LLM fallback wiring in compliance, seo_monitor, reconciliation. These tests verify that: - The fallback path is INVOKED when the regex confidence is low (or fields are empty) - The LLM result is MERGED into the regex result - If the LLM call raises or returns nothing, the regex result is preserved - If no LLM provider is configured, we degrade gracefully The tests monkeypatch llm_features so we don't need a real LLM. """ # SPDX-License-Identifier: MIT # Copyright (c) 2026 Rug Munch Media LLC # # Part of Pry - https://git.rugmunch.io/RugMunchMedia/pryscraper # Licensed under MIT. See LICENSE. from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch import pytest # ── compliance.py ──────────────────────────────────────────────── @pytest.mark.asyncio async def test_compliance_llm_fallback_merges_into_tos_result(): """When tos_result confidence is low, the LLM result should be merged in with the llm_enhanced flag set to True.""" from compliance import run_compliance_check fake_llm = AsyncMock( return_value={ "risk_level": "red", "confidence": "high", "risk_summary": "Strict scraping prohibition", "recommendation": "Contact site owner", "key_restrictions": ["no bots", "rate limited"], "llm_provider": "openrouter", "llm_cost_usd": 0.001, } ) with patch("llm_features.llm_compliance_analyze", fake_llm): result = await run_compliance_check("https://example.com/") # The LLM should have been called assert fake_llm.called # The merged tos_result should have llm_enhanced: True tos = result.get("terms_of_service", {}) assert tos.get("llm_enhanced") is True assert tos.get("classification") == "red" # from the LLM assert tos.get("confidence") == "high" assert tos.get("llm_provider") == "openrouter" @pytest.mark.asyncio async def test_compliance_llm_fallback_preserves_result_on_error(): """If the LLM call raises, the regex result should be preserved (no crash).""" from compliance import run_compliance_check fake_llm = AsyncMock(side_effect=RuntimeError("LLM down")) with patch("llm_features.llm_compliance_analyze", fake_llm): # Should not raise result = await run_compliance_check("https://example.com/") # Result should still be valid assert "url" in result assert "risk_level" in result # ── seo_monitor.py ─────────────────────────────────────────────── @pytest.mark.asyncio async def test_seo_llm_enhancement_fills_empty_critical_fields(): """When the regex pass leaves title/meta_description/h1 empty, the LLM should be invoked and its suggestions should fill the gaps.""" from seo_monitor import analyze_seo fake_llm = AsyncMock( return_value={ "title": "Pry - Web Intelligence", "meta_description": "Open any website with Pry's free API", "h1": "Web Scraping Made Simple", "llm_provider": "ollama", "llm_cost_usd": 0.0, } ) # Patch the regex helpers to return empty with ( patch("seo_monitor._get_title", return_value=""), patch("seo_monitor._get_meta_content", return_value=""), patch("seo_monitor._get_headings", return_value=[]), patch("seo_monitor._count_words", return_value=100), patch("seo_monitor._has_schema", return_value=False), patch("seo_monitor._get_hreflangs", return_value=[]), patch("seo_monitor._get_charset", return_value="utf-8"), patch("seo_monitor._get_attr", return_value=""), patch("seo_monitor._count_links", return_value=0), patch("llm_features.llm_seo_analyze", fake_llm), patch("client.get_client") as mock_get_client, ): # Mock the HTTP client to return a fake HTML response mock_resp = MagicMock() mock_resp.is_success = True mock_resp.text = "