test_ui.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. """
  2. Tests for Agency Swarm visualization functionality.
  3. """
  4. import tempfile
  5. from pathlib import Path
  6. from unittest.mock import patch
  7. import pytest
  8. from agency_swarm import Agency, Agent
  9. from agency_swarm.ui import HTMLVisualizationGenerator, LayoutAlgorithms
  10. @pytest.fixture
  11. def sample_agency():
  12. """Create a sample agency for testing visualization."""
  13. ceo = Agent(name="CEO", instructions="You are the CEO")
  14. manager = Agent(name="Manager", instructions="You manage projects")
  15. worker = Agent(name="Worker", instructions="You do the work")
  16. agency = Agency(ceo, communication_flows=[ceo > manager, manager > worker])
  17. return agency
  18. @pytest.fixture
  19. def sample_agency_data():
  20. """Sample agency data structure for testing."""
  21. return {
  22. "nodes": [
  23. {
  24. "id": "CEO",
  25. "type": "agent",
  26. "data": {"label": "CEO", "description": "You are the CEO", "isEntryPoint": True},
  27. "position": {"x": 100, "y": 100},
  28. },
  29. {
  30. "id": "Manager",
  31. "type": "agent",
  32. "data": {"label": "Manager", "description": "You manage projects", "isEntryPoint": False},
  33. "position": {"x": 200, "y": 200},
  34. },
  35. {
  36. "id": "Worker",
  37. "type": "agent",
  38. "data": {"label": "Worker", "description": "You do the work", "isEntryPoint": False},
  39. "position": {"x": 300, "y": 300},
  40. },
  41. ],
  42. "edges": [
  43. {"id": "CEO-Manager", "source": "CEO", "target": "Manager", "type": "communication"},
  44. {"id": "Manager-Worker", "source": "Manager", "target": "Worker", "type": "communication"},
  45. ],
  46. "metadata": {"agencyName": "Test Agency", "totalAgents": 3, "totalTools": 0},
  47. }
  48. class TestLayoutAlgorithms:
  49. """Test the layout algorithms."""
  50. def test_hierarchical_layout_basic(self, sample_agency_data):
  51. """Test basic hierarchical layout functionality."""
  52. nodes = sample_agency_data["nodes"]
  53. edges = sample_agency_data["edges"]
  54. positions = LayoutAlgorithms.hierarchical_layout(nodes, edges, width=800, height=600)
  55. # Check that all agents got positions
  56. assert "CEO" in positions
  57. assert "Manager" in positions
  58. assert "Worker" in positions
  59. # Check that positions have x and y coordinates
  60. for _node_id, pos in positions.items():
  61. assert "x" in pos
  62. assert "y" in pos
  63. assert isinstance(pos["x"], int | float)
  64. assert isinstance(pos["y"], int | float)
  65. def test_hierarchical_layout_entry_points_on_top(self, sample_agency_data):
  66. """Test that entry points are positioned at the top."""
  67. nodes = sample_agency_data["nodes"]
  68. edges = sample_agency_data["edges"]
  69. positions = LayoutAlgorithms.hierarchical_layout(nodes, edges, width=800, height=600)
  70. ceo_y = positions["CEO"]["y"]
  71. manager_y = positions["Manager"]["y"]
  72. worker_y = positions["Worker"]["y"]
  73. # CEO (entry point) should be at the top
  74. assert ceo_y < manager_y
  75. assert manager_y < worker_y
  76. def test_hierarchical_layout_with_tools(self):
  77. """Test hierarchical layout with tools included."""
  78. nodes = [
  79. {"id": "CEO", "type": "agent", "data": {"label": "CEO", "isEntryPoint": True}},
  80. {"id": "tool1", "type": "tool", "data": {"label": "Tool1", "parentAgent": "CEO"}},
  81. ]
  82. edges = []
  83. positions = LayoutAlgorithms.hierarchical_layout(nodes, edges, width=800, height=600)
  84. assert "CEO" in positions
  85. assert "tool1" in positions
  86. # Tool should be positioned relative to its parent agent
  87. assert positions["tool1"]["x"] != positions["CEO"]["x"] or positions["tool1"]["y"] != positions["CEO"]["y"]
  88. def test_hierarchical_layout_orphaned_tools(self):
  89. """Test positioning of tools without parent agents."""
  90. nodes = [
  91. {"id": "CEO", "type": "agent", "data": {"label": "CEO", "isEntryPoint": True}},
  92. {
  93. "id": "orphan_tool",
  94. "type": "tool",
  95. "data": {"label": "Orphan Tool"}, # No parentAgent
  96. },
  97. ]
  98. edges = []
  99. positions = LayoutAlgorithms.hierarchical_layout(nodes, edges, width=800, height=600)
  100. assert "CEO" in positions
  101. assert "orphan_tool" in positions
  102. # Orphaned tool should be positioned at bottom
  103. assert positions["orphan_tool"]["y"] > positions["CEO"]["y"]
  104. def test_apply_layout(self, sample_agency_data):
  105. """Test the apply_layout method."""
  106. result = LayoutAlgorithms.apply_layout(sample_agency_data)
  107. # Check that structure is preserved
  108. assert "nodes" in result
  109. assert "edges" in result
  110. assert "metadata" in result
  111. # Check that positions were updated
  112. for node in result["nodes"]:
  113. assert "position" in node
  114. assert "x" in node["position"]
  115. assert "y" in node["position"]
  116. class TestHTMLVisualizationGenerator:
  117. """Test the HTML visualization generator."""
  118. def test_init(self):
  119. """Test HTMLVisualizationGenerator initialization."""
  120. generator = HTMLVisualizationGenerator()
  121. assert generator.template_dir.exists()
  122. assert (generator.template_dir / "visualization.html").exists()
  123. assert (generator.template_dir / "styles.css").exists()
  124. assert (generator.template_dir / "visualization.js").exists()
  125. def test_load_template(self):
  126. """Test template loading."""
  127. generator = HTMLVisualizationGenerator()
  128. # Test loading existing template
  129. html_content = generator._load_template("visualization.html")
  130. assert isinstance(html_content, str)
  131. assert len(html_content) > 0
  132. assert "html" in html_content.lower()
  133. def test_load_template_not_found(self):
  134. """Test error handling for missing template."""
  135. generator = HTMLVisualizationGenerator()
  136. with pytest.raises(FileNotFoundError):
  137. generator._load_template("nonexistent.html")
  138. @patch("webbrowser.open")
  139. def test_open_in_browser_success(self, mock_webbrowser):
  140. """Test opening file in browser successfully."""
  141. generator = HTMLVisualizationGenerator()
  142. test_path = Path("/test/path.html")
  143. generator._open_in_browser(test_path)
  144. mock_webbrowser.assert_called_once_with(f"file://{test_path}")
  145. @patch("webbrowser.open", side_effect=Exception("Browser error"))
  146. @patch("builtins.print")
  147. def test_open_in_browser_error(self, mock_print, mock_webbrowser):
  148. """Test error handling when browser fails to open."""
  149. generator = HTMLVisualizationGenerator()
  150. test_path = Path("/test/path.html")
  151. generator._open_in_browser(test_path)
  152. # Should print error messages
  153. assert mock_print.call_count >= 2
  154. def test_generate_interactive_html(self, sample_agency_data):
  155. """Test interactive HTML generation."""
  156. generator = HTMLVisualizationGenerator()
  157. with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as tmp:
  158. output_file = tmp.name
  159. try:
  160. with patch.object(generator, "_open_in_browser"):
  161. result_path = generator.generate_interactive_html(
  162. agency_data=sample_agency_data, output_file=output_file, open_browser=False
  163. )
  164. assert result_path == str(Path(output_file).resolve())
  165. assert Path(output_file).exists()
  166. # Check file content
  167. with open(output_file) as f:
  168. content = f.read()
  169. assert "Test Agency" in content
  170. assert "CEO" in content
  171. assert "Manager" in content
  172. assert "Worker" in content
  173. finally:
  174. # Cleanup
  175. if Path(output_file).exists():
  176. Path(output_file).unlink()
  177. def test_generate_component_files(self, sample_agency_data):
  178. """Test generation of separate component files."""
  179. generator = HTMLVisualizationGenerator()
  180. with tempfile.TemporaryDirectory() as tmp_dir:
  181. files = generator.generate_component_files(agency_data=sample_agency_data, output_dir=tmp_dir)
  182. # Check that all expected files were created
  183. assert "html" in files
  184. assert "css" in files
  185. assert "js" in files
  186. for file_path in files.values():
  187. assert Path(file_path).exists()
  188. assert Path(file_path).stat().st_size > 0
  189. @patch.object(HTMLVisualizationGenerator, "generate_interactive_html")
  190. def test_create_visualization_from_agency(self, mock_generate, sample_agency):
  191. """Test creating visualization directly from agency."""
  192. mock_generate.return_value = "/path/to/output.html"
  193. result = HTMLVisualizationGenerator.create_visualization_from_agency(
  194. agency=sample_agency,
  195. output_file="test.html",
  196. include_tools=True,
  197. open_browser=False,
  198. )
  199. assert result == "/path/to/output.html"
  200. mock_generate.assert_called_once()
  201. class TestAgencyTUI:
  202. """Test terminal UI entry points on Agency."""
  203. def test_tui_delegates_to_visualization(self, sample_agency):
  204. """Agency.tui should delegate to the visualization entry point."""
  205. with patch("agency_swarm.agency.visualization.tui") as mock_tui:
  206. sample_agency.tui(show_reasoning=True, reload=False)
  207. mock_tui.assert_called_once_with(sample_agency, show_reasoning=True, reload=False)
  208. def test_terminal_demo_alias_delegates_to_tui(self, sample_agency):
  209. """Agency.terminal_demo should remain a compatibility alias for tui."""
  210. with patch.object(sample_agency, "tui") as mock_tui:
  211. sample_agency.terminal_demo(show_reasoning=True, reload=False)
  212. mock_tui.assert_called_once_with(show_reasoning=True, reload=False)
  213. class TestAgencyVisualizationIntegration:
  214. """Test Agency class visualization methods."""
  215. def test_visualize(self, sample_agency):
  216. """Test Agency.visualize method."""
  217. with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as tmp:
  218. output_file = tmp.name
  219. try:
  220. with patch("webbrowser.open"):
  221. result_path = sample_agency.visualize(output_file=output_file, include_tools=True, open_browser=False)
  222. assert result_path == str(Path(output_file).resolve())
  223. assert Path(output_file).exists()
  224. # Check that file contains expected content
  225. with open(output_file) as f:
  226. content = f.read()
  227. assert "CEO" in content
  228. assert "Manager" in content
  229. assert "Worker" in content
  230. finally:
  231. if Path(output_file).exists():
  232. Path(output_file).unlink()
  233. def test_visualize_import_error(self, sample_agency):
  234. """Test handling of import errors in visualization."""
  235. # Patch the import inside the visualize method
  236. with patch("agency_swarm.agency.Agency.visualize") as mock_method:
  237. mock_method.side_effect = ImportError("Visualization module not available")
  238. with pytest.raises(ImportError, match="Visualization module not available"):
  239. sample_agency.visualize(open_browser=False)
  240. def test_get_agency_graph_basic(self, sample_agency):
  241. """Test basic agency graph generation."""
  242. structure = sample_agency.get_agency_graph()
  243. assert "nodes" in structure
  244. assert "edges" in structure
  245. assert "metadata" not in structure
  246. # Check nodes
  247. nodes = structure["nodes"]
  248. assert len(nodes) >= 3 # At least CEO, Manager, Worker
  249. agent_nodes = [n for n in nodes if n["type"] == "agent"]
  250. assert len(agent_nodes) == 3
  251. # Check edges
  252. edges = structure["edges"]
  253. communication_edges = [e for e in edges if e["type"] == "communication"]
  254. assert len(communication_edges) >= 2 # CEO->Manager, Manager->Worker
  255. def test_get_agency_graph_with_tools(self, sample_agency):
  256. """Test agency graph generation with tools included."""
  257. # Test that the method works with include_tools=True
  258. # We'll test the structure without actually adding tools to avoid tool type complications
  259. structure = sample_agency.get_agency_graph(include_tools=True)
  260. assert "nodes" in structure
  261. assert "edges" in structure
  262. assert "metadata" not in structure
  263. # Check that the structure is valid even when no tools are present
  264. agent_nodes = [n for n in structure["nodes"] if n["type"] == "agent"]
  265. assert len(agent_nodes) >= 3
  266. # Tool nodes and edges should be empty if no tools are added, which is fine
  267. tool_nodes = [n for n in structure["nodes"] if n["type"] == "tool"]
  268. tool_edges = [e for e in structure["edges"] if e["type"] == "tool"]
  269. # These should be lists, even if empty
  270. assert isinstance(tool_nodes, list)
  271. assert isinstance(tool_edges, list)
  272. def test_get_agency_graph_without_tools(self, sample_agency):
  273. """Test agency graph generation without tools."""
  274. structure = sample_agency.get_agency_graph(include_tools=False)
  275. # Should only have agent nodes
  276. tool_nodes = [n for n in structure["nodes"] if n["type"] == "tool"]
  277. assert len(tool_nodes) == 0
  278. # Should only have communication edges
  279. tool_edges = [e for e in structure["edges"] if e["type"] == "tool"]
  280. assert len(tool_edges) == 0
  281. def test_get_agency_graph_hierarchical_layout(self, sample_agency):
  282. """Test hierarchical layout in get_agency_graph."""
  283. # Test hierarchical layout
  284. structure = sample_agency.get_agency_graph()
  285. # Check that nodes have positions
  286. for node in structure["nodes"]:
  287. assert "position" in node
  288. assert "x" in node["position"]
  289. assert "y" in node["position"]
  290. def test_layout_algorithms_manager_vs_leaf_positioning(self):
  291. """Test that manager agents and leaf agents position tools differently."""
  292. # Create a more complex structure with manager and leaf agents
  293. nodes = [
  294. {"id": "Manager", "type": "agent", "data": {"label": "Manager", "isEntryPoint": True}},
  295. {"id": "Worker1", "type": "agent", "data": {"label": "Worker1", "isEntryPoint": False}},
  296. {"id": "Worker2", "type": "agent", "data": {"label": "Worker2", "isEntryPoint": False}},
  297. {"id": "manager_tool", "type": "tool", "data": {"label": "Manager Tool", "parentAgent": "Manager"}},
  298. {"id": "worker_tool", "type": "tool", "data": {"label": "Worker Tool", "parentAgent": "Worker1"}},
  299. ]
  300. # Manager has multiple outgoing connections
  301. edges = [
  302. {"source": "Manager", "target": "Worker1", "type": "communication"},
  303. {"source": "Manager", "target": "Worker2", "type": "communication"},
  304. ]
  305. positions = LayoutAlgorithms.hierarchical_layout(nodes, edges)
  306. # Both tools should be positioned
  307. assert "manager_tool" in positions
  308. assert "worker_tool" in positions
  309. # Manager tool should be positioned to the right (x > manager x)
  310. # Worker tool should be positioned below (y > worker y)
  311. manager_pos = positions["Manager"]
  312. worker_pos = positions["Worker1"]
  313. manager_tool_pos = positions["manager_tool"]
  314. worker_tool_pos = positions["worker_tool"]
  315. # These assertions test the smart positioning logic
  316. assert manager_tool_pos["x"] > manager_pos["x"] # Tool to the right of manager
  317. assert worker_tool_pos["y"] > worker_pos["y"] # Tool below worker