Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:26:08 +08:00
commit 8f22ddf339
295 changed files with 59710 additions and 0 deletions

278
skills/code.format/SKILL.md Normal file
View File

@@ -0,0 +1,278 @@
# code.format
Format code using Prettier, supporting multiple languages and file types. This skill can format individual files or entire directories, check formatting without making changes, and respect custom Prettier configurations.
## Overview
**Purpose:** Automatically format code using Prettier to maintain consistent code style across your project.
**Command:** `/code/format`
**Version:** 0.1.0
## Features
- Format individual files or entire directories
- Support for 15+ file types (JavaScript, TypeScript, CSS, HTML, JSON, YAML, Markdown, and more)
- Auto-detect Prettier configuration files (.prettierrc, prettier.config.js, etc.)
- Check-only mode to validate formatting without modifying files
- Custom file pattern filtering
- Detailed formatting reports
- Automatic discovery of local and global Prettier installations
## Supported File Types
- **JavaScript**: .js, .jsx, .mjs, .cjs
- **TypeScript**: .ts, .tsx
- **CSS/Styles**: .css, .scss, .less
- **HTML**: .html, .htm
- **JSON**: .json
- **YAML**: .yaml, .yml
- **Markdown**: .md, .mdx
- **GraphQL**: .graphql, .gql
- **Vue**: .vue
## Prerequisites
Prettier must be installed either globally or locally in your project:
```bash
# Global installation
npm install -g prettier
# Or local installation (recommended)
npm install --save-dev prettier
```
## Usage
### Basic Usage
Format a single file:
```bash
python3 skills/code.format/code_format.py --path src/index.js
```
Format an entire directory:
```bash
python3 skills/code.format/code_format.py --path src/
```
### Advanced Usage
**Check formatting without modifying files:**
```bash
python3 skills/code.format/code_format.py --path src/ --check
```
**Format only specific file types:**
```bash
python3 skills/code.format/code_format.py --path src/ --patterns "**/*.ts,**/*.tsx"
```
**Use custom Prettier configuration:**
```bash
python3 skills/code.format/code_format.py --path src/ --config-path .prettierrc.custom
```
**Dry run (check without writing):**
```bash
python3 skills/code.format/code_format.py --path src/ --no-write
```
**Output as YAML:**
```bash
python3 skills/code.format/code_format.py --path src/ --output-format yaml
```
## CLI Arguments
| Argument | Required | Default | Description |
|----------|----------|---------|-------------|
| `--path` | Yes | - | File or directory path to format |
| `--config-path` | No | Auto-detect | Path to custom Prettier configuration file |
| `--check` | No | false | Only check formatting without modifying files |
| `--patterns` | No | All supported | Comma-separated glob patterns (e.g., "**/*.js,**/*.ts") |
| `--no-write` | No | false | Don't write changes (dry run mode) |
| `--output-format` | No | json | Output format: json or yaml |
## Configuration
The skill will automatically search for Prettier configuration files in this order:
1. Custom config specified via `--config-path`
2. `.prettierrc` in the target directory or parent directories
3. `.prettierrc.json`, `.prettierrc.yml`, `.prettierrc.yaml`
4. `.prettierrc.js`, `.prettierrc.cjs`
5. `prettier.config.js`, `prettier.config.cjs`
6. Prettier defaults if no config found
## Output Format
The skill returns a JSON object with detailed formatting results:
```json
{
"ok": true,
"status": "success",
"message": "Formatted 5 files. 3 already formatted.",
"formatted_count": 5,
"already_formatted_count": 3,
"needs_formatting_count": 0,
"checked_count": 8,
"error_count": 0,
"files_formatted": [
"src/components/Header.tsx",
"src/utils/helpers.js"
],
"files_already_formatted": [
"src/index.ts",
"src/App.tsx",
"src/config.json"
],
"files_need_formatting": [],
"files_with_errors": []
}
```
### Response Fields
- **ok**: Boolean indicating overall success
- **status**: Status string ("success" or "failed")
- **message**: Human-readable summary
- **formatted_count**: Number of files that were formatted
- **already_formatted_count**: Number of files that were already properly formatted
- **needs_formatting_count**: Number of files that need formatting (check mode only)
- **checked_count**: Total number of files processed
- **error_count**: Number of files that encountered errors
- **files_formatted**: List of files that were formatted
- **files_already_formatted**: List of files that were already formatted
- **files_need_formatting**: List of files needing formatting (check mode)
- **files_with_errors**: List of files with errors and error messages
## Error Handling
The skill gracefully handles various error scenarios:
- **Prettier not installed**: Clear error message with installation instructions
- **Invalid path**: Validation error if path doesn't exist
- **Syntax errors**: Reports files with syntax errors without stopping
- **Permission errors**: Reports files that couldn't be read/written
- **Timeouts**: 30-second timeout per file with clear error reporting
## Integration with Agents
Include this skill in your agent's configuration:
```yaml
name: my.agent
version: 1.0.0
skills_available:
- code.format
```
Then invoke it programmatically:
```python
from skills.code_format.code_format import CodeFormat
formatter = CodeFormat()
result = formatter.execute(
path="src/",
check_only=True,
file_patterns="**/*.{ts,tsx}"
)
if result["ok"]:
print(f"Checked {result['checked_count']} files")
print(f"{result['needs_formatting_count']} files need formatting")
```
## Examples
### Example 1: Format a React Project
```bash
python3 skills/code.format/code_format.py \
--path src/ \
--patterns "**/*.{js,jsx,ts,tsx,css,json}"
```
### Example 2: Pre-commit Check
```bash
python3 skills/code.format/code_format.py \
--path src/ \
--check \
--output-format json
# Exit code 0 if all files formatted, 1 otherwise
```
### Example 3: Format Only Changed Files
```bash
# Get changed files from git
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMR | grep -E '\.(js|ts|jsx|tsx)$' | tr '\n' ',')
# Format only those files
python3 skills/code.format/code_format.py \
--path . \
--patterns "$CHANGED_FILES"
```
## Testing
Run the test suite:
```bash
pytest skills/code.format/test_code_format.py -v
```
Run specific tests:
```bash
pytest skills/code.format/test_code_format.py::TestCodeFormat::test_single_file -v
```
## Permissions
This skill requires the following permissions:
- **filesystem:read** - To read files and configurations
- **filesystem:write** - To write formatted files
- **process:execute** - To run the Prettier command
## Artifact Metadata
**Produces:**
- `formatting-report` (application/json) - Detailed formatting operation results
## Troubleshooting
**Issue**: "Prettier is not installed"
- **Solution**: Install Prettier globally (`npm install -g prettier`) or locally in your project
**Issue**: No files found to format
- **Solution**: Check your file patterns and ensure files exist in the target path
**Issue**: Configuration file not found
- **Solution**: Ensure your config file exists and the path is correct, or let it auto-detect
**Issue**: Timeout errors
- **Solution**: Very large files may timeout (30s limit). Format them individually or increase timeout in code
## Created By
This skill was generated by **meta.skill**, the skill creator meta-agent, and enhanced with full Prettier integration.
---
*Part of the Betty Framework*

