feat(A1.7): Add install_skill MCP tool for one-command workflow automation
Implements complete end-to-end skill installation in a single command: fetch_config → scrape_docs → enhance_skill_local → package_skill → upload_skill Changes: - MCP Tool: Added install_skill_tool() to server.py (~300 lines) - Input validation (config_name XOR config_path) - 5-phase orchestration with error handling - Dry-run mode for workflow preview - Mandatory AI enhancement (30-60 sec, 3/10→9/10 quality boost) - Auto-upload to Claude (if ANTHROPIC_API_KEY set) - CLI Integration: New install command - Created install_skill.py CLI wrapper (~150 lines) - Updated main.py with install subcommand - Added entry point to pyproject.toml - Testing: Comprehensive test suite - Created test_install_skill.py with 13 tests - Tests cover validation, dry-run, orchestration, error handling - All tests passing (13/13) - Documentation: Updated all user-facing docs - CLAUDE.md: Added MCP tool (10 tools total) and CLI examples - README.md: Added prominent one-command workflow section - FLEXIBLE_ROADMAP.md: Marked A1.7 as complete Features: - Zero friction: One command instead of 5 separate steps - Quality guaranteed: Mandatory enhancement ensures 9/10 quality - Complete automation: From config to uploaded skill - Intelligent: Auto-detects config type (name vs path) - Flexible: Dry-run, unlimited, no-upload modes - Well-tested: 13 unit tests with mocking Usage: skill-seekers install --config react skill-seekers install --config configs/custom.json --no-upload skill-seekers install --config django --unlimited skill-seekers install --config react --dry-run Closes #204 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
52
CLAUDE.md
52
CLAUDE.md
@@ -67,14 +67,15 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## 🔌 MCP Integration Available
|
||||
|
||||
**This repository includes a fully tested MCP server with 9 tools:**
|
||||
**This repository includes a fully tested MCP server with 10 tools:**
|
||||
- `mcp__skill-seeker__list_configs` - List all available preset configurations
|
||||
- `mcp__skill-seeker__generate_config` - Generate a new config file for any docs site
|
||||
- `mcp__skill-seeker__validate_config` - Validate a config file structure
|
||||
- `mcp__skill-seeker__estimate_pages` - Estimate page count before scraping
|
||||
- `mcp__skill-seeker__scrape_docs` - Scrape and build a skill
|
||||
- `mcp__skill-seeker__package_skill` - Package skill into .zip file (with auto-upload)
|
||||
- `mcp__skill-seeker__upload_skill` - Upload .zip to Claude (NEW)
|
||||
- `mcp__skill-seeker__upload_skill` - Upload .zip to Claude
|
||||
- `mcp__skill-seeker__install_skill` - **NEW!** Complete one-command workflow (fetch → scrape → enhance → package → upload)
|
||||
- `mcp__skill-seeker__split_config` - Split large documentation configs
|
||||
- `mcp__skill-seeker__generate_router` - Generate router/hub skills
|
||||
|
||||
@@ -188,6 +189,53 @@ skill-seekers package output/godot/
|
||||
# Result: godot.zip ready to upload to Claude
|
||||
```
|
||||
|
||||
### **NEW!** One-Command Install Workflow (v2.1.1)
|
||||
|
||||
The fastest way to install a skill - complete automation from config to uploaded skill:
|
||||
|
||||
```bash
|
||||
# Install React skill from official configs (auto-uploads to Claude)
|
||||
skill-seekers install --config react
|
||||
# Time: 20-45 minutes total (scraping 20-40 min + enhancement 60 sec + upload 5 sec)
|
||||
|
||||
# Install from local config file
|
||||
skill-seekers install --config configs/custom.json
|
||||
|
||||
# Install without uploading (package only)
|
||||
skill-seekers install --config django --no-upload
|
||||
|
||||
# Unlimited scraping (no page limits - WARNING: can take hours)
|
||||
skill-seekers install --config godot --unlimited
|
||||
|
||||
# Preview workflow without executing
|
||||
skill-seekers install --config react --dry-run
|
||||
|
||||
# Custom output directory
|
||||
skill-seekers install --config vue --destination /tmp/skills
|
||||
```
|
||||
|
||||
**What it does automatically:**
|
||||
1. ✅ Fetches config from API (if config name provided)
|
||||
2. ✅ Scrapes documentation
|
||||
3. ✅ **AI Enhancement (MANDATORY)** - 30-60 sec, quality boost from 3/10 → 9/10
|
||||
4. ✅ Packages skill to .zip
|
||||
5. ✅ Uploads to Claude (if ANTHROPIC_API_KEY set)
|
||||
|
||||
**Why use this:**
|
||||
- **Zero friction** - One command instead of 5 separate steps
|
||||
- **Quality guaranteed** - Enhancement is mandatory, ensures professional output
|
||||
- **Complete automation** - From config name to uploaded skill
|
||||
- **Time savings** - Fully automated workflow
|
||||
|
||||
**Phases executed:**
|
||||
```
|
||||
📥 PHASE 1: Fetch Config (if config name provided)
|
||||
📖 PHASE 2: Scrape Documentation
|
||||
✨ PHASE 3: AI Enhancement (MANDATORY - no skip option)
|
||||
📦 PHASE 4: Package Skill
|
||||
☁️ PHASE 5: Upload to Claude (optional)
|
||||
```
|
||||
|
||||
### Interactive Mode
|
||||
|
||||
```bash
|
||||
|
||||
@@ -58,12 +58,13 @@ Small tasks that build community features incrementally
|
||||
- **Approach:** Use GitHub Issues with labels (no custom code needed)
|
||||
- **Workflow:** Review → Validate → Test → Approve/Reject
|
||||
- **Time:** 1-2 hours (GitHub Issues) or 4-6 hours (custom dashboard)
|
||||
- [ ] **Task A1.7:** Add MCP tool `install_skill` for one-command workflow (Issue #204)
|
||||
- [x] **Task A1.7:** Add MCP tool `install_skill` for one-command workflow (Issue #204) ✅ **COMPLETE!**
|
||||
- **Purpose:** Complete one-command workflow: fetch → scrape → **enhance** → package → upload
|
||||
- **Features:** Single command install, smart config detection, automatic AI enhancement (LOCAL)
|
||||
- **Workflow:** fetch_config → scrape_docs → enhance_skill_local → package_skill → upload_skill
|
||||
- **Critical:** Always includes AI enhancement step (30-60 sec, 3/10→9/10 quality boost)
|
||||
- **Time:** 3-4 hours
|
||||
- **Completed:** December 21, 2025 - 10 tools total, 13 tests passing, full automation working
|
||||
- [ ] **Task A1.8:** Add smart skill detection and auto-install (Issue #205)
|
||||
- **Purpose:** Auto-detect missing skills from user queries and offer to install them
|
||||
- **Features:** Topic extraction, skill gap analysis, API search, smart suggestions
|
||||
|
||||
67
README.md
67
README.md
@@ -187,6 +187,73 @@ python3 src/skill_seekers/cli/doc_scraper.py --config configs/react.json
|
||||
|
||||
**Time:** ~25 minutes | **Quality:** Production-ready | **Cost:** Free
|
||||
|
||||
---
|
||||
|
||||
## 🚀 **NEW!** One-Command Install Workflow (v2.1.1)
|
||||
|
||||
**The fastest way to go from config to uploaded skill - complete automation:**
|
||||
|
||||
```bash
|
||||
# Install React skill from official configs (auto-uploads to Claude)
|
||||
skill-seekers install --config react
|
||||
|
||||
# Install from local config file
|
||||
skill-seekers install --config configs/custom.json
|
||||
|
||||
# Install without uploading (package only)
|
||||
skill-seekers install --config django --no-upload
|
||||
|
||||
# Unlimited scraping (no page limits)
|
||||
skill-seekers install --config godot --unlimited
|
||||
|
||||
# Preview workflow without executing
|
||||
skill-seekers install --config react --dry-run
|
||||
```
|
||||
|
||||
**Time:** 20-45 minutes total | **Quality:** Production-ready (9/10) | **Cost:** Free
|
||||
|
||||
### What it does automatically:
|
||||
|
||||
1. ✅ **Fetches config** from API (if config name provided)
|
||||
2. ✅ **Scrapes documentation** (respects rate limits, handles pagination)
|
||||
3. ✅ **AI Enhancement (MANDATORY)** - 30-60 sec, quality boost from 3/10 → 9/10
|
||||
4. ✅ **Packages skill** to .zip file
|
||||
5. ✅ **Uploads to Claude** (if ANTHROPIC_API_KEY set)
|
||||
|
||||
### Why use this?
|
||||
|
||||
- **Zero friction** - One command instead of 5 separate steps
|
||||
- **Quality guaranteed** - Enhancement is mandatory, ensures professional output
|
||||
- **Complete automation** - From config name to uploaded skill in Claude
|
||||
- **Time savings** - Fully automated end-to-end workflow
|
||||
|
||||
### Phases executed:
|
||||
|
||||
```
|
||||
📥 PHASE 1: Fetch Config (if config name provided)
|
||||
📖 PHASE 2: Scrape Documentation
|
||||
✨ PHASE 3: AI Enhancement (MANDATORY - no skip option)
|
||||
📦 PHASE 4: Package Skill
|
||||
☁️ PHASE 5: Upload to Claude (optional, requires API key)
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
- ANTHROPIC_API_KEY environment variable (for auto-upload)
|
||||
- Claude Code Max plan (for local AI enhancement)
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Set API key once
|
||||
export ANTHROPIC_API_KEY=sk-ant-your-key-here
|
||||
|
||||
# Run one command - sit back and relax!
|
||||
skill-seekers install --config react
|
||||
|
||||
# Result: React skill uploaded to Claude in 20-45 minutes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Documentation Scraping
|
||||
|
||||
@@ -107,6 +107,7 @@ skill-seekers-enhance = "skill_seekers.cli.enhance_skill_local:main"
|
||||
skill-seekers-package = "skill_seekers.cli.package_skill:main"
|
||||
skill-seekers-upload = "skill_seekers.cli.upload_skill:main"
|
||||
skill-seekers-estimate = "skill_seekers.cli.estimate_pages:main"
|
||||
skill-seekers-install = "skill_seekers.cli.install_skill:main"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["skill_seekers", "skill_seekers.cli", "skill_seekers.mcp", "skill_seekers.mcp.tools"]
|
||||
|
||||
153
src/skill_seekers/cli/install_skill.py
Normal file
153
src/skill_seekers/cli/install_skill.py
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Complete Skill Installation Workflow
|
||||
One-command installation: fetch → scrape → enhance → package → upload
|
||||
|
||||
This CLI tool orchestrates the complete skill installation workflow by calling
|
||||
the install_skill MCP tool.
|
||||
|
||||
Usage:
|
||||
skill-seekers install --config react
|
||||
skill-seekers install --config configs/custom.json --no-upload
|
||||
skill-seekers install --config django --unlimited
|
||||
skill-seekers install --config react --dry-run
|
||||
|
||||
Examples:
|
||||
# Install React skill from official configs
|
||||
skill-seekers install --config react
|
||||
|
||||
# Install from local config file
|
||||
skill-seekers install --config configs/custom.json
|
||||
|
||||
# Install without uploading
|
||||
skill-seekers install --config django --no-upload
|
||||
|
||||
# Preview workflow without executing
|
||||
skill-seekers install --config react --dry-run
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path to import MCP server
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
# Import the MCP tool function
|
||||
from skill_seekers.mcp.server import install_skill_tool
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for CLI"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Complete skill installation workflow (fetch → scrape → enhance → package → upload)",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Install React skill from official API
|
||||
skill-seekers install --config react
|
||||
|
||||
# Install from local config file
|
||||
skill-seekers install --config configs/custom.json
|
||||
|
||||
# Install without uploading
|
||||
skill-seekers install --config django --no-upload
|
||||
|
||||
# Unlimited scraping (no page limits)
|
||||
skill-seekers install --config godot --unlimited
|
||||
|
||||
# Preview workflow (dry run)
|
||||
skill-seekers install --config react --dry-run
|
||||
|
||||
Important:
|
||||
- Enhancement is MANDATORY (30-60 sec) for quality (3/10→9/10)
|
||||
- Total time: 20-45 minutes (mostly scraping)
|
||||
- Auto-uploads to Claude if ANTHROPIC_API_KEY is set
|
||||
|
||||
Phases:
|
||||
1. Fetch config (if config name provided)
|
||||
2. Scrape documentation
|
||||
3. AI Enhancement (MANDATORY - no skip option)
|
||||
4. Package to .zip
|
||||
5. Upload to Claude (optional)
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
required=True,
|
||||
help="Config name (e.g., 'react') or path (e.g., 'configs/custom.json')"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--destination",
|
||||
default="output",
|
||||
help="Output directory for skill files (default: output/)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--no-upload",
|
||||
action="store_true",
|
||||
help="Skip automatic upload to Claude"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--unlimited",
|
||||
action="store_true",
|
||||
help="Remove page limits during scraping (WARNING: Can take hours)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Preview workflow without executing"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Determine if config is a name or path
|
||||
config_arg = args.config
|
||||
if config_arg.endswith('.json') or '/' in config_arg or '\\' in config_arg:
|
||||
# It's a path
|
||||
config_path = config_arg
|
||||
config_name = None
|
||||
else:
|
||||
# It's a name
|
||||
config_name = config_arg
|
||||
config_path = None
|
||||
|
||||
# Build arguments for install_skill_tool
|
||||
tool_args = {
|
||||
"config_name": config_name,
|
||||
"config_path": config_path,
|
||||
"destination": args.destination,
|
||||
"auto_upload": not args.no_upload,
|
||||
"unlimited": args.unlimited,
|
||||
"dry_run": args.dry_run
|
||||
}
|
||||
|
||||
# Run async tool
|
||||
try:
|
||||
result = asyncio.run(install_skill_tool(tool_args))
|
||||
|
||||
# Print output
|
||||
for content in result:
|
||||
print(content.text)
|
||||
|
||||
# Return success/failure based on output
|
||||
output_text = result[0].text
|
||||
if "❌" in output_text and "WORKFLOW COMPLETE" not in output_text:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n⚠️ Workflow interrupted by user")
|
||||
return 130 # Standard exit code for SIGINT
|
||||
except Exception as e:
|
||||
print(f"\n\n❌ Unexpected error: {str(e)}")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -156,6 +156,38 @@ For more information: https://github.com/yusufkaraaslan/Skill_Seekers
|
||||
estimate_parser.add_argument("config", help="Config JSON file")
|
||||
estimate_parser.add_argument("--max-discovery", type=int, help="Max pages to discover")
|
||||
|
||||
# === install subcommand ===
|
||||
install_parser = subparsers.add_parser(
|
||||
"install",
|
||||
help="Complete workflow: fetch → scrape → enhance → package → upload",
|
||||
description="One-command skill installation (AI enhancement MANDATORY)"
|
||||
)
|
||||
install_parser.add_argument(
|
||||
"--config",
|
||||
required=True,
|
||||
help="Config name (e.g., 'react') or path (e.g., 'configs/custom.json')"
|
||||
)
|
||||
install_parser.add_argument(
|
||||
"--destination",
|
||||
default="output",
|
||||
help="Output directory (default: output/)"
|
||||
)
|
||||
install_parser.add_argument(
|
||||
"--no-upload",
|
||||
action="store_true",
|
||||
help="Skip automatic upload to Claude"
|
||||
)
|
||||
install_parser.add_argument(
|
||||
"--unlimited",
|
||||
action="store_true",
|
||||
help="Remove page limits during scraping"
|
||||
)
|
||||
install_parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Preview workflow without executing"
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
@@ -268,6 +300,21 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
sys.argv.extend(["--max-discovery", str(args.max_discovery)])
|
||||
return estimate_main() or 0
|
||||
|
||||
elif args.command == "install":
|
||||
from skill_seekers.cli.install_skill import main as install_main
|
||||
sys.argv = ["install_skill.py"]
|
||||
if args.config:
|
||||
sys.argv.extend(["--config", args.config])
|
||||
if args.destination:
|
||||
sys.argv.extend(["--destination", args.destination])
|
||||
if args.no_upload:
|
||||
sys.argv.append("--no-upload")
|
||||
if args.unlimited:
|
||||
sys.argv.append("--unlimited")
|
||||
if args.dry_run:
|
||||
sys.argv.append("--dry-run")
|
||||
return install_main() or 0
|
||||
|
||||
else:
|
||||
print(f"Error: Unknown command '{args.command}'", file=sys.stderr)
|
||||
parser.print_help()
|
||||
|
||||
@@ -418,6 +418,44 @@ async def list_tools() -> list[Tool]:
|
||||
"required": [],
|
||||
},
|
||||
),
|
||||
Tool(
|
||||
name="install_skill",
|
||||
description="Complete one-command workflow: fetch config → scrape docs → AI enhance (MANDATORY) → package → upload. Enhancement required for quality (3/10→9/10). Takes 20-45 min depending on config size. Automatically uploads to Claude if ANTHROPIC_API_KEY is set.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config_name": {
|
||||
"type": "string",
|
||||
"description": "Config name from API (e.g., 'react', 'django'). Mutually exclusive with config_path. Tool will fetch this config from the official API before scraping.",
|
||||
},
|
||||
"config_path": {
|
||||
"type": "string",
|
||||
"description": "Path to existing config JSON file (e.g., 'configs/custom.json'). Mutually exclusive with config_name. Use this if you already have a config file.",
|
||||
},
|
||||
"destination": {
|
||||
"type": "string",
|
||||
"description": "Output directory for skill files (default: 'output')",
|
||||
"default": "output",
|
||||
},
|
||||
"auto_upload": {
|
||||
"type": "boolean",
|
||||
"description": "Auto-upload to Claude after packaging (requires ANTHROPIC_API_KEY). Default: true. Set to false to skip upload.",
|
||||
"default": True,
|
||||
},
|
||||
"unlimited": {
|
||||
"type": "boolean",
|
||||
"description": "Remove page limits during scraping (default: false). WARNING: Can take hours for large sites.",
|
||||
"default": False,
|
||||
},
|
||||
"dry_run": {
|
||||
"type": "boolean",
|
||||
"description": "Preview workflow without executing (default: false). Shows all phases that would run.",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
),
|
||||
Tool(
|
||||
name="fetch_config",
|
||||
description="Fetch config from API, git URL, or registered source. Supports three modes: (1) Named source from registry, (2) Direct git URL, (3) API (default). List available configs or download a specific one by name.",
|
||||
@@ -605,6 +643,8 @@ async def call_tool(name: str, arguments: Any) -> list[TextContent]:
|
||||
return await list_config_sources_tool(arguments)
|
||||
elif name == "remove_config_source":
|
||||
return await remove_config_source_tool(arguments)
|
||||
elif name == "install_skill":
|
||||
return await install_skill_tool(arguments)
|
||||
else:
|
||||
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
||||
|
||||
@@ -1462,6 +1502,311 @@ Next steps:
|
||||
return [TextContent(type="text", text=f"❌ Error: {str(e)}")]
|
||||
|
||||
|
||||
async def install_skill_tool(args: dict) -> list[TextContent]:
|
||||
"""
|
||||
Complete skill installation workflow.
|
||||
|
||||
Orchestrates the complete workflow:
|
||||
1. Fetch config (if config_name provided)
|
||||
2. Scrape documentation
|
||||
3. AI Enhancement (MANDATORY - no skip option)
|
||||
4. Package to .zip
|
||||
5. Upload to Claude (optional)
|
||||
|
||||
Args:
|
||||
config_name: Config to fetch from API (mutually exclusive with config_path)
|
||||
config_path: Path to existing config (mutually exclusive with config_name)
|
||||
destination: Output directory (default: "output")
|
||||
auto_upload: Upload after packaging (default: True)
|
||||
unlimited: Remove page limits (default: False)
|
||||
dry_run: Preview only (default: False)
|
||||
|
||||
Returns:
|
||||
List of TextContent with workflow progress and results
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
|
||||
# Extract and validate inputs
|
||||
config_name = args.get("config_name")
|
||||
config_path = args.get("config_path")
|
||||
destination = args.get("destination", "output")
|
||||
auto_upload = args.get("auto_upload", True)
|
||||
unlimited = args.get("unlimited", False)
|
||||
dry_run = args.get("dry_run", False)
|
||||
|
||||
# Validation: Must provide exactly one of config_name or config_path
|
||||
if not config_name and not config_path:
|
||||
return [TextContent(
|
||||
type="text",
|
||||
text="❌ Error: Must provide either config_name or config_path\n\nExamples:\n install_skill(config_name='react')\n install_skill(config_path='configs/custom.json')"
|
||||
)]
|
||||
|
||||
if config_name and config_path:
|
||||
return [TextContent(
|
||||
type="text",
|
||||
text="❌ Error: Cannot provide both config_name and config_path\n\nChoose one:\n - config_name: Fetch from API (e.g., 'react')\n - config_path: Use existing file (e.g., 'configs/custom.json')"
|
||||
)]
|
||||
|
||||
# Initialize output
|
||||
output_lines = []
|
||||
output_lines.append("🚀 SKILL INSTALLATION WORKFLOW")
|
||||
output_lines.append("=" * 70)
|
||||
output_lines.append("")
|
||||
|
||||
if dry_run:
|
||||
output_lines.append("🔍 DRY RUN MODE - Preview only, no actions taken")
|
||||
output_lines.append("")
|
||||
|
||||
# Track workflow state
|
||||
workflow_state = {
|
||||
'config_path': config_path,
|
||||
'skill_name': None,
|
||||
'skill_dir': None,
|
||||
'zip_path': None,
|
||||
'phases_completed': []
|
||||
}
|
||||
|
||||
try:
|
||||
# ===== PHASE 1: Fetch Config (if needed) =====
|
||||
if config_name:
|
||||
output_lines.append("📥 PHASE 1/5: Fetch Config")
|
||||
output_lines.append("-" * 70)
|
||||
output_lines.append(f"Config: {config_name}")
|
||||
output_lines.append(f"Destination: {destination}/")
|
||||
output_lines.append("")
|
||||
|
||||
if not dry_run:
|
||||
# Call fetch_config_tool directly
|
||||
fetch_result = await fetch_config_tool({
|
||||
"config_name": config_name,
|
||||
"destination": destination
|
||||
})
|
||||
|
||||
# Parse result to extract config path
|
||||
fetch_output = fetch_result[0].text
|
||||
output_lines.append(fetch_output)
|
||||
output_lines.append("")
|
||||
|
||||
# Extract config path from output
|
||||
# Expected format: "✅ Config saved to: configs/react.json"
|
||||
match = re.search(r"saved to:\s*(.+\.json)", fetch_output)
|
||||
if match:
|
||||
workflow_state['config_path'] = match.group(1).strip()
|
||||
output_lines.append(f"✅ Config fetched: {workflow_state['config_path']}")
|
||||
else:
|
||||
return [TextContent(type="text", text="\n".join(output_lines) + "\n\n❌ Failed to fetch config")]
|
||||
|
||||
workflow_state['phases_completed'].append('fetch_config')
|
||||
else:
|
||||
output_lines.append(" [DRY RUN] Would fetch config from API")
|
||||
workflow_state['config_path'] = f"{destination}/{config_name}.json"
|
||||
|
||||
output_lines.append("")
|
||||
|
||||
# ===== PHASE 2: Scrape Documentation =====
|
||||
phase_num = "2/5" if config_name else "1/4"
|
||||
output_lines.append(f"📄 PHASE {phase_num}: Scrape Documentation")
|
||||
output_lines.append("-" * 70)
|
||||
output_lines.append(f"Config: {workflow_state['config_path']}")
|
||||
output_lines.append(f"Unlimited mode: {unlimited}")
|
||||
output_lines.append("")
|
||||
|
||||
if not dry_run:
|
||||
# Load config to get skill name
|
||||
try:
|
||||
with open(workflow_state['config_path'], 'r') as f:
|
||||
config = json.load(f)
|
||||
workflow_state['skill_name'] = config.get('name', 'unknown')
|
||||
except Exception as e:
|
||||
return [TextContent(type="text", text="\n".join(output_lines) + f"\n\n❌ Failed to read config: {str(e)}")]
|
||||
|
||||
# Call scrape_docs_tool (does NOT include enhancement)
|
||||
output_lines.append("Scraping documentation (this may take 20-45 minutes)...")
|
||||
output_lines.append("")
|
||||
|
||||
scrape_result = await scrape_docs_tool({
|
||||
"config_path": workflow_state['config_path'],
|
||||
"unlimited": unlimited,
|
||||
"enhance_local": False, # Enhancement is separate phase
|
||||
"skip_scrape": False,
|
||||
"dry_run": False
|
||||
})
|
||||
|
||||
scrape_output = scrape_result[0].text
|
||||
output_lines.append(scrape_output)
|
||||
output_lines.append("")
|
||||
|
||||
# Check for success
|
||||
if "❌" in scrape_output:
|
||||
return [TextContent(type="text", text="\n".join(output_lines) + "\n\n❌ Scraping failed - see error above")]
|
||||
|
||||
workflow_state['skill_dir'] = f"{destination}/{workflow_state['skill_name']}"
|
||||
workflow_state['phases_completed'].append('scrape_docs')
|
||||
else:
|
||||
output_lines.append(" [DRY RUN] Would scrape documentation")
|
||||
workflow_state['skill_name'] = "example"
|
||||
workflow_state['skill_dir'] = f"{destination}/example"
|
||||
|
||||
output_lines.append("")
|
||||
|
||||
# ===== PHASE 3: AI Enhancement (MANDATORY) =====
|
||||
phase_num = "3/5" if config_name else "2/4"
|
||||
output_lines.append(f"✨ PHASE {phase_num}: AI Enhancement (MANDATORY)")
|
||||
output_lines.append("-" * 70)
|
||||
output_lines.append("⚠️ Enhancement is REQUIRED for quality (3/10→9/10 boost)")
|
||||
output_lines.append(f"Skill directory: {workflow_state['skill_dir']}")
|
||||
output_lines.append("Mode: Headless (runs in background)")
|
||||
output_lines.append("Estimated time: 30-60 seconds")
|
||||
output_lines.append("")
|
||||
|
||||
if not dry_run:
|
||||
# Run enhance_skill_local in headless mode
|
||||
# Build command directly
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(CLI_DIR / "enhance_skill_local.py"),
|
||||
workflow_state['skill_dir']
|
||||
# Headless is default, no flag needed
|
||||
]
|
||||
|
||||
timeout = 900 # 15 minutes max for enhancement
|
||||
|
||||
output_lines.append("Running AI enhancement...")
|
||||
|
||||
stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout)
|
||||
|
||||
if returncode != 0:
|
||||
output_lines.append(f"\n❌ Enhancement failed (exit code {returncode}):")
|
||||
output_lines.append(stderr if stderr else stdout)
|
||||
return [TextContent(type="text", text="\n".join(output_lines))]
|
||||
|
||||
output_lines.append(stdout)
|
||||
workflow_state['phases_completed'].append('enhance_skill')
|
||||
else:
|
||||
output_lines.append(" [DRY RUN] Would enhance SKILL.md with Claude Code")
|
||||
|
||||
output_lines.append("")
|
||||
|
||||
# ===== PHASE 4: Package Skill =====
|
||||
phase_num = "4/5" if config_name else "3/4"
|
||||
output_lines.append(f"📦 PHASE {phase_num}: Package Skill")
|
||||
output_lines.append("-" * 70)
|
||||
output_lines.append(f"Skill directory: {workflow_state['skill_dir']}")
|
||||
output_lines.append("")
|
||||
|
||||
if not dry_run:
|
||||
# Call package_skill_tool (auto_upload=False, we handle upload separately)
|
||||
package_result = await package_skill_tool({
|
||||
"skill_dir": workflow_state['skill_dir'],
|
||||
"auto_upload": False # We handle upload in next phase
|
||||
})
|
||||
|
||||
package_output = package_result[0].text
|
||||
output_lines.append(package_output)
|
||||
output_lines.append("")
|
||||
|
||||
# Extract zip path from output
|
||||
# Expected format: "Saved to: output/react.zip"
|
||||
match = re.search(r"Saved to:\s*(.+\.zip)", package_output)
|
||||
if match:
|
||||
workflow_state['zip_path'] = match.group(1).strip()
|
||||
else:
|
||||
# Fallback: construct zip path
|
||||
workflow_state['zip_path'] = f"{destination}/{workflow_state['skill_name']}.zip"
|
||||
|
||||
workflow_state['phases_completed'].append('package_skill')
|
||||
else:
|
||||
output_lines.append(" [DRY RUN] Would package to .zip file")
|
||||
workflow_state['zip_path'] = f"{destination}/{workflow_state['skill_name']}.zip"
|
||||
|
||||
output_lines.append("")
|
||||
|
||||
# ===== PHASE 5: Upload (Optional) =====
|
||||
if auto_upload:
|
||||
phase_num = "5/5" if config_name else "4/4"
|
||||
output_lines.append(f"📤 PHASE {phase_num}: Upload to Claude")
|
||||
output_lines.append("-" * 70)
|
||||
output_lines.append(f"Zip file: {workflow_state['zip_path']}")
|
||||
output_lines.append("")
|
||||
|
||||
# Check for API key
|
||||
has_api_key = os.environ.get('ANTHROPIC_API_KEY', '').strip()
|
||||
|
||||
if not dry_run:
|
||||
if has_api_key:
|
||||
# Call upload_skill_tool
|
||||
upload_result = await upload_skill_tool({
|
||||
"skill_zip": workflow_state['zip_path']
|
||||
})
|
||||
|
||||
upload_output = upload_result[0].text
|
||||
output_lines.append(upload_output)
|
||||
|
||||
workflow_state['phases_completed'].append('upload_skill')
|
||||
else:
|
||||
output_lines.append("⚠️ ANTHROPIC_API_KEY not set - skipping upload")
|
||||
output_lines.append("")
|
||||
output_lines.append("To enable automatic upload:")
|
||||
output_lines.append(" 1. Get API key from https://console.anthropic.com/")
|
||||
output_lines.append(" 2. Set: export ANTHROPIC_API_KEY=sk-ant-...")
|
||||
output_lines.append("")
|
||||
output_lines.append("📤 Manual upload:")
|
||||
output_lines.append(" 1. Go to https://claude.ai/skills")
|
||||
output_lines.append(" 2. Click 'Upload Skill'")
|
||||
output_lines.append(f" 3. Select: {workflow_state['zip_path']}")
|
||||
else:
|
||||
output_lines.append(" [DRY RUN] Would upload to Claude (if API key set)")
|
||||
|
||||
output_lines.append("")
|
||||
|
||||
# ===== WORKFLOW SUMMARY =====
|
||||
output_lines.append("=" * 70)
|
||||
output_lines.append("✅ WORKFLOW COMPLETE")
|
||||
output_lines.append("=" * 70)
|
||||
output_lines.append("")
|
||||
|
||||
if not dry_run:
|
||||
output_lines.append("Phases completed:")
|
||||
for phase in workflow_state['phases_completed']:
|
||||
output_lines.append(f" ✓ {phase}")
|
||||
output_lines.append("")
|
||||
|
||||
output_lines.append("📁 Output:")
|
||||
output_lines.append(f" Skill directory: {workflow_state['skill_dir']}")
|
||||
if workflow_state['zip_path']:
|
||||
output_lines.append(f" Skill package: {workflow_state['zip_path']}")
|
||||
output_lines.append("")
|
||||
|
||||
if auto_upload and has_api_key:
|
||||
output_lines.append("🎉 Your skill is now available in Claude!")
|
||||
output_lines.append(" Go to https://claude.ai/skills to use it")
|
||||
elif auto_upload:
|
||||
output_lines.append("📝 Manual upload required (see instructions above)")
|
||||
else:
|
||||
output_lines.append("📤 To upload:")
|
||||
output_lines.append(" skill-seekers upload " + workflow_state['zip_path'])
|
||||
else:
|
||||
output_lines.append("This was a dry run. No actions were taken.")
|
||||
output_lines.append("")
|
||||
output_lines.append("To execute for real, remove the --dry-run flag:")
|
||||
if config_name:
|
||||
output_lines.append(f" install_skill(config_name='{config_name}')")
|
||||
else:
|
||||
output_lines.append(f" install_skill(config_path='{config_path}')")
|
||||
|
||||
return [TextContent(type="text", text="\n".join(output_lines))]
|
||||
|
||||
except Exception as e:
|
||||
output_lines.append("")
|
||||
output_lines.append(f"❌ Workflow failed: {str(e)}")
|
||||
output_lines.append("")
|
||||
output_lines.append("Phases completed before failure:")
|
||||
for phase in workflow_state['phases_completed']:
|
||||
output_lines.append(f" ✓ {phase}")
|
||||
return [TextContent(type="text", text="\n".join(output_lines))]
|
||||
|
||||
|
||||
async def submit_config_tool(args: dict) -> list[TextContent]:
|
||||
"""Submit a custom config to skill-seekers-configs repository via GitHub issue"""
|
||||
try:
|
||||
|
||||
402
tests/test_install_skill.py
Normal file
402
tests/test_install_skill.py
Normal file
@@ -0,0 +1,402 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests for install_skill MCP tool and CLI
|
||||
|
||||
Tests the complete workflow orchestration for A1.7:
|
||||
- Input validation
|
||||
- Dry-run mode
|
||||
- Phase orchestration
|
||||
- Error handling
|
||||
- CLI integration
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from mcp.types import TextContent
|
||||
|
||||
# Import the function to test
|
||||
from skill_seekers.mcp.server import install_skill_tool
|
||||
|
||||
|
||||
class TestInstallSkillValidation:
|
||||
"""Test input validation"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validation_no_config(self):
|
||||
"""Test error when neither config_name nor config_path provided"""
|
||||
result = await install_skill_tool({})
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TextContent)
|
||||
assert "❌ Error: Must provide either config_name or config_path" in result[0].text
|
||||
assert "Examples:" in result[0].text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validation_both_configs(self):
|
||||
"""Test error when both config_name and config_path provided"""
|
||||
result = await install_skill_tool({
|
||||
"config_name": "react",
|
||||
"config_path": "configs/react.json"
|
||||
})
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TextContent)
|
||||
assert "❌ Error: Cannot provide both config_name and config_path" in result[0].text
|
||||
assert "Choose one:" in result[0].text
|
||||
|
||||
|
||||
class TestInstallSkillDryRun:
|
||||
"""Test dry-run mode"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dry_run_with_config_name(self):
|
||||
"""Test dry run with config name (includes fetch phase)"""
|
||||
result = await install_skill_tool({
|
||||
"config_name": "react",
|
||||
"dry_run": True
|
||||
})
|
||||
|
||||
assert len(result) == 1
|
||||
output = result[0].text
|
||||
|
||||
# Verify dry run mode is indicated
|
||||
assert "🔍 DRY RUN MODE" in output
|
||||
assert "Preview only, no actions taken" in output
|
||||
|
||||
# Verify all 5 phases are shown
|
||||
assert "PHASE 1/5: Fetch Config" in output
|
||||
assert "PHASE 2/5: Scrape Documentation" in output
|
||||
assert "PHASE 3/5: AI Enhancement (MANDATORY)" in output
|
||||
assert "PHASE 4/5: Package Skill" in output
|
||||
assert "PHASE 5/5: Upload to Claude" in output
|
||||
|
||||
# Verify dry run indicators
|
||||
assert "[DRY RUN]" in output
|
||||
assert "This was a dry run. No actions were taken." in output
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dry_run_with_config_path(self):
|
||||
"""Test dry run with config path (skips fetch phase)"""
|
||||
result = await install_skill_tool({
|
||||
"config_path": "configs/react.json",
|
||||
"dry_run": True
|
||||
})
|
||||
|
||||
assert len(result) == 1
|
||||
output = result[0].text
|
||||
|
||||
# Verify dry run mode
|
||||
assert "🔍 DRY RUN MODE" in output
|
||||
|
||||
# Verify only 4 phases (no fetch)
|
||||
assert "PHASE 1/4: Scrape Documentation" in output
|
||||
assert "PHASE 2/4: AI Enhancement (MANDATORY)" in output
|
||||
assert "PHASE 3/4: Package Skill" in output
|
||||
assert "PHASE 4/4: Upload to Claude" in output
|
||||
|
||||
# Should not show fetch phase
|
||||
assert "PHASE 1/5" not in output
|
||||
assert "Fetch Config" not in output
|
||||
|
||||
|
||||
class TestInstallSkillEnhancementMandatory:
|
||||
"""Test that enhancement is always included"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_enhancement_is_mandatory(self):
|
||||
"""Test that enhancement phase is always present and mandatory"""
|
||||
result = await install_skill_tool({
|
||||
"config_name": "react",
|
||||
"dry_run": True
|
||||
})
|
||||
|
||||
output = result[0].text
|
||||
|
||||
# Verify enhancement phase is present
|
||||
assert "AI Enhancement (MANDATORY)" in output
|
||||
assert "Enhancement is REQUIRED for quality (3/10→9/10 boost)" in output or \
|
||||
"REQUIRED for quality" in output
|
||||
|
||||
# Verify it's not optional
|
||||
assert "MANDATORY" in output
|
||||
assert "no skip option" in output.lower() or "MANDATORY" in output
|
||||
|
||||
|
||||
class TestInstallSkillPhaseOrchestration:
|
||||
"""Test phase orchestration and data flow"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('skill_seekers.mcp.server.fetch_config_tool')
|
||||
@patch('skill_seekers.mcp.server.scrape_docs_tool')
|
||||
@patch('skill_seekers.mcp.server.run_subprocess_with_streaming')
|
||||
@patch('skill_seekers.mcp.server.package_skill_tool')
|
||||
@patch('skill_seekers.mcp.server.upload_skill_tool')
|
||||
@patch('builtins.open')
|
||||
@patch('os.environ.get')
|
||||
async def test_full_workflow_with_fetch(
|
||||
self,
|
||||
mock_env_get,
|
||||
mock_open,
|
||||
mock_upload,
|
||||
mock_package,
|
||||
mock_subprocess,
|
||||
mock_scrape,
|
||||
mock_fetch
|
||||
):
|
||||
"""Test complete workflow when config_name is provided"""
|
||||
|
||||
# Mock fetch_config response
|
||||
mock_fetch.return_value = [TextContent(
|
||||
type="text",
|
||||
text="✅ Config fetched successfully\n\nConfig saved to: configs/react.json"
|
||||
)]
|
||||
|
||||
# Mock config file read
|
||||
import json
|
||||
mock_file = MagicMock()
|
||||
mock_file.__enter__.return_value.read.return_value = json.dumps({"name": "react"})
|
||||
mock_open.return_value = mock_file
|
||||
|
||||
# Mock scrape_docs response
|
||||
mock_scrape.return_value = [TextContent(
|
||||
type="text",
|
||||
text="✅ Scraping complete\n\nSkill built at: output/react/"
|
||||
)]
|
||||
|
||||
# Mock enhancement subprocess
|
||||
mock_subprocess.return_value = ("✅ Enhancement complete", "", 0)
|
||||
|
||||
# Mock package response
|
||||
mock_package.return_value = [TextContent(
|
||||
type="text",
|
||||
text="✅ Package complete\n\nSaved to: output/react.zip"
|
||||
)]
|
||||
|
||||
# Mock upload response
|
||||
mock_upload.return_value = [TextContent(
|
||||
type="text",
|
||||
text="✅ Upload successful"
|
||||
)]
|
||||
|
||||
# Mock env (has API key)
|
||||
mock_env_get.return_value = "sk-ant-test-key"
|
||||
|
||||
# Run the workflow
|
||||
result = await install_skill_tool({
|
||||
"config_name": "react",
|
||||
"auto_upload": True
|
||||
})
|
||||
|
||||
output = result[0].text
|
||||
|
||||
# Verify all phases executed
|
||||
assert "PHASE 1/5: Fetch Config" in output
|
||||
assert "PHASE 2/5: Scrape Documentation" in output
|
||||
assert "PHASE 3/5: AI Enhancement" in output
|
||||
assert "PHASE 4/5: Package Skill" in output
|
||||
assert "PHASE 5/5: Upload to Claude" in output
|
||||
|
||||
# Verify workflow completion
|
||||
assert "✅ WORKFLOW COMPLETE" in output
|
||||
assert "fetch_config" in output
|
||||
assert "scrape_docs" in output
|
||||
assert "enhance_skill" in output
|
||||
assert "package_skill" in output
|
||||
assert "upload_skill" in output
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('skill_seekers.mcp.server.scrape_docs_tool')
|
||||
@patch('skill_seekers.mcp.server.run_subprocess_with_streaming')
|
||||
@patch('skill_seekers.mcp.server.package_skill_tool')
|
||||
@patch('builtins.open')
|
||||
@patch('os.environ.get')
|
||||
async def test_workflow_with_existing_config(
|
||||
self,
|
||||
mock_env_get,
|
||||
mock_open,
|
||||
mock_package,
|
||||
mock_subprocess,
|
||||
mock_scrape
|
||||
):
|
||||
"""Test workflow when config_path is provided (skips fetch)"""
|
||||
|
||||
# Mock config file read
|
||||
import json
|
||||
mock_file = MagicMock()
|
||||
mock_file.__enter__.return_value.read.return_value = json.dumps({"name": "custom"})
|
||||
mock_open.return_value = mock_file
|
||||
|
||||
# Mock scrape response
|
||||
mock_scrape.return_value = [TextContent(
|
||||
type="text",
|
||||
text="✅ Scraping complete"
|
||||
)]
|
||||
|
||||
# Mock enhancement subprocess
|
||||
mock_subprocess.return_value = ("✅ Enhancement complete", "", 0)
|
||||
|
||||
# Mock package response
|
||||
mock_package.return_value = [TextContent(
|
||||
type="text",
|
||||
text="✅ Package complete\n\nSaved to: output/custom.zip"
|
||||
)]
|
||||
|
||||
# Mock env (no API key - should skip upload)
|
||||
mock_env_get.return_value = ""
|
||||
|
||||
# Run the workflow
|
||||
result = await install_skill_tool({
|
||||
"config_path": "configs/custom.json",
|
||||
"auto_upload": True
|
||||
})
|
||||
|
||||
output = result[0].text
|
||||
|
||||
# Should only have 4 phases (no fetch)
|
||||
assert "PHASE 1/4: Scrape Documentation" in output
|
||||
assert "PHASE 2/4: AI Enhancement" in output
|
||||
assert "PHASE 3/4: Package Skill" in output
|
||||
assert "PHASE 4/4: Upload to Claude" in output
|
||||
|
||||
# Should not have fetch phase
|
||||
assert "Fetch Config" not in output
|
||||
|
||||
# Should show manual upload instructions (no API key)
|
||||
assert "⚠️ ANTHROPIC_API_KEY not set" in output
|
||||
assert "Manual upload:" in output
|
||||
|
||||
|
||||
class TestInstallSkillErrorHandling:
|
||||
"""Test error handling at each phase"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('skill_seekers.mcp.server.fetch_config_tool')
|
||||
async def test_fetch_phase_failure(self, mock_fetch):
|
||||
"""Test handling of fetch phase failure"""
|
||||
|
||||
# Mock fetch failure
|
||||
mock_fetch.return_value = [TextContent(
|
||||
type="text",
|
||||
text="❌ Failed to fetch config: Network error"
|
||||
)]
|
||||
|
||||
result = await install_skill_tool({
|
||||
"config_name": "react"
|
||||
})
|
||||
|
||||
output = result[0].text
|
||||
|
||||
# Verify error is shown
|
||||
assert "❌ Failed to fetch config" in output
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('skill_seekers.mcp.server.scrape_docs_tool')
|
||||
@patch('builtins.open')
|
||||
async def test_scrape_phase_failure(self, mock_open, mock_scrape):
|
||||
"""Test handling of scrape phase failure"""
|
||||
|
||||
# Mock config read
|
||||
import json
|
||||
mock_file = MagicMock()
|
||||
mock_file.__enter__.return_value.read.return_value = json.dumps({"name": "test"})
|
||||
mock_open.return_value = mock_file
|
||||
|
||||
# Mock scrape failure
|
||||
mock_scrape.return_value = [TextContent(
|
||||
type="text",
|
||||
text="❌ Scraping failed: Connection timeout"
|
||||
)]
|
||||
|
||||
result = await install_skill_tool({
|
||||
"config_path": "configs/test.json"
|
||||
})
|
||||
|
||||
output = result[0].text
|
||||
|
||||
# Verify error is shown and workflow stops
|
||||
assert "❌ Scraping failed" in output
|
||||
assert "WORKFLOW COMPLETE" not in output
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('skill_seekers.mcp.server.scrape_docs_tool')
|
||||
@patch('skill_seekers.mcp.server.run_subprocess_with_streaming')
|
||||
@patch('builtins.open')
|
||||
async def test_enhancement_phase_failure(self, mock_open, mock_subprocess, mock_scrape):
|
||||
"""Test handling of enhancement phase failure"""
|
||||
|
||||
# Mock config read
|
||||
import json
|
||||
mock_file = MagicMock()
|
||||
mock_file.__enter__.return_value.read.return_value = json.dumps({"name": "test"})
|
||||
mock_open.return_value = mock_file
|
||||
|
||||
# Mock scrape success
|
||||
mock_scrape.return_value = [TextContent(
|
||||
type="text",
|
||||
text="✅ Scraping complete"
|
||||
)]
|
||||
|
||||
# Mock enhancement failure
|
||||
mock_subprocess.return_value = ("", "Enhancement error: Claude not found", 1)
|
||||
|
||||
result = await install_skill_tool({
|
||||
"config_path": "configs/test.json"
|
||||
})
|
||||
|
||||
output = result[0].text
|
||||
|
||||
# Verify error is shown
|
||||
assert "❌ Enhancement failed" in output
|
||||
assert "exit code 1" in output
|
||||
|
||||
|
||||
class TestInstallSkillOptions:
|
||||
"""Test various option combinations"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_upload_option(self):
|
||||
"""Test that no_upload option skips upload phase"""
|
||||
result = await install_skill_tool({
|
||||
"config_name": "react",
|
||||
"auto_upload": False,
|
||||
"dry_run": True
|
||||
})
|
||||
|
||||
output = result[0].text
|
||||
|
||||
# Should not show upload phase
|
||||
assert "PHASE 5/5: Upload" not in output
|
||||
assert "PHASE 4/5: Package" in output # Should still be 4/5 for fetch path
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unlimited_option(self):
|
||||
"""Test that unlimited option is passed to scraper"""
|
||||
result = await install_skill_tool({
|
||||
"config_path": "configs/react.json",
|
||||
"unlimited": True,
|
||||
"dry_run": True
|
||||
})
|
||||
|
||||
output = result[0].text
|
||||
|
||||
# Verify unlimited mode is indicated
|
||||
assert "Unlimited mode: True" in output
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_custom_destination(self):
|
||||
"""Test custom destination directory"""
|
||||
result = await install_skill_tool({
|
||||
"config_name": "react",
|
||||
"destination": "/tmp/skills",
|
||||
"dry_run": True
|
||||
})
|
||||
|
||||
output = result[0].text
|
||||
|
||||
# Verify custom destination
|
||||
assert "Destination: /tmp/skills/" in output
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user