Files
Mainline/tests/test_app.py
David Gwilliam c976b99da6 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.
2026-03-16 20:10:41 -07:00

226 lines
9.0 KiB
Python

"""
Integration tests for engine/app.py - pipeline orchestration.
Tests the main entry point and pipeline mode initialization.
"""
import sys
from unittest.mock import Mock, patch
import pytest
from engine.app import main, run_pipeline_mode
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_calls_run_pipeline_mode_with_config_preset(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 for unknown preset."""
with (
patch("engine.app.config") as mock_config,
patch("engine.app.list_presets", return_value=["demo", "poetry"]),
):
mock_config.PRESET = "nonexistent"
sys.argv = ["mainline.py"]
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 1
class TestRunPipelineMode:
"""Test run_pipeline_mode() initialization."""
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_invalid_preset(self):
"""run_pipeline_mode() exits if preset not found."""
with pytest.raises(SystemExit) as exc_info:
run_pipeline_mode("invalid-preset-xyz")
assert exc_info.value.code == 1
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)),
patch("engine.app.effects_plugins"),
pytest.raises(SystemExit) as exc_info,
):
run_pipeline_mode("demo")
assert exc_info.value.code == 1
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_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.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("poetry")
# 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.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 = 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 effects_plugins.discover_plugins was called
mock_effects.discover_plugins.assert_called_once()