forked from genewildish/Mainline
feat(integration): Complete feature rewrite with pipeline architecture, effects system, and display improvements
Major changes: - Pipeline architecture with capability-based dependency resolution - Effects plugin system with performance monitoring - Display abstraction with multiple backends (terminal, null, websocket) - Camera system for viewport scrolling - Sensor framework for real-time input - Command-and-control system via ntfy - WebSocket display backend for browser clients - Comprehensive test suite and documentation Issue #48: ADR for preset scripting language included This commit consolidates 110 individual commits into a single feature integration that can be reviewed and tested before further refinement.
This commit is contained in:
206
tests/test_tint_acceptance.py
Normal file
206
tests/test_tint_acceptance.py
Normal file
@@ -0,0 +1,206 @@
|
||||
"""Integration test: TintEffect in the pipeline."""
|
||||
|
||||
import queue
|
||||
|
||||
from engine.data_sources.sources import ListDataSource, SourceItem
|
||||
from engine.effects.plugins.tint import TintEffect
|
||||
from engine.effects.types import EffectConfig
|
||||
from engine.pipeline import Pipeline, PipelineConfig
|
||||
from engine.pipeline.adapters import (
|
||||
DataSourceStage,
|
||||
DisplayStage,
|
||||
EffectPluginStage,
|
||||
SourceItemsToBufferStage,
|
||||
)
|
||||
from engine.pipeline.core import PipelineContext
|
||||
from engine.pipeline.params import PipelineParams
|
||||
|
||||
|
||||
class QueueDisplay:
|
||||
"""Stub display that captures every frame into a queue."""
|
||||
|
||||
def __init__(self):
|
||||
self.frames: queue.Queue[list[str]] = queue.Queue()
|
||||
self.width = 80
|
||||
self.height = 24
|
||||
self._init_called = False
|
||||
|
||||
def init(self, width: int, height: int, reuse: bool = False) -> None:
|
||||
self.width = width
|
||||
self.height = height
|
||||
self._init_called = True
|
||||
|
||||
def show(self, buffer: list[str], border: bool = False) -> None:
|
||||
self.frames.put(list(buffer))
|
||||
|
||||
def clear(self) -> None:
|
||||
pass
|
||||
|
||||
def cleanup(self) -> None:
|
||||
pass
|
||||
|
||||
def get_dimensions(self) -> tuple[int, int]:
|
||||
return (self.width, self.height)
|
||||
|
||||
|
||||
def _build_pipeline(
|
||||
items: list[SourceItem],
|
||||
tint_config: EffectConfig | None = None,
|
||||
width: int = 80,
|
||||
height: int = 24,
|
||||
) -> tuple[Pipeline, QueueDisplay, PipelineContext]:
|
||||
"""Build pipeline: source -> render -> tint effect -> display."""
|
||||
display = QueueDisplay()
|
||||
|
||||
ctx = PipelineContext()
|
||||
params = PipelineParams()
|
||||
params.viewport_width = width
|
||||
params.viewport_height = height
|
||||
params.frame_number = 0
|
||||
ctx.params = params
|
||||
ctx.set("items", items)
|
||||
|
||||
pipeline = Pipeline(
|
||||
config=PipelineConfig(enable_metrics=True),
|
||||
context=ctx,
|
||||
)
|
||||
|
||||
# Source
|
||||
source = ListDataSource(items, name="test-source")
|
||||
pipeline.add_stage("source", DataSourceStage(source, name="test-source"))
|
||||
|
||||
# Render (simple)
|
||||
pipeline.add_stage("render", SourceItemsToBufferStage(name="items-to-buffer"))
|
||||
|
||||
# Tint effect
|
||||
tint_effect = TintEffect()
|
||||
if tint_config is not None:
|
||||
tint_effect.configure(tint_config)
|
||||
pipeline.add_stage("tint", EffectPluginStage(tint_effect, name="tint"))
|
||||
|
||||
# Display
|
||||
pipeline.add_stage("display", DisplayStage(display, name="queue"))
|
||||
|
||||
pipeline.build()
|
||||
pipeline.initialize()
|
||||
|
||||
return pipeline, display, ctx
|
||||
|
||||
|
||||
class TestTintAcceptance:
|
||||
"""Test TintEffect in a full pipeline."""
|
||||
|
||||
def test_tint_applies_default_color(self):
|
||||
"""Default tint should apply ANSI color codes to output."""
|
||||
items = [SourceItem(content="Hello World", source="test", timestamp="0")]
|
||||
pipeline, display, ctx = _build_pipeline(items)
|
||||
|
||||
result = pipeline.execute(items)
|
||||
|
||||
assert result.success, f"Pipeline failed: {result.error}"
|
||||
frame = display.frames.get(timeout=1)
|
||||
|
||||
text = "\n".join(frame)
|
||||
assert "\033[" in text, f"Expected ANSI codes in frame: {frame}"
|
||||
assert "Hello World" in text
|
||||
|
||||
def test_tint_applies_red_color(self):
|
||||
"""Configured red tint should produce red ANSI code (196-197)."""
|
||||
items = [SourceItem(content="Red Text", source="test", timestamp="0")]
|
||||
config = EffectConfig(
|
||||
enabled=True,
|
||||
intensity=1.0,
|
||||
params={"r": 255, "g": 0, "b": 0, "a": 0.8},
|
||||
)
|
||||
pipeline, display, ctx = _build_pipeline(items, tint_config=config)
|
||||
|
||||
result = pipeline.execute(items)
|
||||
|
||||
assert result.success
|
||||
frame = display.frames.get(timeout=1)
|
||||
line = frame[0]
|
||||
|
||||
# Should contain red ANSI code (196 or 197 in 256 color)
|
||||
assert "\033[38;5;196m" in line or "\033[38;5;197m" in line, (
|
||||
f"Missing red tint: {line}"
|
||||
)
|
||||
assert "Red Text" in line
|
||||
|
||||
def test_tint_disabled_does_nothing(self):
|
||||
"""Disabled tint stage should pass through buffer unchanged."""
|
||||
items = [SourceItem(content="Plain Text", source="test", timestamp="0")]
|
||||
pipeline, display, ctx = _build_pipeline(items)
|
||||
|
||||
# Disable the tint stage
|
||||
stage = pipeline.get_stage("tint")
|
||||
stage.set_enabled(False)
|
||||
|
||||
result = pipeline.execute(items)
|
||||
|
||||
assert result.success
|
||||
frame = display.frames.get(timeout=1)
|
||||
text = "\n".join(frame)
|
||||
|
||||
# Should contain Plain Text with NO ANSI color codes
|
||||
assert "Plain Text" in text
|
||||
assert "\033[" not in text, f"Unexpected ANSI codes in frame: {frame}"
|
||||
|
||||
def test_tint_zero_transparency(self):
|
||||
"""Alpha=0 should pass through buffer unchanged (no tint)."""
|
||||
items = [SourceItem(content="Transparent", source="test", timestamp="0")]
|
||||
config = EffectConfig(
|
||||
enabled=True,
|
||||
intensity=1.0,
|
||||
params={"r": 255, "g": 128, "b": 64, "a": 0.0},
|
||||
)
|
||||
pipeline, display, ctx = _build_pipeline(items, tint_config=config)
|
||||
|
||||
result = pipeline.execute(items)
|
||||
|
||||
assert result.success
|
||||
frame = display.frames.get(timeout=1)
|
||||
text = "\n".join(frame)
|
||||
|
||||
assert "Transparent" in text
|
||||
assert "\033[" not in text, f"Expected no ANSI codes with alpha=0: {frame}"
|
||||
|
||||
def test_tint_with_multiples_lines(self):
|
||||
"""Tint should apply to all non-empty lines."""
|
||||
items = [
|
||||
SourceItem(content="Line1\nLine2\n\nLine4", source="test", timestamp="0")
|
||||
]
|
||||
config = EffectConfig(
|
||||
enabled=True,
|
||||
intensity=1.0,
|
||||
params={"r": 0, "g": 255, "b": 0, "a": 0.7},
|
||||
)
|
||||
pipeline, display, ctx = _build_pipeline(items, tint_config=config)
|
||||
|
||||
result = pipeline.execute(items)
|
||||
|
||||
assert result.success
|
||||
frame = display.frames.get(timeout=1)
|
||||
|
||||
# All non-empty lines should have green ANSI codes
|
||||
green_codes = ["\033[38;5;", "m"]
|
||||
for line in frame:
|
||||
if line.strip():
|
||||
assert green_codes[0] in line and green_codes[1] in line, (
|
||||
f"Missing green tint: {line}"
|
||||
)
|
||||
else:
|
||||
assert line == "", f"Empty lines should be exactly empty: {line}"
|
||||
|
||||
def test_tint_preserves_empty_lines(self):
|
||||
"""Empty lines should remain empty (no ANSI codes)."""
|
||||
items = [SourceItem(content="A\n\nB", source="test", timestamp="0")]
|
||||
pipeline, display, ctx = _build_pipeline(items)
|
||||
|
||||
result = pipeline.execute(items)
|
||||
|
||||
assert result.success
|
||||
frame = display.frames.get(timeout=1)
|
||||
|
||||
assert frame[0].strip() != ""
|
||||
assert frame[1] == "" # Empty line unchanged
|
||||
assert frame[2].strip() != ""
|
||||
Reference in New Issue
Block a user