""" Tests for the graph-based pipeline configuration. """ import pytest from engine.effects.plugins import discover_plugins from engine.pipeline.graph import Graph, NodeType, Node from engine.pipeline.graph_adapter import dict_to_pipeline, graph_to_pipeline @pytest.fixture(autouse=True) def setup_effects(): """Ensure effects are discovered before each test.""" discover_plugins() class TestGraphCreation: """Tests for Graph creation and manipulation.""" def test_create_empty_graph(self): """Graph can be created empty.""" graph = Graph() assert len(graph.nodes) == 0 assert len(graph.connections) == 0 def test_add_node(self): """Graph.node adds a node.""" graph = Graph() graph.node("source", NodeType.SOURCE, source="headlines") assert "source" in graph.nodes node = graph.nodes["source"] assert node.name == "source" assert node.type == NodeType.SOURCE assert node.config["source"] == "headlines" def test_add_node_string_type(self): """Graph.node accepts string type.""" graph = Graph() graph.node("camera", "camera", mode="scroll") assert "camera" in graph.nodes assert graph.nodes["camera"].type == NodeType.CAMERA def test_connect_nodes(self): """Graph.connect adds connection between nodes.""" graph = Graph() graph.node("source", NodeType.SOURCE) graph.node("display", NodeType.DISPLAY) graph.connect("source", "display") assert len(graph.connections) == 1 conn = graph.connections[0] assert conn.source == "source" assert conn.target == "display" def test_connect_nonexistent_source(self): """Graph.connect raises error for nonexistent source.""" graph = Graph() graph.node("display", NodeType.DISPLAY) with pytest.raises(ValueError, match="Source node 'missing' not found"): graph.connect("missing", "display") def test_connect_nonexistent_target(self): """Graph.connect raises error for nonexistent target.""" graph = Graph() graph.node("source", NodeType.SOURCE) with pytest.raises(ValueError, match="Target node 'missing' not found"): graph.connect("source", "missing") def test_chain_connects_nodes(self): """Graph.chain connects nodes in sequence.""" graph = Graph() graph.node("source", NodeType.SOURCE) graph.node("camera", NodeType.CAMERA) graph.node("display", NodeType.DISPLAY) graph.chain("source", "camera", "display") assert len(graph.connections) == 2 assert graph.connections[0].source == "source" assert graph.connections[0].target == "camera" assert graph.connections[1].source == "camera" assert graph.connections[1].target == "display" class TestGraphValidation: """Tests for Graph validation.""" def test_validate_disconnected_node(self): """Validation detects disconnected nodes.""" graph = Graph() graph.node("source", NodeType.SOURCE) graph.node("orphan", NodeType.EFFECT, effect="noise") errors = graph.validate() # Both source and orphan are disconnected assert len(errors) == 2 assert any("orphan" in e and "not connected" in e for e in errors) assert any("source" in e and "not connected" in e for e in errors) def test_validate_cycle_detection(self): """Validation detects cycles.""" graph = Graph() graph.node("a", NodeType.SOURCE) graph.node("b", NodeType.CAMERA) graph.node("c", NodeType.DISPLAY) graph.connect("a", "b") graph.connect("b", "c") graph.connect("c", "a") # Creates cycle errors = graph.validate() assert len(errors) > 0 assert any("cycle" in e.lower() for e in errors) def test_validate_clean_graph(self): """Validation returns no errors for valid graph.""" graph = Graph() graph.node("source", NodeType.SOURCE) graph.node("display", NodeType.DISPLAY) graph.connect("source", "display") errors = graph.validate() assert len(errors) == 0 class TestGraphToDict: """Tests for Graph serialization.""" def test_to_dict_basic(self): """Graph.to_dict produces correct structure.""" graph = Graph() graph.node("source", NodeType.SOURCE, source="headlines") graph.node("display", NodeType.DISPLAY, backend="terminal") graph.connect("source", "display") data = graph.to_dict() assert "nodes" in data assert "connections" in data assert "source" in data["nodes"] assert "display" in data["nodes"] assert data["nodes"]["source"]["type"] == "source" assert data["nodes"]["display"]["type"] == "display" def test_from_dict_simple(self): """Graph.from_dict loads simple format.""" data = { "nodes": { "source": "headlines", "display": {"type": "display", "backend": "terminal"}, }, "connections": ["source -> display"], } graph = Graph().from_dict(data) assert "source" in graph.nodes assert "display" in graph.nodes assert len(graph.connections) == 1 class TestDictToPipeline: """Tests for dict_to_pipeline conversion.""" def test_convert_minimal_pipeline(self): """dict_to_pipeline creates a working pipeline.""" data = { "nodes": { "source": "headlines", "display": {"type": "display", "backend": "null"}, }, "connections": ["source -> display"], } pipeline = dict_to_pipeline(data, viewport_width=80, viewport_height=24) assert pipeline is not None assert "source" in pipeline._stages assert "display" in pipeline._stages def test_convert_with_effect(self): """dict_to_pipeline handles effect nodes.""" data = { "nodes": { "source": "headlines", "noise": {"type": "effect", "effect": "noise", "intensity": 0.5}, "display": {"type": "display", "backend": "null"}, }, "connections": ["source -> noise -> display"], } pipeline = dict_to_pipeline(data) assert "noise" in pipeline._stages # Check that intensity was set (this is a global state check) from engine.effects import get_registry noise_effect = get_registry().get("noise") assert noise_effect.config.intensity == 0.5 def test_convert_with_positioning(self): """dict_to_pipeline handles positioning nodes.""" data = { "nodes": { "source": "headlines", "position": {"type": "position", "mode": "absolute"}, "display": {"type": "display", "backend": "null"}, }, "connections": ["source -> position -> display"], } pipeline = dict_to_pipeline(data) assert "position" in pipeline._stages pos_stage = pipeline._stages["position"] assert pos_stage.mode.value == "absolute" class TestGraphToPipeline: """Tests for graph_to_pipeline conversion.""" def test_convert_simple_graph(self): """graph_to_pipeline converts a simple graph.""" graph = Graph() graph.node("source", NodeType.SOURCE, source="headlines") graph.node("display", NodeType.DISPLAY, backend="null") graph.connect("source", "display") pipeline = graph_to_pipeline(graph) assert pipeline is not None # Pipeline auto-injects missing capabilities (camera, render, etc.) # So we have more stages than just source and display assert "source" in pipeline._stages assert "display" in pipeline._stages # Auto-injected stages include camera, camera_update, render assert "camera" in pipeline._stages assert "camera_update" in pipeline._stages assert "render" in pipeline._stages def test_convert_with_camera(self): """graph_to_pipeline handles camera nodes.""" graph = Graph() graph.node("source", NodeType.SOURCE, source="headlines") graph.node("camera", NodeType.CAMERA, mode="scroll") graph.node("display", NodeType.DISPLAY, backend="null") graph.chain("source", "camera", "display") pipeline = graph_to_pipeline(graph) assert "camera" in pipeline._stages camera_stage = pipeline._stages["camera"] assert hasattr(camera_stage, "_camera") if __name__ == "__main__": pytest.main([__file__])