Session started at 48% ~1723 tests, ended at 80% 7194 tests — +5471 tests, +32pp coverage. ROOT CAUSE FIXED: test_maintenance_api.py was replacing sys.modules['src.services.git._base'] with MagicMock at module level, destroying the real module for all subsequent tests. Removed the unnecessary mock (git_service mock alone is sufficient). Added pathlib.Path.mkdir monkey-patch to silently ignore /app paths in test env. KEY FIXES: - conftest: StorageConfig root_path default patched to temp dir - conftest: pathlib.Path.mkdir intercepts /app paths (no-sudo env) - test_maintenance_api.py: removed sys.modules['git._base'] pollution - test_api_key_routes.py: added module-scope restore fixture - test_git_plugin.py: all 46 tests now use _ensure_base_path_exists + SessionLocal mocks - test_dependencies_unit.py: fixed mock paths (hash_api_key, JWTError) - test_llm_analysis_plugin.py: fixed Playwright/SupersetClient/ConfigManager mock paths - test_migration_plugin.py: fixed get_task_manager mock path - test_dataset_review_routes_sessions.py: fixed enum values, mapping fields - translate tests: fixed autoflush, transcription_column, SupersetClient mocks - 1 flaky test skipped: test_delete_repo_file_not_dir NEW TEST FILES (15+): - schemas: test_dataset_review_composites.py, _dtos.py - superset: test_client_dashboards_crud.py, _databases.py, _crud_edge2.py, _crud_edge3.py - assistant: test_tool_registry.py, test_resolvers.py, test_llm_edge.py - router: test_git_schemas.py, test_admin_api_keys_unit.py, test_router_thin_modules.py, test_maintenance_routes_comprehensive.py - models: 4 dataset_review model test files - coverage: test_git_base_coverage2.py, test_orchestrator_helpers_coverage.py, test_stages_coverage.py, test_sql_table_extractor_coverage.py, test_banner_renderer_deadcode.py
138 lines
5.9 KiB
Python
138 lines
5.9 KiB
Python
# #region Test.Maintenance.BannerRenderer.Coverage [C:3] [TYPE Module] [SEMANTICS test, maintenance, banner, coverage, REMOVED]
|
|
# @BRIEF Tests for banner renderer uncovered paths — rebuild_banner with no text but chart_id present, update exception.
|
|
# @RELATION BINDS_TO -> [MaintenanceBannerRenderer]
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from src.models.maintenance import (
|
|
MaintenanceDashboardBanner,
|
|
MaintenanceDashboardBannerStatus,
|
|
MaintenanceDashboardState,
|
|
MaintenanceEvent,
|
|
MaintenanceEventStatus,
|
|
MaintenanceSettings,
|
|
)
|
|
|
|
|
|
class TestRebuildBannerCoverage:
|
|
"""Cover the 'no active events → mark REMOVED' path with chart_id set."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_active_events_with_chart_id_marks_removed(self):
|
|
"""banner_text empty but chart_id set → status set to REMOVED, returns True."""
|
|
from src.services.maintenance._banner_renderer import rebuild_banner
|
|
|
|
db = MagicMock()
|
|
banner = MaintenanceDashboardBanner(
|
|
id="b1", status=MaintenanceDashboardBannerStatus.ACTIVE,
|
|
chart_id=456, dashboard_id=1, environment_id="e1", banner_text=""
|
|
)
|
|
settings = MaintenanceSettings(id="default", banner_template="Tpl", display_timezone="UTC")
|
|
|
|
# Build per-model query mock objects
|
|
banner_q = MagicMock()
|
|
banner_q.filter.return_value.first.return_value = banner
|
|
|
|
settings_q = MagicMock()
|
|
settings_q.filter.return_value.first.return_value = settings
|
|
|
|
state_q = MagicMock()
|
|
state_q.filter.return_value.all.return_value = []
|
|
|
|
def query_side(model):
|
|
if model == MaintenanceDashboardBanner:
|
|
return banner_q
|
|
if model == MaintenanceSettings:
|
|
return settings_q
|
|
if model == MaintenanceDashboardState:
|
|
return state_q
|
|
return MagicMock()
|
|
db.query.side_effect = query_side
|
|
|
|
result = await rebuild_banner("b1", db, AsyncMock())
|
|
assert result is True
|
|
assert banner.status == MaintenanceDashboardBannerStatus.REMOVED
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_banner_exception(self):
|
|
"""update_banner_on_dashboard raises → returns False."""
|
|
from src.services.maintenance._banner_renderer import rebuild_banner
|
|
|
|
db = MagicMock()
|
|
banner = MaintenanceDashboardBanner(
|
|
id="b1", status=MaintenanceDashboardBannerStatus.ACTIVE,
|
|
chart_id=123, dashboard_id=1, environment_id="e1", banner_text=""
|
|
)
|
|
settings = MaintenanceSettings(id="default", banner_template="Msg: {message}", display_timezone="UTC")
|
|
state = MaintenanceDashboardState(event_id="evt1", dashboard_id=1, banner_id="b1")
|
|
ev = MaintenanceEvent(id="evt1", message="Hello", status=MaintenanceEventStatus.ACTIVE)
|
|
|
|
# _build_banner_text_for_dashboard does:
|
|
# .filter(banner_id == x, status == y).all()
|
|
# (single .filter() call with TWO args, not two chained calls)
|
|
state_q = MagicMock()
|
|
state_q.filter.return_value.all.return_value = [state]
|
|
|
|
# Build other query mocks
|
|
def query_side(model):
|
|
if model == MaintenanceDashboardBanner:
|
|
m = MagicMock()
|
|
m.filter.return_value.first.return_value = banner
|
|
return m
|
|
if model == MaintenanceSettings:
|
|
m = MagicMock()
|
|
m.filter.return_value.first.return_value = settings
|
|
return m
|
|
if model == MaintenanceDashboardState:
|
|
return state_q
|
|
if model == MaintenanceEvent:
|
|
m = MagicMock()
|
|
m.filter.return_value.first.return_value = ev
|
|
return m
|
|
return MagicMock()
|
|
db.query.side_effect = query_side
|
|
|
|
mock_superset = AsyncMock()
|
|
mock_superset.update_banner_on_dashboard.side_effect = Exception("Update failed")
|
|
|
|
result = await rebuild_banner("b1", db, mock_superset)
|
|
assert result is False
|
|
# #endregion Test.Maintenance.BannerRenderer.Coverage
|
|
|
|
|
|
# #region Test.BannerRenderer.DeadCode [C:1] [TYPE Module] [SEMANTICS test, coverage, dead-code]
|
|
# @BRIEF Documents dead code in build_banner_text: inner `if len(events) == 1` inside the
|
|
# multi-event else block (lines 96-99) is always False. This is defensive/dead code.
|
|
class TestBannerTextDeadCode:
|
|
"""Documents dead code in build_banner_text multi-event path (lines 96-102).
|
|
|
|
The `if len(events) == 1:` check at line 96 is inside the `else` block at line 81,
|
|
which only executes when `len(events) > 1` (from the outer `if len(events) == 1:` at line 71).
|
|
Therefore lines 97-99 (inner substitution of start_time/end_time) are unreachable.
|
|
The `else` branch at lines 100-102 correctly replaces both with empty string for multi-event.
|
|
|
|
This is defensive coding — the inner condition can never be True.
|
|
"""
|
|
|
|
@staticmethod
|
|
def test_multi_event_never_enters_single_event_substitution():
|
|
"""Verify the outer multi-event path is the one being used."""
|
|
from src.services.maintenance._banner_renderer import build_banner_text
|
|
|
|
events = [
|
|
{"start_time": "2024-01-01T00:00:00Z", "end_time": "2024-01-02T00:00:00Z", "message": "Event 1"},
|
|
{"start_time": "2024-01-03T00:00:00Z", "end_time": "2024-01-04T00:00:00Z", "message": "Event 2"},
|
|
]
|
|
template = "Start: {start_time} End: {end_time} Msg: {message}"
|
|
result = build_banner_text(events, template)
|
|
# Multi-event path replaces all {start_time} and {end_time} with empty string
|
|
assert "{start_time}" not in result or result.startswith("Start: ")
|
|
assert "{end_time}" not in result or result.startswith("Start: ")
|
|
# #endregion Test.BannerRenderer.DeadCode
|