adding missing files docs
This commit is contained in:
602
docs/contributing/code-style.md
Normal file
602
docs/contributing/code-style.md
Normal file
@@ -0,0 +1,602 @@
|
||||
# Code Style Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This document establishes coding standards and style guidelines for the Gemini MCP Server project. Consistent code style improves readability, maintainability, and collaboration efficiency.
|
||||
|
||||
## Python Style Guidelines
|
||||
|
||||
### PEP 8 Compliance
|
||||
|
||||
**Base Standard**: Follow [PEP 8](https://peps.python.org/pep-0008/) as the foundation for all Python code.
|
||||
|
||||
**Automated Formatting**: Use Black formatter with default settings:
|
||||
```bash
|
||||
black tools/ utils/ tests/ --line-length 88
|
||||
```
|
||||
|
||||
**Line Length**: 88 characters (Black default)
|
||||
```python
|
||||
# Good
|
||||
result = some_function_with_long_name(
|
||||
parameter_one, parameter_two, parameter_three
|
||||
)
|
||||
|
||||
# Avoid
|
||||
result = some_function_with_long_name(parameter_one, parameter_two, parameter_three)
|
||||
```
|
||||
|
||||
### Import Organization
|
||||
|
||||
**Import Order** (enforced by isort):
|
||||
```python
|
||||
# 1. Standard library imports
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
# 2. Third-party imports
|
||||
import redis
|
||||
from pydantic import BaseModel
|
||||
|
||||
# 3. Local application imports
|
||||
from tools.base import BaseTool
|
||||
from utils.file_utils import validate_file_path
|
||||
```
|
||||
|
||||
**Import Formatting**:
|
||||
```python
|
||||
# Good - Explicit imports
|
||||
from typing import Dict, List, Optional
|
||||
from utils.conversation_memory import ThreadContext, ConversationMemory
|
||||
|
||||
# Avoid - Wildcard imports
|
||||
from utils.conversation_memory import *
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
**Functions and Variables**: snake_case
|
||||
```python
|
||||
def process_file_content(file_path: str) -> str:
|
||||
context_tokens = calculate_token_count(content)
|
||||
return formatted_content
|
||||
```
|
||||
|
||||
**Classes**: PascalCase
|
||||
```python
|
||||
class GeminiClient:
|
||||
pass
|
||||
|
||||
class ThreadContext:
|
||||
pass
|
||||
```
|
||||
|
||||
**Constants**: UPPER_SNAKE_CASE
|
||||
```python
|
||||
MAX_CONTEXT_TOKENS = 1000000
|
||||
THINKING_MODE_TOKENS = {
|
||||
'minimal': 128,
|
||||
'low': 2048,
|
||||
'medium': 8192
|
||||
}
|
||||
```
|
||||
|
||||
**Private Methods**: Leading underscore
|
||||
```python
|
||||
class ToolBase:
|
||||
def execute(self):
|
||||
return self._process_internal_logic()
|
||||
|
||||
def _process_internal_logic(self):
|
||||
# Private implementation
|
||||
pass
|
||||
```
|
||||
|
||||
## Type Hints
|
||||
|
||||
### Mandatory Type Hints
|
||||
|
||||
**Function Signatures**: Always include type hints
|
||||
```python
|
||||
# Good
|
||||
def validate_file_path(file_path: str) -> bool:
|
||||
return os.path.exists(file_path)
|
||||
|
||||
async def process_request(request: dict) -> ToolOutput:
|
||||
# Implementation
|
||||
pass
|
||||
|
||||
# Avoid
|
||||
def validate_file_path(file_path):
|
||||
return os.path.exists(file_path)
|
||||
```
|
||||
|
||||
**Complex Types**: Use typing module
|
||||
```python
|
||||
from typing import Dict, List, Optional, Union, Any
|
||||
|
||||
def process_files(files: List[str]) -> Dict[str, Any]:
|
||||
return {"processed": files}
|
||||
|
||||
def get_config(key: str) -> Optional[str]:
|
||||
return os.getenv(key)
|
||||
```
|
||||
|
||||
**Generic Types**: Use TypeVar for reusable generics
|
||||
```python
|
||||
from typing import TypeVar, Generic
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
class Repository(Generic[T]):
|
||||
def get(self, id: str) -> Optional[T]:
|
||||
# Implementation
|
||||
pass
|
||||
```
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
### Docstring Format
|
||||
|
||||
**Use Google Style** docstrings:
|
||||
```python
|
||||
def execute_tool(name: str, arguments: dict, context: Optional[str] = None) -> ToolOutput:
|
||||
"""Execute a tool with given arguments and context.
|
||||
|
||||
Args:
|
||||
name: The name of the tool to execute
|
||||
arguments: Tool-specific parameters and configuration
|
||||
context: Optional conversation context for threading
|
||||
|
||||
Returns:
|
||||
ToolOutput containing the execution result and metadata
|
||||
|
||||
Raises:
|
||||
ToolNotFoundError: If the specified tool doesn't exist
|
||||
ValidationError: If arguments don't match tool schema
|
||||
|
||||
Example:
|
||||
>>> output = execute_tool("chat", {"prompt": "Hello"})
|
||||
>>> print(output.content)
|
||||
"Hello! How can I help you today?"
|
||||
"""
|
||||
# Implementation
|
||||
pass
|
||||
```
|
||||
|
||||
**Class Documentation**:
|
||||
```python
|
||||
class ConversationMemory:
|
||||
"""Manages conversation threading and context persistence.
|
||||
|
||||
This class handles storing and retrieving conversation contexts
|
||||
using Redis as the backend storage. It supports thread-based
|
||||
organization and automatic cleanup of expired conversations.
|
||||
|
||||
Attributes:
|
||||
redis_client: Redis connection for storage operations
|
||||
default_ttl: Default time-to-live for conversation threads
|
||||
|
||||
Example:
|
||||
>>> memory = ConversationMemory("redis://localhost:6379")
|
||||
>>> context = ThreadContext("thread-123")
|
||||
>>> await memory.store_thread(context)
|
||||
"""
|
||||
|
||||
def __init__(self, redis_url: str, default_ttl: int = 86400):
|
||||
"""Initialize conversation memory with Redis connection.
|
||||
|
||||
Args:
|
||||
redis_url: Redis connection string
|
||||
default_ttl: Default TTL in seconds (default: 24 hours)
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
### Inline Comments
|
||||
|
||||
**When to Comment**:
|
||||
```python
|
||||
# Good - Explain complex business logic
|
||||
def calculate_token_budget(files: List[str], total_budget: int) -> Dict[str, int]:
|
||||
# Priority 1 files (source code) get 60% of budget
|
||||
priority_1_budget = int(total_budget * 0.6)
|
||||
|
||||
# Group files by priority based on extension
|
||||
priority_groups = defaultdict(list)
|
||||
for file in files:
|
||||
ext = Path(file).suffix.lower()
|
||||
priority = FILE_PRIORITIES.get(ext, 4)
|
||||
priority_groups[priority].append(file)
|
||||
|
||||
return allocate_budget_by_priority(priority_groups, total_budget)
|
||||
|
||||
# Avoid - Stating the obvious
|
||||
def get_file_size(file_path: str) -> int:
|
||||
# Get the size of the file
|
||||
return os.path.getsize(file_path)
|
||||
```
|
||||
|
||||
**Security and Performance Notes**:
|
||||
```python
|
||||
def validate_file_path(file_path: str) -> bool:
|
||||
# Security: Prevent directory traversal attacks
|
||||
if '..' in file_path or file_path.startswith('/etc/'):
|
||||
return False
|
||||
|
||||
# Performance: Early return for non-existent files
|
||||
if not os.path.exists(file_path):
|
||||
return False
|
||||
|
||||
return True
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Exception Handling Patterns
|
||||
|
||||
**Specific Exceptions**:
|
||||
```python
|
||||
# Good - Specific exception handling
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
content = f.read()
|
||||
except FileNotFoundError:
|
||||
logger.warning(f"File not found: {file_path}")
|
||||
return None
|
||||
except PermissionError:
|
||||
logger.error(f"Permission denied: {file_path}")
|
||||
raise SecurityError(f"Access denied to {file_path}")
|
||||
except UnicodeDecodeError:
|
||||
logger.warning(f"Encoding error in {file_path}")
|
||||
return f"Error: Cannot decode file {file_path}"
|
||||
|
||||
# Avoid - Bare except clauses
|
||||
try:
|
||||
content = f.read()
|
||||
except:
|
||||
return None
|
||||
```
|
||||
|
||||
**Custom Exceptions**:
|
||||
```python
|
||||
class GeminiMCPError(Exception):
|
||||
"""Base exception for Gemini MCP Server errors."""
|
||||
pass
|
||||
|
||||
class ToolNotFoundError(GeminiMCPError):
|
||||
"""Raised when a requested tool is not found."""
|
||||
pass
|
||||
|
||||
class ValidationError(GeminiMCPError):
|
||||
"""Raised when input validation fails."""
|
||||
pass
|
||||
```
|
||||
|
||||
### Logging Standards
|
||||
|
||||
**Logging Levels**:
|
||||
```python
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# DEBUG: Detailed diagnostic information
|
||||
logger.debug(f"Processing file: {file_path}, size: {file_size}")
|
||||
|
||||
# INFO: General operational information
|
||||
logger.info(f"Tool '{tool_name}' executed successfully")
|
||||
|
||||
# WARNING: Something unexpected but recoverable
|
||||
logger.warning(f"File {file_path} exceeds recommended size limit")
|
||||
|
||||
# ERROR: Error condition but application continues
|
||||
logger.error(f"Failed to process file {file_path}: {str(e)}")
|
||||
|
||||
# CRITICAL: Serious error, application may not continue
|
||||
logger.critical(f"Redis connection failed: {connection_error}")
|
||||
```
|
||||
|
||||
**Structured Logging**:
|
||||
```python
|
||||
# Good - Structured logging with context
|
||||
logger.info(
|
||||
"Tool execution completed",
|
||||
extra={
|
||||
"tool_name": tool_name,
|
||||
"execution_time": execution_time,
|
||||
"files_processed": len(files),
|
||||
"thinking_mode": thinking_mode
|
||||
}
|
||||
)
|
||||
|
||||
# Avoid - Unstructured string formatting
|
||||
logger.info(f"Tool {tool_name} took {execution_time}s to process {len(files)} files")
|
||||
```
|
||||
|
||||
## Async/Await Patterns
|
||||
|
||||
### Async Function Design
|
||||
|
||||
**Async When Needed**:
|
||||
```python
|
||||
# Good - I/O operations should be async
|
||||
async def fetch_gemini_response(prompt: str) -> str:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(GEMINI_API_URL, json=payload) as response:
|
||||
return await response.text()
|
||||
|
||||
# Good - CPU-bound work remains sync
|
||||
def parse_stack_trace(trace_text: str) -> List[StackFrame]:
|
||||
# CPU-intensive parsing logic
|
||||
return parsed_frames
|
||||
```
|
||||
|
||||
**Async Context Managers**:
|
||||
```python
|
||||
class AsyncRedisClient:
|
||||
async def __aenter__(self):
|
||||
self.connection = await redis.connect(self.url)
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.connection.close()
|
||||
|
||||
# Usage
|
||||
async with AsyncRedisClient(redis_url) as client:
|
||||
await client.store_data(key, value)
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### Input Validation
|
||||
|
||||
**Path Validation**:
|
||||
```python
|
||||
def validate_file_path(file_path: str) -> bool:
|
||||
"""Validate file path for security and accessibility."""
|
||||
# Convert to absolute path
|
||||
abs_path = os.path.abspath(file_path)
|
||||
|
||||
# Check for directory traversal
|
||||
if not abs_path.startswith(PROJECT_ROOT):
|
||||
raise SecurityError(f"Path outside project root: {abs_path}")
|
||||
|
||||
# Check for dangerous patterns
|
||||
dangerous_patterns = ['../', '~/', '/etc/', '/var/']
|
||||
if any(pattern in file_path for pattern in dangerous_patterns):
|
||||
raise SecurityError(f"Dangerous path pattern detected: {file_path}")
|
||||
|
||||
return True
|
||||
```
|
||||
|
||||
**Data Sanitization**:
|
||||
```python
|
||||
def sanitize_user_input(user_input: str) -> str:
|
||||
"""Sanitize user input to prevent injection attacks."""
|
||||
# Remove null bytes
|
||||
sanitized = user_input.replace('\x00', '')
|
||||
|
||||
# Limit length
|
||||
if len(sanitized) > MAX_INPUT_LENGTH:
|
||||
sanitized = sanitized[:MAX_INPUT_LENGTH]
|
||||
|
||||
# Remove control characters
|
||||
sanitized = ''.join(char for char in sanitized if ord(char) >= 32)
|
||||
|
||||
return sanitized
|
||||
```
|
||||
|
||||
### Secret Management
|
||||
|
||||
**Environment Variables**:
|
||||
```python
|
||||
# Good - Environment variable with validation
|
||||
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
|
||||
if not GEMINI_API_KEY:
|
||||
raise ConfigurationError("GEMINI_API_KEY environment variable required")
|
||||
|
||||
# Avoid - Hardcoded secrets
|
||||
API_KEY = "sk-1234567890abcdef" # Never do this
|
||||
```
|
||||
|
||||
**Secret Logging Prevention**:
|
||||
```python
|
||||
def log_request_safely(request_data: dict) -> None:
|
||||
"""Log request data while excluding sensitive fields."""
|
||||
safe_data = request_data.copy()
|
||||
|
||||
# Remove sensitive fields
|
||||
sensitive_fields = ['api_key', 'token', 'password', 'secret']
|
||||
for field in sensitive_fields:
|
||||
if field in safe_data:
|
||||
safe_data[field] = '[REDACTED]'
|
||||
|
||||
logger.info(f"Processing request: {safe_data}")
|
||||
```
|
||||
|
||||
## Performance Guidelines
|
||||
|
||||
### Memory Management
|
||||
|
||||
**Generator Usage**:
|
||||
```python
|
||||
# Good - Memory efficient for large datasets
|
||||
def process_large_file(file_path: str) -> Generator[str, None, None]:
|
||||
with open(file_path, 'r') as f:
|
||||
for line in f:
|
||||
yield process_line(line)
|
||||
|
||||
# Avoid - Loading entire file into memory
|
||||
def process_large_file(file_path: str) -> List[str]:
|
||||
with open(file_path, 'r') as f:
|
||||
return [process_line(line) for line in f.readlines()]
|
||||
```
|
||||
|
||||
**Context Managers**:
|
||||
```python
|
||||
# Good - Automatic resource cleanup
|
||||
class FileProcessor:
|
||||
def __enter__(self):
|
||||
self.temp_files = []
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
# Cleanup temporary files
|
||||
for temp_file in self.temp_files:
|
||||
os.unlink(temp_file)
|
||||
```
|
||||
|
||||
### Caching Patterns
|
||||
|
||||
**LRU Cache for Expensive Operations**:
|
||||
```python
|
||||
from functools import lru_cache
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def parse_file_content(file_path: str, file_hash: str) -> str:
|
||||
"""Parse file content with caching based on file hash."""
|
||||
with open(file_path, 'r') as f:
|
||||
return expensive_parsing_operation(f.read())
|
||||
```
|
||||
|
||||
## Testing Standards
|
||||
|
||||
### Test File Organization
|
||||
|
||||
**Test Structure**:
|
||||
```python
|
||||
# tests/test_tools.py
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from tools.chat import ChatTool
|
||||
from tools.models import ToolOutput
|
||||
|
||||
class TestChatTool:
|
||||
"""Test suite for ChatTool functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures before each test method."""
|
||||
self.chat_tool = ChatTool()
|
||||
self.mock_gemini_client = Mock()
|
||||
|
||||
def test_basic_chat_execution(self):
|
||||
"""Test basic chat tool execution with simple prompt."""
|
||||
# Arrange
|
||||
request = {"prompt": "Hello"}
|
||||
|
||||
# Act
|
||||
result = self.chat_tool.execute(request)
|
||||
|
||||
# Assert
|
||||
assert isinstance(result, ToolOutput)
|
||||
assert result.status == "success"
|
||||
|
||||
@patch('tools.chat.GeminiClient')
|
||||
def test_chat_with_mocked_api(self, mock_client):
|
||||
"""Test chat tool with mocked Gemini API responses."""
|
||||
# Test implementation
|
||||
pass
|
||||
```
|
||||
|
||||
### Test Naming Conventions
|
||||
|
||||
**Test Method Names**:
|
||||
```python
|
||||
def test_should_validate_file_path_when_path_is_safe():
|
||||
"""Test that safe file paths are correctly validated."""
|
||||
pass
|
||||
|
||||
def test_should_raise_security_error_when_path_contains_traversal():
|
||||
"""Test that directory traversal attempts raise SecurityError."""
|
||||
pass
|
||||
|
||||
def test_should_return_none_when_file_not_found():
|
||||
"""Test that missing files return None gracefully."""
|
||||
pass
|
||||
```
|
||||
|
||||
## Configuration Management
|
||||
|
||||
### Environment-Based Configuration
|
||||
|
||||
**Configuration Class**:
|
||||
```python
|
||||
class Config:
|
||||
"""Application configuration with validation."""
|
||||
|
||||
def __init__(self):
|
||||
self.gemini_api_key = self._require_env('GEMINI_API_KEY')
|
||||
self.redis_url = os.getenv('REDIS_URL', 'redis://localhost:6379')
|
||||
self.project_root = os.getenv('PROJECT_ROOT', '/workspace')
|
||||
self.max_context_tokens = int(os.getenv('MAX_CONTEXT_TOKENS', '1000000'))
|
||||
|
||||
# Validate configuration
|
||||
self._validate_configuration()
|
||||
|
||||
def _require_env(self, key: str) -> str:
|
||||
"""Require environment variable or raise error."""
|
||||
value = os.getenv(key)
|
||||
if not value:
|
||||
raise ConfigurationError(f"Required environment variable: {key}")
|
||||
return value
|
||||
|
||||
def _validate_configuration(self) -> None:
|
||||
"""Validate configuration values."""
|
||||
if not os.path.exists(self.project_root):
|
||||
raise ConfigurationError(f"PROJECT_ROOT not found: {self.project_root}")
|
||||
```
|
||||
|
||||
## Pre-commit Hooks
|
||||
|
||||
### Automated Quality Checks
|
||||
|
||||
**Required Tools**:
|
||||
```bash
|
||||
# Install development dependencies
|
||||
pip install black isort flake8 mypy pytest
|
||||
|
||||
# Format code
|
||||
black tools/ utils/ tests/
|
||||
isort tools/ utils/ tests/
|
||||
|
||||
# Check code quality
|
||||
flake8 tools/ utils/ tests/
|
||||
mypy tools/ utils/
|
||||
|
||||
# Run tests
|
||||
pytest tests/ -v --cov=tools --cov=utils
|
||||
```
|
||||
|
||||
**Pre-commit Configuration** (`.pre-commit-config.yaml`):
|
||||
```yaml
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.9
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.3.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Following these code style guidelines ensures consistent, maintainable, and secure code across the Gemini MCP Server project. All team members should adhere to these standards and use the automated tools to enforce compliance.
|
||||
382
docs/contributing/file-overview.md
Normal file
382
docs/contributing/file-overview.md
Normal file
@@ -0,0 +1,382 @@
|
||||
# Repository File Overview
|
||||
|
||||
## Purpose
|
||||
|
||||
This document provides a comprehensive guide to the repository structure, explaining the purpose and role of each directory and key file within the Gemini MCP Server project.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
gemini-mcp-server/
|
||||
├── CLAUDE.md # Collaboration framework and development guidelines
|
||||
├── README.md # Project overview and quick start guide
|
||||
├── LICENSE # Project license (MIT)
|
||||
├── requirements.txt # Python dependencies
|
||||
├── pyproject.toml # Poetry configuration and project metadata
|
||||
├── pytest.ini # Test configuration
|
||||
├── Dockerfile # Container image definition
|
||||
├── docker-compose.yml # Multi-service Docker orchestration
|
||||
├── setup.py # Python package setup (legacy)
|
||||
├── config.py # Centralized configuration management
|
||||
├── server.py # Main MCP server entry point
|
||||
├── gemini_server.py # Gemini-specific server implementation
|
||||
├── log_monitor.py # Logging and monitoring utilities
|
||||
├── setup-docker.sh # Docker setup automation script
|
||||
├── claude_config_example.json # Example Claude Desktop configuration
|
||||
├── examples/ # Configuration examples for different platforms
|
||||
├── docs/ # Complete project documentation
|
||||
├── tools/ # MCP tool implementations
|
||||
├── utils/ # Shared utility modules
|
||||
├── prompts/ # System prompts for different tool types
|
||||
├── tests/ # Comprehensive test suite
|
||||
└── memory-bank/ # Memory Bank files for context preservation
|
||||
```
|
||||
|
||||
## Core Configuration Files
|
||||
|
||||
### CLAUDE.md
|
||||
**Purpose**: Defines the collaboration framework between Claude, Gemini, and human developers
|
||||
**Key Components**:
|
||||
- Tool selection matrix for appropriate AI collaboration
|
||||
- Memory Bank integration protocols
|
||||
- Mandatory collaboration patterns and workflows
|
||||
- Quality gates and documentation standards
|
||||
|
||||
**When to Update**: When changing collaboration patterns, adding new tools, or modifying development workflows
|
||||
|
||||
### config.py
|
||||
**Purpose**: Centralized configuration management for the MCP server
|
||||
**Key Components**:
|
||||
- Environment variable handling (`GEMINI_API_KEY`, `REDIS_URL`)
|
||||
- Model configuration (`GEMINI_MODEL`, `MAX_CONTEXT_TOKENS`)
|
||||
- Security settings (`PROJECT_ROOT`, path validation)
|
||||
- Redis connection settings for conversation memory
|
||||
|
||||
**Dependencies**: Environment variables, Docker configuration
|
||||
**Extension Points**: Add new configuration parameters for tools or features
|
||||
|
||||
### server.py
|
||||
**Purpose**: Main MCP server implementation providing the protocol interface
|
||||
**Key Components**:
|
||||
- MCP protocol compliance (`@server.list_tools()`, `@server.call_tool()`)
|
||||
- Tool registration and discovery system
|
||||
- Request routing and response formatting
|
||||
- Error handling and graceful degradation
|
||||
|
||||
**Dependencies**: `tools/` modules, `utils/` modules, MCP library
|
||||
**Data Flow**: Claude → MCP Protocol → Tool Selection → Gemini API → Response
|
||||
|
||||
## Tool Architecture
|
||||
|
||||
### tools/ Directory
|
||||
**Purpose**: Contains individual MCP tool implementations following plugin architecture
|
||||
|
||||
#### tools/base.py
|
||||
**Purpose**: Abstract base class defining the tool interface contract
|
||||
**Key Components**:
|
||||
- `BaseTool` abstract class with `execute()` and `get_schema()` methods
|
||||
- Standardized error handling patterns
|
||||
- Response formatting utilities (`ToolOutput` dataclass)
|
||||
|
||||
**Extension Points**: Inherit from `BaseTool` to create new tools
|
||||
|
||||
#### Individual Tool Files
|
||||
|
||||
**tools/chat.py**
|
||||
- **Purpose**: Quick questions, brainstorming, general collaboration
|
||||
- **Thinking Mode**: Default 'medium' (8192 tokens)
|
||||
- **Use Cases**: Immediate answers, idea exploration, simple code discussions
|
||||
|
||||
**tools/thinkdeep.py**
|
||||
- **Purpose**: Complex architecture, system design, strategic planning
|
||||
- **Thinking Mode**: Default 'high' (16384 tokens)
|
||||
- **Use Cases**: Major features, refactoring strategies, design decisions
|
||||
|
||||
**tools/analyze.py**
|
||||
- **Purpose**: Code exploration, understanding existing systems
|
||||
- **Thinking Mode**: Variable based on analysis scope
|
||||
- **Use Cases**: Dependency analysis, pattern detection, codebase comprehension
|
||||
|
||||
**tools/codereview.py**
|
||||
- **Purpose**: Code quality, security, bug detection
|
||||
- **Thinking Mode**: Default 'medium' (8192 tokens)
|
||||
- **Use Cases**: PR reviews, pre-commit validation, security audits
|
||||
|
||||
**tools/debug.py**
|
||||
- **Purpose**: Root cause analysis, error investigation
|
||||
- **Thinking Mode**: Default 'medium' (8192 tokens)
|
||||
- **Use Cases**: Stack trace analysis, performance issues, bug diagnosis
|
||||
|
||||
**tools/precommit.py**
|
||||
- **Purpose**: Automated quality gates before commits
|
||||
- **Thinking Mode**: Default 'medium' (8192 tokens)
|
||||
- **Use Cases**: Git repository validation, change analysis, quality assurance
|
||||
|
||||
#### tools/models.py
|
||||
**Purpose**: Shared data models and Gemini API integration
|
||||
**Key Components**:
|
||||
- `ToolOutput` dataclass for standardized responses
|
||||
- `GeminiClient` for API communication
|
||||
- Thinking mode token allocations (`THINKING_MODE_TOKENS`)
|
||||
- Pydantic models for request/response validation
|
||||
|
||||
**Dependencies**: `google-generativeai`, `pydantic`
|
||||
|
||||
## Utility Modules
|
||||
|
||||
### utils/ Directory
|
||||
**Purpose**: Shared utilities used across multiple tools and components
|
||||
|
||||
#### utils/file_utils.py
|
||||
**Purpose**: Secure file operations and content processing
|
||||
**Key Components**:
|
||||
- `validate_file_path()`: Multi-layer security validation
|
||||
- `read_file_with_token_limit()`: Token-aware file reading
|
||||
- `translate_docker_path()`: Host-to-container path mapping
|
||||
- Priority-based file processing (source code > docs > logs)
|
||||
|
||||
**Security Features**:
|
||||
- Directory traversal prevention
|
||||
- Sandbox boundary enforcement (PROJECT_ROOT)
|
||||
- Dangerous path pattern detection
|
||||
|
||||
**Data Flow**: File Request → Security Validation → Path Translation → Content Processing → Formatted Output
|
||||
|
||||
#### utils/git_utils.py
|
||||
**Purpose**: Git repository operations for code analysis
|
||||
**Key Components**:
|
||||
- Repository state detection (staged, unstaged, committed changes)
|
||||
- Branch comparison and diff analysis
|
||||
- Commit history processing
|
||||
- Change validation for precommit tool
|
||||
|
||||
**Dependencies**: `git` command-line tool
|
||||
**Integration**: Primary used by `precommit` tool for change analysis
|
||||
|
||||
#### utils/conversation_memory.py
|
||||
**Purpose**: Cross-session context preservation and threading
|
||||
**Key Components**:
|
||||
- `ThreadContext` dataclass for conversation state
|
||||
- `ConversationMemory` class for Redis-based persistence
|
||||
- Thread reconstruction and continuation support
|
||||
- Automatic cleanup of expired conversations
|
||||
|
||||
**Data Flow**: Tool Execution → Context Storage → Redis Persistence → Context Retrieval → Thread Reconstruction
|
||||
|
||||
**Dependencies**: Redis server, `redis-py` library
|
||||
|
||||
#### utils/token_utils.py
|
||||
**Purpose**: Token management and context optimization
|
||||
**Key Components**:
|
||||
- Token counting and estimation
|
||||
- Context budget allocation
|
||||
- Content truncation with structure preservation
|
||||
- Priority-based token distribution
|
||||
|
||||
**Integration**: Used by all tools for managing Gemini API token limits
|
||||
|
||||
## System Prompts
|
||||
|
||||
### prompts/ Directory
|
||||
**Purpose**: Standardized system prompts for different tool types
|
||||
|
||||
#### prompts/tool_prompts.py
|
||||
**Purpose**: Template prompts for consistent tool behavior
|
||||
**Key Components**:
|
||||
- Base prompt templates for each tool type
|
||||
- Context formatting patterns
|
||||
- Error message templates
|
||||
- Response structure guidelines
|
||||
|
||||
**Extension Points**: Add new prompt templates for new tools or specialized use cases
|
||||
|
||||
## Testing Infrastructure
|
||||
|
||||
### tests/ Directory
|
||||
**Purpose**: Comprehensive test suite ensuring code quality and reliability
|
||||
|
||||
#### Test Organization
|
||||
```
|
||||
tests/
|
||||
├── __init__.py # Test package initialization
|
||||
├── conftest.py # Shared test fixtures and configuration
|
||||
├── test_server.py # MCP server integration tests
|
||||
├── test_tools.py # Individual tool functionality tests
|
||||
├── test_utils.py # Utility module tests
|
||||
├── test_config.py # Configuration validation tests
|
||||
└── specialized test files... # Feature-specific test suites
|
||||
```
|
||||
|
||||
#### Key Test Files
|
||||
|
||||
**conftest.py**
|
||||
- **Purpose**: Shared pytest fixtures and test configuration
|
||||
- **Components**: Mock clients, temporary directories, sample data
|
||||
|
||||
**test_server.py**
|
||||
- **Purpose**: MCP protocol and server integration testing
|
||||
- **Coverage**: Tool registration, request routing, error handling
|
||||
|
||||
**test_tools.py**
|
||||
- **Purpose**: Individual tool functionality validation
|
||||
- **Coverage**: Tool execution, parameter validation, response formatting
|
||||
|
||||
**test_utils.py**
|
||||
- **Purpose**: Utility module testing
|
||||
- **Coverage**: File operations, security validation, token management
|
||||
|
||||
## Memory Bank System
|
||||
|
||||
### memory-bank/ Directory
|
||||
**Purpose**: Local file-based context preservation system
|
||||
|
||||
#### Memory Bank Files
|
||||
|
||||
**productContext.md**
|
||||
- **Purpose**: High-level project overview and goals
|
||||
- **Content**: Project description, key features, overall architecture
|
||||
- **Update Triggers**: Fundamental project changes, feature additions
|
||||
|
||||
**activeContext.md**
|
||||
- **Purpose**: Current development status and recent changes
|
||||
- **Content**: Current focus, recent changes, open questions/issues
|
||||
- **Update Triggers**: Session changes, progress updates
|
||||
|
||||
**progress.md**
|
||||
- **Purpose**: Task tracking using structured format
|
||||
- **Content**: Completed tasks, current tasks, next steps
|
||||
- **Update Triggers**: Task completion, milestone achievements
|
||||
|
||||
**decisionLog.md**
|
||||
- **Purpose**: Architectural decisions with rationale
|
||||
- **Content**: Technical decisions, rationale, implementation details
|
||||
- **Update Triggers**: Significant architectural choices, design decisions
|
||||
|
||||
**systemPatterns.md**
|
||||
- **Purpose**: Recurring patterns and standards documentation
|
||||
- **Content**: Coding patterns, architectural patterns, testing patterns
|
||||
- **Update Triggers**: Pattern introduction, standard modifications
|
||||
|
||||
**Data Flow**: Development Activity → Memory Bank Updates → Context Preservation → Cross-Session Continuity
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
### docs/ Directory
|
||||
**Purpose**: Complete project documentation following CLAUDE.md standards
|
||||
|
||||
#### Documentation Categories
|
||||
|
||||
**docs/architecture/**
|
||||
- `overview.md`: High-level system architecture and component relationships
|
||||
- `components.md`: Detailed component descriptions and interactions
|
||||
- `data-flow.md`: Data flow patterns and processing pipelines
|
||||
- `decisions/`: Architecture Decision Records (ADRs)
|
||||
|
||||
**docs/api/**
|
||||
- `mcp-protocol.md`: MCP protocol implementation details
|
||||
- `tools/`: Individual tool API documentation
|
||||
|
||||
**docs/contributing/**
|
||||
- `setup.md`: Development environment setup
|
||||
- `workflows.md`: Development workflows and processes
|
||||
- `code-style.md`: Coding standards and style guide
|
||||
- `testing.md`: Testing strategies and requirements
|
||||
- `file-overview.md`: This file - repository structure guide
|
||||
|
||||
**docs/user-guides/**
|
||||
- `installation.md`: Installation and setup instructions
|
||||
- `configuration.md`: Configuration options and examples
|
||||
- `troubleshooting.md`: Common issues and solutions
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### examples/ Directory
|
||||
**Purpose**: Platform-specific configuration examples for different deployment scenarios
|
||||
|
||||
**claude_config_macos.json**
|
||||
- macOS-specific Claude Desktop configuration
|
||||
- Local development setup patterns
|
||||
- File path configurations for macOS
|
||||
|
||||
**claude_config_wsl.json**
|
||||
- Windows Subsystem for Linux configuration
|
||||
- Path translation patterns for WSL environment
|
||||
- Docker integration considerations
|
||||
|
||||
**claude_config_docker_home.json**
|
||||
- Docker-based deployment configuration
|
||||
- Container path mapping examples
|
||||
- Volume mount configurations
|
||||
|
||||
## Container Configuration
|
||||
|
||||
### Dockerfile
|
||||
**Purpose**: Container image definition for consistent deployment
|
||||
**Key Components**:
|
||||
- Python 3.9 base image
|
||||
- Dependency installation (requirements.txt)
|
||||
- Application code copying
|
||||
- Entry point configuration (`server.py`)
|
||||
|
||||
**Build Process**: Source Code → Dependency Installation → Application Setup → Runnable Container
|
||||
|
||||
### docker-compose.yml
|
||||
**Purpose**: Multi-service orchestration for complete system deployment
|
||||
**Services**:
|
||||
- `gemini-server`: Main MCP server application
|
||||
- `redis`: Conversation memory persistence
|
||||
- Volume mounts for configuration and data persistence
|
||||
|
||||
**Data Flow**: Docker Compose → Service Orchestration → Network Configuration → Volume Mounting → System Startup
|
||||
|
||||
## Extension Guidelines
|
||||
|
||||
### Adding New Tools
|
||||
|
||||
1. **Create Tool Class**: Inherit from `BaseTool` in `tools/new_tool.py`
|
||||
2. **Implement Interface**: Define `execute()` and `get_schema()` methods
|
||||
3. **Add Registration**: Update `server.py` tool discovery
|
||||
4. **Create Tests**: Add comprehensive tests in `tests/`
|
||||
5. **Update Documentation**: Add API documentation in `docs/api/tools/`
|
||||
|
||||
### Adding New Utilities
|
||||
|
||||
1. **Create Module**: Add new utility in `utils/new_utility.py`
|
||||
2. **Define Interface**: Clear function signatures with type hints
|
||||
3. **Add Security**: Validate inputs and handle errors gracefully
|
||||
4. **Write Tests**: Comprehensive unit tests with mocking
|
||||
5. **Update Dependencies**: Document component interactions
|
||||
|
||||
### Modifying Configuration
|
||||
|
||||
1. **Update config.py**: Add new configuration parameters
|
||||
2. **Environment Variables**: Define environment variable mappings
|
||||
3. **Validation**: Add configuration validation logic
|
||||
4. **Documentation**: Update configuration guide
|
||||
5. **Examples**: Provide example configurations
|
||||
|
||||
## Dependencies & Integration Points
|
||||
|
||||
### External Dependencies
|
||||
- **MCP Library**: Protocol implementation and compliance
|
||||
- **Google Generative AI**: Gemini API integration
|
||||
- **Redis**: Conversation memory persistence
|
||||
- **Docker**: Containerization and deployment
|
||||
- **pytest**: Testing framework
|
||||
|
||||
### Internal Integration Points
|
||||
- **Tool Registration**: `server.py` ↔ `tools/` modules
|
||||
- **Configuration**: `config.py` → All modules
|
||||
- **File Operations**: `utils/file_utils.py` → All file-accessing tools
|
||||
- **Memory Management**: `utils/conversation_memory.py` → All tools supporting continuation
|
||||
- **Security**: `utils/file_utils.py` validation → All file operations
|
||||
|
||||
### Data Flow Integration
|
||||
1. **Request Flow**: Claude → `server.py` → Tool Selection → `tools/` → `utils/` → Gemini API
|
||||
2. **Response Flow**: Gemini API → `tools/` → `utils/` → `server.py` → Claude
|
||||
3. **Memory Flow**: Tool Execution → `utils/conversation_memory.py` → Redis → Context Retrieval
|
||||
4. **Security Flow**: File Request → `utils/file_utils.py` → Validation → Safe Processing
|
||||
|
||||
---
|
||||
|
||||
This file overview provides the foundation for understanding the repository structure and serves as a guide for contributors to navigate the codebase effectively and make informed architectural decisions.
|
||||
684
docs/contributing/testing.md
Normal file
684
docs/contributing/testing.md
Normal file
@@ -0,0 +1,684 @@
|
||||
# Testing Strategy & Guidelines
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the comprehensive testing strategy for the Gemini MCP Server project, including unit testing, integration testing, and quality assurance practices that align with CLAUDE.md collaboration patterns.
|
||||
|
||||
## Testing Philosophy
|
||||
|
||||
### Test-Driven Development (TDD)
|
||||
|
||||
**TDD Cycle**:
|
||||
1. **Red**: Write failing test for new functionality
|
||||
2. **Green**: Implement minimal code to pass the test
|
||||
3. **Refactor**: Improve code while maintaining test coverage
|
||||
4. **Repeat**: Continue cycle for all new features
|
||||
|
||||
**Example TDD Flow**:
|
||||
```python
|
||||
# 1. Write failing test
|
||||
def test_chat_tool_should_process_simple_prompt():
|
||||
tool = ChatTool()
|
||||
result = tool.execute({"prompt": "Hello"})
|
||||
assert result.status == "success"
|
||||
assert "hello" in result.content.lower()
|
||||
|
||||
# 2. Implement minimal functionality
|
||||
class ChatTool:
|
||||
def execute(self, request):
|
||||
return ToolOutput(content="Hello!", status="success")
|
||||
|
||||
# 3. Refactor and enhance
|
||||
```
|
||||
|
||||
### Testing Pyramid
|
||||
|
||||
```
|
||||
/\
|
||||
/ \ E2E Tests (Few, High-Value)
|
||||
/____\ Integration Tests (Some, Key Paths)
|
||||
/______\ Unit Tests (Many, Fast, Isolated)
|
||||
/________\
|
||||
```
|
||||
|
||||
**Distribution**:
|
||||
- **70% Unit Tests**: Fast, isolated, comprehensive coverage
|
||||
- **20% Integration Tests**: Component interaction validation
|
||||
- **10% End-to-End Tests**: Complete workflow validation
|
||||
|
||||
## Test Categories
|
||||
|
||||
### 1. Unit Tests
|
||||
|
||||
**Purpose**: Test individual functions and classes in isolation
|
||||
|
||||
**Location**: `tests/unit/`
|
||||
|
||||
**Example Structure**:
|
||||
```python
|
||||
# tests/unit/test_file_utils.py
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, mock_open
|
||||
|
||||
from utils.file_utils import validate_file_path, read_file_with_token_limit
|
||||
|
||||
class TestFileUtils:
|
||||
"""Unit tests for file utility functions."""
|
||||
|
||||
def test_validate_file_path_with_safe_path(self):
|
||||
"""Test that safe file paths pass validation."""
|
||||
safe_path = "/workspace/tools/chat.py"
|
||||
assert validate_file_path(safe_path) is True
|
||||
|
||||
def test_validate_file_path_with_traversal_attack(self):
|
||||
"""Test that directory traversal attempts are blocked."""
|
||||
dangerous_path = "/workspace/../../../etc/passwd"
|
||||
with pytest.raises(SecurityError):
|
||||
validate_file_path(dangerous_path)
|
||||
|
||||
@patch('builtins.open', new_callable=mock_open, read_data="test content")
|
||||
def test_read_file_with_token_limit(self, mock_file):
|
||||
"""Test file reading with token budget enforcement."""
|
||||
content = read_file_with_token_limit("/test/file.py", max_tokens=100)
|
||||
assert "test content" in content
|
||||
mock_file.assert_called_once_with("/test/file.py", 'r', encoding='utf-8')
|
||||
```
|
||||
|
||||
**Unit Test Guidelines**:
|
||||
- **Isolation**: Mock external dependencies (file system, network, database)
|
||||
- **Fast Execution**: Each test should complete in milliseconds
|
||||
- **Single Responsibility**: One test per behavior/scenario
|
||||
- **Descriptive Names**: Test names should describe the scenario and expected outcome
|
||||
|
||||
### 2. Integration Tests
|
||||
|
||||
**Purpose**: Test component interactions and system integration
|
||||
|
||||
**Location**: `tests/integration/`
|
||||
|
||||
**Example Structure**:
|
||||
```python
|
||||
# tests/integration/test_tool_execution.py
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import patch
|
||||
|
||||
from server import call_tool
|
||||
from tools.chat import ChatTool
|
||||
from utils.conversation_memory import ConversationMemory
|
||||
|
||||
class TestToolExecution:
|
||||
"""Integration tests for tool execution pipeline."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_redis(self):
|
||||
"""Mock Redis for conversation memory testing."""
|
||||
with patch('redis.Redis') as mock:
|
||||
yield mock
|
||||
|
||||
@pytest.fixture
|
||||
def conversation_memory(self, mock_redis):
|
||||
"""Create conversation memory with mocked Redis."""
|
||||
return ConversationMemory("redis://mock")
|
||||
|
||||
async def test_chat_tool_execution_with_memory(self, conversation_memory):
|
||||
"""Test chat tool execution with conversation memory integration."""
|
||||
# Arrange
|
||||
request = {
|
||||
"name": "chat",
|
||||
"arguments": {
|
||||
"prompt": "Hello",
|
||||
"continuation_id": "test-thread-123"
|
||||
}
|
||||
}
|
||||
|
||||
# Act
|
||||
result = await call_tool(request["name"], request["arguments"])
|
||||
|
||||
# Assert
|
||||
assert len(result) == 1
|
||||
assert result[0].type == "text"
|
||||
assert "hello" in result[0].text.lower()
|
||||
|
||||
async def test_tool_execution_error_handling(self):
|
||||
"""Test error handling in tool execution pipeline."""
|
||||
# Test with invalid tool name
|
||||
with pytest.raises(ToolNotFoundError):
|
||||
await call_tool("nonexistent_tool", {})
|
||||
```
|
||||
|
||||
**Integration Test Guidelines**:
|
||||
- **Real Component Interaction**: Test actual component communication
|
||||
- **Mock External Services**: Mock external APIs (Gemini, Redis) for reliability
|
||||
- **Error Scenarios**: Test error propagation and handling
|
||||
- **Async Testing**: Use pytest-asyncio for async code testing
|
||||
|
||||
### 3. Live Integration Tests
|
||||
|
||||
**Purpose**: Test real API integration with external services
|
||||
|
||||
**Location**: `tests/live/`
|
||||
|
||||
**Requirements**:
|
||||
- Valid `GEMINI_API_KEY` environment variable
|
||||
- Redis server running (for conversation memory tests)
|
||||
- Network connectivity
|
||||
|
||||
**Example Structure**:
|
||||
```python
|
||||
# tests/live/test_gemini_integration.py
|
||||
import pytest
|
||||
import os
|
||||
|
||||
from tools.chat import ChatTool
|
||||
from tools.models import GeminiClient
|
||||
|
||||
@pytest.mark.live
|
||||
@pytest.mark.skipif(not os.getenv("GEMINI_API_KEY"), reason="API key required")
|
||||
class TestGeminiIntegration:
|
||||
"""Live tests requiring actual Gemini API access."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up for live testing."""
|
||||
self.api_key = os.getenv("GEMINI_API_KEY")
|
||||
self.client = GeminiClient(self.api_key)
|
||||
|
||||
async def test_basic_gemini_request(self):
|
||||
"""Test basic Gemini API request/response."""
|
||||
response = await self.client.generate_response(
|
||||
prompt="Say 'test successful'",
|
||||
thinking_mode="minimal"
|
||||
)
|
||||
assert "test successful" in response.lower()
|
||||
|
||||
async def test_chat_tool_with_real_api(self):
|
||||
"""Test ChatTool with real Gemini API integration."""
|
||||
tool = ChatTool()
|
||||
result = await tool.execute({
|
||||
"prompt": "What is 2+2?",
|
||||
"thinking_mode": "minimal"
|
||||
})
|
||||
|
||||
assert result.status == "success"
|
||||
assert "4" in result.content
|
||||
```
|
||||
|
||||
**Live Test Guidelines**:
|
||||
- **Skip When Unavailable**: Skip if API keys or services unavailable
|
||||
- **Rate Limiting**: Respect API rate limits with delays
|
||||
- **Minimal Mode**: Use minimal thinking mode for speed
|
||||
- **Cleanup**: Clean up any created resources
|
||||
|
||||
### 4. Security Tests
|
||||
|
||||
**Purpose**: Validate security measures and vulnerability prevention
|
||||
|
||||
**Location**: `tests/security/`
|
||||
|
||||
**Example Structure**:
|
||||
```python
|
||||
# tests/security/test_path_validation.py
|
||||
import pytest
|
||||
|
||||
from utils.file_utils import validate_file_path
|
||||
from exceptions import SecurityError
|
||||
|
||||
class TestSecurityValidation:
|
||||
"""Security-focused tests for input validation."""
|
||||
|
||||
@pytest.mark.parametrize("dangerous_path", [
|
||||
"../../../etc/passwd",
|
||||
"/etc/shadow",
|
||||
"~/../../root/.ssh/id_rsa",
|
||||
"/var/log/auth.log",
|
||||
"\\..\\..\\windows\\system32\\config\\sam"
|
||||
])
|
||||
def test_dangerous_path_rejection(self, dangerous_path):
|
||||
"""Test that dangerous file paths are rejected."""
|
||||
with pytest.raises(SecurityError):
|
||||
validate_file_path(dangerous_path)
|
||||
|
||||
def test_secret_sanitization_in_logs(self):
|
||||
"""Test that sensitive data is sanitized in log output."""
|
||||
request_data = {
|
||||
"prompt": "Hello",
|
||||
"api_key": "sk-secret123",
|
||||
"token": "bearer-token-456"
|
||||
}
|
||||
|
||||
sanitized = sanitize_for_logging(request_data)
|
||||
|
||||
assert sanitized["api_key"] == "[REDACTED]"
|
||||
assert sanitized["token"] == "[REDACTED]"
|
||||
assert sanitized["prompt"] == "Hello" # Non-sensitive data preserved
|
||||
```
|
||||
|
||||
## Test Configuration
|
||||
|
||||
### pytest Configuration
|
||||
|
||||
**pytest.ini**:
|
||||
```ini
|
||||
[tool:pytest]
|
||||
testpaths = tests
|
||||
python_files = test_*.py *_test.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
addopts =
|
||||
-v
|
||||
--strict-markers
|
||||
--disable-warnings
|
||||
--cov=tools
|
||||
--cov=utils
|
||||
--cov-report=html
|
||||
--cov-report=term-missing
|
||||
--cov-fail-under=80
|
||||
|
||||
markers =
|
||||
unit: Unit tests (fast, isolated)
|
||||
integration: Integration tests (component interaction)
|
||||
live: Live tests requiring API keys and external services
|
||||
security: Security-focused tests
|
||||
slow: Tests that take more than 1 second
|
||||
```
|
||||
|
||||
**conftest.py**:
|
||||
```python
|
||||
# tests/conftest.py
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
"""Create an instance of the default event loop for the test session."""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_gemini_client():
|
||||
"""Mock Gemini client for testing without API calls."""
|
||||
with patch('tools.models.GeminiClient') as mock:
|
||||
mock_instance = Mock()
|
||||
mock_instance.generate_response.return_value = "Mocked response"
|
||||
mock.return_value = mock_instance
|
||||
yield mock_instance
|
||||
|
||||
@pytest.fixture
|
||||
def mock_redis():
|
||||
"""Mock Redis client for testing without Redis server."""
|
||||
with patch('redis.Redis') as mock:
|
||||
yield mock
|
||||
|
||||
@pytest.fixture
|
||||
def sample_file_content():
|
||||
"""Sample file content for testing file processing."""
|
||||
return """
|
||||
def example_function():
|
||||
# This is a sample function
|
||||
return "hello world"
|
||||
|
||||
class ExampleClass:
|
||||
def method(self):
|
||||
pass
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_project_directory(tmp_path):
|
||||
"""Create temporary project directory structure for testing."""
|
||||
project_dir = tmp_path / "test_project"
|
||||
project_dir.mkdir()
|
||||
|
||||
# Create subdirectories
|
||||
(project_dir / "tools").mkdir()
|
||||
(project_dir / "utils").mkdir()
|
||||
(project_dir / "tests").mkdir()
|
||||
|
||||
# Create sample files
|
||||
(project_dir / "tools" / "sample.py").write_text("# Sample tool")
|
||||
(project_dir / "utils" / "helper.py").write_text("# Helper utility")
|
||||
|
||||
return project_dir
|
||||
```
|
||||
|
||||
## Test Data Management
|
||||
|
||||
### Test Fixtures
|
||||
|
||||
**File-based Fixtures**:
|
||||
```python
|
||||
# tests/fixtures/sample_code.py
|
||||
PYTHON_CODE_SAMPLE = '''
|
||||
import asyncio
|
||||
from typing import Dict, List
|
||||
|
||||
async def process_data(items: List[str]) -> Dict[str, int]:
|
||||
"""Process a list of items and return counts."""
|
||||
result = {}
|
||||
for item in items:
|
||||
result[item] = len(item)
|
||||
return result
|
||||
'''
|
||||
|
||||
JAVASCRIPT_CODE_SAMPLE = '''
|
||||
async function processData(items) {
|
||||
const result = {};
|
||||
for (const item of items) {
|
||||
result[item] = item.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
'''
|
||||
|
||||
ERROR_LOGS_SAMPLE = '''
|
||||
2025-01-11 23:45:12 ERROR [tool_execution] Tool 'analyze' failed: File not found
|
||||
Traceback (most recent call last):
|
||||
File "/app/tools/analyze.py", line 45, in execute
|
||||
content = read_file(file_path)
|
||||
File "/app/utils/file_utils.py", line 23, in read_file
|
||||
with open(file_path, 'r') as f:
|
||||
FileNotFoundError: [Errno 2] No such file or directory: '/nonexistent/file.py'
|
||||
'''
|
||||
```
|
||||
|
||||
### Mock Data Factories
|
||||
|
||||
**ToolOutput Factory**:
|
||||
```python
|
||||
# tests/factories.py
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Any, List
|
||||
|
||||
def create_tool_output(
|
||||
content: str = "Default response",
|
||||
status: str = "success",
|
||||
metadata: Dict[str, Any] = None,
|
||||
files_processed: List[str] = None
|
||||
) -> ToolOutput:
|
||||
"""Factory for creating ToolOutput test instances."""
|
||||
return ToolOutput(
|
||||
content=content,
|
||||
metadata=metadata or {},
|
||||
files_processed=files_processed or [],
|
||||
status=status
|
||||
)
|
||||
|
||||
def create_thread_context(
|
||||
thread_id: str = "test-thread-123",
|
||||
files: List[str] = None
|
||||
) -> ThreadContext:
|
||||
"""Factory for creating ThreadContext test instances."""
|
||||
return ThreadContext(
|
||||
thread_id=thread_id,
|
||||
conversation_files=set(files or []),
|
||||
tool_history=[],
|
||||
context_tokens=0
|
||||
)
|
||||
```
|
||||
|
||||
## Mocking Strategies
|
||||
|
||||
### External Service Mocking
|
||||
|
||||
**Gemini API Mocking**:
|
||||
```python
|
||||
class MockGeminiClient:
|
||||
"""Mock Gemini client for testing."""
|
||||
|
||||
def __init__(self, responses: Dict[str, str] = None):
|
||||
self.responses = responses or {
|
||||
"default": "This is a mocked response from Gemini"
|
||||
}
|
||||
self.call_count = 0
|
||||
|
||||
async def generate_response(self, prompt: str, **kwargs) -> str:
|
||||
"""Mock response generation."""
|
||||
self.call_count += 1
|
||||
|
||||
# Return specific response for specific prompts
|
||||
for key, response in self.responses.items():
|
||||
if key in prompt.lower():
|
||||
return response
|
||||
|
||||
return self.responses.get("default", "Mock response")
|
||||
|
||||
# Usage in tests
|
||||
@patch('tools.models.GeminiClient', MockGeminiClient)
|
||||
def test_with_mocked_gemini():
|
||||
# Test implementation
|
||||
pass
|
||||
```
|
||||
|
||||
**File System Mocking**:
|
||||
```python
|
||||
@patch('builtins.open', mock_open(read_data="file content"))
|
||||
@patch('os.path.exists', return_value=True)
|
||||
@patch('os.path.getsize', return_value=1024)
|
||||
def test_file_operations():
|
||||
"""Test file operations with mocked file system."""
|
||||
content = read_file("/mocked/file.py")
|
||||
assert content == "file content"
|
||||
```
|
||||
|
||||
## Performance Testing
|
||||
|
||||
### Load Testing
|
||||
|
||||
**Concurrent Tool Execution**:
|
||||
```python
|
||||
# tests/performance/test_load.py
|
||||
import asyncio
|
||||
import pytest
|
||||
import time
|
||||
|
||||
@pytest.mark.slow
|
||||
class TestPerformance:
|
||||
"""Performance tests for system load handling."""
|
||||
|
||||
async def test_concurrent_tool_execution(self):
|
||||
"""Test system performance under concurrent load."""
|
||||
start_time = time.time()
|
||||
|
||||
# Create 10 concurrent tool execution tasks
|
||||
tasks = []
|
||||
for i in range(10):
|
||||
task = asyncio.create_task(
|
||||
call_tool("chat", {"prompt": f"Request {i}"})
|
||||
)
|
||||
tasks.append(task)
|
||||
|
||||
# Wait for all tasks to complete
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
end_time = time.time()
|
||||
execution_time = end_time - start_time
|
||||
|
||||
# Verify all requests succeeded
|
||||
assert len(results) == 10
|
||||
assert all(len(result) == 1 for result in results)
|
||||
|
||||
# Performance assertion (adjust based on requirements)
|
||||
assert execution_time < 30.0 # All requests should complete within 30s
|
||||
|
||||
async def test_memory_usage_stability(self):
|
||||
"""Test that memory usage remains stable under load."""
|
||||
import psutil
|
||||
import gc
|
||||
|
||||
process = psutil.Process()
|
||||
initial_memory = process.memory_info().rss
|
||||
|
||||
# Execute multiple operations
|
||||
for i in range(100):
|
||||
await call_tool("chat", {"prompt": f"Memory test {i}"})
|
||||
|
||||
# Force garbage collection periodically
|
||||
if i % 10 == 0:
|
||||
gc.collect()
|
||||
|
||||
final_memory = process.memory_info().rss
|
||||
memory_growth = final_memory - initial_memory
|
||||
|
||||
# Memory growth should be reasonable (adjust threshold as needed)
|
||||
assert memory_growth < 100 * 1024 * 1024 # Less than 100MB growth
|
||||
```
|
||||
|
||||
## Test Execution
|
||||
|
||||
### Running Tests
|
||||
|
||||
**Basic Test Execution**:
|
||||
```bash
|
||||
# Run all tests
|
||||
pytest
|
||||
|
||||
# Run specific test categories
|
||||
pytest -m unit # Unit tests only
|
||||
pytest -m integration # Integration tests only
|
||||
pytest -m "not live" # All tests except live tests
|
||||
pytest -m "live and not slow" # Live tests that are fast
|
||||
|
||||
# Run with coverage
|
||||
pytest --cov=tools --cov=utils --cov-report=html
|
||||
|
||||
# Run specific test file
|
||||
pytest tests/unit/test_file_utils.py -v
|
||||
|
||||
# Run specific test method
|
||||
pytest tests/unit/test_file_utils.py::TestFileUtils::test_validate_file_path -v
|
||||
```
|
||||
|
||||
**Continuous Integration**:
|
||||
```bash
|
||||
# CI test script
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Running unit tests..."
|
||||
pytest -m unit --cov=tools --cov=utils --cov-fail-under=80
|
||||
|
||||
echo "Running integration tests..."
|
||||
pytest -m integration
|
||||
|
||||
echo "Running security tests..."
|
||||
pytest -m security
|
||||
|
||||
echo "Checking code quality..."
|
||||
flake8 tools/ utils/ tests/
|
||||
mypy tools/ utils/
|
||||
|
||||
echo "All tests passed!"
|
||||
```
|
||||
|
||||
### Test Reports
|
||||
|
||||
**Coverage Reports**:
|
||||
```bash
|
||||
# Generate HTML coverage report
|
||||
pytest --cov=tools --cov=utils --cov-report=html
|
||||
open htmlcov/index.html
|
||||
|
||||
# Generate terminal coverage report
|
||||
pytest --cov=tools --cov=utils --cov-report=term-missing
|
||||
```
|
||||
|
||||
**Test Results Export**:
|
||||
```bash
|
||||
# Export test results to JUnit XML (for CI integration)
|
||||
pytest --junitxml=test-results.xml
|
||||
|
||||
# Export test results with timing information
|
||||
pytest --durations=10 # Show 10 slowest tests
|
||||
```
|
||||
|
||||
## Quality Metrics
|
||||
|
||||
### Coverage Targets
|
||||
|
||||
**Minimum Coverage Requirements**:
|
||||
- **Overall Coverage**: 80%
|
||||
- **Critical Modules**: 90% (security, file_utils, conversation_memory)
|
||||
- **Tool Modules**: 85%
|
||||
- **Utility Modules**: 80%
|
||||
|
||||
**Coverage Enforcement**:
|
||||
```bash
|
||||
# Fail build if coverage drops below threshold
|
||||
pytest --cov-fail-under=80
|
||||
```
|
||||
|
||||
### Test Quality Metrics
|
||||
|
||||
**Test Suite Characteristics**:
|
||||
- **Fast Execution**: Unit test suite should complete in <30 seconds
|
||||
- **Reliable**: Tests should have <1% flaky failure rate
|
||||
- **Maintainable**: Test code should follow same quality standards as production code
|
||||
- **Comprehensive**: All critical paths and edge cases covered
|
||||
|
||||
## Integration with Development Workflow
|
||||
|
||||
### Pre-commit Testing
|
||||
|
||||
**Git Hook Integration**:
|
||||
```bash
|
||||
#!/bin/sh
|
||||
# .git/hooks/pre-commit
|
||||
|
||||
echo "Running pre-commit tests..."
|
||||
|
||||
# Run fast tests before commit
|
||||
pytest -m "unit and not slow" --cov-fail-under=80
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Tests failed. Commit blocked."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Pre-commit tests passed."
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
**GitHub Actions Workflow**:
|
||||
```yaml
|
||||
name: Test Suite
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9, 3.10, 3.11]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install pytest pytest-cov pytest-asyncio
|
||||
|
||||
- name: Run unit tests
|
||||
run: pytest -m unit --cov=tools --cov=utils --cov-fail-under=80
|
||||
|
||||
- name: Run integration tests
|
||||
run: pytest -m integration
|
||||
|
||||
- name: Run security tests
|
||||
run: pytest -m security
|
||||
|
||||
- name: Upload coverage reports
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
This comprehensive testing strategy ensures high-quality, reliable code while maintaining development velocity and supporting the collaborative patterns defined in CLAUDE.md.
|
||||
Reference in New Issue
Block a user