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
This commit is contained in:
2026-03-16 20:06:56 -07:00
parent 73ca72d920
commit b20b4973b5

286
tests/test_app.py Normal file
View File

@@ -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