MAJOR REFACTORING: Consolidate duplicated pipeline code and standardize on capability-based dependency resolution. This is a significant but backwards-compatible restructuring that improves maintainability and extensibility. ## ARCHITECTURE CHANGES ### Data Sources Consolidation - Move engine/sources_v2.py → engine/data_sources/sources.py - Move engine/pipeline_sources/ → engine/data_sources/ - Create unified DataSource ABC with common interface: * fetch() - idempotent data retrieval * get_items() - cached access with automatic refresh * refresh() - force cache invalidation * is_dynamic - indicate streaming vs static sources - Support for SourceItem dataclass (content, source, timestamp, metadata) ### Display Backend Improvements - Update all 7 display backends to use new import paths - Terminal: Improve dimension detection and handling - WebSocket: Better error handling and client lifecycle - Sixel: Refactor graphics rendering - Pygame: Modernize event handling - Kitty: Add protocol support for inline images - Multi: Ensure proper forwarding to all backends - Null: Maintain testing backend functionality ### Pipeline Adapter Consolidation - Refactor adapter stages for clarity and flexibility - RenderStage now handles both item-based and buffer-based rendering - Add SourceItemsToBufferStage for converting data source items - Improve DataSourceStage to work with all source types - Add DisplayStage wrapper for display backends ### Camera & Viewport Refinements - Update Camera class for new architecture - Improve viewport dimension detection - Better handling of resize events across backends ### New Effect Plugins - border.py: Frame rendering effect with configurable style - crop.py: Viewport clipping effect for selective display - tint.py: Color filtering effect for atmosphere ### Tests & Quality - Add test_border_effect.py with comprehensive border tests - Add test_crop_effect.py with viewport clipping tests - Add test_tint_effect.py with color filtering tests - Update test_pipeline.py for new architecture - Update test_pipeline_introspection.py for new data source location - All 463 tests pass with 56% coverage - Linting: All checks pass with ruff ### Removals (Code Cleanup) - Delete engine/benchmark.py (deprecated performance testing) - Delete engine/pipeline_sources/__init__.py (moved to data_sources) - Delete engine/sources_v2.py (replaced by data_sources/sources.py) - Update AGENTS.md to reflect new structure ### Import Path Updates - Update engine/pipeline/controller.py::create_default_pipeline() * Old: from engine.sources_v2 import HeadlinesDataSource * New: from engine.data_sources.sources import HeadlinesDataSource - All display backends import from new locations - All tests import from new locations ## BACKWARDS COMPATIBILITY This refactoring is intended to be backwards compatible: - Pipeline execution unchanged (DAG-based with capability matching) - Effect plugins unchanged (EffectPlugin interface same) - Display protocol unchanged (Display duck-typing works as before) - Config system unchanged (presets.toml format same) ## TESTING - 463 tests pass (0 failures, 19 skipped) - Full linting check passes - Manual testing on demo, poetry, websocket modes - All new effect plugins tested ## FILES CHANGED - 24 files modified/added/deleted - 723 insertions, 1,461 deletions (net -738 LOC - cleanup!) - No breaking changes to public APIs - All transitive imports updated correctly
113 lines
3.4 KiB
Python
113 lines
3.4 KiB
Python
"""
|
|
Tests for BorderEffect.
|
|
"""
|
|
|
|
|
|
from effects_plugins.border import BorderEffect
|
|
from engine.effects.types import EffectContext
|
|
|
|
|
|
def make_ctx(terminal_width: int = 80, terminal_height: int = 24) -> EffectContext:
|
|
"""Create a mock EffectContext."""
|
|
return EffectContext(
|
|
terminal_width=terminal_width,
|
|
terminal_height=terminal_height,
|
|
scroll_cam=0,
|
|
ticker_height=terminal_height,
|
|
)
|
|
|
|
|
|
class TestBorderEffect:
|
|
"""Tests for BorderEffect."""
|
|
|
|
def test_basic_init(self):
|
|
"""BorderEffect initializes with defaults."""
|
|
effect = BorderEffect()
|
|
assert effect.name == "border"
|
|
assert effect.config.enabled is True
|
|
|
|
def test_adds_border(self):
|
|
"""BorderEffect adds border around content."""
|
|
effect = BorderEffect()
|
|
buf = [
|
|
"Hello World",
|
|
"Test Content",
|
|
"Third Line",
|
|
]
|
|
ctx = make_ctx(terminal_width=20, terminal_height=10)
|
|
|
|
result = effect.process(buf, ctx)
|
|
|
|
# Should have top and bottom borders
|
|
assert len(result) >= 3
|
|
# First line should start with border character
|
|
assert result[0][0] in "┌┎┍"
|
|
# Last line should end with border character
|
|
assert result[-1][-1] in "┘┖┚"
|
|
|
|
def test_border_with_small_buffer(self):
|
|
"""BorderEffect handles small buffer (too small for border)."""
|
|
effect = BorderEffect()
|
|
buf = ["ab"] # Too small for proper border
|
|
ctx = make_ctx(terminal_width=10, terminal_height=5)
|
|
|
|
result = effect.process(buf, ctx)
|
|
|
|
# Should still try to add border but result may differ
|
|
# At minimum should have output
|
|
assert len(result) >= 1
|
|
|
|
def test_metrics_in_border(self):
|
|
"""BorderEffect includes FPS and frame time in border."""
|
|
effect = BorderEffect()
|
|
buf = ["x" * 10] * 5
|
|
ctx = make_ctx(terminal_width=20, terminal_height=10)
|
|
|
|
# Add metrics to context
|
|
ctx.set_state(
|
|
"metrics",
|
|
{
|
|
"avg_ms": 16.5,
|
|
"frame_count": 100,
|
|
"fps": 60.0,
|
|
},
|
|
)
|
|
|
|
result = effect.process(buf, ctx)
|
|
|
|
# Check for FPS in top border
|
|
top_line = result[0]
|
|
assert "FPS" in top_line or "60" in top_line
|
|
|
|
# Check for frame time in bottom border
|
|
bottom_line = result[-1]
|
|
assert "ms" in bottom_line or "16" in bottom_line
|
|
|
|
def test_no_metrics(self):
|
|
"""BorderEffect works without metrics."""
|
|
effect = BorderEffect()
|
|
buf = ["content"] * 5
|
|
ctx = make_ctx(terminal_width=20, terminal_height=10)
|
|
# No metrics set
|
|
|
|
result = effect.process(buf, ctx)
|
|
|
|
# Should still have border characters
|
|
assert len(result) >= 3
|
|
assert result[0][0] in "┌┎┍"
|
|
|
|
def test_crops_before_bordering(self):
|
|
"""BorderEffect crops input before adding border."""
|
|
effect = BorderEffect()
|
|
buf = ["x" * 100] * 50 # Very large buffer
|
|
ctx = make_ctx(terminal_width=20, terminal_height=10)
|
|
|
|
result = effect.process(buf, ctx)
|
|
|
|
# Should be cropped to fit, then bordered
|
|
# Result should be <= terminal_height with border
|
|
assert len(result) <= ctx.terminal_height
|
|
# Each line should be <= terminal_width
|
|
for line in result:
|
|
assert len(line) <= ctx.terminal_width
|