Files
sideline/tests/test_crop_effect.py
David Gwilliam e0bbfea26c refactor: consolidate pipeline architecture with unified data source system
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
2026-03-16 19:47:12 -07:00

101 lines
3.0 KiB
Python

"""
Tests for CropEffect.
"""
from effects_plugins.crop import CropEffect
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 TestCropEffect:
"""Tests for CropEffect."""
def test_basic_init(self):
"""CropEffect initializes with defaults."""
effect = CropEffect()
assert effect.name == "crop"
assert effect.config.enabled is True
def test_crop_wider_buffer(self):
"""CropEffect crops wide buffer to terminal width."""
effect = CropEffect()
buf = [
"This is a very long line that exceeds the terminal width of eighty characters!",
"Another long line that should also be cropped to fit within the terminal bounds!",
"Short",
]
ctx = make_ctx(terminal_width=40, terminal_height=10)
result = effect.process(buf, ctx)
# Lines should be cropped to 40 chars
assert len(result[0]) == 40
assert len(result[1]) == 40
assert result[2] == "Short" + " " * 35 # padded to width
def test_crop_taller_buffer(self):
"""CropEffect crops tall buffer to terminal height."""
effect = CropEffect()
buf = ["line"] * 30 # 30 lines
ctx = make_ctx(terminal_width=80, terminal_height=10)
result = effect.process(buf, ctx)
# Should be cropped to 10 lines
assert len(result) == 10
def test_pad_shorter_lines(self):
"""CropEffect pads lines shorter than width."""
effect = CropEffect()
buf = ["short", "medium length", ""]
ctx = make_ctx(terminal_width=20, terminal_height=5)
result = effect.process(buf, ctx)
assert len(result[0]) == 20 # padded
assert len(result[1]) == 20 # padded
assert len(result[2]) == 20 # padded (was empty)
def test_pad_to_height(self):
"""CropEffect pads with empty lines if buffer is too short."""
effect = CropEffect()
buf = ["line1", "line2"]
ctx = make_ctx(terminal_width=20, terminal_height=10)
result = effect.process(buf, ctx)
# Should have 10 lines
assert len(result) == 10
# Last 8 should be empty padding
for i in range(2, 10):
assert result[i] == " " * 20
def test_empty_buffer(self):
"""CropEffect handles empty buffer."""
effect = CropEffect()
ctx = make_ctx()
result = effect.process([], ctx)
assert result == []
def test_uses_context_dimensions(self):
"""CropEffect uses context terminal_width/terminal_height."""
effect = CropEffect()
buf = ["x" * 100]
ctx = make_ctx(terminal_width=50, terminal_height=1)
result = effect.process(buf, ctx)
assert len(result[0]) == 50