v4: 7643 tests passing, 83% raw / 96.9% real coverage (excl __tests__).
12 known failures — all from Agent 3 new unverified tests (mock setup issues): - 3 dataset_review_routes_extended (DTO field mismatches) - 1 settings_consolidated (dict key access) - 1 llm_analysis_service_coverage (rate_limit mock) - 1 migration_plugin (SessionLocal side_effect exhaustion) - 1 preview (DB query vs dict key) - 5 scheduler (datetime timezone + async mock mismatches) NEW TEST FILES THIS SESSION: - test_batch_insert_coverage.py — 3 tests - test_storage_plugin.py — +3 tests - test_search.py — +2 tests - test_mapper.py — already 100% - test_llm_analysis_migration_v1_to_v2.py — 14 tests - test_llm_async_http.py — +1 test - test_prompt_builder.py — +1 test - test_service_datasource.py — +1 test - test_lang_detect.py — +1 test - test_scheduler.py — +6 tests - test_llm_analysis_service_coverage.py — +15 tests - test_dataset_review_routes_extended.py — +14 tests - test_settings_consolidated.py — +13 tests Modules pushed to 100%: _batch_insert, dictionary_entries, service_datasource, _llm_async_http, prompt_builder, dictionary_crud, _batch_sizer, storage/plugin, mapper.py Session: 7194→7643 tests (+449), 80%→83% raw (+3pp), 93.4%→96.9% real (+3.5pp). Remaining: 12 failures to fix + ~300 statements to reach 98% real coverage.
This commit is contained in:
@@ -987,4 +987,510 @@ class TestDatasetHealthCheckerEdge:
|
||||
# Check affected_charts mapping
|
||||
ds_101 = [d for d in result["datasets"] if d["dataset_id"] == 101][0]
|
||||
assert len(ds_101["affected_charts"]) == 2
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Remaining edge cases for 98%+ coverage
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
class TestIterLoginRootsException:
|
||||
"""_iter_login_roots exception handler (line 79-80)."""
|
||||
|
||||
def test_frames_iteration_raises(self):
|
||||
"""Exception during frames iteration is caught."""
|
||||
from src.plugins.llm_analysis.service import ScreenshotService
|
||||
svc = ScreenshotService(MagicMock())
|
||||
page = MagicMock()
|
||||
# frames accessible, but equality check raises when checked with `in`
|
||||
bad_frame = MagicMock()
|
||||
bad_frame.__eq__ = MagicMock(side_effect=Exception("frames error"))
|
||||
page.frames = [bad_frame]
|
||||
roots = svc._iter_login_roots(page)
|
||||
assert len(roots) == 1 # only the page itself
|
||||
|
||||
|
||||
class TestLaunchLoginApiV1Suffix:
|
||||
"""_launch_and_login with env.url ending in /api/v1 (line 371)."""
|
||||
|
||||
def _make_env(self):
|
||||
env = MagicMock()
|
||||
env.url = "https://superset.example.com/api/v1"
|
||||
env.username = "admin"
|
||||
env.password = "pass"
|
||||
return env
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_strips_api_v1(self):
|
||||
"""env.url ending with /api/v1 strips it."""
|
||||
from src.plugins.llm_analysis.service import ScreenshotService
|
||||
|
||||
svc = ScreenshotService(self._make_env())
|
||||
mock_page = MagicMock()
|
||||
mock_page.frames = []
|
||||
mock_page.url = "https://superset.example.com/"
|
||||
mock_page.goto = AsyncMock()
|
||||
mock_page.wait_for_load_state = AsyncMock()
|
||||
mock_page.wait_for_selector = AsyncMock()
|
||||
mock_page.wait_for_function = AsyncMock()
|
||||
mock_page.evaluate = AsyncMock()
|
||||
mock_page.add_init_script = AsyncMock()
|
||||
mock_page.set_viewport_size = AsyncMock()
|
||||
mock_page.screenshot = AsyncMock()
|
||||
mock_page.locator = MagicMock()
|
||||
mock_page.locator.count = AsyncMock(return_value=0)
|
||||
|
||||
mock_context = MagicMock()
|
||||
mock_context.new_page = AsyncMock(return_value=mock_page)
|
||||
|
||||
mock_browser = MagicMock()
|
||||
mock_browser.new_context = AsyncMock(return_value=mock_context)
|
||||
|
||||
mock_playwright = MagicMock()
|
||||
mock_playwright.chromium.launch = AsyncMock(return_value=mock_browser)
|
||||
|
||||
username_loc = MagicMock()
|
||||
username_loc.fill = AsyncMock()
|
||||
password_loc = MagicMock()
|
||||
password_loc.fill = AsyncMock()
|
||||
submit_loc = MagicMock()
|
||||
submit_loc.click = AsyncMock()
|
||||
|
||||
with patch.object(svc, '_find_login_field_locator', AsyncMock(side_effect=[username_loc, password_loc])):
|
||||
with patch.object(svc, '_find_submit_locator', AsyncMock(return_value=submit_loc)):
|
||||
with patch.object(svc, '_goto_resilient', new=AsyncMock()):
|
||||
browser, context, page = await svc._launch_and_login(mock_playwright, "42")
|
||||
assert page is not None
|
||||
|
||||
|
||||
class TestLaunchAndLoginWaitTimeouts:
|
||||
"""Wait_for timeout exception handlers (lines 443-444, 492-493, 499-500, 523-524)."""
|
||||
|
||||
def _make_env(self):
|
||||
env = MagicMock()
|
||||
env.url = "https://superset.example.com"
|
||||
env.username = "admin"
|
||||
env.password = "pass"
|
||||
return env
|
||||
|
||||
def _make_basic_mocks(self):
|
||||
mock_page = MagicMock()
|
||||
mock_page.frames = []
|
||||
mock_page.url = "https://superset.example.com/superset/dashboard/42/"
|
||||
mock_page.goto = AsyncMock()
|
||||
mock_page.wait_for_load_state = AsyncMock()
|
||||
mock_page.wait_for_selector = AsyncMock()
|
||||
mock_page.wait_for_function = AsyncMock()
|
||||
mock_page.evaluate = AsyncMock()
|
||||
mock_page.add_init_script = AsyncMock()
|
||||
mock_page.set_viewport_size = AsyncMock()
|
||||
mock_page.screenshot = AsyncMock()
|
||||
mock_page.locator = MagicMock()
|
||||
mock_page.locator.count = AsyncMock(return_value=0)
|
||||
|
||||
mock_context = MagicMock()
|
||||
mock_context.new_page = AsyncMock(return_value=mock_page)
|
||||
|
||||
mock_browser = MagicMock()
|
||||
mock_browser.new_context = AsyncMock(return_value=mock_context)
|
||||
|
||||
mock_playwright = MagicMock()
|
||||
mock_playwright.chromium.launch = AsyncMock(return_value=mock_browser)
|
||||
|
||||
return mock_page, mock_context, mock_browser, mock_playwright
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wait_for_load_state_timeout(self):
|
||||
"""wait_for_load_state('load') raising Exception is caught (line 443-444)."""
|
||||
from src.plugins.llm_analysis.service import ScreenshotService
|
||||
svc = ScreenshotService(self._make_env())
|
||||
mock_page, _, _, mock_playwright = self._make_basic_mocks()
|
||||
|
||||
# First call ("domcontentloaded") succeeds, second ("load") raises after login
|
||||
mock_page.wait_for_load_state = AsyncMock(side_effect=[None, Exception("timeout")])
|
||||
|
||||
username_loc = MagicMock()
|
||||
username_loc.fill = AsyncMock()
|
||||
password_loc = MagicMock()
|
||||
password_loc.fill = AsyncMock()
|
||||
submit_loc = MagicMock()
|
||||
submit_loc.click = AsyncMock()
|
||||
|
||||
with patch.object(svc, '_find_login_field_locator', AsyncMock(side_effect=[username_loc, password_loc])):
|
||||
with patch.object(svc, '_find_submit_locator', AsyncMock(return_value=submit_loc)):
|
||||
browser, context, page = await svc._launch_and_login(mock_playwright, "42")
|
||||
assert page is not None # should not raise
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wait_for_selector_timeouts(self):
|
||||
"""wait_for_selector timeouts during dashboard load are caught (lines 492-493, 499-500)."""
|
||||
from src.plugins.llm_analysis.service import ScreenshotService
|
||||
svc = ScreenshotService(self._make_env())
|
||||
mock_page, _, _, mock_playwright = self._make_basic_mocks()
|
||||
|
||||
# First wait_for_selector succeeds, second and third raise
|
||||
mock_page.wait_for_selector = AsyncMock(side_effect=[
|
||||
MagicMock(), # first: success
|
||||
Exception("loading timeout"), # second: loading hidden timeout
|
||||
Exception("chart canvas timeout"), # third: chart canvas timeout
|
||||
])
|
||||
|
||||
username_loc = MagicMock()
|
||||
username_loc.fill = AsyncMock()
|
||||
password_loc = MagicMock()
|
||||
password_loc.fill = AsyncMock()
|
||||
submit_loc = MagicMock()
|
||||
submit_loc.click = AsyncMock()
|
||||
|
||||
with patch.object(svc, '_find_login_field_locator', AsyncMock(side_effect=[username_loc, password_loc])):
|
||||
with patch.object(svc, '_find_submit_locator', AsyncMock(return_value=submit_loc)):
|
||||
with patch.object(svc, '_wait_for_charts_stabilized', AsyncMock()):
|
||||
browser, context, page = await svc._launch_and_login(mock_playwright, "42")
|
||||
assert page is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wait_for_function_timeout(self):
|
||||
"""wait_for_function timeout during dashboard load is caught (line 523-524)."""
|
||||
from src.plugins.llm_analysis.service import ScreenshotService
|
||||
svc = ScreenshotService(self._make_env())
|
||||
mock_page, _, _, mock_playwright = self._make_basic_mocks()
|
||||
|
||||
# wait_for_function raises timeout
|
||||
mock_page.wait_for_function = AsyncMock(side_effect=Exception("function timeout"))
|
||||
|
||||
username_loc = MagicMock()
|
||||
username_loc.fill = AsyncMock()
|
||||
password_loc = MagicMock()
|
||||
password_loc.fill = AsyncMock()
|
||||
submit_loc = MagicMock()
|
||||
submit_loc.click = AsyncMock()
|
||||
|
||||
with patch.object(svc, '_find_login_field_locator', AsyncMock(side_effect=[username_loc, password_loc])):
|
||||
with patch.object(svc, '_find_submit_locator', AsyncMock(return_value=submit_loc)):
|
||||
with patch.object(svc, '_wait_for_charts_stabilized', AsyncMock()):
|
||||
browser, context, page = await svc._launch_and_login(mock_playwright, "42")
|
||||
assert page is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_failed_after_submit(self):
|
||||
"""After submit, still on /login page with alert (line 452)."""
|
||||
from src.plugins.llm_analysis.service import ScreenshotService
|
||||
svc = ScreenshotService(self._make_env())
|
||||
mock_page, _, _, mock_playwright = self._make_basic_mocks()
|
||||
mock_page.url = "https://superset.example.com/login/"
|
||||
|
||||
# Locator for alert-danger returns 1 count and text content
|
||||
alert_locator = MagicMock()
|
||||
alert_locator.count = AsyncMock(return_value=1)
|
||||
alert_locator.text_content = AsyncMock(return_value="Invalid credentials")
|
||||
|
||||
def locator_side(selector):
|
||||
result = MagicMock()
|
||||
result.count = AsyncMock(return_value=0)
|
||||
return result
|
||||
mock_page.locator = MagicMock(side_effect=locator_side)
|
||||
mock_page.locator.return_value = alert_locator
|
||||
|
||||
username_loc = MagicMock()
|
||||
username_loc.fill = AsyncMock()
|
||||
password_loc = MagicMock()
|
||||
password_loc.fill = AsyncMock()
|
||||
submit_loc = MagicMock()
|
||||
submit_loc.click = AsyncMock()
|
||||
|
||||
with patch.object(svc, '_find_login_field_locator', AsyncMock(side_effect=[username_loc, password_loc])):
|
||||
with patch.object(svc, '_find_submit_locator', AsyncMock(return_value=submit_loc)):
|
||||
with patch.object(svc, '_goto_resilient', new=AsyncMock()):
|
||||
with pytest.raises(RuntimeError, match="Login failed"):
|
||||
await svc._launch_and_login(mock_playwright, "42")
|
||||
|
||||
|
||||
class TestCaptureDashboardChunksEdgeCoverage:
|
||||
"""Edge cases for capture_dashboard_chunks (lines 594, 597, 632)."""
|
||||
|
||||
def _make_env(self):
|
||||
env = MagicMock()
|
||||
env.url = "https://superset.example.com"
|
||||
env.username = "admin"
|
||||
env.password = "pass"
|
||||
return env
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tab_not_visible_skipped(self):
|
||||
"""Tab not visible is skipped (line 597)."""
|
||||
from src.plugins.llm_analysis.service import ScreenshotService
|
||||
|
||||
svc = ScreenshotService(self._make_env())
|
||||
|
||||
mock_page = MagicMock()
|
||||
mock_page.set_viewport_size = AsyncMock()
|
||||
mock_page.screenshot = AsyncMock()
|
||||
mock_page.url = "https://example.com/dashboard/42/"
|
||||
mock_page.frames = []
|
||||
|
||||
mock_context = MagicMock()
|
||||
mock_cdp = MagicMock()
|
||||
mock_cdp.send = AsyncMock(return_value={"data": base64.b64encode(b"img").decode()})
|
||||
mock_context.new_cdp_session = AsyncMock(return_value=mock_cdp)
|
||||
|
||||
mock_browser = MagicMock()
|
||||
mock_browser.close = AsyncMock()
|
||||
|
||||
# Tab that is NOT visible
|
||||
tab_mock = MagicMock()
|
||||
tab_mock.inner_text = AsyncMock(return_value="Tab 1")
|
||||
tab_mock.is_visible = AsyncMock(return_value=False) # not visible → skip
|
||||
tab_mock.get_attribute = AsyncMock(return_value="")
|
||||
|
||||
mock_page.locator = MagicMock()
|
||||
mock_page.locator.return_value.all = AsyncMock(return_value=[tab_mock])
|
||||
|
||||
with patch.object(svc, '_launch_and_login',
|
||||
AsyncMock(return_value=(mock_browser, mock_context, mock_page))):
|
||||
with patch.object(svc, '_wait_for_charts_stabilized', AsyncMock()):
|
||||
with patch.object(svc, '_wait_for_resize_rendered', AsyncMock()):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
results = await svc.capture_dashboard_chunks("42", tmp)
|
||||
# Tab was skipped → falls back to full page
|
||||
assert len(results) == 1
|
||||
assert results[0]["tab_name"] == "full"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_safe_tab_empty_fallback(self):
|
||||
"""Empty tab name after sanitization uses fallback (line 632)."""
|
||||
from src.plugins.llm_analysis.service import ScreenshotService
|
||||
|
||||
svc = ScreenshotService(self._make_env())
|
||||
|
||||
mock_page = MagicMock()
|
||||
mock_page.set_viewport_size = AsyncMock()
|
||||
mock_page.screenshot = AsyncMock()
|
||||
mock_page.url = "https://example.com/dashboard/42/"
|
||||
mock_page.frames = []
|
||||
|
||||
mock_context = MagicMock()
|
||||
mock_cdp = MagicMock()
|
||||
mock_cdp.send = AsyncMock(return_value={"data": base64.b64encode(b"img").decode()})
|
||||
mock_context.new_cdp_session = AsyncMock(return_value=mock_cdp)
|
||||
|
||||
mock_browser = MagicMock()
|
||||
mock_browser.close = AsyncMock()
|
||||
|
||||
# Tab with name that sanitizes to empty string
|
||||
tab_mock = MagicMock()
|
||||
tab_mock.inner_text = AsyncMock(return_value="!!!___!!!")
|
||||
tab_mock.is_visible = AsyncMock(return_value=True)
|
||||
tab_mock.get_attribute = AsyncMock(return_value="ant-tabs-tab-active")
|
||||
tab_mock.click = AsyncMock()
|
||||
|
||||
mock_page.locator = MagicMock()
|
||||
mock_page.locator.return_value.all = AsyncMock(return_value=[tab_mock])
|
||||
|
||||
with patch.object(svc, '_launch_and_login',
|
||||
AsyncMock(return_value=(mock_browser, mock_context, mock_page))):
|
||||
with patch.object(svc, '_wait_for_charts_stabilized', AsyncMock()):
|
||||
with patch.object(svc, '_wait_for_resize_rendered', AsyncMock()):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
results = await svc.capture_dashboard_chunks("42", tmp)
|
||||
assert len(results) >= 1
|
||||
|
||||
|
||||
class TestShouldRetryEdgeCases:
|
||||
"""_should_retry predicate edge cases."""
|
||||
|
||||
def test_auth_error_returns_false(self):
|
||||
"""OpenAIAuthenticationError returns False (line 962)."""
|
||||
from src.plugins.llm_analysis.service import LLMClient
|
||||
from openai import AuthenticationError as OpenAIAuthenticationError
|
||||
|
||||
# We use the actual error class
|
||||
err = OpenAIAuthenticationError(
|
||||
"Invalid API key",
|
||||
response=MagicMock(),
|
||||
body={},
|
||||
)
|
||||
# Access the internal method via the bound reference
|
||||
assert LLMClient._should_retry(err) is False
|
||||
|
||||
def test_rate_limit_returns_true(self):
|
||||
"""RateLimitError returns True."""
|
||||
from src.plugins.llm_analysis.service import LLMClient
|
||||
from openai import RateLimitError
|
||||
|
||||
err = RateLimitError(
|
||||
"rate limited",
|
||||
response=MagicMock(),
|
||||
body={},
|
||||
)
|
||||
assert LLMClient._should_retry(err) is True
|
||||
|
||||
def test_null_content_returns_false(self):
|
||||
"""Exception with 'null content' in message returns False."""
|
||||
from src.plugins.llm_analysis.service import LLMClient
|
||||
|
||||
err = RuntimeError("null content returned")
|
||||
assert LLMClient._should_retry(err) is False
|
||||
|
||||
def test_other_exception_returns_true(self):
|
||||
"""Generic exception returns True."""
|
||||
from src.plugins.llm_analysis.service import LLMClient
|
||||
|
||||
err = RuntimeError("transient error")
|
||||
assert LLMClient._should_retry(err) is True
|
||||
|
||||
|
||||
class TestGetJsonCompletionEdgeCases:
|
||||
"""Edge cases for get_json_completion."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_error_handler(self):
|
||||
"""OpenAIAuthenticationError is logged and re-raised (lines 1021-1023)."""
|
||||
from src.plugins.llm_analysis.service import LLMClient
|
||||
from src.plugins.llm_analysis.models import LLMProviderType
|
||||
from openai import AuthenticationError as OpenAIAuthenticationError
|
||||
|
||||
with patch('src.plugins.llm_analysis.service.httpx.AsyncClient'):
|
||||
with patch('src.plugins.llm_analysis.service.AsyncOpenAI') as MockOpenAI:
|
||||
mock_client_instance = MagicMock()
|
||||
MockOpenAI.return_value = mock_client_instance
|
||||
|
||||
auth_err = OpenAIAuthenticationError(
|
||||
"Invalid API key",
|
||||
response=MagicMock(),
|
||||
body={},
|
||||
)
|
||||
mock_client_instance.chat.completions.create = AsyncMock(
|
||||
side_effect=auth_err
|
||||
)
|
||||
|
||||
real = LLMClient(
|
||||
LLMProviderType.OPENAI, "sk-test", "https://api.openai.com", "gpt-4o",
|
||||
)
|
||||
with patch.object(real, '_supports_json_response_format', return_value=False):
|
||||
with pytest.raises(OpenAIAuthenticationError):
|
||||
await real.get_json_completion([{"role": "user", "content": "test"}])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rate_limit_retry_delay_regex(self):
|
||||
"""RateLimitError with retryDelay in error_str (line 1039)."""
|
||||
from src.plugins.llm_analysis.service import LLMClient
|
||||
from src.plugins.llm_analysis.models import LLMProviderType
|
||||
from openai import RateLimitError
|
||||
|
||||
with patch('src.plugins.llm_analysis.service.httpx.AsyncClient'):
|
||||
with patch('src.plugins.llm_analysis.service.AsyncOpenAI') as MockOpenAI:
|
||||
mock_client_instance = MagicMock()
|
||||
MockOpenAI.return_value = mock_client_instance
|
||||
|
||||
# RateLimitError whose str() contains retryDelay
|
||||
class RateLimitWithRetryDelay(RateLimitError):
|
||||
def __str__(self):
|
||||
return '{"retryDelay": "15s"}'
|
||||
|
||||
rate_err = RateLimitWithRetryDelay(
|
||||
"rate limit",
|
||||
response=MagicMock(),
|
||||
body={},
|
||||
)
|
||||
mock_success = MagicMock()
|
||||
mock_success.choices = [MagicMock()]
|
||||
mock_success.choices[0].message.content = '{"status": "PASS"}'
|
||||
|
||||
mock_client_instance.chat.completions.create = AsyncMock(side_effect=[
|
||||
rate_err,
|
||||
mock_success,
|
||||
])
|
||||
|
||||
real = LLMClient(
|
||||
LLMProviderType.OPENAI, "sk-test", "https://api.openai.com", "gpt-4o",
|
||||
)
|
||||
with patch.object(real, '_supports_json_response_format', return_value=False):
|
||||
result = await real.get_json_completion([{"role": "user", "content": "test"}])
|
||||
assert result["status"] == "PASS"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rate_limit_parse_error(self):
|
||||
"""Exception during retry delay parsing is caught (lines 1050-1051)."""
|
||||
from src.plugins.llm_analysis.service import LLMClient
|
||||
from src.plugins.llm_analysis.models import LLMProviderType
|
||||
from openai import RateLimitError
|
||||
|
||||
with patch('src.plugins.llm_analysis.service.httpx.AsyncClient'):
|
||||
with patch('src.plugins.llm_analysis.service.AsyncOpenAI') as MockOpenAI:
|
||||
mock_client_instance = MagicMock()
|
||||
MockOpenAI.return_value = mock_client_instance
|
||||
|
||||
# RateLimitError whose body attribute raises on access
|
||||
class ExplodingBodyRateLimit(RateLimitError):
|
||||
@property
|
||||
def body(self):
|
||||
raise ValueError("body parse error")
|
||||
|
||||
rate_err = ExplodingBodyRateLimit(
|
||||
"rate limit",
|
||||
response=MagicMock(),
|
||||
body={},
|
||||
)
|
||||
mock_success = MagicMock()
|
||||
mock_success.choices = [MagicMock()]
|
||||
mock_success.choices[0].message.content = '{"status": "PASS"}'
|
||||
|
||||
mock_client_instance.chat.completions.create = AsyncMock(side_effect=[
|
||||
rate_err,
|
||||
mock_success,
|
||||
])
|
||||
|
||||
real = LLMClient(
|
||||
LLMProviderType.OPENAI, "sk-test", "https://api.openai.com", "gpt-4o",
|
||||
)
|
||||
with patch.object(real, '_supports_json_response_format', return_value=False):
|
||||
result = await real.get_json_completion([{"role": "user", "content": "test"}])
|
||||
assert result["status"] == "PASS"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generic_exception_handler(self):
|
||||
"""Generic exception in outer handler is logged and re-raised (lines 1058-1063)."""
|
||||
from src.plugins.llm_analysis.service import LLMClient
|
||||
from src.plugins.llm_analysis.models import LLMProviderType
|
||||
|
||||
with patch('src.plugins.llm_analysis.service.httpx.AsyncClient'):
|
||||
with patch('src.plugins.llm_analysis.service.AsyncOpenAI') as MockOpenAI:
|
||||
mock_client_instance = MagicMock()
|
||||
MockOpenAI.return_value = mock_client_instance
|
||||
|
||||
mock_client_instance.chat.completions.create = AsyncMock(
|
||||
side_effect=ConnectionError("Connection refused")
|
||||
)
|
||||
|
||||
real = LLMClient(
|
||||
LLMProviderType.OPENAI, "sk-test", "https://api.openai.com", "gpt-4o",
|
||||
)
|
||||
with patch.object(real, '_supports_json_response_format', return_value=False):
|
||||
with pytest.raises(ConnectionError):
|
||||
await real.get_json_completion([{"role": "user", "content": "test"}])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_json_parse_fallback_bare_raise(self):
|
||||
"""Non-JSON, non-code-block content re-raises JSONDecodeError (line 1086)."""
|
||||
from src.plugins.llm_analysis.service import LLMClient
|
||||
from src.plugins.llm_analysis.models import LLMProviderType
|
||||
import json
|
||||
|
||||
with patch('src.plugins.llm_analysis.service.httpx.AsyncClient'):
|
||||
with patch('src.plugins.llm_analysis.service.AsyncOpenAI') as MockOpenAI:
|
||||
mock_client_instance = MagicMock()
|
||||
MockOpenAI.return_value = mock_client_instance
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content = "This is plain text, not JSON"
|
||||
|
||||
mock_client_instance.chat.completions.create = AsyncMock(
|
||||
return_value=mock_response
|
||||
)
|
||||
|
||||
real = LLMClient(
|
||||
LLMProviderType.OPENAI, "sk-test", "https://api.openai.com", "gpt-4o",
|
||||
)
|
||||
with patch.object(real, '_supports_json_response_format', return_value=False):
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
await real.get_json_completion([{"role": "user", "content": "test"}])
|
||||
# #endregion Test.LLMAnalysisService.Coverage
|
||||
|
||||
Reference in New Issue
Block a user