diff --git a/CHANGELOG.md b/CHANGELOG.md index 491e233..8eb52b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - 2025-10-28 + +### Added +- **Processing Modes System**: Introduced modular architecture for different diffgraph generation strategies + - Created `BaseProcessor` abstract class for implementing custom processing modes + - Added processor registry and factory pattern for mode instantiation + - Implemented `@register_processor` decorator for automatic mode registration +- **OpenAI Agents Dependency Graph Mode**: Refactored existing AI analysis into `openai-agents-dependency-graph` mode + - Maintains all existing functionality as the default processing mode + - Analyzes code at component level (classes, functions, methods) + - Generates dependency graphs showing component relationships +- **CLI Enhancements**: + - Added `--mode` / `-m` option to select processing mode + - Added `--list-modes` flag to display available processing modes + - Default mode: `openai-agents-dependency-graph` (backward compatible) +- **Documentation**: + - Added comprehensive developer guide: `docs/ADDING_PROCESSING_MODES.md` + - Updated README.md with processing modes information + - Documented how to create custom processing modes + +### Changed +- Refactored `CodeAnalysisAgent` into modular `OpenAIAgentsProcessor` +- Removed direct dependency on `ai_analysis.py` in CLI (now uses processor factory) +- Improved extensibility for adding new analysis approaches (Tree-sitter, data flow, etc.) + +### Removed +- `diffgraph/ai_analysis.py` - Functionality moved to `diffgraph/processing_modes/openai_agents_dependency.py` + + ## [1.0.0] - 2025-08-06 ### Changed diff --git a/README.md b/README.md index ee66c7b..88f4b92 100644 --- a/README.md +++ b/README.md @@ -58,13 +58,53 @@ This will: - `--api-key`: Specify your OpenAI API key (defaults to OPENAI_API_KEY environment variable) - `--output` or `-o`: Specify the output HTML file path (default: diffgraph.html) - `--no-open`: Don't automatically open the HTML report in browser +- `--mode` or `-m`: Select processing mode for diffgraph generation (default: openai-agents-dependency-graph) +- `--list-modes`: List all available processing modes - `--version`: Show version information -Example: +Examples: ```bash -wild --output my-report.html --no-open +# Generate report with default mode +wild diff + +# Generate report with custom output path +wild diff --output my-report.html --no-open + +# List available processing modes +wild diff --list-modes + +# Use a specific processing mode +wild diff --mode openai-agents-dependency-graph ``` +## 🔧 Processing Modes + +DiffGraph supports multiple processing modes for analyzing code changes. Each mode provides a different perspective on your code: + +### Available Modes + +#### `openai-agents-dependency-graph` (default) +Uses OpenAI Agents SDK to analyze code and generate component-level dependency graphs. This mode: +- Identifies classes, functions, and methods in your changes +- Analyzes dependencies between components +- Generates a visual dependency graph showing how components relate to each other +- Best for understanding architectural changes and component interactions + +### Future Modes + +The architecture is designed to support additional processing modes: +- **tree-sitter-dependency-graph**: AST-based analysis using Tree-sitter +- **data-flow-analysis**: Focus on data flow and transformations +- **user-context-analysis**: Analyze changes from a user interaction perspective +- **architecture-analysis**: System-level architectural insights + +### Adding Custom Processing Modes + +Developers can extend DiffGraph by creating custom processing modes. See the `diffgraph/processing_modes/` directory for examples. Each processor must: +1. Inherit from `BaseProcessor` +2. Implement the `analyze_changes()` method +3. Register itself using the `@register_processor` decorator + ## 📊 Example Output The generated HTML report includes: diff --git a/diffgraph/__init__.py b/diffgraph/__init__.py index 317dee1..eb877fc 100644 --- a/diffgraph/__init__.py +++ b/diffgraph/__init__.py @@ -2,4 +2,4 @@ DiffGraph - A CLI tool for visualizing code changes with AI """ -__version__ = "0.1.0" \ No newline at end of file +__version__ = "1.1.0" \ No newline at end of file diff --git a/diffgraph/cli.py b/diffgraph/cli.py index 15e0522..4ac64ca 100644 --- a/diffgraph/cli.py +++ b/diffgraph/cli.py @@ -5,10 +5,10 @@ from click_spinner import spinner from typing import List, Dict import os -from diffgraph.ai_analysis import CodeAnalysisAgent from diffgraph.html_report import generate_html_report, AnalysisResult from diffgraph.env_loader import load_env_file, debug_environment from diffgraph.utils import sanitize_diff_args, involves_working_tree +from diffgraph.processing_modes import get_processor, list_available_modes # Load environment variables load_env_file() @@ -136,9 +136,21 @@ def load_file_contents(changed_files: List[Dict[str, str]], diff_args: List[str] @click.option('--output', '-o', default='diffgraph.html', help='Output HTML file path') @click.option('--no-open', is_flag=True, help='Do not open the HTML report automatically') @click.option('--debug-env', is_flag=True, help='Debug environment variable loading') -def main(args, api_key: str, output: str, no_open: bool, debug_env: bool): +@click.option('--mode', '-m', default='openai-agents-dependency-graph', + help='Processing mode for diffgraph generation (default: openai-agents-dependency-graph)') +@click.option('--list-modes', is_flag=True, help='List available processing modes and exit') +def main(args, api_key: str, output: str, no_open: bool, debug_env: bool, mode: str, list_modes: bool): """wild - Git wrapper CLI with DiffGraph for diff commands.""" + # Handle --list-modes flag + if list_modes: + click.echo("Available processing modes:\n") + modes = list_available_modes() + for mode_name, description in modes.items(): + click.echo(f" • {mode_name}") + click.echo(f" {description}\n") + return + # Check if this is a diff command if args and args[0] == 'diff': # Handle diff command with custom logic @@ -167,9 +179,14 @@ def main(args, api_key: str, output: str, no_open: bool, debug_env: bool): files_with_content = load_file_contents(files, diff_args) try: - # Initialize the AI analysis agent - click.echo("🤖 Initializing AI analysis...") - agent = CodeAnalysisAgent(api_key=api_key) + # Initialize the processor based on selected mode + click.echo(f"🤖 Initializing {mode} processor...") + try: + processor = get_processor(mode, api_key=api_key) + except ValueError as e: + click.echo(f"❌ Error: {e}", err=True) + click.echo("\nUse --list-modes to see available processing modes.", err=True) + sys.exit(1) # Define progress callback def progress_callback(current_file, total_files, status): @@ -178,7 +195,7 @@ def progress_callback(current_file, total_files, status): return file_name = os.path.basename(current_file) - current_index = len(agent.graph_manager.processed_files) + 1 + current_index = len(processor.graph_manager.processed_files) + 1 if status == "processing": click.echo(f"🔄 Processing {file_name} ({current_index}/{total_files})...") @@ -193,7 +210,7 @@ def progress_callback(current_file, total_files, status): # Analyze the changes with progress updates click.echo("🧠 Starting code analysis...") - analysis = agent.analyze_changes(files_with_content, progress_callback) + analysis = processor.analyze_changes(files_with_content, progress_callback) # Create analysis result click.echo("📊 Creating analysis result...") diff --git a/diffgraph/processing_modes/__init__.py b/diffgraph/processing_modes/__init__.py new file mode 100644 index 0000000..5af1a22 --- /dev/null +++ b/diffgraph/processing_modes/__init__.py @@ -0,0 +1,99 @@ +""" +Processing modes module for different diffgraph generation strategies. + +This module provides a registry of available processing modes and factory +functions to create processor instances. +""" + +from typing import Dict, Type, Optional +from .base import BaseProcessor, DiffAnalysis + +# Registry of available processing modes +_PROCESSOR_REGISTRY: Dict[str, Type[BaseProcessor]] = {} + + +def register_processor(mode_name: str): + """ + Decorator to register a processor class. + + Args: + mode_name: The name identifier for this processing mode + + Example: + @register_processor("openai-agents-dependency-graph") + class OpenAIAgentsProcessor(BaseProcessor): + ... + """ + def decorator(cls: Type[BaseProcessor]): + _PROCESSOR_REGISTRY[mode_name] = cls + return cls + return decorator + + +def get_processor(mode_name: str, **kwargs) -> BaseProcessor: + """ + Factory function to create a processor instance. + + Args: + mode_name: The name of the processing mode + **kwargs: Configuration parameters for the processor + + Returns: + An instance of the requested processor + + Raises: + ValueError: If the mode_name is not registered + """ + if mode_name not in _PROCESSOR_REGISTRY: + available_modes = ", ".join(_PROCESSOR_REGISTRY.keys()) + raise ValueError( + f"Unknown processing mode: '{mode_name}'. " + f"Available modes: {available_modes}" + ) + + processor_class = _PROCESSOR_REGISTRY[mode_name] + return processor_class(**kwargs) + + +def list_available_modes() -> Dict[str, str]: + """ + Get a dictionary of available processing modes and their descriptions. + + Returns: + Dictionary mapping mode names to descriptions + """ + modes = {} + for mode_name, processor_class in _PROCESSOR_REGISTRY.items(): + # Get description by creating a minimal instance + try: + # Try to create instance without required parameters to get description + # Most processors should allow getting description without full initialization + temp_instance = processor_class.__new__(processor_class) + if hasattr(temp_instance, 'description'): + desc = temp_instance.description + if isinstance(desc, property): + # For property descriptors, we need to access via class + description = processor_class.description.fget(temp_instance) + else: + description = desc + else: + description = "No description available" + except Exception as e: + # Fallback: try to get from docstring or use default + description = processor_class.__doc__.split('\n')[0] if processor_class.__doc__ else "No description available" + modes[mode_name] = description + return modes + + +# Import processors to trigger registration +# This will be populated as we add more processors +from . import openai_agents_dependency # noqa: F401, E402 + + +__all__ = [ + "BaseProcessor", + "DiffAnalysis", + "register_processor", + "get_processor", + "list_available_modes", +] diff --git a/diffgraph/processing_modes/base.py b/diffgraph/processing_modes/base.py new file mode 100644 index 0000000..3c10fc3 --- /dev/null +++ b/diffgraph/processing_modes/base.py @@ -0,0 +1,77 @@ +""" +Base processor interface for different diffgraph generation modes. + +This module defines the abstract base class that all processing modes must implement. +""" + +from abc import ABC, abstractmethod +from typing import List, Dict, Optional, Callable +from pydantic import BaseModel + + +class DiffAnalysis(BaseModel): + """Model representing the analysis of code changes.""" + summary: str + mermaid_diagram: str + + +class BaseProcessor(ABC): + """ + Abstract base class for diffgraph processors. + + Each processing mode (e.g., OpenAI Agents, Tree-sitter, etc.) should inherit + from this class and implement the analyze_changes method. + """ + + def __init__(self, **kwargs): + """ + Initialize the processor with configuration options. + + Args: + **kwargs: Configuration parameters specific to the processor + """ + self.config = kwargs + + @abstractmethod + def analyze_changes( + self, + files_with_content: List[Dict[str, str]], + progress_callback: Optional[Callable] = None + ) -> DiffAnalysis: + """ + Analyze code changes and generate a diffgraph. + + Args: + files_with_content: List of dictionaries containing: + - path: File path + - status: Change status (modified, untracked, etc.) + - content: File content or diff + progress_callback: Optional callback function to report progress. + Should accept (current_file, total_files, status) + + Returns: + DiffAnalysis object containing summary and mermaid diagram + """ + pass + + @property + @abstractmethod + def name(self) -> str: + """Return the name/identifier of this processing mode.""" + pass + + @property + @abstractmethod + def description(self) -> str: + """Return a human-readable description of this processing mode.""" + pass + + @classmethod + def get_required_config(cls) -> List[str]: + """ + Return list of required configuration parameters for this processor. + + Returns: + List of configuration parameter names + """ + return [] diff --git a/diffgraph/ai_analysis.py b/diffgraph/processing_modes/openai_agents_dependency.py similarity index 89% rename from diffgraph/ai_analysis.py rename to diffgraph/processing_modes/openai_agents_dependency.py index c8c1990..ece34e4 100644 --- a/diffgraph/ai_analysis.py +++ b/diffgraph/processing_modes/openai_agents_dependency.py @@ -1,25 +1,31 @@ -from typing import List, Dict, Optional, Tuple +""" +OpenAI Agents SDK processor for dependency graph generation. + +This processor uses OpenAI's Agents SDK to analyze code changes and generate +dependency graphs at the component level. +""" + +from typing import List, Dict, Optional, Tuple, Callable from agents import Agent, Runner import os from pydantic import BaseModel -from .graph_manager import GraphManager, FileStatus, ChangeType, ComponentNode +from ..graph_manager import GraphManager, FileStatus, ChangeType, ComponentNode import time import random import openai -import re import networkx as nx from enum import Enum +from .base import BaseProcessor, DiffAnalysis +from . import register_processor + + class FileChange(BaseModel): """Model representing a file change.""" path: str status: str content: str -class DiffAnalysis(BaseModel): - """Model representing the analysis of code changes.""" - summary: str - mermaid_diagram: str class ComponentAnalysis(BaseModel): """Model representing a single component's analysis.""" @@ -32,17 +38,20 @@ class ComponentAnalysis(BaseModel): dependents: List[str] = [] nested_components: List[str] = [] # names of components that are nested within this one + class CodeChangeAnalysis(BaseModel): """Model representing the analysis of code changes from the LLM.""" summary: str components: List[ComponentAnalysis] impact: str + class DependencyMode(Enum): """Mode for processing dependency relationships.""" DEPENDENCY = "dependency" # Process components that this component depends on DEPENDENT = "dependent" # Process components that depend on this component + def exponential_backoff_retry(func): """Decorator to implement exponential backoff retry logic using API rate limit information.""" def wrapper(*args, **kwargs): @@ -76,11 +85,19 @@ def wrapper(*args, **kwargs): raise # Re-raise other exceptions immediately return wrapper -class CodeAnalysisAgent: - """Agent for analyzing code changes using OpenAI's Agents SDK.""" - def __init__(self, api_key: Optional[str] = None): - """Initialize the agent with OpenAI API key.""" +@register_processor("openai-agents-dependency-graph") +class OpenAIAgentsProcessor(BaseProcessor): + """ + Processor using OpenAI Agents SDK for dependency graph generation. + + This processor analyzes code changes at the component level (classes, functions, methods) + and builds a dependency graph showing relationships between components. + """ + + def __init__(self, api_key: Optional[str] = None, **kwargs): + """Initialize the processor with OpenAI API key.""" + super().__init__(**kwargs) self.api_key = api_key or os.getenv("OPENAI_API_KEY") if not self.api_key: raise ValueError("OpenAI API key is required. Set OPENAI_API_KEY environment variable.") @@ -122,6 +139,21 @@ def __init__(self, api_key: Optional[str] = None): self.graph_manager = GraphManager() + @property + def name(self) -> str: + """Return the name of this processing mode.""" + return "openai-agents-dependency-graph" + + @property + def description(self) -> str: + """Return a description of this processing mode.""" + return "Uses OpenAI Agents SDK to analyze code and generate component-level dependency graphs" + + @classmethod + def get_required_config(cls) -> List[str]: + """Return required configuration parameters.""" + return ["api_key"] + def _determine_change_type(self, status: str) -> ChangeType: """Convert git status to ChangeType.""" if status == "untracked": @@ -137,7 +169,11 @@ def _run_agent_analysis(self, prompt: str) -> str: result = Runner.run_sync(self.agent, prompt) return result.final_output - def analyze_changes(self, files_with_content: List[Dict[str, str]], progress_callback=None) -> DiffAnalysis: + def analyze_changes( + self, + files_with_content: List[Dict[str, str]], + progress_callback: Optional[Callable] = None + ) -> DiffAnalysis: """ Analyze code changes using the OpenAI agent, processing files incrementally. @@ -341,4 +377,4 @@ def _process_dependencies(self, comp: ComponentNode, current_file: str, mode: De break if not found: - print(f"Warning: Could not resolve {mode.value} '{item}' for component '{comp.name}' in {current_file}") \ No newline at end of file + print(f"Warning: Could not resolve {mode.value} '{item}' for component '{comp.name}' in {current_file}") diff --git a/docs/ADDING_PROCESSING_MODES.md b/docs/ADDING_PROCESSING_MODES.md new file mode 100644 index 0000000..cd474dd --- /dev/null +++ b/docs/ADDING_PROCESSING_MODES.md @@ -0,0 +1,257 @@ +# Adding New Processing Modes + +This guide explains how to add new processing modes to DiffGraph. + +## Overview + +Processing modes allow DiffGraph to analyze code changes from different perspectives. Each mode can focus on different aspects like: +- User context +- System architecture +- Module & component level interactions +- Data flow +- Abstract syntax tree analysis +- Dependency graphs + +## Architecture + +The processing mode system is built on: +1. **BaseProcessor**: Abstract base class that defines the interface +2. **Processor Registry**: Automatic registration of processing modes +3. **Factory Pattern**: `get_processor()` creates instances based on mode name + +## Creating a New Processing Mode + +### Step 1: Create Your Processor Class + +Create a new file in `diffgraph/processing_modes/` (e.g., `tree_sitter_dependency.py`): + +```python +from typing import List, Dict, Optional, Callable +from .base import BaseProcessor, DiffAnalysis +from . import register_processor + +@register_processor("tree-sitter-dependency-graph") +class TreeSitterProcessor(BaseProcessor): + """ + Processor using Tree-sitter for AST-based dependency analysis. + """ + + def __init__(self, **kwargs): + """Initialize the processor with any required configuration.""" + super().__init__(**kwargs) + # Initialize your tools/libraries here + # self.parser = ... + + @property + def name(self) -> str: + """Return the mode identifier.""" + return "tree-sitter-dependency-graph" + + @property + def description(self) -> str: + """Return a human-readable description.""" + return "Uses Tree-sitter for AST-based dependency analysis" + + @classmethod + def get_required_config(cls) -> List[str]: + """List any required configuration parameters.""" + return [] # e.g., ["api_key", "model_name"] + + def analyze_changes( + self, + files_with_content: List[Dict[str, str]], + progress_callback: Optional[Callable] = None + ) -> DiffAnalysis: + """ + Analyze code changes and generate diffgraph. + + Args: + files_with_content: List of dicts with 'path', 'status', 'content' + progress_callback: Optional callback(current_file, total_files, status) + + Returns: + DiffAnalysis with 'summary' and 'mermaid_diagram' + """ + # Your analysis logic here + summary = "Analysis summary..." + mermaid_diagram = "graph LR\\n A --> B" + + return DiffAnalysis( + summary=summary, + mermaid_diagram=mermaid_diagram + ) +``` + +### Step 2: Register the Processor + +Import your processor in `diffgraph/processing_modes/__init__.py`: + +```python +# Add to the imports section at the bottom +from . import tree_sitter_dependency # noqa: F401, E402 +``` + +The `@register_processor` decorator automatically registers your mode. + +### Step 3: Test Your Processor + +```python +# Test your processor +from diffgraph.processing_modes import get_processor, list_available_modes + +# Check it's registered +modes = list_available_modes() +assert "tree-sitter-dependency-graph" in modes + +# Create an instance +processor = get_processor("tree-sitter-dependency-graph") + +# Test analysis +files = [ + { + "path": "test.py", + "status": "modified", + "content": "def hello(): pass" + } +] +result = processor.analyze_changes(files) +print(result.summary) +print(result.mermaid_diagram) +``` + +### Step 4: Use via CLI + +```bash +# List all modes +wild diff --list-modes + +# Use your new mode +wild diff --mode tree-sitter-dependency-graph +``` + +## Best Practices + +### 1. Progress Reporting +Use the `progress_callback` to report progress: + +```python +if progress_callback: + progress_callback(current_file, total_files, "processing") +``` + +Status values: `"processing"`, `"analyzing"`, `"processing_components"`, `"completed"`, `"error"` + +### 2. Error Handling +Handle errors gracefully and provide useful error messages: + +```python +try: + # Your analysis code + pass +except Exception as e: + if progress_callback: + progress_callback(current_file, total_files, "error") + # Log or handle the error appropriately +``` + +### 3. Configuration +Define required configuration in `get_required_config()`: + +```python +@classmethod +def get_required_config(cls) -> List[str]: + return ["api_key", "model_name"] +``` + +The CLI will pass configuration via `**kwargs` to your `__init__` method. + +### 4. Mermaid Diagram Generation +Generate valid Mermaid syntax for visualization: + +```python +mermaid = ["graph LR"] +mermaid.append(" A[Component A] --> B[Component B]") +mermaid.append(" B --> C[Component C]") +return "\\n".join(mermaid) +``` + +### 5. Reusability +Consider using the existing `GraphManager` class for managing component graphs: + +```python +from ..graph_manager import GraphManager, ChangeType + +self.graph_manager = GraphManager() +self.graph_manager.add_file(file_path, ChangeType.MODIFIED) +self.graph_manager.add_component(name, file_path, change_type, ...) +mermaid_diagram = self.graph_manager.get_mermaid_diagram() +``` + +## Example Modes to Implement + +### 1. Tree-sitter Dependency Graph +Focus: AST-based static analysis +- Use Tree-sitter for language-agnostic parsing +- Extract imports, function calls, class inheritance +- Build dependency graph from AST structure + +### 2. Data Flow Analysis +Focus: How data flows through the code +- Track variable assignments and transformations +- Identify data sources and sinks +- Visualize data pipelines + +### 3. User Context Analysis +Focus: User-facing changes +- Identify UI components and API endpoints +- Analyze user interaction flows +- Show impact on user experience + +### 4. Architecture Analysis +Focus: System-level structure +- Identify modules and layers +- Analyze architectural patterns +- Show system component relationships + +## Testing + +Create tests in `tests/processing_modes/`: + +```python +import pytest +from diffgraph.processing_modes import get_processor + +def test_tree_sitter_processor(): + processor = get_processor("tree-sitter-dependency-graph") + + files = [{ + "path": "example.py", + "status": "modified", + "content": "def foo(): return 42" + }] + + result = processor.analyze_changes(files) + + assert result.summary + assert result.mermaid_diagram + assert "graph" in result.mermaid_diagram +``` + +## Documentation + +Update the README.md with your new mode: + +```markdown +### Available Modes + +#### `tree-sitter-dependency-graph` +Uses Tree-sitter for AST-based dependency analysis. This mode: +- Parses code using language-specific grammars +- Extracts structural dependencies from the AST +- Generates dependency graphs without AI +- Best for quick, deterministic analysis +``` + +## Questions? + +If you have questions or need help implementing a new processing mode, please open an issue on GitHub. diff --git a/setup.py b/setup.py index 33e623f..5624d69 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="wild", - version="1.0.0", + version="1.1.0", packages=find_packages(), install_requires=[ "click>=8.1.7", diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..dcf732c --- /dev/null +++ b/tests/README.md @@ -0,0 +1,100 @@ +# Tests + +This directory contains tests for the DiffGraph CLI tool. + +## Structure + +``` +tests/ +├── __init__.py +├── conftest.py # Shared pytest fixtures +├── README.md # This file +├── test_cli.py # CLI integration tests +└── processing_modes/ + ├── __init__.py + ├── test_base.py # Base processor tests + ├── test_registry.py # Registry and factory tests + └── test_openai_agents.py # OpenAI processor tests +``` + +## Running Tests + +### Install pytest + +```bash +pip install pytest pytest-cov +``` + +### Run all tests + +```bash +pytest +``` + +### Run with coverage + +```bash +pytest --cov=diffgraph --cov-report=html +``` + +### Run specific test file + +```bash +pytest tests/test_cli.py +``` + +### Run specific test function + +```bash +pytest tests/processing_modes/test_registry.py::test_list_available_modes +``` + +### Run with verbose output + +```bash +pytest -v +``` + +## Test Categories + +### Unit Tests +- `processing_modes/test_base.py` - Base processor interface +- `processing_modes/test_registry.py` - Registry and factory pattern +- `processing_modes/test_openai_agents.py` - OpenAI processor specifics + +### Integration Tests +- `test_cli.py` - CLI command integration + +## Adding New Tests + +When adding a new processing mode, create a corresponding test file: + +```bash +tests/processing_modes/test_your_mode.py +``` + +Example: +```python +"""Tests for your new processor.""" +import pytest +from diffgraph.processing_modes import get_processor + +def test_your_processor_initialization(): + """Test your processor can be initialized.""" + processor = get_processor("your-mode-name", config="value") + assert processor.name == "your-mode-name" + +def test_your_processor_analyze(): + """Test analysis functionality.""" + processor = get_processor("your-mode-name", config="value") + result = processor.analyze_changes([...]) + assert result.summary + assert result.mermaid_diagram +``` + +## Continuous Integration + +These tests are designed to run in CI/CD pipelines. Make sure: +- All tests pass before committing +- New features include tests +- Test coverage stays high diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..810e6d9 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Tests for DiffGraph CLI. +""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e1979b0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,28 @@ +""" +Pytest configuration and shared fixtures. +""" +import pytest +import os + + +@pytest.fixture +def mock_api_key(): + """Provide a mock API key for testing.""" + return "test-api-key-12345" + + +@pytest.fixture +def sample_file_changes(): + """Provide sample file changes for testing.""" + return [ + { + "path": "test.py", + "status": "modified", + "content": "def hello():\n print('Hello, world!')\n" + }, + { + "path": "new_file.py", + "status": "untracked", + "content": "class MyClass:\n pass\n" + } + ] diff --git a/tests/processing_modes/__init__.py b/tests/processing_modes/__init__.py new file mode 100644 index 0000000..e70e735 --- /dev/null +++ b/tests/processing_modes/__init__.py @@ -0,0 +1,3 @@ +""" +Tests for processing modes. +""" diff --git a/tests/processing_modes/test_base.py b/tests/processing_modes/test_base.py new file mode 100644 index 0000000..9fb31b5 --- /dev/null +++ b/tests/processing_modes/test_base.py @@ -0,0 +1,21 @@ +""" +Tests for base processor interface. +""" +import pytest +from diffgraph.processing_modes import BaseProcessor, DiffAnalysis + + +def test_base_processor_is_abstract(): + """Test that BaseProcessor cannot be instantiated directly.""" + with pytest.raises(TypeError): + BaseProcessor() + + +def test_diff_analysis_model(): + """Test DiffAnalysis model creation.""" + analysis = DiffAnalysis( + summary="Test summary", + mermaid_diagram="graph LR\n A --> B" + ) + assert analysis.summary == "Test summary" + assert "graph LR" in analysis.mermaid_diagram diff --git a/tests/processing_modes/test_openai_agents.py b/tests/processing_modes/test_openai_agents.py new file mode 100644 index 0000000..c0665f4 --- /dev/null +++ b/tests/processing_modes/test_openai_agents.py @@ -0,0 +1,32 @@ +""" +Tests for OpenAI Agents processor. +""" +import pytest +from diffgraph.processing_modes import get_processor + + +def test_openai_processor_initialization(): + """Test OpenAI processor can be initialized.""" + processor = get_processor("openai-agents-dependency-graph", api_key="test-key") + + assert processor.name == "openai-agents-dependency-graph" + assert "OpenAI" in processor.description + assert hasattr(processor, 'graph_manager') + + +def test_openai_processor_requires_api_key(): + """Test that OpenAI processor requires API key.""" + with pytest.raises(ValueError) as exc_info: + get_processor("openai-agents-dependency-graph") + + assert "API key" in str(exc_info.value) + + +def test_openai_processor_metadata(): + """Test OpenAI processor metadata.""" + processor = get_processor("openai-agents-dependency-graph", api_key="test-key") + + assert processor.name == "openai-agents-dependency-graph" + assert len(processor.description) > 0 + assert "dependency" in processor.description.lower() + assert "component" in processor.description.lower() diff --git a/tests/processing_modes/test_registry.py b/tests/processing_modes/test_registry.py new file mode 100644 index 0000000..edd496e --- /dev/null +++ b/tests/processing_modes/test_registry.py @@ -0,0 +1,50 @@ +""" +Tests for processor registry and factory. +""" +import pytest +from diffgraph.processing_modes import get_processor, list_available_modes + + +def test_list_available_modes(): + """Test listing available processing modes.""" + modes = list_available_modes() + + assert isinstance(modes, dict) + assert len(modes) >= 1 + assert "openai-agents-dependency-graph" in modes + assert isinstance(modes["openai-agents-dependency-graph"], str) + assert len(modes["openai-agents-dependency-graph"]) > 0 + + +def test_get_processor_success(): + """Test getting a processor instance.""" + processor = get_processor("openai-agents-dependency-graph", api_key="test-key") + + assert processor is not None + assert processor.name == "openai-agents-dependency-graph" + assert hasattr(processor, 'analyze_changes') + assert callable(processor.analyze_changes) + + +def test_get_processor_invalid_mode(): + """Test that getting an invalid processor raises ValueError.""" + with pytest.raises(ValueError) as exc_info: + get_processor("non-existent-mode") + + assert "Unknown processing mode" in str(exc_info.value) + assert "non-existent-mode" in str(exc_info.value) + + +def test_processor_interface(): + """Test that processor has required interface.""" + processor = get_processor("openai-agents-dependency-graph", api_key="test-key") + + # Check required attributes + assert hasattr(processor, 'name') + assert hasattr(processor, 'description') + assert hasattr(processor, 'analyze_changes') + + # Check properties work + assert isinstance(processor.name, str) + assert isinstance(processor.description, str) + assert callable(processor.analyze_changes) diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..49042e8 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,59 @@ +""" +Tests for CLI functionality. +""" +import pytest +from click.testing import CliRunner +from diffgraph.cli import main + + +@pytest.fixture +def cli_runner(): + """Fixture providing a Click CLI runner.""" + return CliRunner() + + +def test_cli_list_modes(cli_runner): + """Test --list-modes flag.""" + result = cli_runner.invoke(main, ['diff', '--list-modes']) + + assert result.exit_code == 0 + assert 'Available processing modes' in result.output + assert 'openai-agents-dependency-graph' in result.output + + +def test_cli_help_shows_mode_option(cli_runner): + """Test that help shows --mode option.""" + result = cli_runner.invoke(main, ['diff', '--help']) + + assert result.exit_code == 0 + assert '--mode' in result.output or '-m' in result.output + assert '--list-modes' in result.output + + +def test_cli_help_shows_default_mode(cli_runner): + """Test that help shows default mode.""" + result = cli_runner.invoke(main, ['diff', '--help']) + + assert result.exit_code == 0 + # Mode name might be wrapped, so check for components + assert 'openai-agents-dependency-graph' in result.output or \ + ('openai-' in result.output and 'dependency-graph' in result.output) + + +def test_cli_invalid_mode_error(cli_runner): + """Test that invalid mode shows helpful error.""" + # We need a git repo and changes for this to work properly + # This test will fail early with invalid mode error + result = cli_runner.invoke(main, ['diff', '--mode', 'invalid-mode']) + + # Should show error about invalid mode + # (might fail earlier if not in a git repo, which is fine) + assert result.exit_code != 0 or 'Available processing modes' in result.output + + +def test_cli_version(cli_runner): + """Test --version flag.""" + result = cli_runner.invoke(main, ['--version']) + + assert result.exit_code == 0 + assert 'version' in result.output.lower()