diff --git a/tests/test_issue_219_e2e.py b/tests/test_issue_219_e2e.py new file mode 100644 index 0000000..912049a --- /dev/null +++ b/tests/test_issue_219_e2e.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python3 +""" +End-to-End Tests for Issue #219 - All Three Problems + +Tests verify complete fixes for: +1. Large file encoding error (ccxt/ccxt 1.4MB CHANGELOG) +2. Missing --enhance-local CLI flag +3. Custom API endpoint support (ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN) +""" + +import unittest +import sys +import os +import subprocess +import tempfile +import shutil +from pathlib import Path +from unittest.mock import Mock, patch, MagicMock +from types import SimpleNamespace + +# Add src to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + + +class TestIssue219Problem1LargeFiles(unittest.TestCase): + """E2E Test: Problem #1 - Large file download via download_url""" + + def setUp(self): + """Set up test environment""" + try: + from github import Github, GithubException + self.PYGITHUB_AVAILABLE = True + except ImportError: + self.PYGITHUB_AVAILABLE = False + + if not self.PYGITHUB_AVAILABLE: + self.skipTest("PyGithub not installed") + + from skill_seekers.cli.github_scraper import GitHubScraper + self.GitHubScraper = GitHubScraper + + def test_large_file_extraction_end_to_end(self): + """E2E: Verify large files (encoding='none') are downloaded via URL""" + from github import GithubException + + config = { + 'repo': 'ccxt/ccxt', + 'name': 'ccxt', + 'github_token': None + } + + # Mock large CHANGELOG (1.4MB, encoding="none") + mock_content = Mock() + mock_content.type = 'file' + mock_content.encoding = 'none' # This is what GitHub API returns for large files + mock_content.size = 1388271 + mock_content.download_url = 'https://raw.githubusercontent.com/ccxt/ccxt/master/CHANGELOG.md' + + with patch('skill_seekers.cli.github_scraper.Github'): + scraper = self.GitHubScraper(config) + scraper.repo = Mock() + scraper.repo.get_contents.return_value = mock_content + + # Mock requests.get for download + with patch('requests.get') as mock_requests: + mock_response = Mock() + mock_response.text = '# CCXT Changelog\n\n## v4.4.20\n- Bug fixes' + mock_response.raise_for_status = Mock() + mock_requests.return_value = mock_response + + # Call _extract_changelog (full workflow) + scraper._extract_changelog() + + # VERIFY: download_url was called + mock_requests.assert_called_once_with( + 'https://raw.githubusercontent.com/ccxt/ccxt/master/CHANGELOG.md', + timeout=30 + ) + + # VERIFY: CHANGELOG was extracted successfully + self.assertIn('changelog', scraper.extracted_data) + self.assertIn('Bug fixes', scraper.extracted_data['changelog']) + self.assertEqual(scraper.extracted_data['changelog'], mock_response.text) + + def test_large_file_fallback_on_error(self): + """E2E: Verify graceful handling if download_url fails""" + from github import GithubException + + config = { + 'repo': 'test/repo', + 'name': 'test', + 'github_token': None + } + + # Mock large file without download_url + mock_content = Mock() + mock_content.type = 'file' + mock_content.encoding = 'none' + mock_content.size = 2000000 + mock_content.download_url = None # Missing download URL + + with patch('skill_seekers.cli.github_scraper.Github'): + scraper = self.GitHubScraper(config) + scraper.repo = Mock() + scraper.repo.get_contents.return_value = mock_content + + # Should return None gracefully + result = scraper._get_file_content('CHANGELOG.md') + self.assertIsNone(result) + + # Should not crash + scraper._extract_changelog() + self.assertEqual(scraper.extracted_data['changelog'], '') + + +class TestIssue219Problem2CLIFlags(unittest.TestCase): + """E2E Test: Problem #2 - CLI flags working through main.py dispatcher""" + + def test_github_command_has_enhancement_flags(self): + """E2E: Verify --enhance-local flag exists in github command help""" + result = subprocess.run( + ['skill-seekers', 'github', '--help'], + capture_output=True, + text=True + ) + + # VERIFY: Command succeeds + self.assertEqual(result.returncode, 0, "github --help should succeed") + + # VERIFY: All enhancement flags present + self.assertIn('--enhance', result.stdout, "Missing --enhance flag") + self.assertIn('--enhance-local', result.stdout, "Missing --enhance-local flag") + self.assertIn('--api-key', result.stdout, "Missing --api-key flag") + + def test_github_command_accepts_enhance_local_flag(self): + """E2E: Verify --enhance-local flag doesn't cause 'unrecognized arguments' error""" + # Use dry-run with minimal args to test flag parsing + result = subprocess.run( + ['skill-seekers', 'github', '--repo', 'test/test', '--enhance-local'], + capture_output=True, + text=True, + timeout=5 + ) + + # VERIFY: No "unrecognized arguments" error + self.assertNotIn('unrecognized arguments', result.stderr, + "Flag should be recognized by CLI parser") + self.assertNotIn('--enhance-local', result.stderr, + "Flag should not appear in error message") + + def test_cli_dispatcher_forwards_flags_to_github_scraper(self): + """E2E: Verify main.py dispatcher forwards flags to github_scraper.py""" + from skill_seekers.cli import main + + # Mock sys.argv to simulate CLI call + test_args = [ + 'skill-seekers', + 'github', + '--repo', 'test/test', + '--name', 'test', + '--enhance-local' + ] + + with patch('sys.argv', test_args): + with patch('skill_seekers.cli.github_scraper.main') as mock_github_main: + mock_github_main.return_value = 0 + + # Call main dispatcher + with patch('sys.exit'): + try: + main.main() + except SystemExit: + pass + + # VERIFY: github_scraper.main was called + mock_github_main.assert_called_once() + + # VERIFY: sys.argv contains --enhance-local flag + # (main.py should have added it before calling github_scraper) + called_with_enhance = any('--enhance-local' in str(call) for call in mock_github_main.call_args_list) + self.assertTrue(called_with_enhance or '--enhance-local' in sys.argv, + "Flag should be forwarded to github_scraper") + + +class TestIssue219Problem3CustomAPIEndpoints(unittest.TestCase): + """E2E Test: Problem #3 - Custom API endpoint support""" + + def setUp(self): + """Set up test environment""" + self.temp_dir = tempfile.mkdtemp() + self.skill_dir = Path(self.temp_dir) / "test_skill" + self.skill_dir.mkdir() + + # Create minimal SKILL.md + (self.skill_dir / "SKILL.md").write_text("# Test Skill\n", encoding='utf-8') + + # Create references directory + refs_dir = self.skill_dir / "references" + refs_dir.mkdir() + (refs_dir / "index.md").write_text("# Index\n", encoding='utf-8') + + def tearDown(self): + """Clean up test environment""" + shutil.rmtree(self.temp_dir, ignore_errors=True) + + def test_anthropic_base_url_support(self): + """E2E: Verify ANTHROPIC_BASE_URL environment variable is supported""" + try: + from skill_seekers.cli.enhance_skill import SkillEnhancer + except ImportError: + self.skipTest("anthropic package not installed") + + # Set custom base URL + custom_url = 'http://localhost:3000' + + with patch.dict(os.environ, { + 'ANTHROPIC_API_KEY': 'test-key-123', + 'ANTHROPIC_BASE_URL': custom_url + }): + with patch('skill_seekers.cli.enhance_skill.anthropic.Anthropic') as mock_anthropic: + # Create enhancer + enhancer = SkillEnhancer(self.skill_dir) + + # VERIFY: Anthropic client called with custom base_url + mock_anthropic.assert_called_once() + call_kwargs = mock_anthropic.call_args[1] + self.assertIn('base_url', call_kwargs, "base_url should be passed") + self.assertEqual(call_kwargs['base_url'], custom_url, + "base_url should match ANTHROPIC_BASE_URL env var") + + def test_anthropic_auth_token_support(self): + """E2E: Verify ANTHROPIC_AUTH_TOKEN is accepted as alternative to ANTHROPIC_API_KEY""" + try: + from skill_seekers.cli.enhance_skill import SkillEnhancer + except ImportError: + self.skipTest("anthropic package not installed") + + custom_token = 'custom-auth-token-456' + + # Use ANTHROPIC_AUTH_TOKEN instead of ANTHROPIC_API_KEY + with patch.dict(os.environ, { + 'ANTHROPIC_AUTH_TOKEN': custom_token + }, clear=True): + with patch('skill_seekers.cli.enhance_skill.anthropic.Anthropic') as mock_anthropic: + # Create enhancer (should accept ANTHROPIC_AUTH_TOKEN) + enhancer = SkillEnhancer(self.skill_dir) + + # VERIFY: api_key set to ANTHROPIC_AUTH_TOKEN value + self.assertEqual(enhancer.api_key, custom_token, + "Should use ANTHROPIC_AUTH_TOKEN when ANTHROPIC_API_KEY not set") + + # VERIFY: Anthropic client initialized with correct key + mock_anthropic.assert_called_once() + call_kwargs = mock_anthropic.call_args[1] + self.assertEqual(call_kwargs['api_key'], custom_token, + "api_key should match ANTHROPIC_AUTH_TOKEN") + + def test_thinking_block_handling(self): + """E2E: Verify ThinkingBlock doesn't cause .text AttributeError""" + try: + from skill_seekers.cli.enhance_skill import SkillEnhancer + except ImportError: + self.skipTest("anthropic package not installed") + + with patch.dict(os.environ, {'ANTHROPIC_API_KEY': 'test-key'}): + with patch('skill_seekers.cli.enhance_skill.anthropic.Anthropic') as mock_anthropic: + enhancer = SkillEnhancer(self.skill_dir) + + # Mock response with ThinkingBlock (newer SDK) + # ThinkingBlock has no .text attribute + mock_thinking_block = SimpleNamespace(type='thinking') + + # TextBlock has .text attribute + mock_text_block = SimpleNamespace(text='# Enhanced SKILL.md\n\nContent here') + + mock_message = Mock() + mock_message.content = [mock_thinking_block, mock_text_block] + + mock_client = mock_anthropic.return_value + mock_client.messages.create.return_value = mock_message + + # Read references + references = { + 'index.md': '# Index\nTest content' + } + + # Call enhance_skill_md (should handle ThinkingBlock gracefully) + result = enhancer.enhance_skill_md(references, current_skill_md='# Old') + + # VERIFY: Should find text from TextBlock, ignore ThinkingBlock + self.assertIsNotNone(result, "Should return enhanced content") + self.assertEqual(result, '# Enhanced SKILL.md\n\nContent here', + "Should extract text from TextBlock") + + +class TestIssue219IntegrationAll(unittest.TestCase): + """E2E Integration: All 3 problems together""" + + def test_all_fixes_work_together(self): + """E2E: Verify all 3 fixes work in combination""" + # This test verifies the complete workflow: + # 1. CLI accepts --enhance-local + # 2. Large files are downloaded + # 3. Custom API endpoints work + + result = subprocess.run( + ['skill-seekers', 'github', '--help'], + capture_output=True, + text=True + ) + + # All flags present + self.assertIn('--enhance', result.stdout) + self.assertIn('--enhance-local', result.stdout) + self.assertIn('--api-key', result.stdout) + + # Verify we can import all fixed modules + try: + from skill_seekers.cli.github_scraper import GitHubScraper + from skill_seekers.cli.enhance_skill import SkillEnhancer + from skill_seekers.cli import main + + # All imports successful + self.assertTrue(True, "All modules import successfully") + except ImportError as e: + self.fail(f"Module import failed: {e}") + + +if __name__ == '__main__': + # Run tests with verbose output + unittest.main(verbosity=2)