forked from genewildish/Mainline
Implement a Read-Eval-Print Loop (REPL) effect that provides a HUD-style overlay for interactive pipeline control. ## New Files - engine/effects/plugins/repl.py - REPL effect plugin with command processor - engine/display/backends/terminal.py - Added raw mode and input handling - examples/repl_simple.py - Simple demonstration script - tests/test_repl_effect.py - 18 comprehensive tests ## Features ### REPL Interface - HUD-style overlay showing FPS, command history, output buffer size - Command history navigation (Up/Down arrows) - Command execution (Enter) - Character input and backspace support - Output buffer with scrolling ### Commands - help - Show available commands - status - Show pipeline status and metrics - effects - List all effects in pipeline - effect <name> <on|off> - Toggle effect - param <effect> <param> <value> - Set parameter - pipeline - Show current pipeline order - clear - Clear output buffer - quit - Show exit message ### Terminal Input Support - Added set_raw_mode() to TerminalDisplay for capturing keystrokes - Added get_input_keys() to read keyboard input - Proper terminal state restoration on cleanup ## Usage Add 'repl' to effects in your configuration: ## Testing All 18 REPL tests pass, covering: - Effect registration - Command processing - Navigation (history, editing) - Configuration - Rendering ## Integration The REPL effect integrates with the existing pipeline system: - Uses EffectPlugin interface - Supports partial updates - Reads metrics from EffectContext - Can be controlled via keyboard when terminal display is in raw mode Next steps: - Integrate REPL input handling into pipeline_runner.py - Add keyboard event processing loop - Create full demo with interactive features
202 lines
6.9 KiB
Python
202 lines
6.9 KiB
Python
"""Tests for the REPL effect plugin."""
|
|
|
|
import pytest
|
|
from pathlib import Path
|
|
|
|
from engine.effects.plugins import discover_plugins
|
|
from engine.effects.registry import get_registry
|
|
from engine.effects.plugins.repl import ReplEffect, REPLState
|
|
|
|
|
|
class TestReplEffectRegistration:
|
|
"""Tests for REPL effect registration."""
|
|
|
|
def test_repl_registered(self):
|
|
"""REPL effect is registered in the registry."""
|
|
discover_plugins()
|
|
registry = get_registry()
|
|
repl = registry.get("repl")
|
|
assert repl is not None
|
|
assert repl.name == "repl"
|
|
|
|
|
|
class TestReplEffectCreation:
|
|
"""Tests for creating REPL effect instances."""
|
|
|
|
def test_create_repl_effect(self):
|
|
"""Can create REPL effect instance."""
|
|
repl = ReplEffect()
|
|
assert repl.name == "repl"
|
|
assert repl.config.enabled is True
|
|
assert repl.config.intensity == 1.0
|
|
|
|
def test_repl_state(self):
|
|
"""REPL state is initialized correctly."""
|
|
repl = ReplEffect()
|
|
assert repl.state.command_history == []
|
|
assert repl.state.current_command == ""
|
|
assert repl.state.history_index == -1
|
|
assert repl.state.output_buffer == []
|
|
|
|
|
|
class TestReplEffectCommands:
|
|
"""Tests for REPL command processing."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self):
|
|
"""Setup before each test."""
|
|
self.repl = ReplEffect()
|
|
|
|
def test_process_command_help(self):
|
|
"""Help command adds help text to output."""
|
|
self.repl.process_command("help")
|
|
assert "> help" in self.repl.state.output_buffer
|
|
assert any(
|
|
"Available commands:" in line for line in self.repl.state.output_buffer
|
|
)
|
|
|
|
def test_process_command_status(self):
|
|
"""Status command adds status info to output."""
|
|
self.repl.process_command("status")
|
|
assert "> status" in self.repl.state.output_buffer
|
|
assert any("Output lines:" in line for line in self.repl.state.output_buffer)
|
|
|
|
def test_process_command_clear(self):
|
|
"""Clear command clears output buffer."""
|
|
self.repl.process_command("help")
|
|
initial_count = len(self.repl.state.output_buffer)
|
|
assert initial_count > 0
|
|
|
|
self.repl.process_command("clear")
|
|
assert len(self.repl.state.output_buffer) == 0
|
|
|
|
def test_process_command_unknown(self):
|
|
"""Unknown command adds error message."""
|
|
self.repl.process_command("unknown_command_xyz")
|
|
assert "> unknown_command_xyz" in self.repl.state.output_buffer
|
|
assert any("Unknown command" in line for line in self.repl.state.output_buffer)
|
|
|
|
def test_command_history(self):
|
|
"""Commands are added to history."""
|
|
self.repl.process_command("help")
|
|
self.repl.process_command("status")
|
|
assert len(self.repl.state.command_history) == 2
|
|
assert self.repl.state.command_history[0] == "help"
|
|
assert self.repl.state.command_history[1] == "status"
|
|
|
|
def test_current_command_cleared(self):
|
|
"""Current command is cleared after processing."""
|
|
self.repl.state.current_command = "test"
|
|
self.repl.process_command("help")
|
|
assert self.repl.state.current_command == ""
|
|
|
|
|
|
class TestReplNavigation:
|
|
"""Tests for REPL navigation (history, editing)."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self):
|
|
"""Setup before each test."""
|
|
self.repl = ReplEffect()
|
|
self.repl.state.command_history = ["help", "status", "effects"]
|
|
|
|
def test_navigate_history_up(self):
|
|
"""Navigate up through command history."""
|
|
self.repl.navigate_history(-1) # Up
|
|
assert self.repl.state.history_index == 0
|
|
assert self.repl.state.current_command == "help"
|
|
|
|
def test_navigate_history_down(self):
|
|
"""Navigate down through command history."""
|
|
self.repl.state.history_index = 0
|
|
self.repl.navigate_history(1) # Down
|
|
assert self.repl.state.history_index == 1
|
|
assert self.repl.state.current_command == "status"
|
|
|
|
def test_append_to_command(self):
|
|
"""Append character to current command."""
|
|
self.repl.append_to_command("h")
|
|
self.repl.append_to_command("e")
|
|
self.repl.append_to_command("l")
|
|
self.repl.append_to_command("p")
|
|
assert self.repl.state.current_command == "help"
|
|
|
|
def test_backspace(self):
|
|
"""Remove last character from command."""
|
|
self.repl.state.current_command = "hel"
|
|
self.repl.backspace()
|
|
assert self.repl.state.current_command == "he"
|
|
|
|
def test_clear_command(self):
|
|
"""Clear current command."""
|
|
self.repl.state.current_command = "test"
|
|
self.repl.clear_command()
|
|
assert self.repl.state.current_command == ""
|
|
|
|
|
|
class TestReplProcess:
|
|
"""Tests for REPL effect processing."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self):
|
|
"""Setup before each test."""
|
|
discover_plugins()
|
|
self.repl = ReplEffect()
|
|
|
|
def test_process_renders_output(self):
|
|
"""Process renders REPL interface."""
|
|
buf = ["line1", "line2", "line3"]
|
|
from engine.effects.types import EffectContext
|
|
|
|
ctx = EffectContext(
|
|
terminal_width=80, terminal_height=24, scroll_cam=0, ticker_height=0
|
|
)
|
|
result = self.repl.process(buf, ctx)
|
|
|
|
assert len(result) == 24 # Should match terminal height
|
|
assert any("MAINLINE REPL" in line for line in result)
|
|
assert any("COMMANDS:" in line for line in result)
|
|
assert any("OUTPUT:" in line for line in result)
|
|
|
|
def test_process_with_commands(self):
|
|
"""Process shows command output in REPL."""
|
|
buf = ["line1"]
|
|
from engine.effects.types import EffectContext
|
|
|
|
ctx = EffectContext(
|
|
terminal_width=80, terminal_height=24, scroll_cam=0, ticker_height=0
|
|
)
|
|
self.repl.process_command("help")
|
|
result = self.repl.process(buf, ctx)
|
|
|
|
# Check that command output appears in the REPL area
|
|
# (help output may be partially shown due to buffer size limits)
|
|
assert any("effects - List all effects" in line for line in result)
|
|
|
|
|
|
class TestReplConfig:
|
|
"""Tests for REPL configuration."""
|
|
|
|
def test_config_params(self):
|
|
"""REPL config has expected parameters."""
|
|
repl = ReplEffect()
|
|
assert "display_height" in repl.config.params
|
|
assert "show_hud" in repl.config.params
|
|
assert repl.config.params["display_height"] == 8
|
|
assert repl.config.params["show_hud"] is True
|
|
|
|
def test_configure(self):
|
|
"""Can configure REPL effect."""
|
|
repl = ReplEffect()
|
|
from engine.effects.types import EffectConfig
|
|
|
|
config = EffectConfig(
|
|
enabled=False,
|
|
intensity=0.5,
|
|
params={"display_height": 10, "show_hud": False},
|
|
)
|
|
repl.configure(config)
|
|
assert repl.config.enabled is False
|
|
assert repl.config.intensity == 0.5
|
|
assert repl.config.params["display_height"] == 10
|