test_terminal_detection.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. """
  2. Tests for terminal detection functionality in enhance_skill_local.py
  3. This module tests the detect_terminal_app() function and terminal launching logic
  4. to ensure correct terminal selection across different environments.
  5. """
  6. import unittest
  7. import os
  8. import sys
  9. from unittest.mock import patch, MagicMock
  10. from pathlib import Path
  11. # Add parent directory to path for imports
  12. sys.path.insert(0, str(Path(__file__).parent.parent))
  13. from skill_seekers.cli.enhance_skill_local import detect_terminal_app, LocalSkillEnhancer
  14. class TestDetectTerminalApp(unittest.TestCase):
  15. """Test the detect_terminal_app() function."""
  16. original_skill_seeker: str | None = None
  17. original_term_program: str | None = None
  18. def setUp(self):
  19. """Save original environment variables."""
  20. self.original_skill_seeker = os.environ.get('SKILL_SEEKER_TERMINAL')
  21. self.original_term_program = os.environ.get('TERM_PROGRAM')
  22. def tearDown(self):
  23. """Restore original environment variables."""
  24. # Remove test env vars
  25. if 'SKILL_SEEKER_TERMINAL' in os.environ:
  26. del os.environ['SKILL_SEEKER_TERMINAL']
  27. if 'TERM_PROGRAM' in os.environ:
  28. del os.environ['TERM_PROGRAM']
  29. # Restore originals if they existed
  30. if self.original_skill_seeker is not None:
  31. os.environ['SKILL_SEEKER_TERMINAL'] = self.original_skill_seeker
  32. if self.original_term_program is not None:
  33. os.environ['TERM_PROGRAM'] = self.original_term_program
  34. # HIGH PRIORITY TESTS
  35. def test_detect_terminal_with_skill_seeker_env(self):
  36. """Test that SKILL_SEEKER_TERMINAL env var takes highest priority."""
  37. os.environ['SKILL_SEEKER_TERMINAL'] = 'Ghostty'
  38. terminal_app, detection_method = detect_terminal_app()
  39. self.assertEqual(terminal_app, 'Ghostty')
  40. self.assertEqual(detection_method, 'SKILL_SEEKER_TERMINAL')
  41. def test_detect_terminal_with_term_program_known(self):
  42. """Test detection from TERM_PROGRAM with known terminal (iTerm)."""
  43. # Ensure SKILL_SEEKER_TERMINAL is not set
  44. if 'SKILL_SEEKER_TERMINAL' in os.environ:
  45. del os.environ['SKILL_SEEKER_TERMINAL']
  46. os.environ['TERM_PROGRAM'] = 'iTerm.app'
  47. terminal_app, detection_method = detect_terminal_app()
  48. self.assertEqual(terminal_app, 'iTerm')
  49. self.assertEqual(detection_method, 'TERM_PROGRAM')
  50. def test_detect_terminal_with_term_program_ghostty(self):
  51. """Test detection from TERM_PROGRAM with Ghostty terminal."""
  52. if 'SKILL_SEEKER_TERMINAL' in os.environ:
  53. del os.environ['SKILL_SEEKER_TERMINAL']
  54. os.environ['TERM_PROGRAM'] = 'ghostty'
  55. terminal_app, detection_method = detect_terminal_app()
  56. self.assertEqual(terminal_app, 'Ghostty')
  57. self.assertEqual(detection_method, 'TERM_PROGRAM')
  58. def test_detect_terminal_with_term_program_apple_terminal(self):
  59. """Test detection from TERM_PROGRAM with Apple Terminal."""
  60. if 'SKILL_SEEKER_TERMINAL' in os.environ:
  61. del os.environ['SKILL_SEEKER_TERMINAL']
  62. os.environ['TERM_PROGRAM'] = 'Apple_Terminal'
  63. terminal_app, detection_method = detect_terminal_app()
  64. self.assertEqual(terminal_app, 'Terminal')
  65. self.assertEqual(detection_method, 'TERM_PROGRAM')
  66. def test_detect_terminal_with_term_program_wezterm(self):
  67. """Test detection from TERM_PROGRAM with WezTerm."""
  68. if 'SKILL_SEEKER_TERMINAL' in os.environ:
  69. del os.environ['SKILL_SEEKER_TERMINAL']
  70. os.environ['TERM_PROGRAM'] = 'WezTerm'
  71. terminal_app, detection_method = detect_terminal_app()
  72. self.assertEqual(terminal_app, 'WezTerm')
  73. self.assertEqual(detection_method, 'TERM_PROGRAM')
  74. def test_detect_terminal_with_term_program_unknown(self):
  75. """Test fallback behavior when TERM_PROGRAM is unknown (e.g., IDE terminals)."""
  76. if 'SKILL_SEEKER_TERMINAL' in os.environ:
  77. del os.environ['SKILL_SEEKER_TERMINAL']
  78. os.environ['TERM_PROGRAM'] = 'zed'
  79. terminal_app, detection_method = detect_terminal_app()
  80. self.assertEqual(terminal_app, 'Terminal')
  81. self.assertEqual(detection_method, 'unknown TERM_PROGRAM (zed)')
  82. def test_detect_terminal_default_fallback(self):
  83. """Test default fallback when no environment variables are set."""
  84. # Remove both env vars
  85. if 'SKILL_SEEKER_TERMINAL' in os.environ:
  86. del os.environ['SKILL_SEEKER_TERMINAL']
  87. if 'TERM_PROGRAM' in os.environ:
  88. del os.environ['TERM_PROGRAM']
  89. terminal_app, detection_method = detect_terminal_app()
  90. self.assertEqual(terminal_app, 'Terminal')
  91. self.assertEqual(detection_method, 'default')
  92. def test_detect_terminal_priority_order(self):
  93. """Test that SKILL_SEEKER_TERMINAL takes priority over TERM_PROGRAM."""
  94. os.environ['SKILL_SEEKER_TERMINAL'] = 'Ghostty'
  95. os.environ['TERM_PROGRAM'] = 'iTerm.app'
  96. terminal_app, detection_method = detect_terminal_app()
  97. # SKILL_SEEKER_TERMINAL should win
  98. self.assertEqual(terminal_app, 'Ghostty')
  99. self.assertEqual(detection_method, 'SKILL_SEEKER_TERMINAL')
  100. @patch('subprocess.Popen')
  101. def test_subprocess_popen_called_with_correct_args(self, mock_popen):
  102. """Test that subprocess.Popen is called with correct arguments on macOS."""
  103. # Only test on macOS
  104. if sys.platform != 'darwin':
  105. self.skipTest("This test only runs on macOS")
  106. # Setup
  107. os.environ['SKILL_SEEKER_TERMINAL'] = 'Ghostty'
  108. # Create a test skill directory with minimal setup
  109. import tempfile
  110. with tempfile.TemporaryDirectory() as tmpdir:
  111. skill_dir = Path(tmpdir) / 'test_skill'
  112. skill_dir.mkdir()
  113. # Create references directory (required by LocalSkillEnhancer)
  114. (skill_dir / 'references').mkdir()
  115. (skill_dir / 'references' / 'test.md').write_text('# Test')
  116. # Create SKILL.md (required)
  117. (skill_dir / 'SKILL.md').write_text('---\nname: test\n---\n# Test')
  118. # Mock Popen to prevent actual terminal launch
  119. mock_popen.return_value = MagicMock()
  120. # Run enhancer in interactive mode (not headless)
  121. enhancer = LocalSkillEnhancer(skill_dir)
  122. result = enhancer.run(headless=False)
  123. # Verify Popen was called
  124. self.assertTrue(mock_popen.called)
  125. # Verify call arguments
  126. call_args = mock_popen.call_args[0][0]
  127. self.assertEqual(call_args[0], 'open')
  128. self.assertEqual(call_args[1], '-a')
  129. self.assertEqual(call_args[2], 'Ghostty')
  130. # call_args[3] should be the script file path
  131. self.assertTrue(call_args[3].endswith('.sh'))
  132. # MEDIUM PRIORITY TESTS
  133. def test_detect_terminal_whitespace_handling(self):
  134. """Test that whitespace is stripped from environment variables."""
  135. os.environ['SKILL_SEEKER_TERMINAL'] = ' Ghostty '
  136. terminal_app, detection_method = detect_terminal_app()
  137. self.assertEqual(terminal_app, 'Ghostty')
  138. self.assertEqual(detection_method, 'SKILL_SEEKER_TERMINAL')
  139. def test_detect_terminal_empty_string_env_vars(self):
  140. """Test that empty string env vars fall through to next priority."""
  141. os.environ['SKILL_SEEKER_TERMINAL'] = ''
  142. os.environ['TERM_PROGRAM'] = 'iTerm.app'
  143. terminal_app, detection_method = detect_terminal_app()
  144. # Should skip empty SKILL_SEEKER_TERMINAL and use TERM_PROGRAM
  145. self.assertEqual(terminal_app, 'iTerm')
  146. self.assertEqual(detection_method, 'TERM_PROGRAM')
  147. def test_detect_terminal_empty_string_both_vars(self):
  148. """Test that empty strings on both vars falls back to default."""
  149. os.environ['SKILL_SEEKER_TERMINAL'] = ''
  150. os.environ['TERM_PROGRAM'] = ''
  151. terminal_app, detection_method = detect_terminal_app()
  152. # Should fall back to default
  153. self.assertEqual(terminal_app, 'Terminal')
  154. # Empty TERM_PROGRAM should be treated as not set
  155. self.assertEqual(detection_method, 'default')
  156. @patch('subprocess.Popen')
  157. def test_terminal_launch_error_handling(self, mock_popen):
  158. """Test error handling when terminal launch fails."""
  159. # Only test on macOS
  160. if sys.platform != 'darwin':
  161. self.skipTest("This test only runs on macOS")
  162. # Setup Popen to raise exception
  163. mock_popen.side_effect = Exception("Terminal not found")
  164. import tempfile
  165. with tempfile.TemporaryDirectory() as tmpdir:
  166. skill_dir = Path(tmpdir) / 'test_skill'
  167. skill_dir.mkdir()
  168. (skill_dir / 'references').mkdir()
  169. (skill_dir / 'references' / 'test.md').write_text('# Test')
  170. (skill_dir / 'SKILL.md').write_text('---\nname: test\n---\n# Test')
  171. enhancer = LocalSkillEnhancer(skill_dir)
  172. # Capture stdout to check error message
  173. from io import StringIO
  174. captured_output = StringIO()
  175. old_stdout = sys.stdout
  176. sys.stdout = captured_output
  177. # Run in interactive mode (not headless) to test terminal launch
  178. result = enhancer.run(headless=False)
  179. # Restore stdout
  180. sys.stdout = old_stdout
  181. # Should return False on error
  182. self.assertFalse(result)
  183. # Should print error message
  184. output = captured_output.getvalue()
  185. self.assertIn('Error launching', output)
  186. def test_output_message_unknown_terminal(self):
  187. """Test that unknown terminal prints warning message."""
  188. if sys.platform != 'darwin':
  189. self.skipTest("This test only runs on macOS")
  190. os.environ['TERM_PROGRAM'] = 'vscode'
  191. if 'SKILL_SEEKER_TERMINAL' in os.environ:
  192. del os.environ['SKILL_SEEKER_TERMINAL']
  193. import tempfile
  194. with tempfile.TemporaryDirectory() as tmpdir:
  195. skill_dir = Path(tmpdir) / 'test_skill'
  196. skill_dir.mkdir()
  197. (skill_dir / 'references').mkdir()
  198. (skill_dir / 'references' / 'test.md').write_text('# Test')
  199. (skill_dir / 'SKILL.md').write_text('---\nname: test\n---\n# Test')
  200. enhancer = LocalSkillEnhancer(skill_dir)
  201. # Capture stdout
  202. from io import StringIO
  203. captured_output = StringIO()
  204. old_stdout = sys.stdout
  205. sys.stdout = captured_output
  206. # Mock Popen to prevent actual launch
  207. with patch('subprocess.Popen') as mock_popen:
  208. mock_popen.return_value = MagicMock()
  209. # Run in interactive mode (not headless) to test terminal detection
  210. enhancer.run(headless=False)
  211. # Restore stdout
  212. sys.stdout = old_stdout
  213. output = captured_output.getvalue()
  214. # Should contain warning about unknown terminal
  215. self.assertIn('⚠️', output)
  216. self.assertIn('unknown TERM_PROGRAM', output)
  217. self.assertIn('vscode', output)
  218. self.assertIn('Using Terminal.app as fallback', output)
  219. class TestTerminalMapCompleteness(unittest.TestCase):
  220. """Test that TERMINAL_MAP covers all documented terminals."""
  221. def test_terminal_map_has_all_documented_terminals(self):
  222. """Verify TERMINAL_MAP contains all terminals mentioned in documentation."""
  223. from skill_seekers.cli.enhance_skill_local import detect_terminal_app
  224. # Get the TERMINAL_MAP from the function's scope
  225. # We need to test this indirectly by checking each known terminal
  226. known_terminals = [
  227. ('Apple_Terminal', 'Terminal'),
  228. ('iTerm.app', 'iTerm'),
  229. ('ghostty', 'Ghostty'),
  230. ('WezTerm', 'WezTerm'),
  231. ]
  232. for term_program_value, expected_app_name in known_terminals:
  233. # Set TERM_PROGRAM and verify detection
  234. os.environ['TERM_PROGRAM'] = term_program_value
  235. if 'SKILL_SEEKER_TERMINAL' in os.environ:
  236. del os.environ['SKILL_SEEKER_TERMINAL']
  237. terminal_app, detection_method = detect_terminal_app()
  238. self.assertEqual(
  239. terminal_app,
  240. expected_app_name,
  241. f"TERM_PROGRAM='{term_program_value}' should map to '{expected_app_name}'"
  242. )
  243. self.assertEqual(detection_method, 'TERM_PROGRAM')
  244. if __name__ == '__main__':
  245. unittest.main()