test_terminal_demo.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. """
  2. Test suite using pytest's capsys for output capture with Agency.
  3. This approach uses pytest's built-in capsys fixture to capture output
  4. of the terminal.
  5. """
  6. from unittest.mock import patch
  7. import pytest
  8. from agents import ModelSettings
  9. from prompt_toolkit.document import Document
  10. from agency_swarm import Agency, Agent
  11. from agency_swarm.tools.send_message import Handoff
  12. from agency_swarm.ui.demos import terminal as terminal_module
  13. from agency_swarm.ui.demos.terminal import start_terminal
  14. class MockInputProvider:
  15. """Provides sequential input responses for testing."""
  16. def __init__(self, inputs: list[str]):
  17. self.inputs = inputs
  18. self.index = 0
  19. def __call__(self, prompt=""):
  20. """Mock input function that returns sequential inputs."""
  21. if self.index < len(self.inputs):
  22. result = self.inputs[self.index]
  23. self.index += 1
  24. # Echo input to simulate terminal behavior
  25. print(f"{prompt}{result}")
  26. return result
  27. return "/exit"
  28. async def async_call(self, prompt="", **kwargs):
  29. """Mock async prompt for prompt_toolkit."""
  30. return self(prompt)
  31. def _application_factory(provider: MockInputProvider):
  32. """Return a PromptToolkit Application stub that replays provider inputs."""
  33. class _ApplicationStub:
  34. def __init__(self, *args, **kwargs):
  35. self._provider = provider
  36. self._layout = kwargs.get("layout")
  37. def invalidate(self) -> None: # noqa: D401 - simple no-op
  38. """No-op invalidate used by dropdown integration."""
  39. async def run_async(self): # noqa: ANN201 - signature mirrors prompt_toolkit
  40. value = await self._provider.async_call()
  41. if value is None:
  42. value = "/exit"
  43. buffer = getattr(self._layout, "current_buffer", None)
  44. if buffer is not None:
  45. buffer.document = Document(value, len(value))
  46. return value
  47. return _ApplicationStub
  48. @pytest.fixture
  49. def agency():
  50. """Create an agency for testing."""
  51. test_agent = Agent(
  52. name="TestAgent",
  53. description="A test agent for terminal testing",
  54. instructions=(
  55. "You are a helpful test assistant. Keep responses very brief (max 10 words). "
  56. "Always respond with 'Test response: [user message]'. "
  57. "You can hand off to the Developer agent by using transfer_to_ tool."
  58. ),
  59. model_settings=ModelSettings(temperature=0),
  60. )
  61. developer_agent = Agent(
  62. name="Developer",
  63. description="A developer agent",
  64. instructions="You are a developer. Respond with 'Dev response: [message]'.",
  65. model_settings=ModelSettings(temperature=0),
  66. )
  67. # Agent with a bad formatted name
  68. security_expert_agent = Agent(
  69. name="SecUrity ExperT_Agent",
  70. description="A security expert agent",
  71. instructions="You are a security expert. Respond with 'Security expert response: [message]'.",
  72. model_settings=ModelSettings(temperature=0),
  73. )
  74. agency = Agency(
  75. test_agent,
  76. developer_agent,
  77. security_expert_agent,
  78. communication_flows=[
  79. (security_expert_agent < test_agent > developer_agent, Handoff),
  80. ],
  81. name="TestAgency",
  82. shared_instructions="This is a test agency. Keep all responses very brief.",
  83. )
  84. return agency
  85. class TestTerminalCapsys:
  86. """Test terminal demo using capsys for output capture."""
  87. def test_help_command_output(self, agency, capsys):
  88. """Test /help command output capture."""
  89. input_provider = MockInputProvider(["/help", "/exit"])
  90. with patch("builtins.input", input_provider):
  91. with patch("agency_swarm.ui.demos.terminal.Application", new=_application_factory(input_provider)):
  92. start_terminal(agency, reload=False)
  93. captured = capsys.readouterr()
  94. output = captured.out
  95. assert "/help" in output
  96. assert "/new" in output
  97. assert "/compact" in output
  98. assert "/resume" in output
  99. assert "/status" in output
  100. assert "/exit" in output
  101. def test_status_command_output(self, agency, capsys):
  102. """Test /status command output capture."""
  103. input_provider = MockInputProvider(["/status", "/exit"])
  104. with patch("builtins.input", input_provider):
  105. with patch("agency_swarm.ui.demos.terminal.Application", new=_application_factory(input_provider)):
  106. start_terminal(agency, reload=False)
  107. captured = capsys.readouterr()
  108. output = captured.out
  109. assert "Agency: TestAgency" in output
  110. assert "Entry Points: TestAgent" in output
  111. assert "Default Recipient: TestAgent" in output
  112. assert "cwd:" in output
  113. def test_new_command_functionality(self, agency, capsys):
  114. """Test /new command functionality."""
  115. input_provider = MockInputProvider(["/new", "/exit"])
  116. with patch("builtins.input", input_provider):
  117. with patch("agency_swarm.ui.demos.terminal.Application", new=_application_factory(input_provider)):
  118. start_terminal(agency, reload=False)
  119. captured = capsys.readouterr()
  120. output = captured.out
  121. assert "Started a new chat session" in output
  122. def test_message_sending_and_response(self, agency, capsys):
  123. """Test sending message and receiving response from agent."""
  124. input_provider = MockInputProvider(["Hello world", "/exit"])
  125. with patch("builtins.input", input_provider):
  126. with patch("agency_swarm.ui.demos.terminal.Application", new=_application_factory(input_provider)):
  127. start_terminal(agency, reload=False)
  128. captured = capsys.readouterr()
  129. output = captured.out
  130. assert "Hello world" in output
  131. assert "🤖 TestAgent → 👤 user" in output
  132. assert "Test response:" in output
  133. def test_agent_mention_parsing(self, agency, capsys):
  134. """Test @agent mention parsing with multi-agent setup."""
  135. input_provider = MockInputProvider(["@Developer help me", "/exit"])
  136. with patch("builtins.input", input_provider):
  137. with patch("agency_swarm.ui.demos.terminal.Application", new=_application_factory(input_provider)):
  138. start_terminal(agency, reload=False)
  139. captured = capsys.readouterr()
  140. output = captured.out
  141. assert "🤖 Developer → 👤 user" in output
  142. assert "Dev response:" in output
  143. def test_empty_input_handling(self, agency, capsys):
  144. """Test handling of empty input."""
  145. input_provider = MockInputProvider(["", " ", "/exit"])
  146. with patch("builtins.input", input_provider):
  147. with patch("agency_swarm.ui.demos.terminal.Application", new=_application_factory(input_provider)):
  148. start_terminal(agency, reload=False)
  149. captured = capsys.readouterr()
  150. assert "message cannot be empty" in captured.out
  151. def test_handoff_chat_transfer(self, agency, capsys):
  152. """Test handoff chat transfer."""
  153. input_provider = MockInputProvider(["Transfer to Developer", "Hi", "/exit"])
  154. with patch("builtins.input", input_provider):
  155. with patch("agency_swarm.ui.demos.terminal.Application", new=_application_factory(input_provider)):
  156. start_terminal(agency, reload=False)
  157. captured = capsys.readouterr()
  158. output = captured.out
  159. # Verify that initial input is sent to main agent
  160. assert "🤖 TestAgent 🛠️ Executing Function"
  161. assert "Calling transfer_to_Developer tool with:" in output
  162. # Verify that after handoff, response is sent by developer
  163. assert "🤖 Developer → 👤 user" in output
  164. assert "Dev response:" in output
  165. # Verify that next input went to developer
  166. assert "🤖 Developer → 👤 user" in output.split("USER: Hi")[-1]
  167. assert "Dev response:" in output.split("USER: Hi")[-1]
  168. def test_slash_dropdown_populates_commands(self, agency):
  169. """Ensure slash input populates dropdown items."""
  170. input_provider = MockInputProvider(["/", "/exit"])
  171. captured_labels: list[list[str]] = []
  172. original_set_items = terminal_module.DropdownMenu.set_items
  173. def _capture(self, items): # noqa: ANN001
  174. captured_labels.append([item.label for item in items])
  175. original_set_items(self, items)
  176. with patch("builtins.input", input_provider):
  177. with patch("agency_swarm.ui.demos.terminal.Application", new=_application_factory(input_provider)):
  178. with patch.object(terminal_module.DropdownMenu, "set_items", _capture):
  179. start_terminal(agency, reload=False)
  180. assert captured_labels, "Expected dropdown items to be set"
  181. assert any(label.startswith("/") for label in captured_labels[0])
  182. class TestTerminalEdgeCases:
  183. """Test edge cases and error handling with agency."""
  184. def test_invalid_agent_mention_logs_error(self, agency, capsys, caplog):
  185. """Test that invalid @agent mentions are logged as errors."""
  186. input_provider = MockInputProvider(["@NonExistentAgent help", "/exit"])
  187. with patch("builtins.input", input_provider):
  188. with patch("agency_swarm.ui.demos.terminal.Application", new=_application_factory(input_provider)):
  189. start_terminal(agency, reload=False)
  190. # Check that error was logged
  191. assert "Recipient agent NonExistentAgent not found" in caplog.text
  192. def test_very_long_message(self, agency, capsys):
  193. """Test handling of very long messages."""
  194. long_message = "This is a very long message. " * 50
  195. input_provider = MockInputProvider([long_message, "/exit"])
  196. with patch("builtins.input", input_provider):
  197. with patch("agency_swarm.ui.demos.terminal.Application", new=_application_factory(input_provider)):
  198. start_terminal(agency, reload=False)
  199. captured = capsys.readouterr()
  200. assert "🤖 TestAgent → 👤 user" in captured.out
  201. assert "Test response:" in captured.out
  202. def test_special_characters_in_message(self, agency, capsys):
  203. """Test handling of special characters in messages."""
  204. special_message = "Test with émojis 🎉 and symbols @#$%!"
  205. input_provider = MockInputProvider([special_message, "/exit"])
  206. with patch("builtins.input", input_provider):
  207. with patch("agency_swarm.ui.demos.terminal.Application", new=_application_factory(input_provider)):
  208. start_terminal(agency, reload=False)
  209. captured = capsys.readouterr()
  210. assert "🤖 TestAgent → 👤 user" in captured.out
  211. assert "Test response: Test with émojis 🎉 and symbols @#$%!" in captured.out
  212. def test_bad_formatted_agent_name(self, agency, capsys):
  213. """Test handling of bad formatted agent name."""
  214. input_provider = MockInputProvider(["@SecUrity ExperT_Agent hi", "/exit"])
  215. with patch("builtins.input", input_provider):
  216. with patch("agency_swarm.ui.demos.terminal.Application", new=_application_factory(input_provider)):
  217. start_terminal(agency, reload=False)
  218. captured = capsys.readouterr()
  219. assert "🤖 SecUrity ExperT_Agent → 👤 user" in captured.out
  220. assert "Security expert response:" in captured.out
  221. def test_bad_formatted_agent_name_handoff(self, agency, capsys):
  222. """Test handling of bad formatted agent name."""
  223. # Use the exact tool name so this test validates UI handoff rendering, not
  224. # the live model's ability to normalize a mismatched tool name.
  225. input_provider = MockInputProvider(["Use the transfer_to_SecUrity_ExperT_Agent tool", "Hi", "/exit"])
  226. with patch("builtins.input", input_provider):
  227. with patch("agency_swarm.ui.demos.terminal.Application", new=_application_factory(input_provider)):
  228. start_terminal(agency, reload=False)
  229. captured = capsys.readouterr()
  230. output = captured.out
  231. # Handoffs will replace spaces with underscores
  232. assert "🤖 TestAgent 🛠️ Executing Function"
  233. assert "Calling transfer_to_SecUrity_ExperT_Agent tool with:" in output
  234. # Verify that after handoff, name is shown correctly
  235. assert "🤖 SecUrity ExperT_Agent → 👤 user" in output
  236. assert "Security expert response:" in output
  237. # Verify that next input went didn't cause any errors
  238. assert "🤖 SecUrity ExperT_Agent → 👤 user" in output.split("USER: Hi")[-1]
  239. assert "Security expert response:" in output.split("USER: Hi")[-1]
  240. if __name__ == "__main__":
  241. pytest.main([__file__, "-v"])