style: Auto-format 48 files with ruff format
- Fixed formatting to comply with ruff standards - No functional changes, only formatting/style - Completes CI/CD pipeline formatting requirements
This commit is contained in:
@@ -13,15 +13,14 @@ import pytest
|
||||
import subprocess
|
||||
import argparse
|
||||
|
||||
|
||||
class TestParserSync:
|
||||
"""E2E tests for parser synchronization (Issue #285)."""
|
||||
|
||||
def test_scrape_interactive_flag_works(self):
|
||||
"""Test that --interactive flag (previously missing) now works."""
|
||||
result = subprocess.run(
|
||||
["skill-seekers", "scrape", "--interactive", "--help"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
["skill-seekers", "scrape", "--interactive", "--help"], capture_output=True, text=True
|
||||
)
|
||||
assert result.returncode == 0, "Command should execute successfully"
|
||||
assert "--interactive" in result.stdout, "Help should show --interactive flag"
|
||||
@@ -30,9 +29,7 @@ class TestParserSync:
|
||||
def test_scrape_chunk_for_rag_flag_works(self):
|
||||
"""Test that --chunk-for-rag flag (previously missing) now works."""
|
||||
result = subprocess.run(
|
||||
["skill-seekers", "scrape", "--help"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
["skill-seekers", "scrape", "--help"], capture_output=True, text=True
|
||||
)
|
||||
assert "--chunk-for-rag" in result.stdout, "Help should show --chunk-for-rag flag"
|
||||
assert "--chunk-size" in result.stdout, "Help should show --chunk-size flag"
|
||||
@@ -41,9 +38,7 @@ class TestParserSync:
|
||||
def test_scrape_verbose_flag_works(self):
|
||||
"""Test that --verbose flag (previously missing) now works."""
|
||||
result = subprocess.run(
|
||||
["skill-seekers", "scrape", "--help"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
["skill-seekers", "scrape", "--help"], capture_output=True, text=True
|
||||
)
|
||||
assert "--verbose" in result.stdout, "Help should show --verbose flag"
|
||||
assert "-v" in result.stdout, "Help should show short form -v"
|
||||
@@ -51,18 +46,14 @@ class TestParserSync:
|
||||
def test_scrape_url_flag_works(self):
|
||||
"""Test that --url flag (previously missing) now works."""
|
||||
result = subprocess.run(
|
||||
["skill-seekers", "scrape", "--help"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
["skill-seekers", "scrape", "--help"], capture_output=True, text=True
|
||||
)
|
||||
assert "--url URL" in result.stdout, "Help should show --url flag"
|
||||
|
||||
def test_github_all_flags_present(self):
|
||||
"""Test that github command has all expected flags."""
|
||||
result = subprocess.run(
|
||||
["skill-seekers", "github", "--help"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
["skill-seekers", "github", "--help"], capture_output=True, text=True
|
||||
)
|
||||
# Key github flags that should be present
|
||||
expected_flags = [
|
||||
@@ -74,15 +65,14 @@ class TestParserSync:
|
||||
for flag in expected_flags:
|
||||
assert flag in result.stdout, f"Help should show {flag} flag"
|
||||
|
||||
|
||||
class TestPresetSystem:
|
||||
"""E2E tests for preset system (Issue #268)."""
|
||||
|
||||
def test_analyze_preset_flag_exists(self):
|
||||
"""Test that analyze command has --preset flag."""
|
||||
result = subprocess.run(
|
||||
["skill-seekers", "analyze", "--help"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
["skill-seekers", "analyze", "--help"], capture_output=True, text=True
|
||||
)
|
||||
assert "--preset" in result.stdout, "Help should show --preset flag"
|
||||
assert "quick" in result.stdout, "Help should mention 'quick' preset"
|
||||
@@ -92,18 +82,14 @@ class TestPresetSystem:
|
||||
def test_analyze_preset_list_flag_exists(self):
|
||||
"""Test that analyze command has --preset-list flag."""
|
||||
result = subprocess.run(
|
||||
["skill-seekers", "analyze", "--help"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
["skill-seekers", "analyze", "--help"], capture_output=True, text=True
|
||||
)
|
||||
assert "--preset-list" in result.stdout, "Help should show --preset-list flag"
|
||||
|
||||
def test_preset_list_shows_presets(self):
|
||||
"""Test that --preset-list shows all available presets."""
|
||||
result = subprocess.run(
|
||||
["skill-seekers", "analyze", "--preset-list"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
["skill-seekers", "analyze", "--preset-list"], capture_output=True, text=True
|
||||
)
|
||||
assert result.returncode == 0, "Command should execute successfully"
|
||||
assert "Available presets" in result.stdout, "Should show preset list header"
|
||||
@@ -118,7 +104,7 @@ class TestPresetSystem:
|
||||
result = subprocess.run(
|
||||
["skill-seekers", "analyze", "--directory", ".", "--quick"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
text=True,
|
||||
)
|
||||
# Note: Deprecation warnings go to stderr
|
||||
output = result.stdout + result.stderr
|
||||
@@ -131,22 +117,19 @@ class TestPresetSystem:
|
||||
result = subprocess.run(
|
||||
["skill-seekers", "analyze", "--directory", ".", "--comprehensive"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
text=True,
|
||||
)
|
||||
output = result.stdout + result.stderr
|
||||
assert "DEPRECATED" in output, "Should show deprecation warning"
|
||||
assert "--preset comprehensive" in output, "Should suggest alternative"
|
||||
|
||||
|
||||
class TestBackwardCompatibility:
|
||||
"""E2E tests for backward compatibility."""
|
||||
|
||||
def test_old_scrape_command_still_works(self):
|
||||
"""Test that old scrape command invocations still work."""
|
||||
result = subprocess.run(
|
||||
["skill-seekers-scrape", "--help"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
result = subprocess.run(["skill-seekers-scrape", "--help"], capture_output=True, text=True)
|
||||
assert result.returncode == 0, "Old command should still work"
|
||||
assert "documentation" in result.stdout.lower(), "Help should mention documentation"
|
||||
|
||||
@@ -154,16 +137,12 @@ class TestBackwardCompatibility:
|
||||
"""Test that unified CLI and standalone have identical arguments."""
|
||||
# Get help from unified CLI
|
||||
unified_result = subprocess.run(
|
||||
["skill-seekers", "scrape", "--help"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
["skill-seekers", "scrape", "--help"], capture_output=True, text=True
|
||||
)
|
||||
|
||||
# Get help from standalone
|
||||
standalone_result = subprocess.run(
|
||||
["skill-seekers-scrape", "--help"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
["skill-seekers-scrape", "--help"], capture_output=True, text=True
|
||||
)
|
||||
|
||||
# Both should have the same key flags
|
||||
@@ -180,6 +159,7 @@ class TestBackwardCompatibility:
|
||||
assert flag in unified_result.stdout, f"Unified should have {flag}"
|
||||
assert flag in standalone_result.stdout, f"Standalone should have {flag}"
|
||||
|
||||
|
||||
class TestProgrammaticAPI:
|
||||
"""Test that the shared argument functions work programmatically."""
|
||||
|
||||
@@ -221,16 +201,13 @@ class TestProgrammaticAPI:
|
||||
# Note: enhance_level is not part of AnalysisPreset anymore.
|
||||
# It's controlled separately via --enhance-level flag (default 2)
|
||||
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests for the complete flow."""
|
||||
|
||||
def test_unified_cli_subcommands_registered(self):
|
||||
"""Test that all subcommands are properly registered."""
|
||||
result = subprocess.run(
|
||||
["skill-seekers", "--help"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
result = subprocess.run(["skill-seekers", "--help"], capture_output=True, text=True)
|
||||
|
||||
# All major commands should be listed
|
||||
expected_commands = [
|
||||
@@ -250,9 +227,7 @@ class TestIntegration:
|
||||
def test_scrape_help_detailed(self):
|
||||
"""Test that scrape help shows all argument details."""
|
||||
result = subprocess.run(
|
||||
["skill-seekers", "scrape", "--help"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
["skill-seekers", "scrape", "--help"], capture_output=True, text=True
|
||||
)
|
||||
|
||||
# Check for argument categories
|
||||
@@ -263,13 +238,14 @@ class TestIntegration:
|
||||
def test_analyze_help_shows_presets(self):
|
||||
"""Test that analyze help prominently shows preset information."""
|
||||
result = subprocess.run(
|
||||
["skill-seekers", "analyze", "--help"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
["skill-seekers", "analyze", "--help"], capture_output=True, text=True
|
||||
)
|
||||
|
||||
assert "--preset" in result.stdout, "Should show --preset flag"
|
||||
assert "DEFAULT" in result.stdout or "default" in result.stdout, "Should indicate default preset"
|
||||
assert "DEFAULT" in result.stdout or "default" in result.stdout, (
|
||||
"Should indicate default preset"
|
||||
)
|
||||
|
||||
|
||||
class TestE2EWorkflow:
|
||||
"""End-to-end workflow tests."""
|
||||
@@ -279,15 +255,18 @@ class TestE2EWorkflow:
|
||||
"""Test scraping with previously missing arguments (dry run)."""
|
||||
result = subprocess.run(
|
||||
[
|
||||
"skill-seekers", "scrape",
|
||||
"--url", "https://example.com",
|
||||
"--interactive", "false", # Would fail if arg didn't exist
|
||||
"skill-seekers",
|
||||
"scrape",
|
||||
"--url",
|
||||
"https://example.com",
|
||||
"--interactive",
|
||||
"false", # Would fail if arg didn't exist
|
||||
"--verbose", # Would fail if arg didn't exist
|
||||
"--dry-run",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
# Dry run should complete without errors
|
||||
@@ -314,5 +293,6 @@ class TestE2EWorkflow:
|
||||
assert "--preset" in result.stdout, "Should have --preset flag"
|
||||
assert "unrecognized arguments" not in result.stderr.lower()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v", "-s"])
|
||||
|
||||
@@ -19,6 +19,7 @@ from skill_seekers.cli.arguments.create import (
|
||||
add_create_arguments,
|
||||
)
|
||||
|
||||
|
||||
class TestUniversalArguments:
|
||||
"""Test universal argument definitions."""
|
||||
|
||||
@@ -29,25 +30,34 @@ class TestUniversalArguments:
|
||||
def test_universal_argument_names(self):
|
||||
"""Universal arguments should have expected names."""
|
||||
expected_names = {
|
||||
'name', 'description', 'output',
|
||||
'enhance_level', 'api_key', # Phase 1: consolidated from enhance + enhance_local
|
||||
'dry_run', 'verbose', 'quiet',
|
||||
'chunk_for_rag', 'chunk_size', 'chunk_overlap', # Phase 2: RAG args from common.py
|
||||
'preset', 'config'
|
||||
"name",
|
||||
"description",
|
||||
"output",
|
||||
"enhance_level",
|
||||
"api_key", # Phase 1: consolidated from enhance + enhance_local
|
||||
"dry_run",
|
||||
"verbose",
|
||||
"quiet",
|
||||
"chunk_for_rag",
|
||||
"chunk_size",
|
||||
"chunk_overlap", # Phase 2: RAG args from common.py
|
||||
"preset",
|
||||
"config",
|
||||
}
|
||||
assert set(UNIVERSAL_ARGUMENTS.keys()) == expected_names
|
||||
|
||||
def test_all_universal_have_flags(self):
|
||||
"""All universal arguments should have flags."""
|
||||
for arg_name, arg_def in UNIVERSAL_ARGUMENTS.items():
|
||||
assert 'flags' in arg_def
|
||||
assert len(arg_def['flags']) > 0
|
||||
assert "flags" in arg_def
|
||||
assert len(arg_def["flags"]) > 0
|
||||
|
||||
def test_all_universal_have_kwargs(self):
|
||||
"""All universal arguments should have kwargs."""
|
||||
for arg_name, arg_def in UNIVERSAL_ARGUMENTS.items():
|
||||
assert 'kwargs' in arg_def
|
||||
assert 'help' in arg_def['kwargs']
|
||||
assert "kwargs" in arg_def
|
||||
assert "help" in arg_def["kwargs"]
|
||||
|
||||
|
||||
class TestSourceSpecificArguments:
|
||||
"""Test source-specific argument definitions."""
|
||||
@@ -55,29 +65,29 @@ class TestSourceSpecificArguments:
|
||||
def test_web_arguments_exist(self):
|
||||
"""Web-specific arguments should be defined."""
|
||||
assert len(WEB_ARGUMENTS) > 0
|
||||
assert 'max_pages' in WEB_ARGUMENTS
|
||||
assert 'rate_limit' in WEB_ARGUMENTS
|
||||
assert 'workers' in WEB_ARGUMENTS
|
||||
assert "max_pages" in WEB_ARGUMENTS
|
||||
assert "rate_limit" in WEB_ARGUMENTS
|
||||
assert "workers" in WEB_ARGUMENTS
|
||||
|
||||
def test_github_arguments_exist(self):
|
||||
"""GitHub-specific arguments should be defined."""
|
||||
assert len(GITHUB_ARGUMENTS) > 0
|
||||
assert 'repo' in GITHUB_ARGUMENTS
|
||||
assert 'token' in GITHUB_ARGUMENTS
|
||||
assert 'max_issues' in GITHUB_ARGUMENTS
|
||||
assert "repo" in GITHUB_ARGUMENTS
|
||||
assert "token" in GITHUB_ARGUMENTS
|
||||
assert "max_issues" in GITHUB_ARGUMENTS
|
||||
|
||||
def test_local_arguments_exist(self):
|
||||
"""Local-specific arguments should be defined."""
|
||||
assert len(LOCAL_ARGUMENTS) > 0
|
||||
assert 'directory' in LOCAL_ARGUMENTS
|
||||
assert 'languages' in LOCAL_ARGUMENTS
|
||||
assert 'skip_patterns' in LOCAL_ARGUMENTS
|
||||
assert "directory" in LOCAL_ARGUMENTS
|
||||
assert "languages" in LOCAL_ARGUMENTS
|
||||
assert "skip_patterns" in LOCAL_ARGUMENTS
|
||||
|
||||
def test_pdf_arguments_exist(self):
|
||||
"""PDF-specific arguments should be defined."""
|
||||
assert len(PDF_ARGUMENTS) > 0
|
||||
assert 'pdf' in PDF_ARGUMENTS
|
||||
assert 'ocr' in PDF_ARGUMENTS
|
||||
assert "pdf" in PDF_ARGUMENTS
|
||||
assert "ocr" in PDF_ARGUMENTS
|
||||
|
||||
def test_no_duplicate_flags_across_sources(self):
|
||||
"""Source-specific arguments should not have duplicate flags."""
|
||||
@@ -86,21 +96,25 @@ class TestSourceSpecificArguments:
|
||||
|
||||
for source_args in [WEB_ARGUMENTS, GITHUB_ARGUMENTS, LOCAL_ARGUMENTS, PDF_ARGUMENTS]:
|
||||
for arg_name, arg_def in source_args.items():
|
||||
flags = arg_def['flags']
|
||||
flags = arg_def["flags"]
|
||||
for flag in flags:
|
||||
# Check if this flag already exists in source-specific args
|
||||
if flag not in [f for arg in UNIVERSAL_ARGUMENTS.values() for f in arg['flags']]:
|
||||
if flag not in [
|
||||
f for arg in UNIVERSAL_ARGUMENTS.values() for f in arg["flags"]
|
||||
]:
|
||||
assert flag not in all_flags, f"Duplicate flag: {flag}"
|
||||
all_flags.add(flag)
|
||||
|
||||
|
||||
class TestAdvancedArguments:
|
||||
"""Test advanced/rare argument definitions."""
|
||||
|
||||
def test_advanced_arguments_exist(self):
|
||||
"""Advanced arguments should be defined."""
|
||||
assert len(ADVANCED_ARGUMENTS) > 0
|
||||
assert 'no_rate_limit' in ADVANCED_ARGUMENTS
|
||||
assert 'interactive_enhancement' in ADVANCED_ARGUMENTS
|
||||
assert "no_rate_limit" in ADVANCED_ARGUMENTS
|
||||
assert "interactive_enhancement" in ADVANCED_ARGUMENTS
|
||||
|
||||
|
||||
class TestArgumentHelpers:
|
||||
"""Test helper functions."""
|
||||
@@ -110,106 +124,108 @@ class TestArgumentHelpers:
|
||||
names = get_universal_argument_names()
|
||||
assert isinstance(names, set)
|
||||
assert len(names) == 13
|
||||
assert 'name' in names
|
||||
assert 'enhance_level' in names # Phase 1: consolidated flag
|
||||
assert "name" in names
|
||||
assert "enhance_level" in names # Phase 1: consolidated flag
|
||||
|
||||
def test_get_source_specific_web(self):
|
||||
"""Should return web-specific arguments."""
|
||||
args = get_source_specific_arguments('web')
|
||||
args = get_source_specific_arguments("web")
|
||||
assert args == WEB_ARGUMENTS
|
||||
|
||||
def test_get_source_specific_github(self):
|
||||
"""Should return github-specific arguments."""
|
||||
args = get_source_specific_arguments('github')
|
||||
args = get_source_specific_arguments("github")
|
||||
assert args == GITHUB_ARGUMENTS
|
||||
|
||||
def test_get_source_specific_local(self):
|
||||
"""Should return local-specific arguments."""
|
||||
args = get_source_specific_arguments('local')
|
||||
args = get_source_specific_arguments("local")
|
||||
assert args == LOCAL_ARGUMENTS
|
||||
|
||||
def test_get_source_specific_pdf(self):
|
||||
"""Should return pdf-specific arguments."""
|
||||
args = get_source_specific_arguments('pdf')
|
||||
args = get_source_specific_arguments("pdf")
|
||||
assert args == PDF_ARGUMENTS
|
||||
|
||||
def test_get_source_specific_config(self):
|
||||
"""Config should return empty dict (no extra args)."""
|
||||
args = get_source_specific_arguments('config')
|
||||
args = get_source_specific_arguments("config")
|
||||
assert args == {}
|
||||
|
||||
def test_get_source_specific_unknown(self):
|
||||
"""Unknown source should return empty dict."""
|
||||
args = get_source_specific_arguments('unknown')
|
||||
args = get_source_specific_arguments("unknown")
|
||||
assert args == {}
|
||||
|
||||
|
||||
class TestCompatibleArguments:
|
||||
"""Test compatible argument detection."""
|
||||
|
||||
def test_web_compatible_arguments(self):
|
||||
"""Web source should include universal + web + advanced."""
|
||||
compatible = get_compatible_arguments('web')
|
||||
compatible = get_compatible_arguments("web")
|
||||
|
||||
# Should include universal arguments
|
||||
assert 'name' in compatible
|
||||
assert 'enhance_level' in compatible # Phase 1: consolidated flag
|
||||
assert "name" in compatible
|
||||
assert "enhance_level" in compatible # Phase 1: consolidated flag
|
||||
|
||||
# Should include web-specific arguments
|
||||
assert 'max_pages' in compatible
|
||||
assert 'rate_limit' in compatible
|
||||
assert "max_pages" in compatible
|
||||
assert "rate_limit" in compatible
|
||||
|
||||
# Should include advanced arguments
|
||||
assert 'no_rate_limit' in compatible
|
||||
assert "no_rate_limit" in compatible
|
||||
|
||||
def test_github_compatible_arguments(self):
|
||||
"""GitHub source should include universal + github + advanced."""
|
||||
compatible = get_compatible_arguments('github')
|
||||
compatible = get_compatible_arguments("github")
|
||||
|
||||
# Should include universal arguments
|
||||
assert 'name' in compatible
|
||||
assert "name" in compatible
|
||||
|
||||
# Should include github-specific arguments
|
||||
assert 'repo' in compatible
|
||||
assert 'token' in compatible
|
||||
assert "repo" in compatible
|
||||
assert "token" in compatible
|
||||
|
||||
# Should include advanced arguments
|
||||
assert 'interactive_enhancement' in compatible
|
||||
assert "interactive_enhancement" in compatible
|
||||
|
||||
def test_local_compatible_arguments(self):
|
||||
"""Local source should include universal + local + advanced."""
|
||||
compatible = get_compatible_arguments('local')
|
||||
compatible = get_compatible_arguments("local")
|
||||
|
||||
# Should include universal arguments
|
||||
assert 'description' in compatible
|
||||
assert "description" in compatible
|
||||
|
||||
# Should include local-specific arguments
|
||||
assert 'directory' in compatible
|
||||
assert 'languages' in compatible
|
||||
assert "directory" in compatible
|
||||
assert "languages" in compatible
|
||||
|
||||
def test_pdf_compatible_arguments(self):
|
||||
"""PDF source should include universal + pdf + advanced."""
|
||||
compatible = get_compatible_arguments('pdf')
|
||||
compatible = get_compatible_arguments("pdf")
|
||||
|
||||
# Should include universal arguments
|
||||
assert 'output' in compatible
|
||||
assert "output" in compatible
|
||||
|
||||
# Should include pdf-specific arguments
|
||||
assert 'pdf' in compatible
|
||||
assert 'ocr' in compatible
|
||||
assert "pdf" in compatible
|
||||
assert "ocr" in compatible
|
||||
|
||||
def test_config_compatible_arguments(self):
|
||||
"""Config source should include universal + advanced only."""
|
||||
compatible = get_compatible_arguments('config')
|
||||
compatible = get_compatible_arguments("config")
|
||||
|
||||
# Should include universal arguments
|
||||
assert 'config' in compatible
|
||||
assert "config" in compatible
|
||||
|
||||
# Should include advanced arguments
|
||||
assert 'no_preserve_code_blocks' in compatible
|
||||
assert "no_preserve_code_blocks" in compatible
|
||||
|
||||
# Should not include source-specific arguments
|
||||
assert 'repo' not in compatible
|
||||
assert 'directory' not in compatible
|
||||
assert "repo" not in compatible
|
||||
assert "directory" not in compatible
|
||||
|
||||
|
||||
class TestAddCreateArguments:
|
||||
"""Test add_create_arguments function."""
|
||||
@@ -217,16 +233,17 @@ class TestAddCreateArguments:
|
||||
def test_default_mode_adds_universal_only(self):
|
||||
"""Default mode should add only universal arguments + source positional."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
add_create_arguments(parser, mode='default')
|
||||
add_create_arguments(parser, mode="default")
|
||||
|
||||
# Parse to get all arguments
|
||||
args = vars(parser.parse_args([]))
|
||||
|
||||
# Should have universal arguments
|
||||
assert 'name' in args
|
||||
assert 'enhance_level' in args
|
||||
assert 'chunk_for_rag' in args
|
||||
assert "name" in args
|
||||
assert "enhance_level" in args
|
||||
assert "chunk_for_rag" in args
|
||||
|
||||
# Should not have source-specific arguments (they're not added in default mode)
|
||||
# Note: argparse won't error on unknown args, but they won't be in namespace
|
||||
@@ -234,62 +251,60 @@ class TestAddCreateArguments:
|
||||
def test_web_mode_adds_web_arguments(self):
|
||||
"""Web mode should add universal + web arguments."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
add_create_arguments(parser, mode='web')
|
||||
add_create_arguments(parser, mode="web")
|
||||
|
||||
args = vars(parser.parse_args([]))
|
||||
|
||||
# Should have universal arguments
|
||||
assert 'name' in args
|
||||
assert "name" in args
|
||||
|
||||
# Should have web-specific arguments
|
||||
assert 'max_pages' in args
|
||||
assert 'rate_limit' in args
|
||||
assert "max_pages" in args
|
||||
assert "rate_limit" in args
|
||||
|
||||
def test_all_mode_adds_all_arguments(self):
|
||||
"""All mode should add every argument."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
add_create_arguments(parser, mode='all')
|
||||
add_create_arguments(parser, mode="all")
|
||||
|
||||
args = vars(parser.parse_args([]))
|
||||
|
||||
# Should have universal arguments
|
||||
assert 'name' in args
|
||||
assert "name" in args
|
||||
|
||||
# Should have all source-specific arguments
|
||||
assert 'max_pages' in args # web
|
||||
assert 'repo' in args # github
|
||||
assert 'directory' in args # local
|
||||
assert 'pdf' in args # pdf
|
||||
assert "max_pages" in args # web
|
||||
assert "repo" in args # github
|
||||
assert "directory" in args # local
|
||||
assert "pdf" in args # pdf
|
||||
|
||||
# Should have advanced arguments
|
||||
assert 'no_rate_limit' in args
|
||||
assert "no_rate_limit" in args
|
||||
|
||||
def test_positional_source_argument_always_added(self):
|
||||
"""Source positional argument should always be added."""
|
||||
import argparse
|
||||
for mode in ['default', 'web', 'github', 'local', 'pdf', 'all']:
|
||||
|
||||
for mode in ["default", "web", "github", "local", "pdf", "all"]:
|
||||
parser = argparse.ArgumentParser()
|
||||
add_create_arguments(parser, mode=mode)
|
||||
|
||||
# Should accept source as positional
|
||||
args = parser.parse_args(['some_source'])
|
||||
assert args.source == 'some_source'
|
||||
args = parser.parse_args(["some_source"])
|
||||
assert args.source == "some_source"
|
||||
|
||||
|
||||
class TestNoDuplicates:
|
||||
"""Test that there are no duplicate arguments across tiers."""
|
||||
|
||||
def test_no_duplicates_between_universal_and_web(self):
|
||||
"""Universal and web args should not overlap."""
|
||||
universal_flags = {
|
||||
flag for arg in UNIVERSAL_ARGUMENTS.values()
|
||||
for flag in arg['flags']
|
||||
}
|
||||
web_flags = {
|
||||
flag for arg in WEB_ARGUMENTS.values()
|
||||
for flag in arg['flags']
|
||||
}
|
||||
universal_flags = {flag for arg in UNIVERSAL_ARGUMENTS.values() for flag in arg["flags"]}
|
||||
web_flags = {flag for arg in WEB_ARGUMENTS.values() for flag in arg["flags"]}
|
||||
|
||||
# Allow some overlap since we intentionally include common args
|
||||
# in multiple places, but check that they're properly defined
|
||||
@@ -299,10 +314,10 @@ class TestNoDuplicates:
|
||||
|
||||
def test_no_duplicates_between_source_specific_args(self):
|
||||
"""Different source-specific arg groups should not overlap."""
|
||||
web_flags = {flag for arg in WEB_ARGUMENTS.values() for flag in arg['flags']}
|
||||
github_flags = {flag for arg in GITHUB_ARGUMENTS.values() for flag in arg['flags']}
|
||||
local_flags = {flag for arg in LOCAL_ARGUMENTS.values() for flag in arg['flags']}
|
||||
pdf_flags = {flag for arg in PDF_ARGUMENTS.values() for flag in arg['flags']}
|
||||
web_flags = {flag for arg in WEB_ARGUMENTS.values() for flag in arg["flags"]}
|
||||
github_flags = {flag for arg in GITHUB_ARGUMENTS.values() for flag in arg["flags"]}
|
||||
local_flags = {flag for arg in LOCAL_ARGUMENTS.values() for flag in arg["flags"]}
|
||||
pdf_flags = {flag for arg in PDF_ARGUMENTS.values() for flag in arg["flags"]}
|
||||
|
||||
# No overlap between different source types
|
||||
assert len(web_flags & github_flags) == 0
|
||||
@@ -312,6 +327,7 @@ class TestNoDuplicates:
|
||||
assert len(github_flags & pdf_flags) == 0
|
||||
assert len(local_flags & pdf_flags) == 0
|
||||
|
||||
|
||||
class TestArgumentQuality:
|
||||
"""Test argument definition quality."""
|
||||
|
||||
@@ -327,8 +343,8 @@ class TestArgumentQuality:
|
||||
}
|
||||
|
||||
for arg_name, arg_def in all_args.items():
|
||||
assert 'help' in arg_def['kwargs'], f"{arg_name} missing help text"
|
||||
assert len(arg_def['kwargs']['help']) > 0, f"{arg_name} has empty help text"
|
||||
assert "help" in arg_def["kwargs"], f"{arg_name} missing help text"
|
||||
assert len(arg_def["kwargs"]["help"]) > 0, f"{arg_name} has empty help text"
|
||||
|
||||
def test_boolean_arguments_use_store_true(self):
|
||||
"""Boolean flags should use store_true action."""
|
||||
@@ -342,13 +358,25 @@ class TestArgumentQuality:
|
||||
}
|
||||
|
||||
boolean_args = [
|
||||
'dry_run', 'verbose', 'quiet',
|
||||
'chunk_for_rag', 'skip_scrape', 'resume', 'fresh', 'async_mode',
|
||||
'no_issues', 'no_changelog', 'no_releases', 'scrape_only',
|
||||
'skip_patterns', 'skip_test_examples', 'ocr', 'no_rate_limit'
|
||||
"dry_run",
|
||||
"verbose",
|
||||
"quiet",
|
||||
"chunk_for_rag",
|
||||
"skip_scrape",
|
||||
"resume",
|
||||
"fresh",
|
||||
"async_mode",
|
||||
"no_issues",
|
||||
"no_changelog",
|
||||
"no_releases",
|
||||
"scrape_only",
|
||||
"skip_patterns",
|
||||
"skip_test_examples",
|
||||
"ocr",
|
||||
"no_rate_limit",
|
||||
]
|
||||
|
||||
for arg_name in boolean_args:
|
||||
if arg_name in all_args:
|
||||
action = all_args[arg_name]['kwargs'].get('action')
|
||||
assert action == 'store_true', f"{arg_name} should use store_true"
|
||||
action = all_args[arg_name]["kwargs"].get("action")
|
||||
assert action == "store_true", f"{arg_name} should use store_true"
|
||||
|
||||
@@ -6,21 +6,21 @@ and routes to the correct scrapers without actually scraping.
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestCreateCommandBasic:
|
||||
"""Basic integration tests for create command (dry-run mode)."""
|
||||
|
||||
def test_create_command_help(self):
|
||||
"""Test that create command help works."""
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(
|
||||
['skill-seekers', 'create', '--help'],
|
||||
capture_output=True,
|
||||
text=True
|
||||
["skill-seekers", "create", "--help"], capture_output=True, text=True
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert 'Auto-detects source type' in result.stdout
|
||||
assert 'auto-detected' in result.stdout
|
||||
assert '--help-web' in result.stdout
|
||||
assert "Auto-detects source type" in result.stdout
|
||||
assert "auto-detected" in result.stdout
|
||||
assert "--help-web" in result.stdout
|
||||
|
||||
def test_create_detects_web_url(self):
|
||||
"""Test that web URLs are detected and routed correctly."""
|
||||
@@ -31,11 +31,12 @@ class TestCreateCommandBasic:
|
||||
def test_create_detects_github_repo(self):
|
||||
"""Test that GitHub repos are detected."""
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(
|
||||
['skill-seekers', 'create', 'facebook/react', '--help'],
|
||||
["skill-seekers", "create", "facebook/react", "--help"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
timeout=10,
|
||||
)
|
||||
# Just verify help works - actual scraping would need API token
|
||||
assert result.returncode in [0, 2] # 0 for success, 2 for argparse help
|
||||
@@ -49,10 +50,10 @@ class TestCreateCommandBasic:
|
||||
test_dir.mkdir()
|
||||
|
||||
result = subprocess.run(
|
||||
['skill-seekers', 'create', str(test_dir), '--help'],
|
||||
["skill-seekers", "create", str(test_dir), "--help"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
timeout=10,
|
||||
)
|
||||
# Verify help works
|
||||
assert result.returncode in [0, 2]
|
||||
@@ -66,10 +67,10 @@ class TestCreateCommandBasic:
|
||||
pdf_file.touch()
|
||||
|
||||
result = subprocess.run(
|
||||
['skill-seekers', 'create', str(pdf_file), '--help'],
|
||||
["skill-seekers", "create", str(pdf_file), "--help"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
timeout=10,
|
||||
)
|
||||
# Verify help works
|
||||
assert result.returncode in [0, 2]
|
||||
@@ -81,17 +82,14 @@ class TestCreateCommandBasic:
|
||||
|
||||
# Create a minimal config file
|
||||
config_file = tmp_path / "test.json"
|
||||
config_data = {
|
||||
"name": "test",
|
||||
"base_url": "https://example.com/"
|
||||
}
|
||||
config_data = {"name": "test", "base_url": "https://example.com/"}
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
result = subprocess.run(
|
||||
['skill-seekers', 'create', str(config_file), '--help'],
|
||||
["skill-seekers", "create", str(config_file), "--help"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
timeout=10,
|
||||
)
|
||||
# Verify help works
|
||||
assert result.returncode in [0, 2]
|
||||
@@ -105,20 +103,19 @@ class TestCreateCommandBasic:
|
||||
def test_create_supports_universal_flags(self):
|
||||
"""Test that universal flags are accepted."""
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(
|
||||
['skill-seekers', 'create', '--help'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
["skill-seekers", "create", "--help"], capture_output=True, text=True, timeout=10
|
||||
)
|
||||
assert result.returncode == 0
|
||||
|
||||
# Check that universal flags are present
|
||||
assert '--name' in result.stdout
|
||||
assert '--enhance' in result.stdout
|
||||
assert '--chunk-for-rag' in result.stdout
|
||||
assert '--preset' in result.stdout
|
||||
assert '--dry-run' in result.stdout
|
||||
assert "--name" in result.stdout
|
||||
assert "--enhance" in result.stdout
|
||||
assert "--chunk-for-rag" in result.stdout
|
||||
assert "--preset" in result.stdout
|
||||
assert "--dry-run" in result.stdout
|
||||
|
||||
|
||||
class TestBackwardCompatibility:
|
||||
"""Test that old commands still work."""
|
||||
@@ -126,53 +123,45 @@ class TestBackwardCompatibility:
|
||||
def test_scrape_command_still_works(self):
|
||||
"""Old scrape command should still function."""
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(
|
||||
['skill-seekers', 'scrape', '--help'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
["skill-seekers", "scrape", "--help"], capture_output=True, text=True, timeout=10
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert 'scrape' in result.stdout.lower()
|
||||
assert "scrape" in result.stdout.lower()
|
||||
|
||||
def test_github_command_still_works(self):
|
||||
"""Old github command should still function."""
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(
|
||||
['skill-seekers', 'github', '--help'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
["skill-seekers", "github", "--help"], capture_output=True, text=True, timeout=10
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert 'github' in result.stdout.lower()
|
||||
assert "github" in result.stdout.lower()
|
||||
|
||||
def test_analyze_command_still_works(self):
|
||||
"""Old analyze command should still function."""
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(
|
||||
['skill-seekers', 'analyze', '--help'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
["skill-seekers", "analyze", "--help"], capture_output=True, text=True, timeout=10
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert 'analyze' in result.stdout.lower()
|
||||
assert "analyze" in result.stdout.lower()
|
||||
|
||||
def test_main_help_shows_all_commands(self):
|
||||
"""Main help should show both old and new commands."""
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(
|
||||
['skill-seekers', '--help'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
["skill-seekers", "--help"], capture_output=True, text=True, timeout=10
|
||||
)
|
||||
assert result.returncode == 0
|
||||
# Should show create command
|
||||
assert 'create' in result.stdout
|
||||
assert "create" in result.stdout
|
||||
|
||||
# Should still show old commands
|
||||
assert 'scrape' in result.stdout
|
||||
assert 'github' in result.stdout
|
||||
assert 'analyze' in result.stdout
|
||||
assert "scrape" in result.stdout
|
||||
assert "github" in result.stdout
|
||||
assert "analyze" in result.stdout
|
||||
|
||||
@@ -167,7 +167,8 @@ class TestIssue219Problem2CLIFlags(unittest.TestCase):
|
||||
"test/test",
|
||||
"--name",
|
||||
"test",
|
||||
"--enhance-level", "2",
|
||||
"--enhance-level",
|
||||
"2",
|
||||
]
|
||||
|
||||
with (
|
||||
|
||||
@@ -7,6 +7,7 @@ the same arguments as the standalone scraper modules. This prevents the
|
||||
|
||||
import argparse
|
||||
|
||||
|
||||
class TestScrapeParserSync:
|
||||
"""Ensure scrape_parser has all arguments from doc_scraper."""
|
||||
|
||||
@@ -17,12 +18,12 @@ class TestScrapeParserSync:
|
||||
|
||||
# Get source arguments from doc_scraper
|
||||
source_parser = setup_argument_parser()
|
||||
source_count = len([a for a in source_parser._actions if a.dest != 'help'])
|
||||
source_count = len([a for a in source_parser._actions if a.dest != "help"])
|
||||
|
||||
# Get target arguments from unified CLI parser
|
||||
target_parser = argparse.ArgumentParser()
|
||||
ScrapeParser().add_arguments(target_parser)
|
||||
target_count = len([a for a in target_parser._actions if a.dest != 'help'])
|
||||
target_count = len([a for a in target_parser._actions if a.dest != "help"])
|
||||
|
||||
assert source_count == target_count, (
|
||||
f"Argument count mismatch: doc_scraper has {source_count}, "
|
||||
@@ -36,12 +37,12 @@ class TestScrapeParserSync:
|
||||
|
||||
# Get source arguments from doc_scraper
|
||||
source_parser = setup_argument_parser()
|
||||
source_dests = {a.dest for a in source_parser._actions if a.dest != 'help'}
|
||||
source_dests = {a.dest for a in source_parser._actions if a.dest != "help"}
|
||||
|
||||
# Get target arguments from unified CLI parser
|
||||
target_parser = argparse.ArgumentParser()
|
||||
ScrapeParser().add_arguments(target_parser)
|
||||
target_dests = {a.dest for a in target_parser._actions if a.dest != 'help'}
|
||||
target_dests = {a.dest for a in target_parser._actions if a.dest != "help"}
|
||||
|
||||
# Check for missing arguments
|
||||
missing = source_dests - target_dests
|
||||
@@ -64,27 +65,28 @@ class TestScrapeParserSync:
|
||||
break
|
||||
|
||||
assert subparsers_action is not None, "No subparsers found"
|
||||
assert 'scrape' in subparsers_action.choices, "scrape subparser not found"
|
||||
assert "scrape" in subparsers_action.choices, "scrape subparser not found"
|
||||
|
||||
scrape_parser = subparsers_action.choices['scrape']
|
||||
arg_dests = {a.dest for a in scrape_parser._actions if a.dest != 'help'}
|
||||
scrape_parser = subparsers_action.choices["scrape"]
|
||||
arg_dests = {a.dest for a in scrape_parser._actions if a.dest != "help"}
|
||||
|
||||
# Check key arguments that were missing in Issue #285
|
||||
required_args = [
|
||||
'interactive',
|
||||
'url',
|
||||
'verbose',
|
||||
'quiet',
|
||||
'resume',
|
||||
'fresh',
|
||||
'rate_limit',
|
||||
'no_rate_limit',
|
||||
'chunk_for_rag',
|
||||
"interactive",
|
||||
"url",
|
||||
"verbose",
|
||||
"quiet",
|
||||
"resume",
|
||||
"fresh",
|
||||
"rate_limit",
|
||||
"no_rate_limit",
|
||||
"chunk_for_rag",
|
||||
]
|
||||
|
||||
for arg in required_args:
|
||||
assert arg in arg_dests, f"Required argument '{arg}' missing from scrape parser"
|
||||
|
||||
|
||||
class TestGitHubParserSync:
|
||||
"""Ensure github_parser has all arguments from github_scraper."""
|
||||
|
||||
@@ -95,12 +97,12 @@ class TestGitHubParserSync:
|
||||
|
||||
# Get source arguments from github_scraper
|
||||
source_parser = setup_argument_parser()
|
||||
source_count = len([a for a in source_parser._actions if a.dest != 'help'])
|
||||
source_count = len([a for a in source_parser._actions if a.dest != "help"])
|
||||
|
||||
# Get target arguments from unified CLI parser
|
||||
target_parser = argparse.ArgumentParser()
|
||||
GitHubParser().add_arguments(target_parser)
|
||||
target_count = len([a for a in target_parser._actions if a.dest != 'help'])
|
||||
target_count = len([a for a in target_parser._actions if a.dest != "help"])
|
||||
|
||||
assert source_count == target_count, (
|
||||
f"Argument count mismatch: github_scraper has {source_count}, "
|
||||
@@ -114,12 +116,12 @@ class TestGitHubParserSync:
|
||||
|
||||
# Get source arguments from github_scraper
|
||||
source_parser = setup_argument_parser()
|
||||
source_dests = {a.dest for a in source_parser._actions if a.dest != 'help'}
|
||||
source_dests = {a.dest for a in source_parser._actions if a.dest != "help"}
|
||||
|
||||
# Get target arguments from unified CLI parser
|
||||
target_parser = argparse.ArgumentParser()
|
||||
GitHubParser().add_arguments(target_parser)
|
||||
target_dests = {a.dest for a in target_parser._actions if a.dest != 'help'}
|
||||
target_dests = {a.dest for a in target_parser._actions if a.dest != "help"}
|
||||
|
||||
# Check for missing arguments
|
||||
missing = source_dests - target_dests
|
||||
@@ -128,6 +130,7 @@ class TestGitHubParserSync:
|
||||
assert not missing, f"github_parser missing arguments: {missing}"
|
||||
assert not extra, f"github_parser has extra arguments not in github_scraper: {extra}"
|
||||
|
||||
|
||||
class TestUnifiedCLI:
|
||||
"""Test the unified CLI main parser."""
|
||||
|
||||
@@ -154,7 +157,7 @@ class TestUnifiedCLI:
|
||||
assert subparsers_action is not None, "No subparsers found"
|
||||
|
||||
# Check expected subcommands
|
||||
expected_commands = ['scrape', 'github']
|
||||
expected_commands = ["scrape", "github"]
|
||||
for cmd in expected_commands:
|
||||
assert cmd in subparsers_action.choices, f"Subcommand '{cmd}' not found"
|
||||
|
||||
@@ -166,7 +169,7 @@ class TestUnifiedCLI:
|
||||
|
||||
# This should not raise an exception
|
||||
try:
|
||||
parser.parse_args(['scrape', '--help'])
|
||||
parser.parse_args(["scrape", "--help"])
|
||||
except SystemExit as e:
|
||||
# --help causes SystemExit(0) which is expected
|
||||
assert e.code == 0
|
||||
@@ -179,7 +182,7 @@ class TestUnifiedCLI:
|
||||
|
||||
# This should not raise an exception
|
||||
try:
|
||||
parser.parse_args(['github', '--help'])
|
||||
parser.parse_args(["github", "--help"])
|
||||
except SystemExit as e:
|
||||
# --help causes SystemExit(0) which is expected
|
||||
assert e.code == 0
|
||||
|
||||
@@ -13,47 +13,49 @@ import pytest
|
||||
|
||||
from skill_seekers.cli.source_detector import SourceDetector, SourceInfo
|
||||
|
||||
|
||||
class TestWebDetection:
|
||||
"""Test web URL detection."""
|
||||
|
||||
def test_detect_full_https_url(self):
|
||||
"""Full HTTPS URL should be detected as web."""
|
||||
info = SourceDetector.detect("https://docs.react.dev/")
|
||||
assert info.type == 'web'
|
||||
assert info.parsed['url'] == "https://docs.react.dev/"
|
||||
assert info.suggested_name == 'react'
|
||||
assert info.type == "web"
|
||||
assert info.parsed["url"] == "https://docs.react.dev/"
|
||||
assert info.suggested_name == "react"
|
||||
|
||||
def test_detect_full_http_url(self):
|
||||
"""Full HTTP URL should be detected as web."""
|
||||
info = SourceDetector.detect("http://example.com/docs")
|
||||
assert info.type == 'web'
|
||||
assert info.parsed['url'] == "http://example.com/docs"
|
||||
assert info.type == "web"
|
||||
assert info.parsed["url"] == "http://example.com/docs"
|
||||
|
||||
def test_detect_domain_only(self):
|
||||
"""Domain without protocol should add https:// and detect as web."""
|
||||
info = SourceDetector.detect("docs.react.dev")
|
||||
assert info.type == 'web'
|
||||
assert info.parsed['url'] == "https://docs.react.dev"
|
||||
assert info.suggested_name == 'react'
|
||||
assert info.type == "web"
|
||||
assert info.parsed["url"] == "https://docs.react.dev"
|
||||
assert info.suggested_name == "react"
|
||||
|
||||
def test_detect_complex_url(self):
|
||||
"""Complex URL with path should be detected as web."""
|
||||
info = SourceDetector.detect("https://docs.python.org/3/library/")
|
||||
assert info.type == 'web'
|
||||
assert info.parsed['url'] == "https://docs.python.org/3/library/"
|
||||
assert info.suggested_name == 'python'
|
||||
assert info.type == "web"
|
||||
assert info.parsed["url"] == "https://docs.python.org/3/library/"
|
||||
assert info.suggested_name == "python"
|
||||
|
||||
def test_suggested_name_removes_www(self):
|
||||
"""Should remove www. prefix from suggested name."""
|
||||
info = SourceDetector.detect("https://www.example.com/")
|
||||
assert info.type == 'web'
|
||||
assert info.suggested_name == 'example'
|
||||
assert info.type == "web"
|
||||
assert info.suggested_name == "example"
|
||||
|
||||
def test_suggested_name_removes_docs(self):
|
||||
"""Should remove docs. prefix from suggested name."""
|
||||
info = SourceDetector.detect("https://docs.vue.org/")
|
||||
assert info.type == 'web'
|
||||
assert info.suggested_name == 'vue'
|
||||
assert info.type == "web"
|
||||
assert info.suggested_name == "vue"
|
||||
|
||||
|
||||
class TestGitHubDetection:
|
||||
"""Test GitHub repository detection."""
|
||||
@@ -61,37 +63,38 @@ class TestGitHubDetection:
|
||||
def test_detect_owner_repo_format(self):
|
||||
"""owner/repo format should be detected as GitHub."""
|
||||
info = SourceDetector.detect("facebook/react")
|
||||
assert info.type == 'github'
|
||||
assert info.parsed['repo'] == "facebook/react"
|
||||
assert info.suggested_name == 'react'
|
||||
assert info.type == "github"
|
||||
assert info.parsed["repo"] == "facebook/react"
|
||||
assert info.suggested_name == "react"
|
||||
|
||||
def test_detect_github_https_url(self):
|
||||
"""Full GitHub HTTPS URL should be detected."""
|
||||
info = SourceDetector.detect("https://github.com/facebook/react")
|
||||
assert info.type == 'github'
|
||||
assert info.parsed['repo'] == "facebook/react"
|
||||
assert info.suggested_name == 'react'
|
||||
assert info.type == "github"
|
||||
assert info.parsed["repo"] == "facebook/react"
|
||||
assert info.suggested_name == "react"
|
||||
|
||||
def test_detect_github_url_with_git_suffix(self):
|
||||
"""GitHub URL with .git should strip suffix."""
|
||||
info = SourceDetector.detect("https://github.com/facebook/react.git")
|
||||
assert info.type == 'github'
|
||||
assert info.parsed['repo'] == "facebook/react"
|
||||
assert info.suggested_name == 'react'
|
||||
assert info.type == "github"
|
||||
assert info.parsed["repo"] == "facebook/react"
|
||||
assert info.suggested_name == "react"
|
||||
|
||||
def test_detect_github_url_without_protocol(self):
|
||||
"""GitHub URL without protocol should be detected."""
|
||||
info = SourceDetector.detect("github.com/vuejs/vue")
|
||||
assert info.type == 'github'
|
||||
assert info.parsed['repo'] == "vuejs/vue"
|
||||
assert info.suggested_name == 'vue'
|
||||
assert info.type == "github"
|
||||
assert info.parsed["repo"] == "vuejs/vue"
|
||||
assert info.suggested_name == "vue"
|
||||
|
||||
def test_owner_repo_with_dots_and_dashes(self):
|
||||
"""Repo names with dots and dashes should work."""
|
||||
info = SourceDetector.detect("microsoft/vscode-python")
|
||||
assert info.type == 'github'
|
||||
assert info.parsed['repo'] == "microsoft/vscode-python"
|
||||
assert info.suggested_name == 'vscode-python'
|
||||
assert info.type == "github"
|
||||
assert info.parsed["repo"] == "microsoft/vscode-python"
|
||||
assert info.suggested_name == "vscode-python"
|
||||
|
||||
|
||||
class TestLocalDetection:
|
||||
"""Test local directory detection."""
|
||||
@@ -107,9 +110,9 @@ class TestLocalDetection:
|
||||
try:
|
||||
os.chdir(tmp_path)
|
||||
info = SourceDetector.detect("./my_project")
|
||||
assert info.type == 'local'
|
||||
assert 'my_project' in info.parsed['directory']
|
||||
assert info.suggested_name == 'my_project'
|
||||
assert info.type == "local"
|
||||
assert "my_project" in info.parsed["directory"]
|
||||
assert info.suggested_name == "my_project"
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
@@ -120,16 +123,17 @@ class TestLocalDetection:
|
||||
test_dir.mkdir()
|
||||
|
||||
info = SourceDetector.detect(str(test_dir))
|
||||
assert info.type == 'local'
|
||||
assert info.parsed['directory'] == str(test_dir.resolve())
|
||||
assert info.suggested_name == 'test_repo'
|
||||
assert info.type == "local"
|
||||
assert info.parsed["directory"] == str(test_dir.resolve())
|
||||
assert info.suggested_name == "test_repo"
|
||||
|
||||
def test_detect_current_directory(self):
|
||||
"""Current directory (.) should be detected."""
|
||||
cwd = os.getcwd()
|
||||
info = SourceDetector.detect(".")
|
||||
assert info.type == 'local'
|
||||
assert info.parsed['directory'] == cwd
|
||||
assert info.type == "local"
|
||||
assert info.parsed["directory"] == cwd
|
||||
|
||||
|
||||
class TestPDFDetection:
|
||||
"""Test PDF file detection."""
|
||||
@@ -137,22 +141,23 @@ class TestPDFDetection:
|
||||
def test_detect_pdf_extension(self):
|
||||
"""File with .pdf extension should be detected."""
|
||||
info = SourceDetector.detect("tutorial.pdf")
|
||||
assert info.type == 'pdf'
|
||||
assert info.parsed['file_path'] == "tutorial.pdf"
|
||||
assert info.suggested_name == 'tutorial'
|
||||
assert info.type == "pdf"
|
||||
assert info.parsed["file_path"] == "tutorial.pdf"
|
||||
assert info.suggested_name == "tutorial"
|
||||
|
||||
def test_detect_pdf_with_path(self):
|
||||
"""PDF file with path should be detected."""
|
||||
info = SourceDetector.detect("/path/to/guide.pdf")
|
||||
assert info.type == 'pdf'
|
||||
assert info.parsed['file_path'] == "/path/to/guide.pdf"
|
||||
assert info.suggested_name == 'guide'
|
||||
assert info.type == "pdf"
|
||||
assert info.parsed["file_path"] == "/path/to/guide.pdf"
|
||||
assert info.suggested_name == "guide"
|
||||
|
||||
def test_suggested_name_removes_pdf_extension(self):
|
||||
"""Suggested name should not include .pdf extension."""
|
||||
info = SourceDetector.detect("my-awesome-guide.pdf")
|
||||
assert info.type == 'pdf'
|
||||
assert info.suggested_name == 'my-awesome-guide'
|
||||
assert info.type == "pdf"
|
||||
assert info.suggested_name == "my-awesome-guide"
|
||||
|
||||
|
||||
class TestConfigDetection:
|
||||
"""Test config file detection."""
|
||||
@@ -160,16 +165,17 @@ class TestConfigDetection:
|
||||
def test_detect_json_extension(self):
|
||||
"""File with .json extension should be detected as config."""
|
||||
info = SourceDetector.detect("react.json")
|
||||
assert info.type == 'config'
|
||||
assert info.parsed['config_path'] == "react.json"
|
||||
assert info.suggested_name == 'react'
|
||||
assert info.type == "config"
|
||||
assert info.parsed["config_path"] == "react.json"
|
||||
assert info.suggested_name == "react"
|
||||
|
||||
def test_detect_config_with_path(self):
|
||||
"""Config file with path should be detected."""
|
||||
info = SourceDetector.detect("configs/django.json")
|
||||
assert info.type == 'config'
|
||||
assert info.parsed['config_path'] == "configs/django.json"
|
||||
assert info.suggested_name == 'django'
|
||||
assert info.type == "config"
|
||||
assert info.parsed["config_path"] == "configs/django.json"
|
||||
assert info.suggested_name == "django"
|
||||
|
||||
|
||||
class TestValidation:
|
||||
"""Test source validation."""
|
||||
@@ -191,10 +197,10 @@ class TestValidation:
|
||||
# First try to detect it (will succeed since it looks like a path)
|
||||
with pytest.raises(ValueError, match="Directory does not exist"):
|
||||
info = SourceInfo(
|
||||
type='local',
|
||||
parsed={'directory': nonexistent},
|
||||
suggested_name='test',
|
||||
raw_input=nonexistent
|
||||
type="local",
|
||||
parsed={"directory": nonexistent},
|
||||
suggested_name="test",
|
||||
raw_input=nonexistent,
|
||||
)
|
||||
SourceDetector.validate_source(info)
|
||||
|
||||
@@ -211,10 +217,10 @@ class TestValidation:
|
||||
"""Validation should fail for nonexistent PDF."""
|
||||
with pytest.raises(ValueError, match="PDF file does not exist"):
|
||||
info = SourceInfo(
|
||||
type='pdf',
|
||||
parsed={'file_path': '/tmp/nonexistent.pdf'},
|
||||
suggested_name='test',
|
||||
raw_input='/tmp/nonexistent.pdf'
|
||||
type="pdf",
|
||||
parsed={"file_path": "/tmp/nonexistent.pdf"},
|
||||
suggested_name="test",
|
||||
raw_input="/tmp/nonexistent.pdf",
|
||||
)
|
||||
SourceDetector.validate_source(info)
|
||||
|
||||
@@ -231,13 +237,14 @@ class TestValidation:
|
||||
"""Validation should fail for nonexistent config."""
|
||||
with pytest.raises(ValueError, match="Config file does not exist"):
|
||||
info = SourceInfo(
|
||||
type='config',
|
||||
parsed={'config_path': '/tmp/nonexistent.json'},
|
||||
suggested_name='test',
|
||||
raw_input='/tmp/nonexistent.json'
|
||||
type="config",
|
||||
parsed={"config_path": "/tmp/nonexistent.json"},
|
||||
suggested_name="test",
|
||||
raw_input="/tmp/nonexistent.json",
|
||||
)
|
||||
SourceDetector.validate_source(info)
|
||||
|
||||
|
||||
class TestAmbiguousCases:
|
||||
"""Test handling of ambiguous inputs."""
|
||||
|
||||
@@ -255,8 +262,8 @@ class TestAmbiguousCases:
|
||||
"""GitHub URL should be detected as github, not web."""
|
||||
# Even though this is a URL, it should be detected as GitHub
|
||||
info = SourceDetector.detect("https://github.com/owner/repo")
|
||||
assert info.type == 'github'
|
||||
assert info.parsed['repo'] == "owner/repo"
|
||||
assert info.type == "github"
|
||||
assert info.parsed["repo"] == "owner/repo"
|
||||
|
||||
def test_directory_takes_precedence_over_domain(self, tmp_path):
|
||||
"""Existing directory should be detected even if it looks like domain."""
|
||||
@@ -266,7 +273,8 @@ class TestAmbiguousCases:
|
||||
|
||||
info = SourceDetector.detect(str(dir_like_domain))
|
||||
# Should detect as local directory, not web
|
||||
assert info.type == 'local'
|
||||
assert info.type == "local"
|
||||
|
||||
|
||||
class TestRawInputPreservation:
|
||||
"""Test that raw_input is preserved correctly."""
|
||||
@@ -292,6 +300,7 @@ class TestRawInputPreservation:
|
||||
info = SourceDetector.detect(original)
|
||||
assert info.raw_input == original
|
||||
|
||||
|
||||
class TestEdgeCases:
|
||||
"""Test edge cases and corner cases."""
|
||||
|
||||
@@ -300,19 +309,19 @@ class TestEdgeCases:
|
||||
info1 = SourceDetector.detect("https://docs.react.dev/")
|
||||
info2 = SourceDetector.detect("https://docs.react.dev")
|
||||
|
||||
assert info1.type == 'web'
|
||||
assert info2.type == 'web'
|
||||
assert info1.type == "web"
|
||||
assert info2.type == "web"
|
||||
|
||||
def test_uppercase_in_github_repo(self):
|
||||
"""GitHub repos with uppercase should be detected."""
|
||||
info = SourceDetector.detect("Microsoft/TypeScript")
|
||||
assert info.type == 'github'
|
||||
assert info.parsed['repo'] == "Microsoft/TypeScript"
|
||||
assert info.type == "github"
|
||||
assert info.parsed["repo"] == "Microsoft/TypeScript"
|
||||
|
||||
def test_numbers_in_repo_name(self):
|
||||
"""GitHub repos with numbers should be detected."""
|
||||
info = SourceDetector.detect("python/cpython3.11")
|
||||
assert info.type == 'github'
|
||||
assert info.type == "github"
|
||||
|
||||
def test_nested_directory_path(self, tmp_path):
|
||||
"""Nested directory paths should work."""
|
||||
@@ -320,5 +329,5 @@ class TestEdgeCases:
|
||||
nested.mkdir(parents=True)
|
||||
|
||||
info = SourceDetector.detect(str(nested))
|
||||
assert info.type == 'local'
|
||||
assert info.suggested_name == 'c'
|
||||
assert info.type == "local"
|
||||
assert info.suggested_name == "c"
|
||||
|
||||
Reference in New Issue
Block a user