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:
2026-06-16 09:34:10 +03:00
parent 005ef0f5c7
commit 8a4310169b
28 changed files with 8511 additions and 1 deletions

View File

@@ -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