diff --git a/.gemini/skills/correct-skill-name/.skilz-manifest.yaml b/.gemini/skills/correct-skill-name/.skilz-manifest.yaml new file mode 100644 index 0000000..9faadc5 --- /dev/null +++ b/.gemini/skills/correct-skill-name/.skilz-manifest.yaml @@ -0,0 +1,8 @@ +installed_at: '2026-01-09T19:30:42+00:00' +skill_id: local/correct-skill-name +git_repo: local +skill_path: /private/var/folders/tm/chrvt43s3rbdld20ghw1qtc40000gn/T/tmp3nhyprfn/wrong-name-skill +git_sha: local +skilz_version: 1.7.0 +install_mode: copy +canonical_path: null diff --git a/.gemini/skills/correct-skill-name/SKILL.md b/.gemini/skills/correct-skill-name/SKILL.md new file mode 100644 index 0000000..8841bdc --- /dev/null +++ b/.gemini/skills/correct-skill-name/SKILL.md @@ -0,0 +1,2 @@ +name: correct-skill-name +description: Test skill \ No newline at end of file diff --git a/.gemini/skills/test-skill/.skilz-manifest.yaml b/.gemini/skills/test-skill/.skilz-manifest.yaml new file mode 100644 index 0000000..be12ffd --- /dev/null +++ b/.gemini/skills/test-skill/.skilz-manifest.yaml @@ -0,0 +1,8 @@ +installed_at: '2026-01-09T19:32:34+00:00' +skill_id: git/test-skill +git_repo: https://github.com/test/repo +skill_path: /private/var/folders/tm/chrvt43s3rbdld20ghw1qtc40000gn/T/tmpk8w9kl2f/test-skill +git_sha: local +skilz_version: 1.7.0 +install_mode: copy +canonical_path: null diff --git a/.gemini/skills/test-skill/SKILL.md b/.gemini/skills/test-skill/SKILL.md new file mode 100644 index 0000000..2d9309f --- /dev/null +++ b/.gemini/skills/test-skill/SKILL.md @@ -0,0 +1,2 @@ +name: test-skill +description: Test skill \ No newline at end of file diff --git a/.speckit/features/01-core-installer/specify.md b/.speckit/features/01-core-installer/specify.md index 8706b30..13501f9 100644 --- a/.speckit/features/01-core-installer/specify.md +++ b/.speckit/features/01-core-installer/specify.md @@ -70,6 +70,7 @@ Implement the core `skilz install` command that: - Re-running install for same skill+SHA is a no-op - If SHA differs from manifest, reinstall with new version - Clear output indicating "already installed" vs "updated" +- Directory name validation only applies to permanent skill directories, not temp git clone directories ## Functional Requirements @@ -99,6 +100,7 @@ Implement the core `skilz install` command that: - SHA not found: show SHA and suggest checking registry - Permission denied: show path and suggest permissions fix - Registry not found: show expected paths +- Directory name validation: only warn for permanent skill directories, not temp git clone directories ## Non-Functional Requirements diff --git a/.speckit/features/10-universal-agent-enhancements/specify.md b/.speckit/features/10-universal-agent-enhancements/specify.md index bf7e7fb..6c4c18f 100644 --- a/.speckit/features/10-universal-agent-enhancements/specify.md +++ b/.speckit/features/10-universal-agent-enhancements/specify.md @@ -141,15 +141,18 @@ cat AGENTS.md | grep web-scraper # ✅ Referenced ### US-3: Config File Override -**As a** power user testing skill installations -**I want to** control which config file gets updated +**As a** power user testing skill installations +**I want to** control which config file gets updated **So that** I can test without breaking my main setup **Acceptance Criteria:** ```bash -# Test installation with custom config +# Test installation with custom config (registry install) skilz install test-skill --agent universal --project --config TEST.md +# Test git installation with custom config +skilz install https://github.com/owner/repo --agent universal --project --config TEST.md + # Verify only TEST.md was modified cat TEST.md | grep test-skill # ✅ Referenced cat AGENTS.md | grep test-skill # ❌ Not modified @@ -298,9 +301,11 @@ def sync_skill_to_configs( ### Phase 10d: Config Sync Enhancement - [ ] Add `target_files` parameter to `sync_skill_to_configs()` - [ ] Update sync logic to use custom files when provided +- [ ] Add `config_file` parameter to `install_from_git()` function +- [ ] Pass `config_file` through git install flow to local install - [ ] Preserve backward compatibility (default behavior unchanged) -- **Files:** `src/skilz/config_sync.py`, `tests/test_config_sync.py` -- **Effort:** 2 hours +- **Files:** `src/skilz/config_sync.py`, `src/skilz/git_install.py`, `tests/test_config_sync.py` +- **Effort:** 3 hours ### Phase 10e: Integration Testing - [ ] Test: Universal project install creates AGENTS.md diff --git a/README.md b/README.md index 0f5e4e4..925a6b9 100644 --- a/README.md +++ b/README.md @@ -625,6 +625,12 @@ task test:fast # Run tests without verbose output task coverage # Run tests with coverage report task coverage:html # Generate HTML coverage report +# E2E Testing +./scripts/end_to_end.sh # Full E2E test suite +./scripts/test_rest_marketplace_e2e.sh # Live API testing +./scripts/test_api_integration.sh # API integration tests +./scripts/test_bug_fixes_e2e.sh # Bug fix regression tests + # Code Quality task lint # Run linter (ruff) task lint:fix # Auto-fix linting issues diff --git a/docs/DEPLOY_PYPI.md b/docs/DEPLOY_PYPI.md index f53ee3b..84444ff 100644 --- a/docs/DEPLOY_PYPI.md +++ b/docs/DEPLOY_PYPI.md @@ -25,6 +25,35 @@ The following tools are required (install globally or in your environment): pip install build twine ``` +### 2. E2E Testing + +Before deploying, run the comprehensive E2E test suite to ensure all functionality works: + +```bash +# Run full E2E test suite (tests all major features) +./scripts/end_to_end.sh + +# Run API integration tests (tests marketplace endpoints) +./scripts/test_api_integration.sh + +# Run REST marketplace tests (tests live API with real data) +./scripts/test_rest_marketplace_e2e.sh + +# Run bug fix regression tests (tests recent fixes) +./scripts/test_bug_fixes_e2e.sh +``` + +**E2E Test Coverage:** +- ✅ Marketplace ID installation (`skilz install owner_repo/skill`) +- ✅ Git URL installation (`skilz install https://github.com/...`) +- ✅ Local file installation (`skilz install -f path/to/skill`) +- ✅ All supported agents (Claude, OpenCode, Gemini, Codex, Copilot, Universal) +- ✅ Project-level installations (`--project` flag) +- ✅ Custom config file targeting (`--config FILE`) +- ✅ List, remove, search, and visit commands +- ✅ API endpoint validation and error handling +- ✅ Regression testing for recent bug fixes + ### 2. PyPI Account Setup 1. **Create account**: https://pypi.org/account/register/ @@ -57,6 +86,27 @@ password = pypi-YOUR_TESTPYPI_TOKEN_HERE chmod 600 ~/.pypirc ``` +## Quality Assurance + +### Testing Strategy + +Skilz uses a comprehensive multi-layer testing approach: + +1. **Unit Tests**: 633+ tests covering individual functions and modules +2. **Integration Tests**: API client and config sync testing +3. **E2E Tests**: Real-world scenario testing with isolated environments +4. **Regression Tests**: Bug fix validation with before/after testing + +### Pre-Deployment Checklist + +- [ ] `task check` passes (lint + typecheck + test) +- [ ] `./scripts/end_to_end.sh` passes (full feature test) +- [ ] `./scripts/test_rest_marketplace_e2e.sh` passes (live API test) +- [ ] `./scripts/test_bug_fixes_e2e.sh` passes (regression test) +- [ ] Version updated in `src/skilz/__init__.py` and `pyproject.toml` +- [ ] CHANGELOG.md updated with release notes +- [ ] GitHub release created and tagged + ## Release Process ### Quick Release (All-in-One) diff --git a/scripts/test_bug_fixes_e2e.sh b/scripts/test_bug_fixes_e2e.sh new file mode 100755 index 0000000..8288dd4 --- /dev/null +++ b/scripts/test_bug_fixes_e2e.sh @@ -0,0 +1,300 @@ +#!/usr/bin/env bash +# +# E2E Tests for Bug Fixes: SKILZ-64 and SKILZ-65 +# +# SKILZ-64: Temp directory warning during git installs should not appear +# SKILZ-65: --config flag should work for git installs +# +# These tests should FAIL initially, then PASS after fixes +# + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Test counters +TESTS_PASSED=0 +TESTS_FAILED=0 + +# Test data +TEST_SKILL_URL="https://github.com/SpillwaveSolutions/sdd-skill" +TEST_SKILL_NAME="sdd" +TEST_PROJECT_DIR="/tmp/skilz-bug-test-$(date +%s)" +TEST_CONFIG_FILE="TEST_CONFIG.md" + +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[PASS]${NC} $1" + ((TESTS_PASSED++)) +} + +log_failure() { + echo -e "${RED}[FAIL]${NC} $1" + ((TESTS_FAILED++)) +} + +log_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +run_test() { + local test_name="$1" + local test_cmd="$2" + local expected_exit="$3" + local description="$4" + + log_info "Running: $test_name" + log_info "Command: $test_cmd" + log_info "Expected: $description" + + # Run command and capture output + local output + local exit_code + output=$(eval "$test_cmd" 2>&1) || exit_code=$? + + if [[ "$expected_exit" == "success" && $exit_code -eq 0 ]]; then + log_success "$test_name: $description" + return 0 + elif [[ "$expected_exit" == "failure" && $exit_code -ne 0 ]]; then + log_success "$test_name: $description" + return 0 + else + log_failure "$test_name: Expected $expected_exit but got exit code $exit_code" + echo "Output: $output" + return 1 + fi +} + +setup_test_env() { + log_info "Setting up test environment..." + + # Create test project directory + mkdir -p "$TEST_PROJECT_DIR" + cd "$TEST_PROJECT_DIR" + + # Initialize as git repo (some tests need this) + git init --quiet + git config user.name "Test User" + git config user.email "test@example.com" + + log_info "Test environment ready: $TEST_PROJECT_DIR" +} + +cleanup_test_env() { + log_info "Cleaning up test environment..." + cd / + rm -rf "$TEST_PROJECT_DIR" + log_info "Cleanup complete" +} + +# ============================================================================ +# BUG 1 TESTS: SKILZ-64 - Temp Directory Warning During Git Installs +# ============================================================================ + +test_bug1_git_install_no_temp_warning() { + # Test that git installs don't show temp directory warnings + # This should PASS after the fix + + local test_name="BUG1_GIT_NO_TEMP_WARNING" + local cmd="skilz install $TEST_SKILL_URL --project --agent gemini 2>&1" + local expected_exit="success" + local description="Git install should not show temp directory name warnings" + + log_info "Testing Bug 1: Git installs should not warn about temp directories" + + # Run the install command + local output + output=$(eval "$cmd") + + # Check for temp directory warnings (should NOT be present after fix) + if echo "$output" | grep -q "doesn't match skill name"; then + log_failure "$test_name: Found temp directory warning (bug still present)" + echo "Output: $output" + return 1 + else + log_success "$test_name: No temp directory warnings found" + return 0 + fi +} + +test_bug1_local_install_shows_warning() { + # Test that local installs STILL show warnings for permanent directories + # This should PASS both before and after the fix + + local test_name="BUG1_LOCAL_STILL_WARNS" + local description="Local installs should still warn about mismatched directory names" + + log_info "Testing Bug 1: Local installs should still warn for permanent directories" + + # Create a local skill directory with wrong name + local wrong_dir="wrong-skill-name" + mkdir -p "$wrong_dir" + echo "name: $TEST_SKILL_NAME" > "$wrong_dir/SKILL.md" + echo "description: Test skill" >> "$wrong_dir/SKILL.md" + + local cmd="skilz install -f $wrong_dir --project --agent gemini 2>&1" + local output + output=$(eval "$cmd") + + # Clean up + rm -rf "$wrong_dir" + + # Check for warnings (should be present for local installs) + if echo "$output" | grep -q "doesn't match skill name"; then + log_success "$test_name: Local install correctly shows warning for mismatched names" + return 0 + else + log_failure "$test_name: Local install should warn about mismatched directory names" + echo "Output: $output" + return 1 + fi +} + +# ============================================================================ +# BUG 2 TESTS: SKILZ-65 - --config Flag Not Working for Git Installs +# ============================================================================ + +test_bug2_git_config_flag_works() { + # Test that --config flag works for git installs + # This should FAIL before fix, PASS after fix + + local test_name="BUG2_GIT_CONFIG_WORKS" + local cmd="skilz install $TEST_SKILL_URL --project --agent gemini --config $TEST_CONFIG_FILE" + local expected_exit="success" + local description="Git install with --config should create/update config file" + + log_info "Testing Bug 2: Git installs should honor --config flag" + + # Run the install command + if eval "$cmd"; then + # Check if config file was created/updated + if [[ -f "$TEST_CONFIG_FILE" ]]; then + # Check if it contains skill reference + if grep -q "$TEST_SKILL_NAME" "$TEST_CONFIG_FILE"; then + log_success "$test_name: --config flag worked for git install" + return 0 + else + log_failure "$test_name: Config file created but doesn't contain skill reference" + cat "$TEST_CONFIG_FILE" + return 1 + fi + else + log_failure "$test_name: --config flag ignored - file not created" + return 1 + fi + else + log_failure "$test_name: Git install with --config failed" + return 1 + fi +} + +test_bug2_git_config_vs_force_config() { + # Test that --config behaves like --force-config but with custom file + # This should FAIL before fix, PASS after fix + + local test_name="BUG2_CONFIG_VS_FORCE" + local description="--config should work like --force-config with custom file" + + log_info "Testing Bug 2: --config vs --force-config behavior" + + # Clean up any existing files + rm -f "$TEST_CONFIG_FILE" "AGENTS.md" + + # Test 1: --force-config should work (baseline) + log_info "Testing --force-config (should work)..." + if skilz install "$TEST_SKILL_URL" --project --agent gemini --force-config; then + if [[ -f "AGENTS.md" ]] && grep -q "$TEST_SKILL_NAME" "AGENTS.md"; then + log_success "Baseline: --force-config works" + else + log_failure "Baseline: --force-config doesn't work" + return 1 + fi + else + log_failure "Baseline: --force-config command failed" + return 1 + fi + + # Clean up for next test + rm -f "$TEST_CONFIG_FILE" "AGENTS.md" + + # Test 2: --config should work like --force-config but with custom file + log_info "Testing --config (should work after fix)..." + if skilz install "$TEST_SKILL_URL" --project --agent gemini --config "$TEST_CONFIG_FILE"; then + if [[ -f "$TEST_CONFIG_FILE" ]] && grep -q "$TEST_SKILL_NAME" "$TEST_CONFIG_FILE"; then + # Also verify AGENTS.md was NOT touched (since we specified custom file) + if [[ ! -f "AGENTS.md" ]]; then + log_success "$test_name: --config works like --force-config with custom file" + return 0 + else + log_failure "$test_name: --config created custom file but also touched default file" + return 1 + fi + else + log_failure "$test_name: --config flag ignored - custom file not created properly" + return 1 + fi + else + log_failure "$test_name: Git install with --config failed" + return 1 + fi +} + +# ============================================================================ +# MAIN TEST EXECUTION +# ============================================================================ + +main() { + log_info "Starting E2E Tests for SKILZ-64 and SKILZ-65 Bug Fixes" + log_info "These tests should FAIL initially, then PASS after fixes" + echo + + # Setup + setup_test_env + + # Bug 1 Tests + echo "==========================================" + log_info "Testing BUG 1: SKILZ-64 - Temp Directory Warnings" + echo "==========================================" + + test_bug1_git_install_no_temp_warning + test_bug1_local_install_shows_warning + + echo + echo "==========================================" + log_info "Testing BUG 2: SKILZ-65 - --config Flag for Git Installs" + echo "==========================================" + + test_bug2_git_config_flag_works + test_bug2_git_config_vs_force_config + + # Cleanup + cleanup_test_env + + # Results + echo + echo "==========================================" + log_info "Test Results Summary" + echo "==========================================" + log_info "Tests Passed: $TESTS_PASSED" + log_info "Tests Failed: $TESTS_FAILED" + log_info "Total Tests: $((TESTS_PASSED + TESTS_FAILED))" + + if [[ $TESTS_FAILED -eq 0 ]]; then + log_success "All tests passed! 🎉" + exit 0 + else + log_failure "Some tests failed. Fix the bugs and run again." + exit 1 + fi +} + +# Run main function +main "$@" \ No newline at end of file diff --git a/src/skilz/commands/install_cmd.py b/src/skilz/commands/install_cmd.py index 8b840c4..7683c9c 100644 --- a/src/skilz/commands/install_cmd.py +++ b/src/skilz/commands/install_cmd.py @@ -121,6 +121,7 @@ def cmd_install(args: argparse.Namespace) -> int: yes_all=yes_all, skill_filter_name=skill_filter_name, force_config=force_config, + config_file=config_file, # SKILZ-65: Pass custom config file ) try: diff --git a/src/skilz/git_install.py b/src/skilz/git_install.py index 77fa1c9..80f2297 100644 --- a/src/skilz/git_install.py +++ b/src/skilz/git_install.py @@ -272,6 +272,7 @@ def install_from_git( yes_all: bool = False, skill_filter_name: str | None = None, force_config: bool = False, + config_file: str | None = None, # SKILZ-65: Custom config file for git installs ) -> int: """ Install skill(s) from a git repository URL. @@ -286,6 +287,7 @@ def install_from_git( yes_all: If True (global -y flag), install all without prompting. skill_filter_name: If provided, install only the skill with this name. force_config: If True, write to config files even for native agents. + config_file: Optional custom config file to update (requires project_level=True). Returns: Exit code (0 for success, non-zero for error). @@ -373,6 +375,7 @@ def install_from_git( # Use skill name from SKILL.md frontmatter skill_name=skill.skill_name, force_config=force_config, + config_file=config_file, # SKILZ-65: Pass custom config file ) installed_count += 1 diff --git a/src/skilz/installer.py b/src/skilz/installer.py index 80cf02f..95793e8 100644 --- a/src/skilz/installer.py +++ b/src/skilz/installer.py @@ -157,20 +157,21 @@ def install_local_skill( error_msg += f"\n---\nname: {validation.suggested_name}\ndescription: ...\n---" raise InstallError(str(source_path), error_msg) - # Check if directory name matches skill name - from skilz.agent_registry import check_skill_directory_name - - matches, suggested_path = check_skill_directory_name(source_path, skill_name) - if not matches and suggested_path: - print( - f"Warning: Directory name '{source_path.name}' doesn't match " - f"skill name '{skill_name}'", - file=sys.stderr, - ) - print( - f" For better organization, consider renaming to: {suggested_path}", - file=sys.stderr, - ) + # Check if directory name matches skill name (skip for git installs with temp dirs) + if git_url is None: # Only validate permanent directories, not temp git clone dirs + from skilz.agent_registry import check_skill_directory_name + + matches, suggested_path = check_skill_directory_name(source_path, skill_name) + if not matches and suggested_path: + print( + f"Warning: Directory name '{source_path.name}' doesn't match " + f"skill name '{skill_name}'", + file=sys.stderr, + ) + print( + f" For better organization, consider renaming to: {suggested_path}", + file=sys.stderr, + ) # Step 1: Determine target agent resolved_agent: AgentType @@ -233,8 +234,10 @@ def install_local_skill( registry = get_registry() agent_config = registry.get_or_raise(resolved_agent) - # Skip config sync for native agents unless --force-config - should_sync = force_config or agent_config.native_skill_support == "none" + # Skip config sync for native agents unless --force-config or --config specified + should_sync = ( + force_config or config_file is not None or agent_config.native_skill_support == "none" + ) if not should_sync: if verbose: @@ -530,8 +533,10 @@ def install_skill( registry = get_registry() agent_config = registry.get_or_raise(resolved_agent) - # Skip config sync for native agents unless --force-config - should_sync = force_config or agent_config.native_skill_support == "none" + # Skip config sync for native agents unless --force-config or --config specified + should_sync = ( + force_config or config_file is not None or agent_config.native_skill_support == "none" + ) if not should_sync: if verbose: diff --git a/tests/test_bug_fixes.py b/tests/test_bug_fixes.py new file mode 100644 index 0000000..85ead03 --- /dev/null +++ b/tests/test_bug_fixes.py @@ -0,0 +1,165 @@ +"""Unit tests for SKILZ-64 and SKILZ-65 bug fixes. + +These tests should FAIL initially due to the bugs, then PASS after fixes. +""" + +import os +import sys +import tempfile +from pathlib import Path +from unittest.mock import patch + +# Add src to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + + +class TestBugFixes: + """Test cases for SKILZ-64 and SKILZ-65 bug fixes.""" + + # ============================================================================ + # BUG 1 TESTS: SKILZ-64 - Temp Directory Warning During Git Installs + # ============================================================================ + + def test_bug1_git_install_no_temp_warning(self): + """Test that git installs don't show temp directory warnings. + + This test should FAIL before the fix, PASS after. + """ + from skilz.installer import install_local_skill + + with tempfile.TemporaryDirectory() as temp_dir: + # Create a mock skill directory with SKILL.md + skill_dir = Path(temp_dir) / "test-skill" + skill_dir.mkdir() + skill_md = skill_dir / "SKILL.md" + skill_md.write_text("name: test-skill\ndescription: Test skill") + + # Capture stderr to check for warnings + import io + + stderr_capture = io.StringIO() + + with patch("sys.stderr", stderr_capture): + # This should NOT produce a warning for git installs (git_url provided) + install_local_skill( + source_path=skill_dir, + agent="gemini", # type: ignore + project_level=True, + verbose=False, + git_url="https://github.com/test/repo", # Indicates git install + skill_name="test-skill", + ) + + # Check stderr for warnings + stderr_output = stderr_capture.getvalue() + assert "doesn't match skill name" not in stderr_output, ( + f"Git install should not warn about temp directories, but got: {stderr_output}" + ) + + def test_bug1_local_install_shows_warning(self): + """Test that local installs still show warnings for permanent directories. + + This test should PASS both before and after the fix. + """ + from skilz.installer import install_local_skill + + with tempfile.TemporaryDirectory() as temp_dir: + # Create a skill directory with WRONG name + wrong_dir = Path(temp_dir) / "wrong-name-skill" + wrong_dir.mkdir() + skill_md = wrong_dir / "SKILL.md" + skill_md.write_text("name: correct-skill-name\ndescription: Test skill") + + # Capture stderr + import io + + stderr_capture = io.StringIO() + + with patch("sys.stderr", stderr_capture): + # This SHOULD produce a warning for local installs (no git_url) + install_local_skill( + source_path=wrong_dir, + agent="gemini", # type: ignore + project_level=True, + verbose=False, + git_url=None, # Indicates local install + skill_name="correct-skill-name", + ) + + # Check stderr for warnings + stderr_output = stderr_capture.getvalue() + assert "doesn't match skill name" in stderr_output, ( + "Local install should warn about mismatched directory names, " + f"but got: {stderr_output}" + ) + + # ============================================================================ + # BUG 2 TESTS: SKILZ-65 - --config Flag Not Working for Git Installs + # ============================================================================ + + def test_bug2_git_install_config_parameter_exists(self): + """Test that install_from_git accepts config_file parameter. + + This test should PASS after the fix (parameter exists), FAIL before. + """ + from skilz.git_install import install_from_git + + # This should NOT raise TypeError after the fix (parameter exists) + # We expect it to fail with git repo not found, not parameter error + try: + install_from_git( + git_url="https://github.com/test/repo", + agent="gemini", # type: ignore + project_level=True, + config_file="TEST_CONFIG.md", # This parameter should exist after fix + ) + # If we get here, something unexpected happened + assert False, "Expected git clone to fail, but it didn't" + except Exception as e: + # We expect some error (git repo not found), but NOT TypeError about missing parameter + assert not isinstance(e, TypeError) or "config_file" not in str(e), ( + f"config_file parameter should exist, but got TypeError: {e}" + ) + # Any other error (like git repo not found) means the parameter exists + assert True, "config_file parameter accepted (git error expected)" + + def test_bug2_config_flag_validation(self): + """Test that --config requires --project flag. + + This should PASS both before and after the fix (existing validation). + """ + import argparse + + from skilz.commands.install_cmd import cmd_install + + # Mock args with --config but no --project + mock_args = argparse.Namespace( + skill_id="test-skill", + agent="gemini", + project=False, # No --project + config="TEST.md", # Has --config + file=None, + git=None, + version_spec=None, + force_config=False, + copy=False, + symlink=False, + verbose=False, + yes_all=False, + install_all=False, + skill=None, + ) + + # This should fail with error message + import io + + stderr_capture = io.StringIO() + + with patch("sys.stderr", stderr_capture): + result = cmd_install(mock_args) + assert result == 1, "Should return error code 1" + + stderr_output = stderr_capture.getvalue() + assert "--config requires --project" in stderr_output, ( + f"Should show config validation error, got: {stderr_output}" + )