Merge PR #173: Add automatic terminal detection for local enhancement
This commit is contained in:
@@ -1013,6 +1013,7 @@ python3 cli/doc_scraper.py --config configs/godot.json
|
|||||||
- **[docs/LARGE_DOCUMENTATION.md](docs/LARGE_DOCUMENTATION.md)** - Handle 10K-40K+ page docs
|
- **[docs/LARGE_DOCUMENTATION.md](docs/LARGE_DOCUMENTATION.md)** - Handle 10K-40K+ page docs
|
||||||
- **[ASYNC_SUPPORT.md](ASYNC_SUPPORT.md)** - Async mode guide (2-3x faster scraping)
|
- **[ASYNC_SUPPORT.md](ASYNC_SUPPORT.md)** - Async mode guide (2-3x faster scraping)
|
||||||
- **[docs/ENHANCEMENT.md](docs/ENHANCEMENT.md)** - AI enhancement guide
|
- **[docs/ENHANCEMENT.md](docs/ENHANCEMENT.md)** - AI enhancement guide
|
||||||
|
- **[docs/TERMINAL_SELECTION.md](docs/TERMINAL_SELECTION.md)** - Configure terminal app for local enhancement
|
||||||
- **[docs/UPLOAD_GUIDE.md](docs/UPLOAD_GUIDE.md)** - How to upload skills to Claude
|
- **[docs/UPLOAD_GUIDE.md](docs/UPLOAD_GUIDE.md)** - How to upload skills to Claude
|
||||||
- **[docs/MCP_SETUP.md](docs/MCP_SETUP.md)** - MCP integration setup
|
- **[docs/MCP_SETUP.md](docs/MCP_SETUP.md)** - MCP integration setup
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ No API key needed - uses your existing Claude Code Max plan!
|
|||||||
Usage:
|
Usage:
|
||||||
python3 cli/enhance_skill_local.py output/steam-inventory/
|
python3 cli/enhance_skill_local.py output/steam-inventory/
|
||||||
python3 cli/enhance_skill_local.py output/react/
|
python3 cli/enhance_skill_local.py output/react/
|
||||||
|
|
||||||
|
Terminal Selection:
|
||||||
|
The script automatically detects which terminal app to use:
|
||||||
|
1. SKILL_SEEKER_TERMINAL env var (highest priority)
|
||||||
|
Example: export SKILL_SEEKER_TERMINAL="Ghostty"
|
||||||
|
2. TERM_PROGRAM env var (current terminal)
|
||||||
|
3. Terminal.app (fallback)
|
||||||
|
|
||||||
|
Supported terminals: Ghostty, iTerm, Terminal, WezTerm
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -23,6 +32,55 @@ from cli.constants import LOCAL_CONTENT_LIMIT, LOCAL_PREVIEW_LIMIT
|
|||||||
from cli.utils import read_reference_files
|
from cli.utils import read_reference_files
|
||||||
|
|
||||||
|
|
||||||
|
def detect_terminal_app():
|
||||||
|
"""Detect which terminal app to use with cascading priority.
|
||||||
|
|
||||||
|
Priority order:
|
||||||
|
1. SKILL_SEEKER_TERMINAL environment variable (explicit user preference)
|
||||||
|
2. TERM_PROGRAM environment variable (inherit current terminal)
|
||||||
|
3. Terminal.app (fallback default)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (terminal_app_name, detection_method)
|
||||||
|
- terminal_app_name (str): Name of terminal app to launch (e.g., "Ghostty", "Terminal")
|
||||||
|
- detection_method (str): How the terminal was detected (for logging)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> os.environ['SKILL_SEEKER_TERMINAL'] = 'Ghostty'
|
||||||
|
>>> detect_terminal_app()
|
||||||
|
('Ghostty', 'SKILL_SEEKER_TERMINAL')
|
||||||
|
|
||||||
|
>>> os.environ['TERM_PROGRAM'] = 'iTerm.app'
|
||||||
|
>>> detect_terminal_app()
|
||||||
|
('iTerm', 'TERM_PROGRAM')
|
||||||
|
"""
|
||||||
|
# Map TERM_PROGRAM values to macOS app names
|
||||||
|
TERMINAL_MAP = {
|
||||||
|
'Apple_Terminal': 'Terminal',
|
||||||
|
'iTerm.app': 'iTerm',
|
||||||
|
'ghostty': 'Ghostty',
|
||||||
|
'WezTerm': 'WezTerm',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Priority 1: Check SKILL_SEEKER_TERMINAL env var (explicit preference)
|
||||||
|
preferred_terminal = os.environ.get('SKILL_SEEKER_TERMINAL', '').strip()
|
||||||
|
if preferred_terminal:
|
||||||
|
return preferred_terminal, 'SKILL_SEEKER_TERMINAL'
|
||||||
|
|
||||||
|
# Priority 2: Check TERM_PROGRAM (inherit current terminal)
|
||||||
|
term_program = os.environ.get('TERM_PROGRAM', '').strip()
|
||||||
|
if term_program and term_program in TERMINAL_MAP:
|
||||||
|
return TERMINAL_MAP[term_program], 'TERM_PROGRAM'
|
||||||
|
|
||||||
|
# Priority 3: Fallback to Terminal.app
|
||||||
|
if term_program:
|
||||||
|
# TERM_PROGRAM is set but unknown
|
||||||
|
return 'Terminal', f'unknown TERM_PROGRAM ({term_program})'
|
||||||
|
else:
|
||||||
|
# No TERM_PROGRAM set
|
||||||
|
return 'Terminal', 'default'
|
||||||
|
|
||||||
|
|
||||||
class LocalSkillEnhancer:
|
class LocalSkillEnhancer:
|
||||||
def __init__(self, skill_dir):
|
def __init__(self, skill_dir):
|
||||||
self.skill_dir = Path(skill_dir)
|
self.skill_dir = Path(skill_dir)
|
||||||
@@ -177,11 +235,24 @@ rm {prompt_file}
|
|||||||
|
|
||||||
# Launch in new terminal (macOS specific)
|
# Launch in new terminal (macOS specific)
|
||||||
if sys.platform == 'darwin':
|
if sys.platform == 'darwin':
|
||||||
# macOS Terminal - simple approach
|
# Detect which terminal app to use
|
||||||
|
terminal_app, detection_method = detect_terminal_app()
|
||||||
|
|
||||||
|
# Show detection info
|
||||||
|
if detection_method == 'SKILL_SEEKER_TERMINAL':
|
||||||
|
print(f" Using terminal: {terminal_app} (from SKILL_SEEKER_TERMINAL)")
|
||||||
|
elif detection_method == 'TERM_PROGRAM':
|
||||||
|
print(f" Using terminal: {terminal_app} (inherited from current terminal)")
|
||||||
|
elif detection_method.startswith('unknown TERM_PROGRAM'):
|
||||||
|
print(f"⚠️ {detection_method}")
|
||||||
|
print(f" → Using Terminal.app as fallback")
|
||||||
|
else:
|
||||||
|
print(f" Using terminal: {terminal_app} (default)")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.Popen(['open', '-a', 'Terminal', script_file])
|
subprocess.Popen(['open', '-a', terminal_app, script_file])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error launching terminal: {e}")
|
print(f"⚠️ Error launching {terminal_app}: {e}")
|
||||||
print(f"\nManually run: {script_file}")
|
print(f"\nManually run: {script_file}")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
|||||||
94
docs/TERMINAL_SELECTION.md
Normal file
94
docs/TERMINAL_SELECTION.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Terminal Selection Guide
|
||||||
|
|
||||||
|
When using `--enhance-local`, Skill Seeker opens a new terminal window to run Claude Code. This guide explains how to control which terminal app is used.
|
||||||
|
|
||||||
|
## Priority Order
|
||||||
|
|
||||||
|
The script automatically detects which terminal to use in this order:
|
||||||
|
|
||||||
|
1. **`SKILL_SEEKER_TERMINAL` environment variable** (highest priority)
|
||||||
|
2. **`TERM_PROGRAM` environment variable** (inherit current terminal)
|
||||||
|
3. **Terminal.app** (fallback default)
|
||||||
|
|
||||||
|
## Setting Your Preferred Terminal
|
||||||
|
|
||||||
|
### Option 1: Set Environment Variable (Recommended)
|
||||||
|
|
||||||
|
Add this to your shell config (`~/.zshrc` or `~/.bashrc`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For Ghostty users
|
||||||
|
export SKILL_SEEKER_TERMINAL="Ghostty"
|
||||||
|
|
||||||
|
# For iTerm users
|
||||||
|
export SKILL_SEEKER_TERMINAL="iTerm"
|
||||||
|
|
||||||
|
# For WezTerm users
|
||||||
|
export SKILL_SEEKER_TERMINAL="WezTerm"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reload your shell:
|
||||||
|
```bash
|
||||||
|
source ~/.zshrc # or source ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Set Per-Session
|
||||||
|
|
||||||
|
Set the variable before running the command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SKILL_SEEKER_TERMINAL="Ghostty" python3 cli/doc_scraper.py --config configs/react.json --enhance-local
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Inherit Current Terminal (Automatic)
|
||||||
|
|
||||||
|
If you run the script from Ghostty, iTerm2, or WezTerm, it will automatically open the enhancement in the same terminal app.
|
||||||
|
|
||||||
|
**Note:** IDE terminals (VS Code, Zed, JetBrains) use unique `TERM_PROGRAM` values, so they fall back to Terminal.app unless you set `SKILL_SEEKER_TERMINAL`.
|
||||||
|
|
||||||
|
## Supported Terminals
|
||||||
|
|
||||||
|
- **Ghostty** (`ghostty`)
|
||||||
|
- **iTerm2** (`iTerm.app`)
|
||||||
|
- **Terminal.app** (`Apple_Terminal`)
|
||||||
|
- **WezTerm** (`WezTerm`)
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
When terminal detection works:
|
||||||
|
```
|
||||||
|
🚀 Launching Claude Code in new terminal...
|
||||||
|
Using terminal: Ghostty (from SKILL_SEEKER_TERMINAL)
|
||||||
|
```
|
||||||
|
|
||||||
|
When running from an IDE terminal:
|
||||||
|
```
|
||||||
|
🚀 Launching Claude Code in new terminal...
|
||||||
|
⚠️ unknown TERM_PROGRAM (zed)
|
||||||
|
→ Using Terminal.app as fallback
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tip:** Set `SKILL_SEEKER_TERMINAL` to avoid the fallback behavior.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Q: The wrong terminal opens even though I set `SKILL_SEEKER_TERMINAL`**
|
||||||
|
|
||||||
|
A: Make sure you reloaded your shell after editing `~/.zshrc`:
|
||||||
|
```bash
|
||||||
|
source ~/.zshrc
|
||||||
|
```
|
||||||
|
|
||||||
|
**Q: I want to use a different terminal temporarily**
|
||||||
|
|
||||||
|
A: Set the variable inline:
|
||||||
|
```bash
|
||||||
|
SKILL_SEEKER_TERMINAL="iTerm" python3 cli/doc_scraper.py --enhance-local ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Q: Can I use a custom terminal app?**
|
||||||
|
|
||||||
|
A: Yes! Just use the app name as it appears in `/Applications/`:
|
||||||
|
```bash
|
||||||
|
export SKILL_SEEKER_TERMINAL="Alacritty"
|
||||||
|
```
|
||||||
330
tests/test_terminal_detection.py
Normal file
330
tests/test_terminal_detection.py
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
"""
|
||||||
|
Tests for terminal detection functionality in enhance_skill_local.py
|
||||||
|
|
||||||
|
This module tests the detect_terminal_app() function and terminal launching logic
|
||||||
|
to ensure correct terminal selection across different environments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add parent directory to path for imports
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
from cli.enhance_skill_local import detect_terminal_app, LocalSkillEnhancer
|
||||||
|
|
||||||
|
|
||||||
|
class TestDetectTerminalApp(unittest.TestCase):
|
||||||
|
"""Test the detect_terminal_app() function."""
|
||||||
|
|
||||||
|
original_skill_seeker: str | None = None
|
||||||
|
original_term_program: str | None = None
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Save original environment variables."""
|
||||||
|
self.original_skill_seeker = os.environ.get('SKILL_SEEKER_TERMINAL')
|
||||||
|
self.original_term_program = os.environ.get('TERM_PROGRAM')
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Restore original environment variables."""
|
||||||
|
# Remove test env vars
|
||||||
|
if 'SKILL_SEEKER_TERMINAL' in os.environ:
|
||||||
|
del os.environ['SKILL_SEEKER_TERMINAL']
|
||||||
|
if 'TERM_PROGRAM' in os.environ:
|
||||||
|
del os.environ['TERM_PROGRAM']
|
||||||
|
|
||||||
|
# Restore originals if they existed
|
||||||
|
if self.original_skill_seeker is not None:
|
||||||
|
os.environ['SKILL_SEEKER_TERMINAL'] = self.original_skill_seeker
|
||||||
|
if self.original_term_program is not None:
|
||||||
|
os.environ['TERM_PROGRAM'] = self.original_term_program
|
||||||
|
|
||||||
|
# HIGH PRIORITY TESTS
|
||||||
|
|
||||||
|
def test_detect_terminal_with_skill_seeker_env(self):
|
||||||
|
"""Test that SKILL_SEEKER_TERMINAL env var takes highest priority."""
|
||||||
|
os.environ['SKILL_SEEKER_TERMINAL'] = 'Ghostty'
|
||||||
|
|
||||||
|
terminal_app, detection_method = detect_terminal_app()
|
||||||
|
|
||||||
|
self.assertEqual(terminal_app, 'Ghostty')
|
||||||
|
self.assertEqual(detection_method, 'SKILL_SEEKER_TERMINAL')
|
||||||
|
|
||||||
|
def test_detect_terminal_with_term_program_known(self):
|
||||||
|
"""Test detection from TERM_PROGRAM with known terminal (iTerm)."""
|
||||||
|
# Ensure SKILL_SEEKER_TERMINAL is not set
|
||||||
|
if 'SKILL_SEEKER_TERMINAL' in os.environ:
|
||||||
|
del os.environ['SKILL_SEEKER_TERMINAL']
|
||||||
|
|
||||||
|
os.environ['TERM_PROGRAM'] = 'iTerm.app'
|
||||||
|
|
||||||
|
terminal_app, detection_method = detect_terminal_app()
|
||||||
|
|
||||||
|
self.assertEqual(terminal_app, 'iTerm')
|
||||||
|
self.assertEqual(detection_method, 'TERM_PROGRAM')
|
||||||
|
|
||||||
|
def test_detect_terminal_with_term_program_ghostty(self):
|
||||||
|
"""Test detection from TERM_PROGRAM with Ghostty terminal."""
|
||||||
|
if 'SKILL_SEEKER_TERMINAL' in os.environ:
|
||||||
|
del os.environ['SKILL_SEEKER_TERMINAL']
|
||||||
|
|
||||||
|
os.environ['TERM_PROGRAM'] = 'ghostty'
|
||||||
|
|
||||||
|
terminal_app, detection_method = detect_terminal_app()
|
||||||
|
|
||||||
|
self.assertEqual(terminal_app, 'Ghostty')
|
||||||
|
self.assertEqual(detection_method, 'TERM_PROGRAM')
|
||||||
|
|
||||||
|
def test_detect_terminal_with_term_program_apple_terminal(self):
|
||||||
|
"""Test detection from TERM_PROGRAM with Apple Terminal."""
|
||||||
|
if 'SKILL_SEEKER_TERMINAL' in os.environ:
|
||||||
|
del os.environ['SKILL_SEEKER_TERMINAL']
|
||||||
|
|
||||||
|
os.environ['TERM_PROGRAM'] = 'Apple_Terminal'
|
||||||
|
|
||||||
|
terminal_app, detection_method = detect_terminal_app()
|
||||||
|
|
||||||
|
self.assertEqual(terminal_app, 'Terminal')
|
||||||
|
self.assertEqual(detection_method, 'TERM_PROGRAM')
|
||||||
|
|
||||||
|
def test_detect_terminal_with_term_program_wezterm(self):
|
||||||
|
"""Test detection from TERM_PROGRAM with WezTerm."""
|
||||||
|
if 'SKILL_SEEKER_TERMINAL' in os.environ:
|
||||||
|
del os.environ['SKILL_SEEKER_TERMINAL']
|
||||||
|
|
||||||
|
os.environ['TERM_PROGRAM'] = 'WezTerm'
|
||||||
|
|
||||||
|
terminal_app, detection_method = detect_terminal_app()
|
||||||
|
|
||||||
|
self.assertEqual(terminal_app, 'WezTerm')
|
||||||
|
self.assertEqual(detection_method, 'TERM_PROGRAM')
|
||||||
|
|
||||||
|
def test_detect_terminal_with_term_program_unknown(self):
|
||||||
|
"""Test fallback behavior when TERM_PROGRAM is unknown (e.g., IDE terminals)."""
|
||||||
|
if 'SKILL_SEEKER_TERMINAL' in os.environ:
|
||||||
|
del os.environ['SKILL_SEEKER_TERMINAL']
|
||||||
|
|
||||||
|
os.environ['TERM_PROGRAM'] = 'zed'
|
||||||
|
|
||||||
|
terminal_app, detection_method = detect_terminal_app()
|
||||||
|
|
||||||
|
self.assertEqual(terminal_app, 'Terminal')
|
||||||
|
self.assertEqual(detection_method, 'unknown TERM_PROGRAM (zed)')
|
||||||
|
|
||||||
|
def test_detect_terminal_default_fallback(self):
|
||||||
|
"""Test default fallback when no environment variables are set."""
|
||||||
|
# Remove both env vars
|
||||||
|
if 'SKILL_SEEKER_TERMINAL' in os.environ:
|
||||||
|
del os.environ['SKILL_SEEKER_TERMINAL']
|
||||||
|
if 'TERM_PROGRAM' in os.environ:
|
||||||
|
del os.environ['TERM_PROGRAM']
|
||||||
|
|
||||||
|
terminal_app, detection_method = detect_terminal_app()
|
||||||
|
|
||||||
|
self.assertEqual(terminal_app, 'Terminal')
|
||||||
|
self.assertEqual(detection_method, 'default')
|
||||||
|
|
||||||
|
def test_detect_terminal_priority_order(self):
|
||||||
|
"""Test that SKILL_SEEKER_TERMINAL takes priority over TERM_PROGRAM."""
|
||||||
|
os.environ['SKILL_SEEKER_TERMINAL'] = 'Ghostty'
|
||||||
|
os.environ['TERM_PROGRAM'] = 'iTerm.app'
|
||||||
|
|
||||||
|
terminal_app, detection_method = detect_terminal_app()
|
||||||
|
|
||||||
|
# SKILL_SEEKER_TERMINAL should win
|
||||||
|
self.assertEqual(terminal_app, 'Ghostty')
|
||||||
|
self.assertEqual(detection_method, 'SKILL_SEEKER_TERMINAL')
|
||||||
|
|
||||||
|
@patch('subprocess.Popen')
|
||||||
|
def test_subprocess_popen_called_with_correct_args(self, mock_popen):
|
||||||
|
"""Test that subprocess.Popen is called with correct arguments on macOS."""
|
||||||
|
# Only test on macOS
|
||||||
|
if sys.platform != 'darwin':
|
||||||
|
self.skipTest("This test only runs on macOS")
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
os.environ['SKILL_SEEKER_TERMINAL'] = 'Ghostty'
|
||||||
|
|
||||||
|
# Create a test skill directory with minimal setup
|
||||||
|
import tempfile
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
skill_dir = Path(tmpdir) / 'test_skill'
|
||||||
|
skill_dir.mkdir()
|
||||||
|
|
||||||
|
# Create references directory (required by LocalSkillEnhancer)
|
||||||
|
(skill_dir / 'references').mkdir()
|
||||||
|
(skill_dir / 'references' / 'test.md').write_text('# Test')
|
||||||
|
|
||||||
|
# Create SKILL.md (required)
|
||||||
|
(skill_dir / 'SKILL.md').write_text('---\nname: test\n---\n# Test')
|
||||||
|
|
||||||
|
# Mock Popen to prevent actual terminal launch
|
||||||
|
mock_popen.return_value = MagicMock()
|
||||||
|
|
||||||
|
# Run enhancer
|
||||||
|
enhancer = LocalSkillEnhancer(skill_dir)
|
||||||
|
result = enhancer.run()
|
||||||
|
|
||||||
|
# Verify Popen was called
|
||||||
|
self.assertTrue(mock_popen.called)
|
||||||
|
|
||||||
|
# Verify call arguments
|
||||||
|
call_args = mock_popen.call_args[0][0]
|
||||||
|
self.assertEqual(call_args[0], 'open')
|
||||||
|
self.assertEqual(call_args[1], '-a')
|
||||||
|
self.assertEqual(call_args[2], 'Ghostty')
|
||||||
|
# call_args[3] should be the script file path
|
||||||
|
self.assertTrue(call_args[3].endswith('.sh'))
|
||||||
|
|
||||||
|
# MEDIUM PRIORITY TESTS
|
||||||
|
|
||||||
|
def test_detect_terminal_whitespace_handling(self):
|
||||||
|
"""Test that whitespace is stripped from environment variables."""
|
||||||
|
os.environ['SKILL_SEEKER_TERMINAL'] = ' Ghostty '
|
||||||
|
|
||||||
|
terminal_app, detection_method = detect_terminal_app()
|
||||||
|
|
||||||
|
self.assertEqual(terminal_app, 'Ghostty')
|
||||||
|
self.assertEqual(detection_method, 'SKILL_SEEKER_TERMINAL')
|
||||||
|
|
||||||
|
def test_detect_terminal_empty_string_env_vars(self):
|
||||||
|
"""Test that empty string env vars fall through to next priority."""
|
||||||
|
os.environ['SKILL_SEEKER_TERMINAL'] = ''
|
||||||
|
os.environ['TERM_PROGRAM'] = 'iTerm.app'
|
||||||
|
|
||||||
|
terminal_app, detection_method = detect_terminal_app()
|
||||||
|
|
||||||
|
# Should skip empty SKILL_SEEKER_TERMINAL and use TERM_PROGRAM
|
||||||
|
self.assertEqual(terminal_app, 'iTerm')
|
||||||
|
self.assertEqual(detection_method, 'TERM_PROGRAM')
|
||||||
|
|
||||||
|
def test_detect_terminal_empty_string_both_vars(self):
|
||||||
|
"""Test that empty strings on both vars falls back to default."""
|
||||||
|
os.environ['SKILL_SEEKER_TERMINAL'] = ''
|
||||||
|
os.environ['TERM_PROGRAM'] = ''
|
||||||
|
|
||||||
|
terminal_app, detection_method = detect_terminal_app()
|
||||||
|
|
||||||
|
# Should fall back to default
|
||||||
|
self.assertEqual(terminal_app, 'Terminal')
|
||||||
|
# Empty TERM_PROGRAM should be treated as not set
|
||||||
|
self.assertEqual(detection_method, 'default')
|
||||||
|
|
||||||
|
@patch('subprocess.Popen')
|
||||||
|
def test_terminal_launch_error_handling(self, mock_popen):
|
||||||
|
"""Test error handling when terminal launch fails."""
|
||||||
|
# Only test on macOS
|
||||||
|
if sys.platform != 'darwin':
|
||||||
|
self.skipTest("This test only runs on macOS")
|
||||||
|
|
||||||
|
# Setup Popen to raise exception
|
||||||
|
mock_popen.side_effect = Exception("Terminal not found")
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
skill_dir = Path(tmpdir) / 'test_skill'
|
||||||
|
skill_dir.mkdir()
|
||||||
|
(skill_dir / 'references').mkdir()
|
||||||
|
(skill_dir / 'references' / 'test.md').write_text('# Test')
|
||||||
|
(skill_dir / 'SKILL.md').write_text('---\nname: test\n---\n# Test')
|
||||||
|
|
||||||
|
enhancer = LocalSkillEnhancer(skill_dir)
|
||||||
|
|
||||||
|
# Capture stdout to check error message
|
||||||
|
from io import StringIO
|
||||||
|
captured_output = StringIO()
|
||||||
|
old_stdout = sys.stdout
|
||||||
|
sys.stdout = captured_output
|
||||||
|
|
||||||
|
result = enhancer.run()
|
||||||
|
|
||||||
|
# Restore stdout
|
||||||
|
sys.stdout = old_stdout
|
||||||
|
|
||||||
|
# Should return False on error
|
||||||
|
self.assertFalse(result)
|
||||||
|
|
||||||
|
# Should print error message
|
||||||
|
output = captured_output.getvalue()
|
||||||
|
self.assertIn('Error launching', output)
|
||||||
|
|
||||||
|
def test_output_message_unknown_terminal(self):
|
||||||
|
"""Test that unknown terminal prints warning message."""
|
||||||
|
if sys.platform != 'darwin':
|
||||||
|
self.skipTest("This test only runs on macOS")
|
||||||
|
|
||||||
|
os.environ['TERM_PROGRAM'] = 'vscode'
|
||||||
|
if 'SKILL_SEEKER_TERMINAL' in os.environ:
|
||||||
|
del os.environ['SKILL_SEEKER_TERMINAL']
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
skill_dir = Path(tmpdir) / 'test_skill'
|
||||||
|
skill_dir.mkdir()
|
||||||
|
(skill_dir / 'references').mkdir()
|
||||||
|
(skill_dir / 'references' / 'test.md').write_text('# Test')
|
||||||
|
(skill_dir / 'SKILL.md').write_text('---\nname: test\n---\n# Test')
|
||||||
|
|
||||||
|
enhancer = LocalSkillEnhancer(skill_dir)
|
||||||
|
|
||||||
|
# Capture stdout
|
||||||
|
from io import StringIO
|
||||||
|
captured_output = StringIO()
|
||||||
|
old_stdout = sys.stdout
|
||||||
|
sys.stdout = captured_output
|
||||||
|
|
||||||
|
# Mock Popen to prevent actual launch
|
||||||
|
with patch('subprocess.Popen') as mock_popen:
|
||||||
|
mock_popen.return_value = MagicMock()
|
||||||
|
enhancer.run()
|
||||||
|
|
||||||
|
# Restore stdout
|
||||||
|
sys.stdout = old_stdout
|
||||||
|
|
||||||
|
output = captured_output.getvalue()
|
||||||
|
|
||||||
|
# Should contain warning about unknown terminal
|
||||||
|
self.assertIn('⚠️', output)
|
||||||
|
self.assertIn('unknown TERM_PROGRAM', output)
|
||||||
|
self.assertIn('vscode', output)
|
||||||
|
self.assertIn('Using Terminal.app as fallback', output)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTerminalMapCompleteness(unittest.TestCase):
|
||||||
|
"""Test that TERMINAL_MAP covers all documented terminals."""
|
||||||
|
|
||||||
|
def test_terminal_map_has_all_documented_terminals(self):
|
||||||
|
"""Verify TERMINAL_MAP contains all terminals mentioned in documentation."""
|
||||||
|
from cli.enhance_skill_local import detect_terminal_app
|
||||||
|
|
||||||
|
# Get the TERMINAL_MAP from the function's scope
|
||||||
|
# We need to test this indirectly by checking each known terminal
|
||||||
|
|
||||||
|
known_terminals = [
|
||||||
|
('Apple_Terminal', 'Terminal'),
|
||||||
|
('iTerm.app', 'iTerm'),
|
||||||
|
('ghostty', 'Ghostty'),
|
||||||
|
('WezTerm', 'WezTerm'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for term_program_value, expected_app_name in known_terminals:
|
||||||
|
# Set TERM_PROGRAM and verify detection
|
||||||
|
os.environ['TERM_PROGRAM'] = term_program_value
|
||||||
|
if 'SKILL_SEEKER_TERMINAL' in os.environ:
|
||||||
|
del os.environ['SKILL_SEEKER_TERMINAL']
|
||||||
|
|
||||||
|
terminal_app, detection_method = detect_terminal_app()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
terminal_app,
|
||||||
|
expected_app_name,
|
||||||
|
f"TERM_PROGRAM='{term_program_value}' should map to '{expected_app_name}'"
|
||||||
|
)
|
||||||
|
self.assertEqual(detection_method, 'TERM_PROGRAM')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user