View File

@@ -0,0 +1 @@
# Auto-generated package initializer for skills.

434
skills/code.format/code_format.py Executable file
View File

@@ -0,0 +1,434 @@
#!/usr/bin/env python3
"""
code.format - Format code using Prettier
This skill formats code using Prettier, supporting multiple languages and file types.
It can format individual files or entire directories, check formatting without making
changes, and respect custom Prettier configurations.
Generated by meta.skill with Betty Framework certification
"""
import os
import sys
import json
import yaml
import subprocess
import shutil
from pathlib import Path
from typing import Dict, List, Any, Optional
from betty.config import BASE_DIR
from betty.logging_utils import setup_logger
from betty.certification import certified_skill
logger = setup_logger(__name__)
class CodeFormat:
"""
Format code using Prettier, supporting multiple languages and file types.
"""
# Supported file extensions
SUPPORTED_EXTENSIONS = {
'.js', '.jsx', '.mjs', '.cjs', # JavaScript
'.ts', '.tsx', # TypeScript
'.css', '.scss', '.less', # Styles
'.html', '.htm', # HTML
'.json', # JSON
'.yaml', '.yml', # YAML
'.md', '.mdx', # Markdown
'.graphql', '.gql', # GraphQL
'.vue', # Vue
}
# Prettier config file names
CONFIG_FILES = [
'.prettierrc',
'.prettierrc.json',
'.prettierrc.yml',
'.prettierrc.yaml',
'.prettierrc.json5',
'.prettierrc.js',
'.prettierrc.cjs',
'prettier.config.js',
'prettier.config.cjs',
]
def __init__(self, base_dir: str = BASE_DIR):
"""Initialize skill"""
self.base_dir = Path(base_dir)
def _check_prettier_installed(self) -> tuple[bool, Optional[str]]:
"""
Check if Prettier is installed and return the command to use.
Returns:
Tuple of (is_installed, command_path)
"""
# Check for npx prettier (local installation)
try:
result = subprocess.run(
['npx', 'prettier', '--version'],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
logger.info(f"Found Prettier via npx: {result.stdout.strip()}")
return True, 'npx prettier'
except (subprocess.SubprocessError, FileNotFoundError):
pass
# Check for global prettier installation
prettier_path = shutil.which('prettier')
if prettier_path:
try:
result = subprocess.run(
['prettier', '--version'],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
logger.info(f"Found Prettier globally: {result.stdout.strip()}")
return True, 'prettier'
except subprocess.SubprocessError:
pass
return False, None
def _find_config_file(self, start_path: Path, custom_config: Optional[str] = None) -> Optional[Path]:
"""
Find Prettier configuration file.
Args:
start_path: Path to start searching from
custom_config: Optional custom config file path
Returns:
Path to config file or None
"""
if custom_config:
config_path = Path(custom_config)
if config_path.exists():
logger.info(f"Using custom config: {config_path}")
return config_path
else:
logger.warning(f"Custom config not found: {config_path}")
# Search upwards from start_path
current = start_path if start_path.is_dir() else start_path.parent
while current != current.parent: # Stop at root
for config_name in self.CONFIG_FILES:
config_path = current / config_name
if config_path.exists():
logger.info(f"Found config: {config_path}")
return config_path
current = current.parent
logger.info("No Prettier config found, will use defaults")
return None
def _discover_files(self, path: Path, patterns: Optional[List[str]] = None) -> List[Path]:
"""
Discover files to format.
Args:
path: File or directory path
patterns: Optional glob patterns to filter files
Returns:
List of file paths to format
"""
if path.is_file():
return [path]
files = []
if patterns:
for pattern in patterns:
files.extend(path.rglob(pattern))
else:
# Find all files with supported extensions
for ext in self.SUPPORTED_EXTENSIONS:
files.extend(path.rglob(f'*{ext}'))
# Filter out common ignore patterns
ignored_dirs = {'node_modules', '.git', 'dist', 'build', '.next', 'coverage', '__pycache__'}
filtered_files = [
f for f in files
if f.is_file() and not any(ignored in f.parts for ignored in ignored_dirs)
]
logger.info(f"Discovered {len(filtered_files)} files to format")
return filtered_files
def _format_file(self, file_path: Path, prettier_cmd: str, check_only: bool = False,
config_path: Optional[Path] = None) -> Dict[str, Any]:
"""
Format a single file using Prettier.
Args:
file_path: Path to file to format
prettier_cmd: Prettier command to use
check_only: Only check formatting without modifying
config_path: Optional path to config file
Returns:
Dict with formatting result
"""
cmd = prettier_cmd.split()
cmd.append(str(file_path))
if check_only:
cmd.append('--check')
else:
cmd.append('--write')
if config_path:
cmd.extend(['--config', str(config_path)])
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
if check_only and result.stdout:
# File is already formatted
return {
'file': str(file_path),
'status': 'already_formatted',
'ok': True
}
else:
# File was formatted
return {
'file': str(file_path),
'status': 'formatted',
'ok': True
}
else:
# Formatting failed or file needs formatting (in check mode)
if check_only and 'Code style issues' in result.stderr:
return {
'file': str(file_path),
'status': 'needs_formatting',
'ok': True
}
else:
return {
'file': str(file_path),
'status': 'error',
'ok': False,
'error': result.stderr or result.stdout
}
except subprocess.TimeoutExpired:
logger.error(f"Timeout formatting {file_path}")
return {
'file': str(file_path),
'status': 'error',
'ok': False,
'error': 'Timeout after 30 seconds'
}
except Exception as e:
logger.error(f"Error formatting {file_path}: {e}")
return {
'file': str(file_path),
'status': 'error',
'ok': False,
'error': str(e)
}
@certified_skill("code.format")
def execute(
self,
path: str,
config_path: Optional[str] = None,
check_only: bool = False,
file_patterns: Optional[str] = None,
write: bool = True
) -> Dict[str, Any]:
"""
Execute the code formatting skill.
Args:
path: File or directory path to format
config_path: Path to custom Prettier configuration file
check_only: Only check formatting without modifying files
file_patterns: Comma-separated glob patterns to filter files
write: Write formatted output to files (default: True)
Returns:
Dict with execution results including:
- ok: Overall success status
- status: Status message
- formatted_count: Number of files formatted
- checked_count: Number of files checked
- error_count: Number of files with errors
- files_formatted: List of formatted file paths
- files_already_formatted: List of already formatted file paths
- files_with_errors: List of files that had errors
"""
try:
logger.info(f"Executing code.format on: {path}")
# Check if Prettier is installed
is_installed, prettier_cmd = self._check_prettier_installed()
if not is_installed:
return {
"ok": False,
"status": "failed",
"error": "Prettier is not installed. Install it with: npm install -g prettier or npm install --save-dev prettier"
}
# Validate path
target_path = Path(path)
if not target_path.exists():
return {
"ok": False,
"status": "failed",
"error": f"Path does not exist: {path}"
}
# Find config file
config_file = self._find_config_file(target_path, config_path)
# Parse file patterns
patterns = None
if file_patterns:
patterns = [p.strip() for p in file_patterns.split(',')]
# Discover files
files = self._discover_files(target_path, patterns)
if not files:
return {
"ok": True,
"status": "success",
"message": "No files found to format",
"formatted_count": 0,
"checked_count": 0,
"error_count": 0
}
# Format files
results = []
for file_path in files:
result = self._format_file(
file_path,
prettier_cmd,
check_only=check_only or not write,
config_path=config_file
)
results.append(result)
# Aggregate results
files_formatted = [r['file'] for r in results if r['status'] == 'formatted']
files_already_formatted = [r['file'] for r in results if r['status'] == 'already_formatted']
files_need_formatting = [r['file'] for r in results if r['status'] == 'needs_formatting']
files_with_errors = [
{'file': r['file'], 'error': r.get('error', 'Unknown error')}
for r in results if r['status'] == 'error'
]
response = {
"ok": True,
"status": "success",
"formatted_count": len(files_formatted),
"already_formatted_count": len(files_already_formatted),
"needs_formatting_count": len(files_need_formatting),
"checked_count": len(files),
"error_count": len(files_with_errors),
"files_formatted": files_formatted,
"files_already_formatted": files_already_formatted,
"files_need_formatting": files_need_formatting,
"files_with_errors": files_with_errors
}
if check_only:
response["message"] = f"Checked {len(files)} files. {len(files_need_formatting)} need formatting."
else:
response["message"] = f"Formatted {len(files_formatted)} files. {len(files_already_formatted)} already formatted."
logger.info(f"Skill completed: {response['message']}")
return response
except Exception as e:
logger.error(f"Error executing skill: {e}")
return {
"ok": False,
"status": "failed",
"error": str(e)
}
def main():
"""CLI entry point"""
import argparse
parser = argparse.ArgumentParser(
description="Format code using Prettier, supporting multiple languages and file types."
)
parser.add_argument(
"--path",
required=True,
help="File or directory path to format"
)
parser.add_argument(
"--config-path",
help="Path to custom Prettier configuration file"
)
parser.add_argument(
"--check",
action="store_true",
help="Only check formatting without modifying files"
)
parser.add_argument(
"--patterns",
help="Comma-separated glob patterns to filter files (e.g., '**/*.js,**/*.ts')"
)
parser.add_argument(
"--no-write",
action="store_true",
help="Don't write changes (dry run)"
)
parser.add_argument(
"--output-format",
choices=["json", "yaml"],
default="json",
help="Output format"
)
args = parser.parse_args()
# Create skill instance
skill = CodeFormat()
# Execute skill
result = skill.execute(
path=args.path,
config_path=args.config_path,
check_only=args.check,
file_patterns=args.patterns,
write=not args.no_write
)
# Output result
if args.output_format == "json":
print(json.dumps(result, indent=2))
else:
print(yaml.dump(result, default_flow_style=False))
# Exit with appropriate code
sys.exit(0 if result.get("ok") else 1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,56 @@
name: code.format
version: 0.1.0
description: Format code using Prettier, supporting multiple languages and file types.
This skill can format individual files or entire directories, check formatting without
making changes, and respect custom Prettier configurations.
inputs:
- name: path
type: string
required: true
description: File or directory path to format
- name: config_path
type: string
required: false
description: Path to custom Prettier configuration file
- name: check_only
type: boolean
required: false
default: false
description: Only check formatting without modifying files
- name: file_patterns
type: string
required: false
description: Comma-separated glob patterns to filter files (e.g., "**/*.js,**/*.ts")
- name: write
type: boolean
required: false
default: true
description: Write formatted output to files (default true, use false for dry run)
outputs:
- name: formatting_report.json
type: application/json
description: JSON report with formatting results, files processed, and any errors
- name: formatted_files
type: text/plain
description: Updated files with proper formatting (when write=true)
status: active
permissions:
- filesystem:read
- filesystem:write
- process:execute
entrypoints:
- command: /code/format
handler: code_format.py
runtime: python
description: Format code using Prettier
artifact_metadata:
produces:
- type: formatting-report
format: application/json
description: Detailed report of formatting operation results

View File

@@ -0,0 +1,392 @@
#!/usr/bin/env python3
"""
Tests for code.format skill
Generated by meta.skill and enhanced with comprehensive test coverage
"""
import pytest
import sys
import os
import tempfile
import shutil
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
# Add parent directory to path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
from skills.code_format import code_format
class TestCodeFormat:
"""Tests for CodeFormat skill"""
def setup_method(self):
"""Setup test fixtures"""
self.skill = code_format.CodeFormat()
self.temp_dir = tempfile.mkdtemp()
def teardown_method(self):
"""Cleanup test fixtures"""
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
def test_initialization(self):
"""Test skill initializes correctly"""
assert self.skill is not None
assert self.skill.base_dir is not None
assert isinstance(self.skill.SUPPORTED_EXTENSIONS, set)
assert len(self.skill.SUPPORTED_EXTENSIONS) > 0
assert isinstance(self.skill.CONFIG_FILES, list)
assert len(self.skill.CONFIG_FILES) > 0
def test_supported_extensions(self):
"""Test that all expected file types are supported"""
extensions = self.skill.SUPPORTED_EXTENSIONS
# Check JavaScript extensions
assert '.js' in extensions
assert '.jsx' in extensions
assert '.mjs' in extensions
assert '.cjs' in extensions
# Check TypeScript extensions
assert '.ts' in extensions
assert '.tsx' in extensions
# Check style extensions
assert '.css' in extensions
assert '.scss' in extensions
assert '.less' in extensions
# Check other formats
assert '.json' in extensions
assert '.yaml' in extensions
assert '.md' in extensions
assert '.html' in extensions
def test_check_prettier_installed_with_npx(self):
"""Test Prettier detection via npx"""
with patch('subprocess.run') as mock_run:
mock_run.return_value = Mock(returncode=0, stdout="3.0.0\n")
is_installed, cmd = self.skill._check_prettier_installed()
assert is_installed is True
assert cmd == 'npx prettier'
def test_check_prettier_installed_global(self):
"""Test Prettier detection via global installation"""
with patch('subprocess.run') as mock_run:
# First call (npx) fails
mock_run.side_effect = [
FileNotFoundError(),
Mock(returncode=0, stdout="3.0.0\n")
]
with patch('shutil.which', return_value='/usr/local/bin/prettier'):
is_installed, cmd = self.skill._check_prettier_installed()
assert is_installed is True
assert cmd == 'prettier'
def test_check_prettier_not_installed(self):
"""Test when Prettier is not installed"""
with patch('subprocess.run', side_effect=FileNotFoundError()):
with patch('shutil.which', return_value=None):
is_installed, cmd = self.skill._check_prettier_installed()
assert is_installed is False
assert cmd is None
def test_find_config_file_custom(self):
"""Test finding custom config file"""
# Create a custom config file
config_path = Path(self.temp_dir) / '.prettierrc.custom'
config_path.write_text('{"semi": true}')
found_config = self.skill._find_config_file(
Path(self.temp_dir),
custom_config=str(config_path)
)
assert found_config == config_path
def test_find_config_file_auto_detect(self):
"""Test auto-detecting config file"""
# Create a .prettierrc file
config_path = Path(self.temp_dir) / '.prettierrc'
config_path.write_text('{"semi": true}')
found_config = self.skill._find_config_file(Path(self.temp_dir))
assert found_config == config_path
def test_find_config_file_none(self):
"""Test when no config file exists"""
found_config = self.skill._find_config_file(Path(self.temp_dir))
assert found_config is None
def test_discover_files_single_file(self):
"""Test discovering a single file"""
test_file = Path(self.temp_dir) / 'test.js'
test_file.write_text('console.log("test");')
files = self.skill._discover_files(test_file)
assert len(files) == 1
assert files[0] == test_file
def test_discover_files_directory(self):
"""Test discovering files in a directory"""
# Create test files
(Path(self.temp_dir) / 'test1.js').write_text('console.log("test1");')
(Path(self.temp_dir) / 'test2.ts').write_text('console.log("test2");')
(Path(self.temp_dir) / 'test.txt').write_text('not supported')
files = self.skill._discover_files(Path(self.temp_dir))
assert len(files) == 2
assert any(f.name == 'test1.js' for f in files)
assert any(f.name == 'test2.ts' for f in files)
def test_discover_files_with_patterns(self):
"""Test discovering files with glob patterns"""
# Create test files
(Path(self.temp_dir) / 'test1.js').write_text('console.log("test1");')
(Path(self.temp_dir) / 'test2.ts').write_text('console.log("test2");')
(Path(self.temp_dir) / 'test3.css').write_text('body { margin: 0; }')
files = self.skill._discover_files(Path(self.temp_dir), patterns=['*.js'])
assert len(files) == 1
assert files[0].name == 'test1.js'
def test_discover_files_ignores_node_modules(self):
"""Test that node_modules is ignored"""
# Create node_modules directory with files
node_modules = Path(self.temp_dir) / 'node_modules'
node_modules.mkdir()
(node_modules / 'test.js').write_text('console.log("test");')
# Create regular file
(Path(self.temp_dir) / 'app.js').write_text('console.log("app");')
files = self.skill._discover_files(Path(self.temp_dir))
assert len(files) == 1
assert files[0].name == 'app.js'
def test_format_file_success(self):
"""Test formatting a file successfully"""
test_file = Path(self.temp_dir) / 'test.js'
test_file.write_text('console.log("test");')
with patch('subprocess.run') as mock_run:
mock_run.return_value = Mock(returncode=0, stdout='', stderr='')
result = self.skill._format_file(test_file, 'prettier', check_only=False)
assert result['ok'] is True
assert result['status'] == 'formatted'
assert result['file'] == str(test_file)
def test_format_file_check_mode_needs_formatting(self):
"""Test check mode when file needs formatting"""
test_file = Path(self.temp_dir) / 'test.js'
test_file.write_text('console.log("test");')
with patch('subprocess.run') as mock_run:
mock_run.return_value = Mock(
returncode=1,
stdout='',
stderr='Code style issues found'
)
result = self.skill._format_file(test_file, 'prettier', check_only=True)
assert result['ok'] is True
assert result['status'] == 'needs_formatting'
def test_format_file_error(self):
"""Test formatting with error"""
test_file = Path(self.temp_dir) / 'test.js'
test_file.write_text('invalid syntax {{{')
with patch('subprocess.run') as mock_run:
mock_run.return_value = Mock(
returncode=1,
stdout='',
stderr='Syntax error'
)
result = self.skill._format_file(test_file, 'prettier', check_only=False)
assert result['ok'] is False
assert result['status'] == 'error'
assert 'error' in result
def test_execute_prettier_not_installed(self):
"""Test execute when Prettier is not installed"""
with patch.object(self.skill, '_check_prettier_installed', return_value=(False, None)):
result = self.skill.execute(path=self.temp_dir)
assert result['ok'] is False
assert result['status'] == 'failed'
assert 'not installed' in result['error'].lower()
def test_execute_invalid_path(self):
"""Test execute with invalid path"""
with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')):
result = self.skill.execute(path='/nonexistent/path')
assert result['ok'] is False
assert result['status'] == 'failed'
assert 'does not exist' in result['error'].lower()
def test_execute_no_files(self):
"""Test execute when no files are found"""
with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')):
with patch.object(self.skill, '_discover_files', return_value=[]):
result = self.skill.execute(path=self.temp_dir)
assert result['ok'] is True
assert result['status'] == 'success'
assert result['formatted_count'] == 0
assert 'No files found' in result['message']
def test_execute_successful_formatting(self):
"""Test successful formatting execution"""
# Create test files
test_file = Path(self.temp_dir) / 'test.js'
test_file.write_text('console.log("test");')
with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')):
with patch.object(self.skill, '_format_file') as mock_format:
mock_format.return_value = {
'ok': True,
'status': 'formatted',
'file': str(test_file)
}
result = self.skill.execute(path=str(test_file))
assert result['ok'] is True
assert result['status'] == 'success'
assert result['formatted_count'] == 1
assert result['error_count'] == 0
def test_execute_check_mode(self):
"""Test execute in check mode"""
test_file = Path(self.temp_dir) / 'test.js'
test_file.write_text('console.log("test");')
with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')):
with patch.object(self.skill, '_format_file') as mock_format:
mock_format.return_value = {
'ok': True,
'status': 'needs_formatting',
'file': str(test_file)
}
result = self.skill.execute(path=str(test_file), check_only=True)
assert result['ok'] is True
assert result['status'] == 'success'
assert result['needs_formatting_count'] == 1
assert 'need formatting' in result['message'].lower()
def test_execute_with_patterns(self):
"""Test execute with file patterns"""
# Create test files
(Path(self.temp_dir) / 'test.js').write_text('console.log("test");')
(Path(self.temp_dir) / 'test.ts').write_text('console.log("test");')
with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')):
with patch.object(self.skill, '_discover_files') as mock_discover:
mock_discover.return_value = [Path(self.temp_dir) / 'test.js']
result = self.skill.execute(
path=self.temp_dir,
file_patterns='*.js'
)
# Verify patterns were parsed correctly
mock_discover.assert_called_once()
call_args = mock_discover.call_args
assert call_args[0][1] == ['*.js']
def test_execute_with_errors(self):
"""Test execute when some files have errors"""
test_file = Path(self.temp_dir) / 'test.js'
test_file.write_text('invalid')
with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')):
with patch.object(self.skill, '_format_file') as mock_format:
mock_format.return_value = {
'ok': False,
'status': 'error',
'file': str(test_file),
'error': 'Syntax error'
}
result = self.skill.execute(path=str(test_file))
assert result['ok'] is True # Overall success even with file errors
assert result['status'] == 'success'
assert result['error_count'] == 1
assert len(result['files_with_errors']) == 1
def test_execute_exception_handling(self):
"""Test execute handles exceptions gracefully"""
with patch.object(self.skill, '_check_prettier_installed', side_effect=Exception('Test error')):
result = self.skill.execute(path=self.temp_dir)
assert result['ok'] is False
assert result['status'] == 'failed'
assert 'error' in result
def test_cli_help(capsys):
"""Test CLI help message"""
sys.argv = ["code_format.py", "--help"]
with pytest.raises(SystemExit) as exc_info:
code_format.main()
assert exc_info.value.code == 0
captured = capsys.readouterr()
assert "Format code using Prettier" in captured.out
def test_cli_missing_path(capsys):
"""Test CLI with missing required path argument"""
sys.argv = ["code_format.py"]
with pytest.raises(SystemExit) as exc_info:
code_format.main()
assert exc_info.value.code != 0
def test_cli_execution():
"""Test CLI execution with mocked skill"""
sys.argv = ["code_format.py", "--path", "/tmp", "--check"]
with patch.object(code_format.CodeFormat, 'execute') as mock_execute:
mock_execute.return_value = {
'ok': True,
'status': 'success',
'message': 'Test'
}
with pytest.raises(SystemExit) as exc_info:
code_format.main()
assert exc_info.value.code == 0
mock_execute.assert_called_once()
if __name__ == "__main__":
pytest.main([__file__, "-v"])