""" Deep coverage tests for notifications/templates.py. Targets uncovered paths: - _get_jinja_env: template dir missing, Environment init exception - format: Jinja2 render exception fallback, RATE_LIMIT_WARNING (no template file) - get_required_context: meta parsing, loader=None branch, exception branch """ from unittest.mock import patch, MagicMock class TestGetJinjaEnvMissingTemplateDir: """Cover _get_jinja_env when template directory doesn't exist.""" def test_returns_none_when_template_dir_missing(self): from local_deep_research.notifications.templates import ( NotificationTemplate, ) from pathlib import Path NotificationTemplate._jinja_env = None # Patch os.path operations at the Path level original_exists = Path.exists def fake_exists(self): if str(self).endswith("templates"): return False return original_exists(self) try: with patch.object(Path, "exists", fake_exists): result = NotificationTemplate._get_jinja_env() assert result is None finally: NotificationTemplate._jinja_env = None class TestGetJinjaEnvInitException: """Cover _get_jinja_env when Environment() raises.""" def test_returns_none_on_environment_init_error(self): from local_deep_research.notifications.templates import ( NotificationTemplate, ) NotificationTemplate._jinja_env = None with patch( "local_deep_research.notifications.templates.Environment", side_effect=Exception("Jinja2 init failed"), ): result = NotificationTemplate._get_jinja_env() assert result is None # Cleanup NotificationTemplate._jinja_env = None class TestFormatJinja2RenderException: """Cover format() when Jinja2 template rendering raises.""" def test_falls_back_when_jinja2_render_fails(self): from local_deep_research.notifications.templates import ( NotificationTemplate, EventType, ) NotificationTemplate._jinja_env = None mock_env = MagicMock() mock_template = MagicMock() mock_template.render.side_effect = Exception("render failed") mock_env.get_template.return_value = mock_template with patch.object( NotificationTemplate, "_get_jinja_env", return_value=mock_env ): result = NotificationTemplate.format( EventType.RESEARCH_COMPLETED, {"query": "test"} ) # Should fall back to _get_fallback_template assert "title" in result assert "body" in result assert "Research Completed" in result["title"] NotificationTemplate._jinja_env = None class TestFormatNoTemplateFile: """Cover format() when event_type not in TEMPLATE_FILES.""" def test_returns_generic_notification_for_unmapped_event(self): from local_deep_research.notifications.templates import ( NotificationTemplate, EventType, ) # RATE_LIMIT_WARNING is not in TEMPLATE_FILES result = NotificationTemplate.format( EventType.RATE_LIMIT_WARNING, {"detail": "too many requests"} ) assert "title" in result assert "body" in result assert "rate_limit_warning" in result["title"] class TestFormatJinja2Unavailable: """Cover format() when _get_jinja_env returns None.""" def test_uses_fallback_when_jinja2_unavailable(self): from local_deep_research.notifications.templates import ( NotificationTemplate, EventType, ) with patch.object( NotificationTemplate, "_get_jinja_env", return_value=None ): result = NotificationTemplate.format( EventType.RESEARCH_FAILED, {"error": "timeout"} ) assert "title" in result assert "body" in result assert "Research Failed" in result["title"] assert "Details" in result["body"] class TestFormatCustomTemplateSafeContext: """Cover safe_context conversion in custom_template branch.""" def test_none_values_become_empty_string(self): from local_deep_research.notifications.templates import ( NotificationTemplate, EventType, ) custom_template = { "title": "Title: {value}", "body": "Body: {other}", } context = {"value": None, "other": 42} result = NotificationTemplate.format( EventType.TEST, context, custom_template=custom_template ) assert result["title"] == "Title: " assert result["body"] == "Body: 42" class TestGetRequiredContextMetaParsing: """Cover get_required_context Jinja2 meta parsing.""" def test_extracts_variables_from_template(self): from local_deep_research.notifications.templates import ( NotificationTemplate, EventType, ) # Use a real event type that has a template result = NotificationTemplate.get_required_context( EventType.RESEARCH_COMPLETED ) # Should return a list (possibly empty if template dir missing) assert isinstance(result, list) def test_returns_empty_when_jinja_env_none(self): from local_deep_research.notifications.templates import ( NotificationTemplate, EventType, ) with patch.object( NotificationTemplate, "_get_jinja_env", return_value=None ): result = NotificationTemplate.get_required_context(EventType.TEST) assert result == [] def test_returns_empty_on_template_parse_exception(self): from local_deep_research.notifications.templates import ( NotificationTemplate, EventType, ) mock_env = MagicMock() mock_env.get_template.side_effect = Exception("template parse error") with patch.object( NotificationTemplate, "_get_jinja_env", return_value=mock_env ): result = NotificationTemplate.get_required_context( EventType.RESEARCH_COMPLETED ) assert result == [] def test_meta_parsing_with_loader_none(self): """Cover the branch where template.environment.loader is None.""" from local_deep_research.notifications.templates import ( NotificationTemplate, EventType, ) mock_env = MagicMock() mock_template = MagicMock() mock_template.environment.loader = None mock_env.get_template.return_value = mock_template with patch.object( NotificationTemplate, "_get_jinja_env", return_value=mock_env ): result = NotificationTemplate.get_required_context( EventType.RESEARCH_COMPLETED ) assert isinstance(result, list)