feat(tests): improve coverage to 56%, add benchmark regression tests

- Add EffectPlugin ABC with @abstractmethod decorators for interface enforcement
- Add runtime interface checking in discover_plugins() with issubclass()
- Add EffectContext factory with sensible defaults
- Standardize Display __init__ (remove redundant init in TerminalDisplay)
- Document effect behavior when ticker_height=0
- Evaluate legacy effects: document coexistence, no deprecation needed
- Research plugin patterns (VST, Python entry points)
- Fix pysixel dependency (removed broken dependency)

Test coverage improvements:
- Add DisplayRegistry tests
- Add MultiDisplay tests
- Add SixelDisplay tests
- Add controller._get_display tests
- Add effects controller command handling tests
- Add benchmark regression tests (@pytest.mark.benchmark)
- Add pytest marker for benchmark tests in pyproject.toml

Documentation updates:
- Update AGENTS.md with 56% coverage stats and effect plugin docs
- Update README.md with Sixel display mode and benchmark commands
- Add new modules to architecture section
This commit is contained in:
2026-03-15 23:26:10 -07:00
parent dcd31469a5
commit 9e4d54a82e
17 changed files with 768 additions and 45 deletions

View File

@@ -10,7 +10,12 @@ from engine.effects.legacy import (
)
from engine.effects.performance import PerformanceMonitor, get_monitor, set_monitor
from engine.effects.registry import EffectRegistry, get_registry, set_registry
from engine.effects.types import EffectConfig, EffectContext, PipelineConfig
from engine.effects.types import (
EffectConfig,
EffectContext,
PipelineConfig,
create_effect_context,
)
def get_effect_chain():
@@ -25,6 +30,7 @@ __all__ = [
"EffectConfig",
"EffectContext",
"PipelineConfig",
"create_effect_context",
"get_registry",
"set_registry",
"get_effect_chain",

View File

@@ -1,6 +1,14 @@
"""
Visual effects: noise, glitch, fade, ANSI-aware truncation, firehose, headline pool.
Depends on: config, terminal, sources.
These are low-level functional implementations of visual effects. They are used
internally by the EffectPlugin system (effects_plugins/*.py) and also directly
by layers.py and scroll.py for rendering.
The plugin system provides a higher-level OOP interface with configuration
support, while these legacy functions provide direct functional access.
Both systems coexist - there are no current plans to deprecate the legacy functions.
"""
import random

View File

@@ -1,3 +1,24 @@
"""
Visual effects type definitions and base classes.
EffectPlugin Architecture:
- Uses ABC (Abstract Base Class) for interface enforcement
- Runtime discovery via directory scanning (effects_plugins/)
- Configuration via EffectConfig dataclass
- Context passed through EffectContext dataclass
Plugin System Research (see AGENTS.md for references):
- VST: Standardized audio interfaces, chaining, presets (FXP/FXB)
- Python Entry Points: Namespace packages, importlib.metadata discovery
- Shadertoy: Shader-based with uniforms as context
Current gaps vs industry patterns:
- No preset save/load system
- No external plugin distribution via entry points
- No plugin metadata (version, author, description)
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any
@@ -22,15 +43,76 @@ class EffectConfig:
params: dict[str, Any] = field(default_factory=dict)
class EffectPlugin:
class EffectPlugin(ABC):
"""Abstract base class for effect plugins.
Subclasses must define:
- name: str - unique identifier for the effect
- config: EffectConfig - current configuration
And implement:
- process(buf, ctx) -> list[str]
- configure(config) -> None
Effect Behavior with ticker_height=0:
- NoiseEffect: Returns buffer unchanged (no ticker to apply noise to)
- FadeEffect: Returns buffer unchanged (no ticker to fade)
- GlitchEffect: Processes normally (doesn't depend on ticker_height)
- FirehoseEffect: Returns buffer unchanged if no items in context
Effects should handle missing or zero context values gracefully by
returning the input buffer unchanged rather than raising errors.
"""
name: str
config: EffectConfig
@abstractmethod
def process(self, buf: list[str], ctx: EffectContext) -> list[str]:
raise NotImplementedError
"""Process the buffer with this effect applied.
Args:
buf: List of lines to process
ctx: Effect context with terminal state
Returns:
Processed buffer (may be same object or new list)
"""
...
@abstractmethod
def configure(self, config: EffectConfig) -> None:
raise NotImplementedError
"""Configure the effect with new settings.
Args:
config: New configuration to apply
"""
...
def create_effect_context(
terminal_width: int = 80,
terminal_height: int = 24,
scroll_cam: int = 0,
ticker_height: int = 0,
mic_excess: float = 0.0,
grad_offset: float = 0.0,
frame_number: int = 0,
has_message: bool = False,
items: list | None = None,
) -> EffectContext:
"""Factory function to create EffectContext with sensible defaults."""
return EffectContext(
terminal_width=terminal_width,
terminal_height=terminal_height,
scroll_cam=scroll_cam,
ticker_height=ticker_height,
mic_excess=mic_excess,
grad_offset=grad_offset,
frame_number=frame_number,
has_message=has_message,
items=items or [],
)
@dataclass