From b20b4973b5bc1bc8e5f15f633f4f977ec07d11bd Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 16 Mar 2026 20:06:56 -0700 Subject: [PATCH] test: add foundation for app.py integration tests (Phase 2 WIP) Added initial integration test suite structure for engine/app.py covering: - main() entry point preset loading - run_pipeline_mode() pipeline setup - Content fetching for different sources - Display initialization - CLI flag overrides STATUS: Tests are currently failing because imports in run_pipeline_mode() are internal to the function, making them difficult to patch. This requires: 1. Refactoring imports to module level, OR 2. Using more sophisticated patching strategies (patch at import time) This provides a foundation for Phase 2. Tests will be fixed in next iteration. PHASE 2 TASKS: - Fix app.py test patching issues - Add data source tests (currently 34% coverage) - Expand adapter tests (currently 50% coverage) - Target: 70% coverage on critical paths --- tests/test_app.py | 286 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 tests/test_app.py diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..3670ab1 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,286 @@ +""" +Integration tests for engine/app.py - pipeline orchestration. + +Tests the main entry point and pipeline mode initialization, +including preset loading, display creation, and stage setup. +""" + +import sys +from unittest.mock import MagicMock, patch + +import pytest + +from engine.app import main, run_pipeline_mode +from engine.display import DisplayRegistry, NullDisplay +from engine.pipeline import get_preset + + +class TestMain: + """Test main() entry point.""" + + def test_main_calls_run_pipeline_mode_with_default_preset(self): + """main() runs default preset (demo) when no args provided.""" + with patch("engine.app.run_pipeline_mode") as mock_run: + sys.argv = ["mainline.py"] + main() + mock_run.assert_called_once_with("demo") + + def test_main_uses_preset_from_config(self): + """main() uses PRESET from config if set.""" + with ( + patch("engine.app.config") as mock_config, + patch("engine.app.run_pipeline_mode") as mock_run, + ): + mock_config.PRESET = "border-test" + sys.argv = ["mainline.py"] + main() + mock_run.assert_called_once_with("border-test") + + def test_main_exits_on_unknown_preset(self): + """main() exits with error message for unknown preset.""" + with ( + patch("engine.app.run_pipeline_mode"), + patch("engine.app.list_presets", return_value=["demo", "poetry"]), + pytest.raises(SystemExit) as exc_info, + patch("engine.app.config") as mock_config, + ): + sys.argv = ["mainline.py"] + mock_config.PRESET = "nonexistent" + main() + assert exc_info.value.code == 1 + + def test_main_handles_pipeline_diagram_flag(self): + """main() generates pipeline diagram if PIPELINE_DIAGRAM is set.""" + with ( + patch("engine.app.config") as mock_config, + patch( + "engine.app.generate_pipeline_diagram", return_value="diagram" + ) as mock_gen, + ): + mock_config.PIPELINE_DIAGRAM = True + with patch("builtins.print") as mock_print: + main() + mock_gen.assert_called_once() + mock_print.assert_called_with("diagram") + + +class TestRunPipelineMode: + """Test run_pipeline_mode() pipeline setup and execution.""" + + def setup_method(self): + """Setup for each test.""" + DisplayRegistry._backends = {} + DisplayRegistry._initialized = False + DisplayRegistry.register("null", NullDisplay) + + def test_run_pipeline_mode_loads_preset(self): + """run_pipeline_mode() loads the specified preset.""" + preset = get_preset("demo") + assert preset is not None + assert preset.name == "demo" + + def test_run_pipeline_mode_exits_on_unknown_preset(self): + """run_pipeline_mode() exits if preset not found.""" + with pytest.raises(SystemExit) as exc_info: + run_pipeline_mode("nonexistent-preset") + assert exc_info.value.code == 1 + + def test_run_pipeline_mode_fetches_content_for_headlines(self): + """run_pipeline_mode() fetches content for headlines preset.""" + with ( + patch("engine.app.load_cache", return_value=None), + patch( + "engine.app.fetch_all", return_value=(["item1", "item2"], None, None) + ), + patch("engine.app.DisplayRegistry.create") as mock_create, + patch("engine.app.effects_plugins"), + ): + mock_display = MagicMock() + mock_create.return_value = mock_display + + with ( + patch("engine.app.Pipeline") as mock_pipeline_class, + patch("engine.app.time.sleep"), + pytest.raises((StopIteration, AttributeError)), + ): + mock_pipeline = MagicMock() + mock_pipeline_class.return_value = mock_pipeline + # Will timeout after first iteration + run_pipeline_mode("demo") + + def test_run_pipeline_mode_handles_empty_source(self): + """run_pipeline_mode() handles empty source without fetching.""" + with ( + patch("engine.app.fetch_all") as mock_fetch, + patch("engine.app.DisplayRegistry.create") as mock_create, + patch("engine.app.effects_plugins"), + ): + mock_display = MagicMock() + mock_create.return_value = mock_display + + with patch("engine.app.Pipeline") as mock_pipeline_class: + mock_pipeline = MagicMock() + mock_pipeline_class.return_value = mock_pipeline + with patch("engine.app.time.sleep"): + try: + run_pipeline_mode("border-test") + except (StopIteration, AttributeError): + pass + # Should NOT call fetch_all for empty source + mock_fetch.assert_not_called() + + def test_run_pipeline_mode_uses_cached_content(self): + """run_pipeline_mode() uses cached content if available.""" + cached_items = ["cached1", "cached2"] + with ( + patch("engine.app.load_cache", return_value=cached_items), + patch("engine.app.fetch_all") as mock_fetch, + patch("engine.app.DisplayRegistry.create") as mock_create, + patch("engine.app.effects_plugins"), + ): + mock_display = MagicMock() + mock_create.return_value = mock_display + + with patch("engine.app.Pipeline") as mock_pipeline_class: + mock_pipeline = MagicMock() + mock_pipeline_class.return_value = mock_pipeline + with patch("engine.app.time.sleep"): + try: + run_pipeline_mode("demo") + except (StopIteration, AttributeError): + pass + # Should NOT call fetch_all when cache exists + mock_fetch.assert_not_called() + + def test_run_pipeline_mode_fetches_poetry_for_poetry_preset(self): + """run_pipeline_mode() fetches poetry for poetry source.""" + with ( + patch("engine.app.load_cache", return_value=None), + patch("engine.app.fetch_poetry", return_value=(["poem1"], None, None)), + patch("engine.app.fetch_all") as mock_fetch_all, + patch("engine.app.DisplayRegistry.create") as mock_create, + patch("engine.app.effects_plugins"), + ): + mock_display = MagicMock() + mock_create.return_value = mock_display + + with patch("engine.app.Pipeline") as mock_pipeline_class: + mock_pipeline = MagicMock() + mock_pipeline_class.return_value = mock_pipeline + with patch("engine.app.time.sleep"): + try: + run_pipeline_mode("poetry") + except (StopIteration, AttributeError): + pass + # Should NOT call fetch_all for poetry preset + mock_fetch_all.assert_not_called() + + def test_run_pipeline_mode_exits_when_no_content(self): + """run_pipeline_mode() exits if no content available.""" + with ( + patch("engine.app.load_cache", return_value=None), + patch("engine.app.fetch_all", return_value=([], None, None)), + patch("engine.app.effects_plugins"), + ): + with pytest.raises(SystemExit) as exc_info: + run_pipeline_mode("demo") + assert exc_info.value.code == 1 + + def test_run_pipeline_mode_display_flag_overrides_preset(self): + """run_pipeline_mode() uses CLI --display flag over preset.""" + sys.argv = ["mainline.py", "--preset", "border-test", "--display", "null"] + with ( + patch("engine.app.load_cache", return_value=None), + patch("engine.app.fetch_all", return_value=(["item"], None, None)), + patch("engine.app.DisplayRegistry.create") as mock_create, + patch("engine.app.effects_plugins"), + ): + mock_display = MagicMock() + mock_create.return_value = mock_display + + with patch("engine.app.Pipeline") as mock_pipeline_class: + mock_pipeline = MagicMock() + mock_pipeline_class.return_value = mock_pipeline + with patch("engine.app.time.sleep"): + try: + run_pipeline_mode("border-test") + except (StopIteration, AttributeError): + pass + # Should create display with null, not terminal + mock_create.assert_called_with("null") + + def test_run_pipeline_mode_discovers_effects_plugins(self): + """run_pipeline_mode() discovers available effect plugins.""" + with ( + patch("engine.app.effects_plugins") as mock_effects, + patch("engine.app.load_cache", return_value=["item"]), + patch("engine.app.DisplayRegistry.create") as mock_create, + patch("engine.app.Pipeline") as mock_pipeline_class, + ): + mock_display = MagicMock() + mock_create.return_value = mock_display + mock_pipeline = MagicMock() + mock_pipeline_class.return_value = mock_pipeline + with patch("engine.app.time.sleep"): + try: + run_pipeline_mode("demo") + except (StopIteration, AttributeError): + pass + # Should call discover_plugins + mock_effects.discover_plugins.assert_called_once() + + def test_run_pipeline_mode_creates_pipeline_with_preset_config(self): + """run_pipeline_mode() creates pipeline with preset configuration.""" + with ( + patch("engine.app.load_cache", return_value=["item"]), + patch("engine.app.DisplayRegistry.create") as mock_create, + patch("engine.app.effects_plugins"), + patch("engine.app.Pipeline") as mock_pipeline_class, + ): + mock_display = MagicMock() + mock_create.return_value = mock_display + mock_pipeline = MagicMock() + mock_pipeline_class.return_value = mock_pipeline + with patch("engine.app.time.sleep"): + try: + run_pipeline_mode("demo") + except (StopIteration, AttributeError): + pass + # Verify Pipeline was created (call may vary, but it should be called) + assert mock_pipeline_class.called + + def test_run_pipeline_mode_initializes_display(self): + """run_pipeline_mode() initializes display with dimensions.""" + with ( + patch("engine.app.load_cache", return_value=["item"]), + patch("engine.app.DisplayRegistry.create") as mock_create, + patch("engine.app.effects_plugins"), + patch("engine.app.Pipeline"), + ): + mock_display = MagicMock() + mock_create.return_value = mock_display + with patch("engine.app.time.sleep"): + try: + run_pipeline_mode("demo") + except (StopIteration, AttributeError): + pass + # Display should be initialized with dimensions + mock_display.init.assert_called() + + def test_run_pipeline_mode_builds_pipeline_before_initialize(self): + """run_pipeline_mode() calls pipeline.build() before initialize().""" + with ( + patch("engine.app.load_cache", return_value=["item"]), + patch("engine.app.DisplayRegistry.create"), + patch("engine.app.effects_plugins"), + patch("engine.app.Pipeline") as mock_pipeline_class, + ): + mock_pipeline = MagicMock() + mock_pipeline_class.return_value = mock_pipeline + with patch("engine.app.time.sleep"): + try: + run_pipeline_mode("demo") + except (StopIteration, AttributeError): + pass + # Build should be called + assert mock_pipeline.build.called