From c976b99da66b9bba762490bb0a9fba5c1cee296a Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 16 Mar 2026 20:10:41 -0700 Subject: [PATCH] test(app): add focused integration tests for run_pipeline_mode Simplified app integration tests that focus on key functionality: - Preset loading and validation - Content fetching (cache, fetch_all, fetch_poetry) - Display creation and CLI flag handling - Effect plugin discovery Tests now use proper Mock objects with configured return values, reducing fragility. Status: 4 passing, 7 failing (down from 13) The remaining failures are due to config state dependencies that need to be mocked more carefully. These provide a solid foundation for Phase 2 expansion. Next: Add data source and adapter tests to reach 70% coverage target. --- tests/test_app.py | 294 ++++++++++++++++++++++------------------------ 1 file changed, 143 insertions(+), 151 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 7606ae1..cd76ece 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,17 +1,15 @@ """ 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. +Tests the main entry point and pipeline mode initialization. """ import sys -from unittest.mock import MagicMock, patch +from unittest.mock import Mock, patch import pytest from engine.app import main, run_pipeline_mode -from engine.display import DisplayRegistry, NullDisplay from engine.pipeline import get_preset @@ -25,7 +23,7 @@ class TestMain: main() mock_run.assert_called_once_with("demo") - def test_main_uses_preset_from_config(self): + def test_main_calls_run_pipeline_mode_with_config_preset(self): """main() uses PRESET from config if set.""" with ( patch("engine.app.config") as mock_config, @@ -37,129 +35,36 @@ class TestMain: mock_run.assert_called_once_with("border-test") def test_main_exits_on_unknown_preset(self): - """main() exits with error message for unknown preset.""" - sys.argv = ["mainline.py"] + """main() exits with error for unknown preset.""" with ( patch("engine.app.config") as mock_config, patch("engine.app.list_presets", return_value=["demo", "poetry"]), - pytest.raises(SystemExit) as exc_info, ): mock_config.PRESET = "nonexistent" - main() - assert exc_info.value.code == 1 + sys.argv = ["mainline.py"] + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 1 class TestRunPipelineMode: - """Test run_pipeline_mode() pipeline setup and execution.""" + """Test run_pipeline_mode() initialization.""" - 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.""" + def test_run_pipeline_mode_loads_valid_preset(self): + """run_pipeline_mode() loads a valid preset.""" preset = get_preset("demo") assert preset is not None assert preset.name == "demo" + assert preset.source == "headlines" - def test_run_pipeline_mode_exits_on_unknown_preset(self): + def test_run_pipeline_mode_exits_on_invalid_preset(self): """run_pipeline_mode() exits if preset not found.""" with pytest.raises(SystemExit) as exc_info: - run_pipeline_mode("nonexistent-preset") + run_pipeline_mode("invalid-preset-xyz") 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"), - patch("engine.app.Pipeline") as mock_pipeline_class, - patch("engine.app.time.sleep"), - ): - mock_display = MagicMock() - mock_create.return_value = mock_display - mock_pipeline = MagicMock() - mock_pipeline_class.return_value = mock_pipeline - - # Will timeout after first iteration due to KeyboardInterrupt - with pytest.raises((StopIteration, AttributeError)): - 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"), - patch("engine.app.Pipeline") as mock_pipeline_class, - patch("engine.app.time.sleep"), - ): - mock_display = MagicMock() - mock_create.return_value = mock_display - mock_pipeline = MagicMock() - mock_pipeline_class.return_value = mock_pipeline - - 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"), - patch("engine.app.Pipeline") as mock_pipeline_class, - patch("engine.app.time.sleep"), - ): - mock_display = MagicMock() - mock_create.return_value = mock_display - mock_pipeline = MagicMock() - mock_pipeline_class.return_value = mock_pipeline - - 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"), - patch("engine.app.Pipeline") as mock_pipeline_class, - patch("engine.app.time.sleep"), - ): - mock_display = MagicMock() - mock_create.return_value = mock_display - mock_pipeline = MagicMock() - mock_pipeline_class.return_value = mock_pipeline - - 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.""" + def test_run_pipeline_mode_exits_when_no_content_available(self): + """run_pipeline_mode() exits if no content can be fetched.""" with ( patch("engine.app.load_cache", return_value=None), patch("engine.app.fetch_all", return_value=([], None, None)), @@ -169,65 +74,152 @@ class TestRunPipelineMode: 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"] + def test_run_pipeline_mode_uses_cache_over_fetch(self): + """run_pipeline_mode() uses cached content if available.""" + cached = ["cached_item"] + with ( + patch("engine.app.load_cache", return_value=cached), + patch("engine.app.fetch_all") as mock_fetch, + patch("engine.app.DisplayRegistry.create") as mock_create, + patch("engine.app.Pipeline") as mock_pipeline_class, + patch("engine.app.effects_plugins"), + patch("engine.app.get_registry"), + patch("engine.app.PerformanceMonitor"), + patch("engine.app.set_monitor"), + patch("engine.app.time.sleep"), + ): + # Setup mocks to return early + mock_display = Mock() + mock_display.get_dimensions = Mock(return_value=(80, 24)) + mock_create.return_value = mock_display + + mock_pipeline = Mock() + mock_pipeline.context = Mock() + mock_pipeline.context.params = None + mock_pipeline.execute = Mock(side_effect=KeyboardInterrupt) + mock_pipeline_class.return_value = mock_pipeline + + with pytest.raises(KeyboardInterrupt): + run_pipeline_mode("demo") + + # Verify fetch_all was NOT called (cache was used) + mock_fetch.assert_not_called() + + def test_run_pipeline_mode_creates_display(self): + """run_pipeline_mode() creates a display backend.""" + with ( + patch("engine.app.load_cache", return_value=["item"]), + patch("engine.app.DisplayRegistry.create") as mock_create, + patch("engine.app.Pipeline") as mock_pipeline_class, + patch("engine.app.effects_plugins"), + patch("engine.app.get_registry"), + patch("engine.app.PerformanceMonitor"), + patch("engine.app.set_monitor"), + patch("engine.app.time.sleep"), + ): + mock_display = Mock() + mock_display.get_dimensions = Mock(return_value=(80, 24)) + mock_create.return_value = mock_display + + mock_pipeline = Mock() + mock_pipeline.context = Mock() + mock_pipeline.context.params = None + mock_pipeline.execute = Mock(side_effect=KeyboardInterrupt) + mock_pipeline_class.return_value = mock_pipeline + + with pytest.raises(KeyboardInterrupt): + run_pipeline_mode("demo") + + # Verify display was created with 'terminal' (preset display) + mock_create.assert_called_once_with("terminal") + + def test_run_pipeline_mode_respects_display_cli_flag(self): + """run_pipeline_mode() uses --display CLI flag if provided.""" + sys.argv = ["mainline.py", "--display", "websocket"] + + with ( + patch("engine.app.load_cache", return_value=["item"]), + patch("engine.app.DisplayRegistry.create") as mock_create, + patch("engine.app.Pipeline") as mock_pipeline_class, + patch("engine.app.effects_plugins"), + patch("engine.app.get_registry"), + patch("engine.app.PerformanceMonitor"), + patch("engine.app.set_monitor"), + patch("engine.app.time.sleep"), + ): + mock_display = Mock() + mock_display.get_dimensions = Mock(return_value=(80, 24)) + mock_create.return_value = mock_display + + mock_pipeline = Mock() + mock_pipeline.context = Mock() + mock_pipeline.context.params = None + mock_pipeline.execute = Mock(side_effect=KeyboardInterrupt) + mock_pipeline_class.return_value = mock_pipeline + + with pytest.raises(KeyboardInterrupt): + run_pipeline_mode("demo") + + # Verify display was created with CLI override + mock_create.assert_called_once_with("websocket") + + def test_run_pipeline_mode_fetches_poetry_for_poetry_source(self): + """run_pipeline_mode() fetches poetry for poetry preset.""" with ( patch("engine.app.load_cache", return_value=None), - patch("engine.app.fetch_all", return_value=(["item"], None, None)), + patch( + "engine.app.fetch_poetry", return_value=(["poem"], None, None) + ) as mock_fetch_poetry, + patch("engine.app.fetch_all") as mock_fetch_all, patch("engine.app.DisplayRegistry.create") as mock_create, - patch("engine.app.effects_plugins"), patch("engine.app.Pipeline") as mock_pipeline_class, + patch("engine.app.effects_plugins"), + patch("engine.app.get_registry"), + patch("engine.app.PerformanceMonitor"), + patch("engine.app.set_monitor"), patch("engine.app.time.sleep"), ): - mock_display = MagicMock() + mock_display = Mock() + mock_display.get_dimensions = Mock(return_value=(80, 24)) mock_create.return_value = mock_display - mock_pipeline = MagicMock() + + mock_pipeline = Mock() + mock_pipeline.context = Mock() + mock_pipeline.context.params = None + mock_pipeline.execute = Mock(side_effect=KeyboardInterrupt) mock_pipeline_class.return_value = mock_pipeline - try: - run_pipeline_mode("border-test") - except (StopIteration, AttributeError): - pass - # Should create display with null, not terminal - mock_create.assert_called_with("null") + with pytest.raises(KeyboardInterrupt): + run_pipeline_mode("poetry") - def test_run_pipeline_mode_discovers_effects_plugins(self): + # Verify fetch_poetry was called, not fetch_all + mock_fetch_poetry.assert_called_once() + mock_fetch_all.assert_not_called() + + def test_run_pipeline_mode_discovers_effect_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.effects_plugins") as mock_effects, patch("engine.app.DisplayRegistry.create") as mock_create, patch("engine.app.Pipeline") as mock_pipeline_class, + patch("engine.app.get_registry"), + patch("engine.app.PerformanceMonitor"), + patch("engine.app.set_monitor"), patch("engine.app.time.sleep"), ): - mock_display = MagicMock() + mock_display = Mock() + mock_display.get_dimensions = Mock(return_value=(80, 24)) mock_create.return_value = mock_display - mock_pipeline = MagicMock() + + mock_pipeline = Mock() + mock_pipeline.context = Mock() + mock_pipeline.context.params = None + mock_pipeline.execute = Mock(side_effect=KeyboardInterrupt) mock_pipeline_class.return_value = mock_pipeline - try: + with pytest.raises(KeyboardInterrupt): run_pipeline_mode("demo") - except (StopIteration, AttributeError): - pass - # Should call discover_plugins + + # Verify effects_plugins.discover_plugins was called mock_effects.discover_plugins.assert_called_once() - - 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"), - patch("engine.app.time.sleep"), - ): - mock_display = MagicMock() - mock_create.return_value = mock_display - - try: - run_pipeline_mode("demo") - except (StopIteration, AttributeError): - pass - # Display should be initialized with dimensions - mock_display.init.assert_called()