""" Acceptance tests for HUD visibility and positioning. These tests verify that HUD appears in the final output frame. Frames are captured and saved as HTML reports for visual verification. """ import queue from engine.data_sources.sources import ListDataSource, SourceItem from engine.effects.plugins.hud import HudEffect 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 from tests.acceptance_report import save_report class FrameCaptureDisplay: """Display that captures frames for HTML report generation.""" def __init__(self): self.frames: queue.Queue[list[str]] = queue.Queue() self.width = 80 self.height = 24 self._recorded_frames: list[list[str]] = [] def init(self, width: int, height: int, reuse: bool = False) -> None: self.width = width self.height = height def show(self, buffer: list[str], border: bool = False) -> None: self._recorded_frames.append(list(buffer)) 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 get_recorded_frames(self) -> list[list[str]]: return self._recorded_frames def _build_pipeline_with_hud( items: list[SourceItem], ) -> tuple[Pipeline, FrameCaptureDisplay, PipelineContext]: """Build a pipeline with HUD effect.""" display = FrameCaptureDisplay() ctx = PipelineContext() params = PipelineParams() params.viewport_width = display.width params.viewport_height = display.height params.frame_number = 0 params.effect_order = ["noise", "hud"] params.effect_enabled = {"noise": False} ctx.params = params pipeline = Pipeline( config=PipelineConfig( source="list", display="terminal", effects=["hud"], enable_metrics=True, ), context=ctx, ) source = ListDataSource(items, name="test-source") pipeline.add_stage("source", DataSourceStage(source, name="test-source")) pipeline.add_stage("render", SourceItemsToBufferStage(name="items-to-buffer")) hud_effect = HudEffect() pipeline.add_stage("hud", EffectPluginStage(hud_effect, name="hud")) pipeline.add_stage("display", DisplayStage(display, name="terminal")) pipeline.build() pipeline.initialize() return pipeline, display, ctx class TestHUDAcceptance: """Acceptance tests for HUD visibility.""" def test_hud_appears_in_final_output(self): """Test that HUD appears in the final display output. This is the key regression test for Issue #47 - HUD was running AFTER the display stage, making it invisible. Now it should appear in the frame captured by the display. """ items = [SourceItem(content="Test content line", source="test", timestamp="0")] pipeline, display, ctx = _build_pipeline_with_hud(items) result = pipeline.execute(items) assert result.success, f"Pipeline execution failed: {result.error}" frame = display.frames.get(timeout=1) frame_text = "\n".join(frame) assert "MAINLINE" in frame_text, "HUD header not found in final output" assert "EFFECT:" in frame_text, "EFFECT line not found in final output" assert "PIPELINE:" in frame_text, "PIPELINE line not found in final output" save_report( test_name="test_hud_appears_in_final_output", frames=display.get_recorded_frames(), status="PASS", metadata={ "description": "Verifies HUD appears in final display output (Issue #47 fix)", "frame_lines": len(frame), "has_mainline": "MAINLINE" in frame_text, "has_effect": "EFFECT:" in frame_text, "has_pipeline": "PIPELINE:" in frame_text, }, ) def test_hud_cursor_positioning(self): """Test that HUD uses correct cursor positioning.""" items = [SourceItem(content="Sample content", source="test", timestamp="0")] pipeline, display, ctx = _build_pipeline_with_hud(items) result = pipeline.execute(items) assert result.success frame = display.frames.get(timeout=1) has_cursor_pos = any("\x1b[" in line and "H" in line for line in frame) save_report( test_name="test_hud_cursor_positioning", frames=display.get_recorded_frames(), status="PASS", metadata={ "description": "Verifies HUD uses cursor positioning", "has_cursor_positioning": has_cursor_pos, }, ) class TestCameraSpeedAcceptance: """Acceptance tests for camera speed modulation.""" def test_camera_speed_modulation(self): """Test that camera speed can be modulated at runtime. This verifies the camera speed modulation feature added in Phase 1. """ from engine.camera import Camera from engine.pipeline.adapters import CameraClockStage, CameraStage display = FrameCaptureDisplay() items = [ SourceItem(content=f"Line {i}", source="test", timestamp=str(i)) for i in range(50) ] ctx = PipelineContext() params = PipelineParams() params.viewport_width = display.width params.viewport_height = display.height params.frame_number = 0 params.camera_speed = 1.0 ctx.params = params pipeline = Pipeline( config=PipelineConfig( source="list", display="terminal", camera="scroll", enable_metrics=False, ), context=ctx, ) source = ListDataSource(items, name="test") pipeline.add_stage("source", DataSourceStage(source, name="test")) pipeline.add_stage("render", SourceItemsToBufferStage(name="render")) camera = Camera.scroll(speed=0.5) pipeline.add_stage( "camera_update", CameraClockStage(camera, name="camera-clock") ) pipeline.add_stage("camera", CameraStage(camera, name="camera")) pipeline.add_stage("display", DisplayStage(display, name="terminal")) pipeline.build() pipeline.initialize() initial_camera_speed = camera.speed for _ in range(3): pipeline.execute(items) speed_after_first_run = camera.speed params.camera_speed = 5.0 ctx.params = params for _ in range(3): pipeline.execute(items) speed_after_increase = camera.speed assert speed_after_increase == 5.0, ( f"Camera speed should be modulated to 5.0, got {speed_after_increase}" ) params.camera_speed = 0.0 ctx.params = params for _ in range(3): pipeline.execute(items) speed_after_stop = camera.speed assert speed_after_stop == 0.0, ( f"Camera speed should be 0.0, got {speed_after_stop}" ) save_report( test_name="test_camera_speed_modulation", frames=display.get_recorded_frames()[:5], status="PASS", metadata={ "description": "Verifies camera speed can be modulated at runtime", "initial_camera_speed": initial_camera_speed, "speed_after_first_run": speed_after_first_run, "speed_after_increase": speed_after_increase, "speed_after_stop": speed_after_stop, }, ) class TestEmptyLinesAcceptance: """Acceptance tests for empty line handling.""" def test_empty_lines_remain_empty(self): """Test that empty lines remain empty in output (regression for padding bug).""" items = [ SourceItem(content="Line1\n\nLine3\n\nLine5", source="test", timestamp="0") ] display = FrameCaptureDisplay() ctx = PipelineContext() params = PipelineParams() params.viewport_width = display.width params.viewport_height = display.height ctx.params = params pipeline = Pipeline( config=PipelineConfig(enable_metrics=False), context=ctx, ) source = ListDataSource(items, name="test") pipeline.add_stage("source", DataSourceStage(source, name="test")) pipeline.add_stage("render", SourceItemsToBufferStage(name="render")) pipeline.add_stage("display", DisplayStage(display, name="terminal")) pipeline.build() pipeline.initialize() result = pipeline.execute(items) assert result.success frame = display.frames.get(timeout=1) has_truly_empty = any(not line for line in frame) save_report( test_name="test_empty_lines_remain_empty", frames=display.get_recorded_frames(), status="PASS", metadata={ "description": "Verifies empty lines remain empty (not padded)", "has_truly_empty_lines": has_truly_empty, }, ) assert has_truly_empty, f"Expected at least one empty line, got: {frame[1]!r}"