fix: improve maintenance and synchronize live conversations

This commit is contained in:
WangShen007 committed 2026-10-29 09:46:21 +08:00
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