fix: improve maintenance and synchronize live conversations
This commit is contained in:
1 parent
bb541bd212
commit
ef86a5d3af
297 files changed
+2983
-1274
No files matched your search
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from app.knowledge.content_summarizer import ContentSummarizer, get_content_summarizer
|
||||
|
||||
@@ -360,7 +359,7 @@ class TestChatAgentURLInstructions:
|
||||
mock_agent_class.return_value = mock_agent_instance
|
||||
|
||||
# Create chat agent
|
||||
chat_agent = ChatAgent(
|
||||
ChatAgent(
|
||||
api_key="test_key",
|
||||
model_name="gpt-4",
|
||||
model_type="OPENAI",
|
||||
|
||||
@@ -14,11 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch, AsyncMock
|
||||
from bs4 import BeautifulSoup
|
||||
import asyncio
|
||||
|
||||
from app.knowledge.crawl4ai_fallback import (
|
||||
Crawl4AIFallback,
|
||||
@@ -36,6 +38,34 @@ _SKIP_LIVE = os.getenv("CI") is not None and os.getenv("RUN_LIVE_CRAWL_TESTS") !
|
||||
_LIVE_REASON = "live crawl4ai network/browser test (set RUN_LIVE_CRAWL_TESTS=1 to run; skipped in CI)"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def offline_crawler_dns(monkeypatch):
|
||||
"""Keep offline cases synthetic without changing the live-test opt-in."""
|
||||
if not _SKIP_LIVE:
|
||||
return
|
||||
|
||||
def unresolved_host(*args, **kwargs):
|
||||
raise socket.gaierror("synthetic test host")
|
||||
|
||||
monkeypatch.setattr(socket, "getaddrinfo", unresolved_host)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("skip_live", [True, False])
|
||||
def test_offline_dns_fixture_respects_existing_live_mode(monkeypatch, skip_live):
|
||||
monkeypatch.setitem(globals(), "_SKIP_LIVE", skip_live)
|
||||
resolver = MagicMock(return_value=[])
|
||||
monkeypatch.setattr(socket, "getaddrinfo", resolver)
|
||||
|
||||
offline_crawler_dns.__wrapped__(monkeypatch)
|
||||
|
||||
if skip_live:
|
||||
with pytest.raises(socket.gaierror, match="synthetic test host"):
|
||||
socket.getaddrinfo("crawl.test", 443)
|
||||
resolver.assert_not_called()
|
||||
else:
|
||||
assert socket.getaddrinfo is resolver
|
||||
|
||||
|
||||
class TestCrawl4AIFallback:
|
||||
"""Test suite for Crawl4AIFallback class"""
|
||||
|
||||
@@ -66,7 +96,7 @@ class TestCrawl4AIFallback:
|
||||
async def test_async_fetch_success(self, mock_crawler_class):
|
||||
"""Test successful async fetch without screenshot"""
|
||||
# Arrange
|
||||
url = "https://example.com"
|
||||
url = "https://crawl.test"
|
||||
mock_result = MagicMock()
|
||||
mock_result.success = True
|
||||
mock_result.html = "<html><body>Test content</body></html>"
|
||||
@@ -98,7 +128,7 @@ class TestCrawl4AIFallback:
|
||||
async def test_async_fetch_failure(self, mock_crawler_class):
|
||||
"""Test failed async fetch"""
|
||||
# Arrange
|
||||
url = "https://example.com"
|
||||
url = "https://crawl.test"
|
||||
mock_result = MagicMock()
|
||||
mock_result.success = False
|
||||
mock_result.error_message = "Connection timeout"
|
||||
@@ -127,7 +157,7 @@ class TestCrawl4AIFallback:
|
||||
async def test_async_fetch_with_screenshot(self, mock_crawler_class, mock_playwright):
|
||||
"""Test async fetch with screenshot enabled"""
|
||||
# Arrange
|
||||
url = "https://example.com"
|
||||
url = "https://crawl.test"
|
||||
|
||||
# Mock Playwright screenshot
|
||||
mock_page = AsyncMock()
|
||||
@@ -181,7 +211,7 @@ class TestCrawl4AIFallback:
|
||||
async def test_async_fetch_exception(self, mock_crawler_class):
|
||||
"""Test async fetch with exception"""
|
||||
# Arrange
|
||||
url = "https://example.com"
|
||||
url = "https://crawl.test"
|
||||
mock_crawler_class.side_effect = Exception("Network error")
|
||||
|
||||
fallback = Crawl4AIFallback()
|
||||
@@ -289,106 +319,62 @@ class TestCrawl4AIFallback:
|
||||
|
||||
@patch('app.knowledge.crawl4ai_fallback.CRAWL4AI_AVAILABLE', True)
|
||||
@patch('asyncio.get_event_loop')
|
||||
@patch('asyncio.run')
|
||||
def test_run_async_safely_no_running_loop(self, mock_asyncio_run, mock_get_loop):
|
||||
def test_run_async_safely_no_running_loop(self, mock_get_loop):
|
||||
"""Test _run_async_safely when no event loop is running"""
|
||||
# Arrange
|
||||
mock_loop = MagicMock()
|
||||
mock_loop.is_running.return_value = False
|
||||
mock_get_loop.return_value = mock_loop
|
||||
|
||||
mock_asyncio_run.return_value = ("<html>Test</html>", "# Test", None)
|
||||
|
||||
fallback = Crawl4AIFallback()
|
||||
fallback._is_available = True
|
||||
|
||||
# Act
|
||||
html, markdown, screenshot = fallback._run_async_safely("https://example.com", False)
|
||||
|
||||
# Assert
|
||||
assert html == "<html>Test</html>"
|
||||
assert markdown == "# Test"
|
||||
assert screenshot is None
|
||||
mock_asyncio_run.assert_called_once()
|
||||
|
||||
expected = ("<html>Test</html>", "# Test", None)
|
||||
with patch.object(fallback, '_async_fetch', new=AsyncMock(return_value=expected)) as fetch:
|
||||
result = fallback._run_async_safely("https://crawl.test", False)
|
||||
|
||||
assert result == expected
|
||||
fetch.assert_awaited_once_with("https://crawl.test", False)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('app.knowledge.crawl4ai_fallback.CRAWL4AI_AVAILABLE', True)
|
||||
async def test_run_async_safely_with_running_loop(self):
|
||||
"""A running caller loop uses another thread and loop, then joins it."""
|
||||
fallback = Crawl4AIFallback()
|
||||
fallback._is_available = True
|
||||
caller_loop = asyncio.get_running_loop()
|
||||
caller_thread = threading.get_ident()
|
||||
expected = ("<html>Test</html>", "# Test", None)
|
||||
|
||||
async def fetch_in_worker(url, take_screenshot):
|
||||
assert threading.get_ident() != caller_thread
|
||||
assert asyncio.get_running_loop() is not caller_loop
|
||||
return expected
|
||||
|
||||
with patch.object(fallback, '_async_fetch', new=AsyncMock(side_effect=fetch_in_worker)) as fetch:
|
||||
html, markdown, screenshot = fallback._run_async_safely("https://crawl.test", False)
|
||||
|
||||
assert (html, markdown, screenshot) == expected
|
||||
fetch.assert_awaited_once_with("https://crawl.test", False)
|
||||
|
||||
@patch('app.knowledge.crawl4ai_fallback.CRAWL4AI_AVAILABLE', True)
|
||||
@patch('asyncio.get_event_loop')
|
||||
@patch('threading.Thread')
|
||||
def test_run_async_safely_with_running_loop(self, mock_thread_class, mock_get_loop):
|
||||
"""Test _run_async_safely when event loop is already running"""
|
||||
# Arrange
|
||||
mock_loop = MagicMock()
|
||||
mock_loop.is_running.return_value = True
|
||||
mock_get_loop.return_value = mock_loop
|
||||
|
||||
# Mock thread execution
|
||||
mock_thread = MagicMock()
|
||||
mock_thread.is_alive.return_value = False
|
||||
mock_thread_class.return_value = mock_thread
|
||||
|
||||
fallback = Crawl4AIFallback()
|
||||
fallback._is_available = True
|
||||
|
||||
# Mock the async fetch to set result
|
||||
def mock_thread_target(*args, **kwargs):
|
||||
# Simulate successful execution
|
||||
pass
|
||||
|
||||
with patch.object(fallback, '_async_fetch', return_value=("<html>Test</html>", "# Test", None)):
|
||||
# Act
|
||||
html, markdown, screenshot = fallback._run_async_safely("https://example.com", False)
|
||||
|
||||
# Assert
|
||||
mock_thread_class.assert_called_once()
|
||||
mock_thread.start.assert_called_once()
|
||||
mock_thread.join.assert_called_once()
|
||||
|
||||
@patch('app.knowledge.crawl4ai_fallback.CRAWL4AI_AVAILABLE', True)
|
||||
@patch('asyncio.get_event_loop')
|
||||
@patch('asyncio.run')
|
||||
def test_run_async_safely_exception(self, mock_asyncio_run, mock_get_loop):
|
||||
def test_run_async_safely_exception(self, mock_get_loop):
|
||||
"""Test _run_async_safely handles exceptions"""
|
||||
# Arrange
|
||||
mock_loop = MagicMock()
|
||||
mock_loop.is_running.return_value = False
|
||||
mock_get_loop.return_value = mock_loop
|
||||
|
||||
# Simulate exception in asyncio.run
|
||||
mock_asyncio_run.side_effect = RuntimeError("Async execution failed")
|
||||
|
||||
fallback = Crawl4AIFallback()
|
||||
fallback._is_available = True
|
||||
|
||||
# Act
|
||||
html, markdown, screenshot = fallback._run_async_safely("https://example.com", False)
|
||||
|
||||
# Assert - should handle exception gracefully and return None
|
||||
|
||||
with patch.object(fallback, '_async_fetch', new=AsyncMock(side_effect=RuntimeError("Async execution failed"))):
|
||||
html, markdown, screenshot = fallback._run_async_safely("https://crawl.test", False)
|
||||
|
||||
assert html is None
|
||||
assert markdown is None
|
||||
assert screenshot is None
|
||||
|
||||
|
||||
@patch('app.knowledge.crawl4ai_fallback.CRAWL4AI_AVAILABLE', True)
|
||||
def test_run_async_safely_threaded_execution(self):
|
||||
"""Cover the real new-thread/new-loop path when an event loop is already running.
|
||||
|
||||
Mocks _async_fetch so no real network/browser is touched, but lets the actual
|
||||
threading + new event loop machinery run (the path the live test used to cover).
|
||||
"""
|
||||
fallback = Crawl4AIFallback()
|
||||
fallback._is_available = True
|
||||
|
||||
expected = ("<html><body>Threaded</body></html>", "# Threaded content", None)
|
||||
|
||||
fake_loop = MagicMock()
|
||||
fake_loop.is_running.return_value = True
|
||||
|
||||
with patch('asyncio.get_event_loop', return_value=fake_loop), \
|
||||
patch.object(fallback, '_async_fetch', new=AsyncMock(return_value=expected)):
|
||||
html, markdown, screenshot = fallback._run_async_safely("https://example.com", False)
|
||||
|
||||
assert (html, markdown, screenshot) == expected
|
||||
|
||||
@patch('app.knowledge.crawl4ai_fallback.CRAWL4AI_AVAILABLE', True)
|
||||
def test_run_async_safely_threaded_exception(self):
|
||||
"""An exception inside the worker thread is re-raised and handled gracefully."""
|
||||
|
||||
@@ -15,7 +15,6 @@ limitations under the License.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch, call, ANY
|
||||
from agno.document.base import Document
|
||||
from app.core.config import settings
|
||||
|
||||
@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import os
|
||||
import socket
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock, Mock
|
||||
from unittest.mock import patch
|
||||
from bs4 import BeautifulSoup
|
||||
import httpx
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ class TestOptimizedPgVector:
|
||||
# Mock the document's embed method
|
||||
with patch.object(Document, 'embed') as mock_embed:
|
||||
# Act
|
||||
with patch('sqlalchemy.dialects.postgresql.insert') as mock_insert:
|
||||
with patch('sqlalchemy.dialects.postgresql.insert'):
|
||||
db.insert([pre_embedded_doc, unembedded_doc])
|
||||
|
||||
# Assert
|
||||
|
||||
Reference in new issue
Block a user