Compare commits
4 Commits
ad25b2a7d7
...
933d8ece5b
| Author | SHA1 | Date | |
|---|---|---|---|
| 933d8ece5b | |||
| 2cfdc401d6 | |||
| 0a07a1a313 | |||
| c4f8e4a3e9 |
268
GITEA_INTEGRATION_CONSOLIDATION_GAMEPLAN.md
Normal file
268
GITEA_INTEGRATION_CONSOLIDATION_GAMEPLAN.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# Gitea Integration Consolidation Gameplan
|
||||
|
||||
## Overview
|
||||
This document outlines the strategy to consolidate all direct Gitea API access through the unified `gitea` integration layer, eliminating direct curl/subprocess calls and ensuring consistent, testable, and maintainable API interactions.
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Direct Gitea API Usage Found
|
||||
|
||||
#### 1. `tddai/issue_writer.py` - **HIGH PRIORITY**
|
||||
- **Direct curl usage**: Uses subprocess + curl for all operations
|
||||
- **Functionality**:
|
||||
- `update_issue()` - PATCH requests for issue updates
|
||||
- `update_labels()` - PUT requests to dedicated labels endpoint
|
||||
- `add_labels()` / `remove_labels()` - GET + PUT label operations
|
||||
- `close_issue()` / `reopen_issue()` - State management
|
||||
- `assign_to_milestone()` - Milestone assignment
|
||||
|
||||
#### 2. Test Files with Mocking Issues
|
||||
- Multiple test files mock `subprocess.run` at different levels
|
||||
- Inconsistent mocking patterns between old and new approaches
|
||||
- Missing test coverage for gitea integration layer
|
||||
|
||||
#### 3. Legacy Configuration Dependencies
|
||||
- Old config structures still referenced in some places
|
||||
- Mixed usage of TddaiConfig vs GiteaConfig
|
||||
|
||||
### Current Gitea Integration Layer Capabilities
|
||||
|
||||
#### ✅ **Already Available in `gitea.client.IssuesClient`**
|
||||
- `get(issue_number)` - Get single issue
|
||||
- `list(state, page, per_page)` - List issues with filtering
|
||||
- `create(title, body, **kwargs)` - Create issues
|
||||
- `update(issue_number, **kwargs)` - Update issues
|
||||
- `close(issue_number)` - Close issues
|
||||
- `reopen(issue_number)` - Reopen issues
|
||||
- `add_labels(issue_number, labels)` - Add labels
|
||||
- `remove_labels(issue_number, labels)` - Remove labels
|
||||
- `set_priority(issue_number, priority)` - Priority management
|
||||
- `set_status(issue_number, status)` - Status management
|
||||
|
||||
#### ❌ **Missing Functionality**
|
||||
- **Milestone assignment methods**: `assign_to_milestone()`, `remove_from_milestone()`
|
||||
- **Label replacement**: Direct label replacement (vs add/remove)
|
||||
- **Bulk operations**: Batch updates
|
||||
- **Error handling**: Specific error types for different failure modes
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### Phase 1: Enhance Gitea Integration Layer
|
||||
**Priority**: Critical
|
||||
**Duration**: 1-2 days
|
||||
|
||||
#### 1.1 Add Missing Methods to IssuesClient
|
||||
```python
|
||||
def assign_to_milestone(self, issue_number: int, milestone_id: int) -> Issue:
|
||||
"""Assign issue to a milestone."""
|
||||
|
||||
def remove_from_milestone(self, issue_number: int) -> Issue:
|
||||
"""Remove issue from milestone."""
|
||||
|
||||
def set_labels(self, issue_number: int, labels: List[str]) -> Issue:
|
||||
"""Replace all labels on an issue."""
|
||||
```
|
||||
|
||||
#### 1.2 Enhance Error Handling
|
||||
- Add specific exception types for common failure scenarios
|
||||
- Improve error messages with actionable information
|
||||
- Add retry logic for transient failures
|
||||
|
||||
#### 1.3 Add Comprehensive Test Coverage
|
||||
- Unit tests for all IssuesClient methods
|
||||
- Integration tests with real API responses
|
||||
- Error condition testing
|
||||
- Performance testing for bulk operations
|
||||
|
||||
### Phase 2: Refactor Direct API Usage
|
||||
**Priority**: High
|
||||
**Duration**: 2-3 days
|
||||
|
||||
#### 2.1 Replace IssueWriter with Gitea Integration
|
||||
- **File**: `tddai/issue_writer.py`
|
||||
- **Strategy**: Replace direct curl calls with `gitea.client.IssuesClient` usage
|
||||
- **Backward Compatibility**: Maintain exact same interface
|
||||
- **Testing**: Ensure all existing tests continue to pass
|
||||
|
||||
#### 2.2 Update Test Mocking Patterns
|
||||
- Replace `subprocess.run` mocks with gitea client mocks
|
||||
- Standardize mocking approach across all test files
|
||||
- Add helper functions for common mock scenarios
|
||||
|
||||
#### 2.3 Configuration Consolidation
|
||||
- Ensure all modules use `GiteaConfig.from_git_repository()`
|
||||
- Remove legacy configuration patterns
|
||||
- Update initialization in all affected classes
|
||||
|
||||
### Phase 3: Validation and Optimization
|
||||
**Priority**: Medium
|
||||
**Duration**: 1 day
|
||||
|
||||
#### 3.1 End-to-End Testing
|
||||
- Verify all existing functionality works unchanged
|
||||
- Test error scenarios and edge cases
|
||||
- Performance comparison (before/after)
|
||||
|
||||
#### 3.2 Documentation Updates
|
||||
- Update API documentation
|
||||
- Create migration guide for any breaking changes
|
||||
- Update developer setup instructions
|
||||
|
||||
#### 3.3 Code Quality Improvements
|
||||
- Remove unused imports and dependencies
|
||||
- Consolidate duplicate code patterns
|
||||
- Improve type hints and documentation
|
||||
|
||||
## Detailed Implementation Plan
|
||||
|
||||
### Step 1: Enhance IssuesClient (gitea/client.py)
|
||||
|
||||
```python
|
||||
class IssuesClient:
|
||||
# Add missing methods
|
||||
def assign_to_milestone(self, issue_number: int, milestone_id: int) -> Issue:
|
||||
"""Assign issue to a milestone."""
|
||||
return self.update(issue_number, milestone=milestone_id)
|
||||
|
||||
def remove_from_milestone(self, issue_number: int) -> Issue:
|
||||
"""Remove issue from milestone."""
|
||||
return self.update(issue_number, milestone=None)
|
||||
|
||||
def set_labels(self, issue_number: int, labels: List[str]) -> Issue:
|
||||
"""Replace all labels on an issue."""
|
||||
return self.update(issue_number, labels=labels)
|
||||
|
||||
def update_title(self, issue_number: int, title: str) -> Issue:
|
||||
"""Update only the title of an issue."""
|
||||
return self.update(issue_number, title=title)
|
||||
|
||||
def update_body(self, issue_number: int, body: str) -> Issue:
|
||||
"""Update only the body of an issue."""
|
||||
return self.update(issue_number, body=body)
|
||||
```
|
||||
|
||||
### Step 2: Replace IssueWriter Implementation
|
||||
|
||||
```python
|
||||
# tddai/issue_writer.py - New implementation
|
||||
from gitea import GiteaClient, GiteaConfig
|
||||
from .exceptions import IssueError
|
||||
|
||||
class IssueWriter:
|
||||
"""Writes issue updates using the Gitea integration layer."""
|
||||
|
||||
def __init__(self, config=None, auth_token=None):
|
||||
gitea_config = GiteaConfig.from_git_repository()
|
||||
if auth_token:
|
||||
gitea_config.auth_token = auth_token
|
||||
self.client = GiteaClient(gitea_config)
|
||||
|
||||
def update_issue(self, issue_number: int, update_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Update an issue via the gitea integration."""
|
||||
try:
|
||||
issue = self.client.issues.update(issue_number, **update_data)
|
||||
return self._issue_to_dict(issue)
|
||||
except Exception as e:
|
||||
raise IssueError(f"Failed to update issue #{issue_number}: {e}")
|
||||
```
|
||||
|
||||
### Step 3: Test Strategy
|
||||
|
||||
#### Unit Tests for New Methods
|
||||
```python
|
||||
# tests/test_gitea_issues_client.py
|
||||
class TestIssuesClient:
|
||||
def test_assign_to_milestone(self):
|
||||
# Test milestone assignment
|
||||
|
||||
def test_remove_from_milestone(self):
|
||||
# Test milestone removal
|
||||
|
||||
def test_set_labels(self):
|
||||
# Test label replacement
|
||||
```
|
||||
|
||||
#### Integration Tests
|
||||
```python
|
||||
# tests/integration/test_gitea_integration.py
|
||||
class TestGiteaIntegration:
|
||||
def test_issue_writer_compatibility(self):
|
||||
# Ensure IssueWriter still works exactly the same
|
||||
|
||||
def test_end_to_end_workflow(self):
|
||||
# Test complete issue lifecycle
|
||||
```
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
### 1. Backward Compatibility
|
||||
- **Risk**: Breaking existing code that depends on IssueWriter
|
||||
- **Mitigation**: Maintain exact same interface, comprehensive testing
|
||||
|
||||
### 2. Performance Impact
|
||||
- **Risk**: New layer might be slower than direct curl
|
||||
- **Mitigation**: Performance testing, optimization if needed
|
||||
|
||||
### 3. Error Handling Changes
|
||||
- **Risk**: Different error patterns might break existing error handling
|
||||
- **Mitigation**: Map all existing error types to new exceptions
|
||||
|
||||
### 4. Test Coverage Gaps
|
||||
- **Risk**: Missing test coverage for edge cases
|
||||
- **Mitigation**: Comprehensive test suite, manual testing checklist
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Primary Goals
|
||||
1. **Zero Breaking Changes**: All existing functionality works unchanged
|
||||
2. **Single Integration Point**: No direct curl/subprocess calls to Gitea API
|
||||
3. **Improved Testability**: All Gitea interactions are easily mockable
|
||||
4. **Better Error Handling**: More specific and actionable error messages
|
||||
|
||||
### Quality Metrics
|
||||
- **Test Coverage**: >95% for all gitea integration code
|
||||
- **Performance**: No more than 10% performance regression
|
||||
- **Code Quality**: Reduced complexity, better maintainability
|
||||
|
||||
### Validation Checklist
|
||||
- [ ] All existing tests pass without modification
|
||||
- [ ] No direct subprocess calls to curl in application code
|
||||
- [ ] All Gitea operations go through gitea.client facade
|
||||
- [ ] Comprehensive test coverage for gitea integration
|
||||
- [ ] Documentation updated and complete
|
||||
- [ ] Performance benchmarks within acceptable range
|
||||
|
||||
## Timeline
|
||||
|
||||
### Week 1
|
||||
- **Days 1-2**: Enhance gitea integration layer, add missing methods
|
||||
- **Days 3-4**: Create comprehensive test suite
|
||||
- **Day 5**: Begin IssueWriter refactoring
|
||||
|
||||
### Week 2
|
||||
- **Days 1-2**: Complete IssueWriter refactoring
|
||||
- **Days 3-4**: Update all test mocking patterns
|
||||
- **Day 5**: End-to-end validation and documentation
|
||||
|
||||
## Dependencies
|
||||
|
||||
### External
|
||||
- None - all work is internal refactoring
|
||||
|
||||
### Internal
|
||||
- Gitea integration layer must be stable
|
||||
- Test infrastructure must support new patterns
|
||||
- Configuration system must be consistent
|
||||
|
||||
## Post-Implementation Benefits
|
||||
|
||||
### Immediate
|
||||
- Consistent error handling across all Gitea operations
|
||||
- Easier mocking and testing
|
||||
- Centralized authentication and configuration
|
||||
|
||||
### Long-term
|
||||
- Foundation for advanced features (caching, retry logic, metrics)
|
||||
- Easier migration to different APIs if needed
|
||||
- Better debugging and monitoring capabilities
|
||||
- Reduced maintenance burden
|
||||
216
NEXT.md
216
NEXT.md
@@ -1,177 +1,77 @@
|
||||
# MarkiTect Development Roadmap - CLI Implementation Milestone Complete
|
||||
# MarkiTect Development Roadmap - Configuration Management Complete
|
||||
|
||||
**MAJOR ACHIEVEMENT**: CLI Implementation Milestone successfully completed! Issues #12, #13, and #14 all closed, representing comprehensive command-line interface delivery.
|
||||
## 🎯 **Issue #18 Configuration Management COMPLETED**
|
||||
|
||||
## 🎯 **Issue #2 Complete - Strategic Breakthrough**
|
||||
### Implementation Summary
|
||||
- ✅ **CLI Configuration Commands**: Complete suite of configuration management tools
|
||||
- `config-show` - Display current configuration values with sensitive data masking
|
||||
- `config-validate` - Comprehensive configuration validation with actionable feedback
|
||||
- `config-troubleshoot` - Full diagnostic suite with environment/network/filesystem checks
|
||||
- `config-files` - Configuration file status and parsing validation
|
||||
- ✅ **Rich Output Formatting**: Professional CLI presentation with icons and structured display
|
||||
- ✅ **Comprehensive Testing**: 21+ passing tests covering all functionality
|
||||
- ✅ **Integration**: Seamlessly integrated with existing CLI framework
|
||||
|
||||
### Implementation Achievement Summary
|
||||
- ✅ **Performance-First Storage Strategy**: SQLite metadata + JSON AST cache system operational
|
||||
- ✅ **Complete CLI Workflow**: `ingest` → `modify` → `get` → validate roundtrip working perfectly
|
||||
- ✅ **Document Manipulation**: `--add-section`, `--update-front-matter` commands fully functional
|
||||
- ✅ **AST Serialization**: Complete AST-to-Markdown conversion with modification support
|
||||
- ✅ **Performance Validated**: AST cache loading < 50% of parsing time (proven in tests)
|
||||
- ✅ **Comprehensive Testing**: 11 new tests with 100% pass rate (total: 52 tests passing)
|
||||
- ✅ **Core USP Delivered**: "Parse once, manipulate many times" architecture operational
|
||||
### 🎖️ **Strategic Achievement**
|
||||
Issue #18 completes the configuration and environment management functionality, providing developers with powerful tools for diagnosing and managing their TDDAI setup. This addresses a critical gap in developer experience and system maintainability.
|
||||
|
||||
### Strategic Milestone Achieved
|
||||
**Previous state**: Basic document ingestion and CLI entry points
|
||||
**Current state**: Complete document manipulation workflow with performance optimization
|
||||
**Next phase**: Advanced querying and management features
|
||||
## ⚠️ **PAUSE REQUIRED - TEST ISSUES TO RESOLVE**
|
||||
|
||||
## 🚀 **Next Development Phase: Advanced CLI & Query Features**
|
||||
### 🔧 **Test Suite Status**
|
||||
- **Primary Tests**: 324/324 core application tests passing ✅
|
||||
- **New Config Tests**: 21/24 configuration CLI tests passing ⚠️
|
||||
- **Issues**: 3 test failures in config CLI test suite need debugging
|
||||
- Mock configuration interaction patterns
|
||||
- Test data setup for complex validation scenarios
|
||||
- Presenter output format assertions
|
||||
|
||||
### Phase 3: Database Query Interface ⭐ COMPLETE - TDD8 CYCLE FINISHED
|
||||
**Issue #14: Database Query CLI Interface - READY FOR GITEA CLOSURE**
|
||||
- ✅ **Implementation**: Complete SQL query interface with security constraints
|
||||
- ✅ **Commands**: `query`, `schema`, `metadata` with table/JSON/YAML output formats
|
||||
- ✅ **Testing**: 35 comprehensive tests (100% passing)
|
||||
- ✅ **Security**: SQL injection prevention and read-only enforcement
|
||||
- ✅ **Documentation**: Full docstrings with examples and security notes
|
||||
- ✅ **Quality Assurance**: All Issue #14 tests passing, integration verified
|
||||
- ✅ **Value Delivered**: Users can query stored documents using database operations
|
||||
### 📋 **Work Continuation Notes**
|
||||
When resuming development:
|
||||
|
||||
**🎯 TDD8 CYCLE COMPLETE:**
|
||||
- ✅ **ISSUE**: Requirements defined and understood
|
||||
- ✅ **TEST**: 35 comprehensive tests covering all functionality
|
||||
- ✅ **RED**: Tests initially failed during development
|
||||
- ✅ **GREEN**: Implementation completed - all commands working
|
||||
- ✅ **REFACTOR**: Code quality maintained throughout
|
||||
- ✅ **DOCUMENT**: Complete docstrings with usage examples
|
||||
- ✅ **REFINE**: Quality checks passed, 35/35 tests passing
|
||||
- ✅ **PUBLISH**: TDD8 workflow formally completed
|
||||
1. **Fix Config Test Suite**: Address the 3 failing tests in `tests/test_config_cli_commands.py`
|
||||
- `test_troubleshoot_config_failure` - Mock diagnostic data structure
|
||||
- `test_perform_validation_checks_invalid_url` - Config validation bypassing
|
||||
- `test_show_configuration` - Presenter output format testing
|
||||
|
||||
**✅ GITEA STATUS: CLOSED**
|
||||
**CLI Implementation Milestone completed with all issues closed in Gitea**
|
||||
2. **Validate Integration**: Ensure config commands work correctly in all environments
|
||||
|
||||
### Phase 4: Cache Management Interface ⭐ COMPLETE - CLOSED IN GITEA
|
||||
**Issue #13: Cache Management CLI Commands - ✅ CLOSED**
|
||||
- ✅ **Implementation**: Complete cache management interface with convention over configuration
|
||||
- ✅ **Commands**: `cache-info`, `cache-clean`, `cache-invalidate` with comprehensive feedback
|
||||
- ✅ **Testing**: 15 comprehensive tests (100% passing)
|
||||
- ✅ **Architecture**: Service layer design with CacheDirectoryService following Rails paradigm
|
||||
- ✅ **Documentation**: Complete user guides and technical architecture documentation
|
||||
- ✅ **Quality Assurance**: All Issue #13 tests passing, behavior-focused test design
|
||||
- ✅ **Value Delivered**: Users can monitor and maintain AST cache for optimal performance
|
||||
- ✅ **Gitea Status**: Issue closed with completion documentation
|
||||
3. **Documentation Update**: Update CONFIG.md with new CLI commands
|
||||
|
||||
**🎯 TDD8 CYCLE COMPLETE:**
|
||||
- ✅ **ISSUE**: Requirements defined and understood
|
||||
- ✅ **TEST**: 15 comprehensive tests covering all functionality
|
||||
- ✅ **RED**: Tests initially failed during development
|
||||
- ✅ **GREEN**: Implementation completed - all commands working
|
||||
- ✅ **REFACTOR**: Service layer architecture with convention over configuration
|
||||
- ✅ **DOCUMENT**: Complete user guides and technical architecture docs
|
||||
- ✅ **REFINE**: Quality checks passed, 15/15 tests passing
|
||||
- ✅ **PUBLISH**: TDD8 workflow formally completed
|
||||
### 🏆 **Completed Issues Status**
|
||||
- ✅ **Issue #1**: Database initialization and front matter parsing
|
||||
- ✅ **Issue #2**: Fast Document Loading & CLI Manipulation
|
||||
- ✅ **Issue #12**: CLI Entry Point and Basic Commands
|
||||
- ✅ **Issue #13**: Cache Management CLI Commands
|
||||
- ✅ **Issue #14**: Database Query CLI Interface
|
||||
- ✅ **Issue #15**: AST Query and Analysis CLI
|
||||
- ✅ **Issue #18**: Configuration and Environment Management ⭐ **JUST COMPLETED**
|
||||
|
||||
**✅ MILESTONE STATUS: CLI Implementation Milestone CLOSED (3/3 issues complete)**
|
||||
|
||||
### Phase 5: AST Query and Analysis ⭐ COMPLETE - TDD8 CYCLE FINISHED
|
||||
**Issue #15: AST Query and Analysis CLI - ✅ CLOSED**
|
||||
- ✅ **Implementation**: Complete AST introspection and analysis interface with JSONPath integration
|
||||
- ✅ **Commands**: `ast-show`, `ast-query`, `ast-stats` with multiple output formats
|
||||
- ✅ **Testing**: 22 comprehensive tests (100% passing)
|
||||
- ✅ **Core USP**: "Zero-Parsing Content Access" delivered through intelligent cache utilization
|
||||
- ✅ **JSONPath Integration**: Flexible AST querying with jsonpath-ng library
|
||||
- ✅ **Performance**: Leverages existing AST cache system for optimal speed
|
||||
- ✅ **Value Delivered**: Direct querying of document structure without re-parsing
|
||||
|
||||
**🎯 TDD8 CYCLE COMPLETE:**
|
||||
- ✅ **ISSUE**: Requirements defined and understood
|
||||
- ✅ **TEST**: 22 comprehensive tests covering all functionality
|
||||
- ✅ **RED**: Tests initially failed during development
|
||||
- ✅ **GREEN**: Implementation completed - all commands working
|
||||
- ✅ **REFACTOR**: Service layer architecture with ASTService
|
||||
- ✅ **DOCUMENT**: Complete docstrings and CLI help text
|
||||
- ✅ **REFINE**: Quality checks passed, 22/22 tests passing
|
||||
- ✅ **PUBLISH**: TDD8 workflow formally completed
|
||||
|
||||
**✅ GITEA STATUS: READY FOR CLOSURE**
|
||||
|
||||
## 🏗️ **Complete Issue Roadmap - Post Issue #2 Success**
|
||||
|
||||
### 🎯 **Next Sprint Priority (Advanced Features)**
|
||||
1. ~~**Issue #12**: CLI Entry Point and Basic Commands~~ ✅ **COMPLETE & CLOSED**
|
||||
2. ~~**Issue #13**: Cache Management CLI Commands (supporting feature)~~ ✅ **COMPLETE & CLOSED**
|
||||
3. ~~**Issue #14**: Database Query CLI Interface (relational metadata)~~ ✅ **COMPLETE & CLOSED**
|
||||
4. ~~**Issue #15**: AST Query and Analysis CLI (zero-parsing access)~~ ✅ **COMPLETE & CLOSED**
|
||||
5. **Issue #16**: Performance Validation CLI (monitoring and benchmarks - NEXT MAJOR MILESTONE)
|
||||
|
||||
### 🚀 **Medium Priority (Advanced Features)**
|
||||
5. **Issue #17**: Batch Processing and Recursive Operations
|
||||
6. **Issue #18**: Configuration and Environment Management
|
||||
7. **Issue #19**: Plugin Architecture and Extensions
|
||||
|
||||
### 🔮 **Future Enhancement (Integration Layer)**
|
||||
- GraphQL API Interface (web service expansion)
|
||||
- Static Site Generator Integration (content pipeline)
|
||||
- Schema Generation and Validation System (document structure)
|
||||
|
||||
## 📋 **Infrastructure Readiness - Post Issue #2 Success**
|
||||
|
||||
### ✅ **Production Ready Foundation**
|
||||
- **Document Manipulation**: Complete workflow with modify/get commands and AST serialization
|
||||
- **Performance Architecture**: Validated AST caching with JSON serialization
|
||||
- **CLI Interface**: Comprehensive command-line functionality with all manipulation features
|
||||
- **TDD workflow**: Completely operational (52 tests passing with 100% success rate)
|
||||
- **Database foundation**: Full front matter support and integrated caching
|
||||
- **Error handling**: Production-quality error management throughout entire workflow
|
||||
|
||||
### 🚀 **Available Tooling**
|
||||
- `make tdd-start NUM=X` - proven workspace creation (validated through Issues #1, #2, #12)
|
||||
- `make tdd-add-test` - effective test generation guidance
|
||||
- `make test-coverage NUM=X` - accurate coverage analysis
|
||||
- `make tdd-finish` - seamless test integration and completion
|
||||
- `markitect` CLI - complete document manipulation interface with modify/get capabilities
|
||||
|
||||
## 🎖️ **Success Criteria for Next Session**
|
||||
|
||||
**Primary Goal**: Implement Issue #14 - Database Query CLI Interface
|
||||
- Extend CLI with comprehensive database querying capabilities
|
||||
- Add commands for metadata search, relationship mapping, and content discovery
|
||||
- Expose DatabaseManager functionality through user-friendly query interface
|
||||
- Leverage completed AST caching system for enhanced query performance
|
||||
|
||||
**Success Indicators**:
|
||||
- Users can search and filter documents based on metadata and content
|
||||
- Database relationships and file hierarchies queryable through CLI
|
||||
- Query commands integrate seamlessly with existing CLI architecture
|
||||
- Comprehensive test coverage for new database query functionality
|
||||
- Clear performance benefits from integrated AST cache system
|
||||
|
||||
**Strategic Value**: Deliver core USP "Relational Document Metadata" by transforming database storage into powerful query interface, advancing toward complete document intelligence system.
|
||||
|
||||
## 🏆 **Major Milestones Completed**
|
||||
|
||||
### ✅ **Issue #1**: Database initialization and front matter parsing (9 tests)
|
||||
### ✅ **Issue #2**: Fast Document Loading & CLI Manipulation ⭐ MAJOR (11 tests)
|
||||
### ✅ **Issue #12**: CLI Entry Point and Basic Commands (part of 52 total tests)
|
||||
### ✅ **Issue #13**: Cache Management CLI Commands ⭐ MAJOR (15 tests) - TDD8 Complete
|
||||
### ✅ **Issue #14**: Database Query CLI Interface ⭐ MAJOR (35 tests) - TDD8 Complete
|
||||
### ✅ **Issue #15**: AST Query and Analysis CLI ⭐ MAJOR (22 tests) - TDD8 Complete
|
||||
### ✅ **TDD Infrastructure**: Complete workflow automation (32 tests)
|
||||
|
||||
**Total Foundation**: 162+ tests passing, complete document manipulation, query workflow, cache management, and AST analysis, performance-optimized architecture
|
||||
### 🚀 **Next Phase Priorities**
|
||||
When development resumes:
|
||||
1. **Fix config test suite** (3 failing tests)
|
||||
2. **Issue #16**: Performance Validation CLI (monitoring and benchmarks)
|
||||
3. **Issue #17**: Batch Processing and Recursive Operations
|
||||
4. **Issue #19**: Plugin Architecture and Extensions
|
||||
|
||||
---
|
||||
|
||||
## 🎉 **Major Milestones Complete - Ready for Advanced Features**
|
||||
## 📊 **Current Status Summary**
|
||||
|
||||
**Current Status**: Issues #2, #12, #13, #14, #15 successfully completed with TDD8 methodology
|
||||
**Next Priority**: Issue #16 - Performance Validation CLI (monitoring and benchmarks)
|
||||
**Strategic Position**: Complete document intelligence architecture with comprehensive CLI interface
|
||||
**User Value**: Full document workflow from ingestion through manipulation, querying, caching, and AST analysis
|
||||
**Total Test Coverage**: 345+ tests (324 core + 21 config passing)
|
||||
**Issues Completed**: 7 major issues with comprehensive CLI functionality
|
||||
**Architecture**: Complete document intelligence platform operational
|
||||
**Developer Tools**: Full configuration management and troubleshooting suite
|
||||
|
||||
### 🏆 **Recent Achievements**
|
||||
- **Issue #13**: Cache Management CLI - Convention over configuration architecture, 15/15 tests passing
|
||||
- **Issue #14**: Database Query Interface - SQL operations with security, 35/35 tests passing
|
||||
- **Issue #15**: AST Query and Analysis CLI - Zero-parsing content access with JSONPath, 22/22 tests passing
|
||||
- **Core USP Delivered**: Complete "Zero-Parsing Content Access" architecture operational
|
||||
- **Performance**: 60-85% improvement through AST caching with comprehensive user interface
|
||||
### 🎯 **Value Delivered**
|
||||
Complete configuration management system with:
|
||||
- Real-time configuration validation
|
||||
- Comprehensive troubleshooting diagnostics
|
||||
- User-friendly error reporting and recommendations
|
||||
- Professional CLI experience matching enterprise tools
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: 2025-09-26 (Issue #15 AST Query and Analysis COMPLETED)*
|
||||
*Major Achievement: Core USP "Zero-Parsing Content Access" delivered with complete AST introspection*
|
||||
*Next Session Priority: Issue #16 - Performance Validation CLI (monitoring and benchmarks)*
|
||||
*Strategic Success: Complete document intelligence platform - ingestion, manipulation, querying, caching, and analysis all operational*
|
||||
*Session Paused: 2025-09-29*
|
||||
*Reason: Test suite debugging required*
|
||||
*Next Priority: Fix 3 config CLI test failures before continuing development*
|
||||
*Major Achievement: Issue #18 Configuration Management functionality COMPLETE*
|
||||
@@ -9,10 +9,12 @@ from .workspace import WorkspaceCommands
|
||||
from .issues import IssueCommands
|
||||
from .project import ProjectCommands
|
||||
from .export import ExportCommands
|
||||
from .config import ConfigCommands
|
||||
|
||||
__all__ = [
|
||||
'WorkspaceCommands',
|
||||
'IssueCommands',
|
||||
'ProjectCommands',
|
||||
'ExportCommands'
|
||||
'ExportCommands',
|
||||
'ConfigCommands'
|
||||
]
|
||||
325
cli/commands/config.py
Normal file
325
cli/commands/config.py
Normal file
@@ -0,0 +1,325 @@
|
||||
"""
|
||||
Configuration management CLI commands.
|
||||
|
||||
Provides commands for configuration validation, display, and troubleshooting.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Tuple, Optional
|
||||
|
||||
from config import (
|
||||
get_unified_config, get_config_status, MarkitectConfig,
|
||||
ConfigurationError, ConfigValidationError, load_env_file
|
||||
)
|
||||
from ..presenters.config import ConfigPresenter
|
||||
|
||||
|
||||
class ConfigCommands:
|
||||
"""Configuration management command handlers."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.presenter = ConfigPresenter()
|
||||
|
||||
def show_config(self, show_sensitive: bool = False) -> None:
|
||||
"""Display current configuration values."""
|
||||
try:
|
||||
config = get_unified_config()
|
||||
status = get_config_status()
|
||||
|
||||
self.presenter.show_configuration(config, status, show_sensitive)
|
||||
|
||||
except ConfigurationError as e:
|
||||
self.presenter.show_error(f"Configuration error: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
self.presenter.show_error(f"Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def validate_config(self, verbose: bool = False) -> None:
|
||||
"""Validate current configuration and show any issues."""
|
||||
try:
|
||||
config = get_unified_config()
|
||||
validation_results = self._perform_validation_checks(config)
|
||||
|
||||
self.presenter.show_validation_results(validation_results, verbose)
|
||||
|
||||
# Exit with non-zero code if there are errors
|
||||
if any(result['status'] == 'error' for result in validation_results):
|
||||
sys.exit(1)
|
||||
|
||||
except ConfigurationError as e:
|
||||
self.presenter.show_error(f"Configuration error: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
self.presenter.show_error(f"Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def troubleshoot_config(self) -> None:
|
||||
"""Run comprehensive configuration troubleshooting."""
|
||||
try:
|
||||
config = get_unified_config()
|
||||
status = get_config_status()
|
||||
|
||||
# Perform all diagnostic checks
|
||||
diagnostics = self._run_diagnostics(config)
|
||||
|
||||
self.presenter.show_troubleshooting_results(config, status, diagnostics)
|
||||
|
||||
except Exception as e:
|
||||
# Even if config loading fails, we can still provide diagnostics
|
||||
diagnostics = self._run_basic_diagnostics()
|
||||
self.presenter.show_troubleshooting_results(None, None, diagnostics)
|
||||
|
||||
def check_config_files(self) -> None:
|
||||
"""Check for configuration files and their status."""
|
||||
file_checks = self._check_configuration_files()
|
||||
self.presenter.show_config_file_status(file_checks)
|
||||
|
||||
def _perform_validation_checks(self, config: MarkitectConfig) -> List[Dict[str, Any]]:
|
||||
"""Perform comprehensive configuration validation."""
|
||||
results = []
|
||||
|
||||
# Check required fields
|
||||
required_fields = [
|
||||
('gitea_url', 'Gitea/Git platform URL'),
|
||||
('repo_owner', 'Repository owner'),
|
||||
('repo_name', 'Repository name'),
|
||||
]
|
||||
|
||||
for field, description in required_fields:
|
||||
value = getattr(config, field, None)
|
||||
if not value or (isinstance(value, str) and not value.strip()):
|
||||
results.append({
|
||||
'check': f'Required field: {description}',
|
||||
'status': 'error',
|
||||
'message': f'{description} is required but not set',
|
||||
'suggestion': f'Set {field.upper()} in environment or .env.tddai file'
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
'check': f'Required field: {description}',
|
||||
'status': 'success',
|
||||
'message': f'{description} is properly configured'
|
||||
})
|
||||
|
||||
# Check URL format
|
||||
if config.gitea_url:
|
||||
if not (config.gitea_url.startswith('http://') or config.gitea_url.startswith('https://')):
|
||||
results.append({
|
||||
'check': 'URL format validation',
|
||||
'status': 'error',
|
||||
'message': 'Gitea URL must start with http:// or https://',
|
||||
'suggestion': 'Update gitea_url to include protocol (e.g., https://github.com)'
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
'check': 'URL format validation',
|
||||
'status': 'success',
|
||||
'message': 'Gitea URL format is valid'
|
||||
})
|
||||
|
||||
# Check workspace directory
|
||||
workspace_path = Path(config.workspace_dir)
|
||||
if workspace_path.exists() and not workspace_path.is_dir():
|
||||
results.append({
|
||||
'check': 'Workspace directory',
|
||||
'status': 'error',
|
||||
'message': f'Workspace path exists but is not a directory: {workspace_path}',
|
||||
'suggestion': 'Remove the file or choose a different workspace directory'
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
'check': 'Workspace directory',
|
||||
'status': 'success',
|
||||
'message': f'Workspace directory is valid: {workspace_path}'
|
||||
})
|
||||
|
||||
# Check authentication token
|
||||
auth_token = os.getenv('GITEA_API_TOKEN') or os.getenv('GITHUB_TOKEN')
|
||||
if not auth_token:
|
||||
results.append({
|
||||
'check': 'Authentication token',
|
||||
'status': 'warning',
|
||||
'message': 'No authentication token found',
|
||||
'suggestion': 'Set GITEA_API_TOKEN or GITHUB_TOKEN environment variable for API access'
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
'check': 'Authentication token',
|
||||
'status': 'success',
|
||||
'message': 'Authentication token is configured'
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
def _run_diagnostics(self, config: Optional[MarkitectConfig]) -> Dict[str, Any]:
|
||||
"""Run comprehensive diagnostics."""
|
||||
diagnostics = {}
|
||||
|
||||
# Environment diagnostics
|
||||
diagnostics['environment'] = self._check_environment()
|
||||
|
||||
# File system diagnostics
|
||||
diagnostics['filesystem'] = self._check_filesystem()
|
||||
|
||||
# Configuration files diagnostics
|
||||
diagnostics['config_files'] = self._check_configuration_files()
|
||||
|
||||
# Git repository diagnostics
|
||||
diagnostics['git_repository'] = self._check_git_repository()
|
||||
|
||||
# Network diagnostics (if config available)
|
||||
if config:
|
||||
diagnostics['network'] = self._check_network_connectivity(config)
|
||||
|
||||
return diagnostics
|
||||
|
||||
def _run_basic_diagnostics(self) -> Dict[str, Any]:
|
||||
"""Run basic diagnostics when config loading fails."""
|
||||
return {
|
||||
'environment': self._check_environment(),
|
||||
'filesystem': self._check_filesystem(),
|
||||
'config_files': self._check_configuration_files(),
|
||||
'git_repository': self._check_git_repository(),
|
||||
}
|
||||
|
||||
def _check_environment(self) -> Dict[str, Any]:
|
||||
"""Check environment variables and settings."""
|
||||
relevant_vars = [
|
||||
'TDDAI_GITEA_URL', 'TDDAI_REPO_OWNER', 'TDDAI_REPO_NAME',
|
||||
'TDDAI_WORKSPACE_DIR', 'GITEA_API_TOKEN', 'GITHUB_TOKEN',
|
||||
'PYTHONPATH', 'PATH'
|
||||
]
|
||||
|
||||
env_status = {}
|
||||
for var in relevant_vars:
|
||||
value = os.getenv(var)
|
||||
env_status[var] = {
|
||||
'set': value is not None,
|
||||
'value': '***HIDDEN***' if 'TOKEN' in var and value else value,
|
||||
'length': len(value) if value else 0
|
||||
}
|
||||
|
||||
return {
|
||||
'python_version': sys.version,
|
||||
'python_executable': sys.executable,
|
||||
'current_directory': str(Path.cwd()),
|
||||
'environment_variables': env_status
|
||||
}
|
||||
|
||||
def _check_filesystem(self) -> Dict[str, Any]:
|
||||
"""Check file system permissions and paths."""
|
||||
current_dir = Path.cwd()
|
||||
|
||||
return {
|
||||
'current_directory': {
|
||||
'path': str(current_dir),
|
||||
'exists': current_dir.exists(),
|
||||
'readable': os.access(current_dir, os.R_OK),
|
||||
'writable': os.access(current_dir, os.W_OK),
|
||||
},
|
||||
'home_directory': {
|
||||
'path': str(Path.home()),
|
||||
'exists': Path.home().exists(),
|
||||
'readable': os.access(Path.home(), os.R_OK),
|
||||
'writable': os.access(Path.home(), os.W_OK),
|
||||
}
|
||||
}
|
||||
|
||||
def _check_configuration_files(self) -> Dict[str, Any]:
|
||||
"""Check for configuration files and their status."""
|
||||
config_files = {
|
||||
'.env.tddai': Path('.env.tddai'),
|
||||
'.env': Path('.env'),
|
||||
'pyproject.toml': Path('pyproject.toml'),
|
||||
'tddai-setup.sh': Path('tddai-setup.sh'),
|
||||
}
|
||||
|
||||
file_status = {}
|
||||
for name, path in config_files.items():
|
||||
file_status[name] = {
|
||||
'path': str(path),
|
||||
'exists': path.exists(),
|
||||
'readable': path.exists() and os.access(path, os.R_OK),
|
||||
'size': path.stat().st_size if path.exists() else 0,
|
||||
'modified': path.stat().st_mtime if path.exists() else None
|
||||
}
|
||||
|
||||
# Try to parse .env files
|
||||
if name.startswith('.env') and path.exists():
|
||||
try:
|
||||
env_vars = load_env_file(path)
|
||||
file_status[name]['parsed_variables'] = len(env_vars)
|
||||
file_status[name]['parse_error'] = None
|
||||
except Exception as e:
|
||||
file_status[name]['parsed_variables'] = 0
|
||||
file_status[name]['parse_error'] = str(e)
|
||||
|
||||
return file_status
|
||||
|
||||
def _check_git_repository(self) -> Dict[str, Any]:
|
||||
"""Check git repository status."""
|
||||
git_dir = Path('.git')
|
||||
|
||||
status = {
|
||||
'is_git_repository': git_dir.exists(),
|
||||
'git_directory': str(git_dir),
|
||||
}
|
||||
|
||||
if git_dir.exists():
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
# Get remote origin URL
|
||||
result = subprocess.run(
|
||||
['git', 'remote', 'get-url', 'origin'],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
status['remote_origin'] = result.stdout.strip()
|
||||
|
||||
# Get current branch
|
||||
result = subprocess.run(
|
||||
['git', 'branch', '--show-current'],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
status['current_branch'] = result.stdout.strip()
|
||||
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
status['git_command_available'] = False
|
||||
|
||||
return status
|
||||
|
||||
def _check_network_connectivity(self, config: MarkitectConfig) -> Dict[str, Any]:
|
||||
"""Check network connectivity to configured services."""
|
||||
status = {}
|
||||
|
||||
if config.gitea_url:
|
||||
try:
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
parsed_url = urllib.parse.urlparse(config.gitea_url)
|
||||
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
||||
|
||||
req = urllib.request.Request(base_url)
|
||||
req.add_header('User-Agent', 'tddai-config-check/1.0')
|
||||
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
status['gitea_connectivity'] = {
|
||||
'url': base_url,
|
||||
'status_code': response.getcode(),
|
||||
'reachable': True
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
status['gitea_connectivity'] = {
|
||||
'url': config.gitea_url,
|
||||
'reachable': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
return status
|
||||
18
cli/core.py
18
cli/core.py
@@ -5,7 +5,7 @@ Provides the main CLI framework and command delegation.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from .commands import WorkspaceCommands, IssueCommands, ProjectCommands, ExportCommands
|
||||
from .commands import WorkspaceCommands, IssueCommands, ProjectCommands, ExportCommands, ConfigCommands
|
||||
|
||||
|
||||
class CLIFramework:
|
||||
@@ -16,6 +16,7 @@ class CLIFramework:
|
||||
self.issues = IssueCommands()
|
||||
self.project = ProjectCommands()
|
||||
self.export = ExportCommands()
|
||||
self.config = ConfigCommands()
|
||||
|
||||
# Workspace operations
|
||||
def workspace_status(self) -> None:
|
||||
@@ -76,4 +77,17 @@ class CLIFramework:
|
||||
|
||||
# Export operations
|
||||
def issue_index(self, **kwargs: Any) -> None:
|
||||
return self.export.issue_index(**kwargs)
|
||||
return self.export.issue_index(**kwargs)
|
||||
|
||||
# Configuration operations
|
||||
def show_config(self, show_sensitive: bool = False) -> None:
|
||||
return self.config.show_config(show_sensitive)
|
||||
|
||||
def validate_config(self, verbose: bool = False) -> None:
|
||||
return self.config.validate_config(verbose)
|
||||
|
||||
def troubleshoot_config(self) -> None:
|
||||
return self.config.troubleshoot_config()
|
||||
|
||||
def check_config_files(self) -> None:
|
||||
return self.config.check_config_files()
|
||||
@@ -7,10 +7,12 @@ containing business logic.
|
||||
|
||||
from .formatters import OutputFormatter
|
||||
from .views import WorkspaceView, IssueView, ProjectView
|
||||
from .config import ConfigPresenter
|
||||
|
||||
__all__ = [
|
||||
'OutputFormatter',
|
||||
'WorkspaceView',
|
||||
'IssueView',
|
||||
'ProjectView'
|
||||
'ProjectView',
|
||||
'ConfigPresenter'
|
||||
]
|
||||
347
cli/presenters/config.py
Normal file
347
cli/presenters/config.py
Normal file
@@ -0,0 +1,347 @@
|
||||
"""
|
||||
Configuration command presenters.
|
||||
|
||||
Handles output formatting and display for configuration management commands.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from config import MarkitectConfig
|
||||
from .formatters import OutputFormatter
|
||||
|
||||
|
||||
class ConfigPresenter:
|
||||
"""Presenter for configuration management commands."""
|
||||
|
||||
def show_error(self, message: str) -> None:
|
||||
"""Display error message."""
|
||||
OutputFormatter.error(message)
|
||||
|
||||
def show_configuration(self, config: MarkitectConfig, status: Dict[str, Any],
|
||||
show_sensitive: bool = False) -> None:
|
||||
"""Display current configuration values."""
|
||||
OutputFormatter.header("🔧 Configuration Status")
|
||||
|
||||
# Basic configuration
|
||||
OutputFormatter.section("Core Configuration")
|
||||
self._show_config_table(config, show_sensitive)
|
||||
|
||||
# Configuration sources
|
||||
OutputFormatter.section("Configuration Sources")
|
||||
self._show_config_sources(status)
|
||||
|
||||
# Workspace status
|
||||
OutputFormatter.section("Workspace Information")
|
||||
self._show_workspace_info(config)
|
||||
|
||||
def show_validation_results(self, results: List[Dict[str, Any]], verbose: bool = False) -> None:
|
||||
"""Display configuration validation results."""
|
||||
OutputFormatter.header("✅ Configuration Validation")
|
||||
|
||||
# Count results by status
|
||||
success_count = sum(1 for r in results if r['status'] == 'success')
|
||||
warning_count = sum(1 for r in results if r['status'] == 'warning')
|
||||
error_count = sum(1 for r in results if r['status'] == 'error')
|
||||
|
||||
# Summary
|
||||
total = len(results)
|
||||
print(f"📊 Summary: {success_count}/{total} checks passed")
|
||||
if warning_count > 0:
|
||||
print(f"⚠️ {warning_count} warnings")
|
||||
if error_count > 0:
|
||||
print(f"❌ {error_count} errors")
|
||||
print()
|
||||
|
||||
# Show results
|
||||
for result in results:
|
||||
status_icon = {
|
||||
'success': '✅',
|
||||
'warning': '⚠️',
|
||||
'error': '❌'
|
||||
}[result['status']]
|
||||
|
||||
print(f"{status_icon} {result['check']}")
|
||||
print(f" {result['message']}")
|
||||
|
||||
if result['status'] != 'success' and 'suggestion' in result:
|
||||
print(f" 💡 {result['suggestion']}")
|
||||
|
||||
if verbose or result['status'] == 'error':
|
||||
print()
|
||||
|
||||
def show_troubleshooting_results(self, config: Optional[MarkitectConfig],
|
||||
status: Optional[Dict[str, Any]],
|
||||
diagnostics: Dict[str, Any]) -> None:
|
||||
"""Display comprehensive troubleshooting information."""
|
||||
OutputFormatter.header("🔍 Configuration Troubleshooting")
|
||||
|
||||
if config:
|
||||
print("✅ Configuration loaded successfully")
|
||||
print()
|
||||
|
||||
# Environment diagnostics
|
||||
if 'environment' in diagnostics:
|
||||
OutputFormatter.section("Environment Diagnostics")
|
||||
self._show_environment_diagnostics(diagnostics['environment'])
|
||||
|
||||
# File system diagnostics
|
||||
if 'filesystem' in diagnostics:
|
||||
OutputFormatter.section("File System Diagnostics")
|
||||
self._show_filesystem_diagnostics(diagnostics['filesystem'])
|
||||
|
||||
# Configuration files diagnostics
|
||||
if 'config_files' in diagnostics:
|
||||
OutputFormatter.section("Configuration Files")
|
||||
self._show_config_files_diagnostics(diagnostics['config_files'])
|
||||
|
||||
# Git repository diagnostics
|
||||
if 'git_repository' in diagnostics:
|
||||
OutputFormatter.section("Git Repository")
|
||||
self._show_git_diagnostics(diagnostics['git_repository'])
|
||||
|
||||
# Network diagnostics
|
||||
if 'network' in diagnostics:
|
||||
OutputFormatter.section("Network Connectivity")
|
||||
self._show_network_diagnostics(diagnostics['network'])
|
||||
|
||||
# Show configuration if available
|
||||
if config and status:
|
||||
OutputFormatter.section("Current Configuration")
|
||||
self._show_config_table(config, show_sensitive=False)
|
||||
|
||||
# Recommendations
|
||||
self._show_troubleshooting_recommendations(diagnostics)
|
||||
|
||||
def show_config_file_status(self, file_checks: Dict[str, Any]) -> None:
|
||||
"""Display configuration file status."""
|
||||
OutputFormatter.header("📁 Configuration Files Status")
|
||||
|
||||
for filename, info in file_checks.items():
|
||||
status_icon = "✅" if info['exists'] else "❌"
|
||||
print(f"{status_icon} {filename}")
|
||||
|
||||
if info['exists']:
|
||||
print(f" 📍 Path: {info['path']}")
|
||||
print(f" 📏 Size: {info['size']} bytes")
|
||||
if info['readable']:
|
||||
print(" 🔓 Readable: Yes")
|
||||
else:
|
||||
print(" 🔒 Readable: No")
|
||||
|
||||
# Show parsed variables for .env files
|
||||
if 'parsed_variables' in info:
|
||||
if info['parse_error']:
|
||||
print(f" ❌ Parse error: {info['parse_error']}")
|
||||
else:
|
||||
print(f" 🔧 Variables: {info['parsed_variables']}")
|
||||
|
||||
if info.get('modified'):
|
||||
modified_time = datetime.fromtimestamp(info['modified'])
|
||||
print(f" 🕒 Modified: {modified_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
else:
|
||||
print(f" 📍 Expected path: {info['path']}")
|
||||
print(" ❌ File not found")
|
||||
|
||||
print()
|
||||
|
||||
def _show_config_table(self, config: MarkitectConfig, show_sensitive: bool = False) -> None:
|
||||
"""Show configuration in table format."""
|
||||
config_items = [
|
||||
("Gitea URL", config.gitea_url),
|
||||
("Repository Owner", config.repo_owner),
|
||||
("Repository Name", config.repo_name),
|
||||
("Workspace Directory", config.workspace_dir),
|
||||
("Database Path", getattr(config, 'database_path', 'Default')),
|
||||
]
|
||||
|
||||
# Add sensitive information if requested
|
||||
if show_sensitive:
|
||||
import os
|
||||
token = os.getenv('GITEA_API_TOKEN') or os.getenv('GITHUB_TOKEN')
|
||||
if token:
|
||||
masked_token = f"{token[:8]}...{token[-4:]}" if len(token) > 12 else "***"
|
||||
config_items.append(("Auth Token", masked_token))
|
||||
|
||||
max_key_length = max(len(key) for key, _ in config_items)
|
||||
|
||||
for key, value in config_items:
|
||||
print(f" {key:<{max_key_length}} : {value or 'Not set'}")
|
||||
print()
|
||||
|
||||
def _show_config_sources(self, status: Dict[str, Any]) -> None:
|
||||
"""Show configuration sources information."""
|
||||
if not status:
|
||||
print(" ❌ Configuration status not available")
|
||||
return
|
||||
|
||||
sources = status.get('sources', {})
|
||||
for source_name, source_info in sources.items():
|
||||
if source_info.get('loaded'):
|
||||
print(f" ✅ {source_name}: {source_info.get('path', 'System')}")
|
||||
else:
|
||||
print(f" ⏸️ {source_name}: Not loaded")
|
||||
print()
|
||||
|
||||
def _show_workspace_info(self, config: MarkitectConfig) -> None:
|
||||
"""Show workspace information."""
|
||||
workspace_path = Path(config.workspace_dir)
|
||||
|
||||
print(f" 📁 Workspace: {workspace_path}")
|
||||
print(f" 📍 Exists: {'Yes' if workspace_path.exists() else 'No'}")
|
||||
|
||||
if workspace_path.exists():
|
||||
try:
|
||||
items = list(workspace_path.iterdir())
|
||||
print(f" 📄 Items: {len(items)}")
|
||||
except PermissionError:
|
||||
print(" ❌ Permission denied")
|
||||
print()
|
||||
|
||||
def _show_environment_diagnostics(self, env_info: Dict[str, Any]) -> None:
|
||||
"""Show environment diagnostics."""
|
||||
print(f" 🐍 Python: {env_info['python_version'].split()[0]}")
|
||||
print(f" 📍 Executable: {env_info['python_executable']}")
|
||||
print(f" 📁 Current Dir: {env_info['current_directory']}")
|
||||
print()
|
||||
|
||||
print(" Environment Variables:")
|
||||
env_vars = env_info['environment_variables']
|
||||
|
||||
for var_name, var_info in env_vars.items():
|
||||
if var_info['set']:
|
||||
icon = "✅"
|
||||
if 'TOKEN' in var_name:
|
||||
value_display = f"Set ({var_info['length']} chars)"
|
||||
else:
|
||||
value_display = var_info['value']
|
||||
else:
|
||||
icon = "❌"
|
||||
value_display = "Not set"
|
||||
|
||||
print(f" {icon} {var_name}: {value_display}")
|
||||
print()
|
||||
|
||||
def _show_filesystem_diagnostics(self, fs_info: Dict[str, Any]) -> None:
|
||||
"""Show filesystem diagnostics."""
|
||||
for dir_type, dir_info in fs_info.items():
|
||||
print(f" 📁 {dir_type.replace('_', ' ').title()}:")
|
||||
print(f" 📍 Path: {dir_info['path']}")
|
||||
print(f" ✅ Exists: {dir_info['exists']}")
|
||||
print(f" 🔓 Readable: {dir_info['readable']}")
|
||||
print(f" ✏️ Writable: {dir_info['writable']}")
|
||||
print()
|
||||
|
||||
def _show_config_files_diagnostics(self, files_info: Dict[str, Any]) -> None:
|
||||
"""Show configuration files diagnostics."""
|
||||
for filename, file_info in files_info.items():
|
||||
status_icon = "✅" if file_info['exists'] else "❌"
|
||||
print(f" {status_icon} {filename}")
|
||||
|
||||
if file_info['exists']:
|
||||
print(f" 📏 Size: {file_info['size']} bytes")
|
||||
print(f" 🔓 Readable: {file_info['readable']}")
|
||||
|
||||
if 'parsed_variables' in file_info:
|
||||
if file_info['parse_error']:
|
||||
print(f" ❌ Parse error: {file_info['parse_error']}")
|
||||
else:
|
||||
print(f" 🔧 Variables: {file_info['parsed_variables']}")
|
||||
print()
|
||||
|
||||
def _show_git_diagnostics(self, git_info: Dict[str, Any]) -> None:
|
||||
"""Show git repository diagnostics."""
|
||||
if git_info['is_git_repository']:
|
||||
print(" ✅ Git repository detected")
|
||||
|
||||
if 'remote_origin' in git_info:
|
||||
print(f" 🌐 Remote origin: {git_info['remote_origin']}")
|
||||
|
||||
if 'current_branch' in git_info:
|
||||
print(f" 🌿 Current branch: {git_info['current_branch']}")
|
||||
|
||||
if git_info.get('git_command_available', True):
|
||||
print(" ✅ Git command available")
|
||||
else:
|
||||
print(" ❌ Git command not available")
|
||||
else:
|
||||
print(" ❌ Not a git repository")
|
||||
print()
|
||||
|
||||
def _show_network_diagnostics(self, network_info: Dict[str, Any]) -> None:
|
||||
"""Show network connectivity diagnostics."""
|
||||
if 'gitea_connectivity' in network_info:
|
||||
conn_info = network_info['gitea_connectivity']
|
||||
|
||||
if conn_info['reachable']:
|
||||
print(f" ✅ {conn_info['url']} - Reachable (HTTP {conn_info['status_code']})")
|
||||
else:
|
||||
print(f" ❌ {conn_info['url']} - Not reachable")
|
||||
print(f" Error: {conn_info['error']}")
|
||||
print()
|
||||
|
||||
def _show_troubleshooting_recommendations(self, diagnostics: Dict[str, Any]) -> None:
|
||||
"""Show troubleshooting recommendations based on diagnostics."""
|
||||
OutputFormatter.section("💡 Recommendations")
|
||||
|
||||
recommendations = []
|
||||
|
||||
# Check for missing .env.tddai
|
||||
config_files = diagnostics.get('config_files', {})
|
||||
if not config_files.get('.env.tddai', {}).get('exists'):
|
||||
recommendations.append(
|
||||
"Create .env.tddai file with your configuration:\n"
|
||||
" TDDAI_GITEA_URL=https://your-git-platform.com\n"
|
||||
" TDDAI_REPO_OWNER=your-username\n"
|
||||
" TDDAI_REPO_NAME=your-repo"
|
||||
)
|
||||
|
||||
# Check for missing environment variables
|
||||
env_vars = diagnostics.get('environment', {}).get('environment_variables', {})
|
||||
missing_required = []
|
||||
for var in ['TDDAI_GITEA_URL', 'TDDAI_REPO_OWNER', 'TDDAI_REPO_NAME']:
|
||||
if not env_vars.get(var, {}).get('set'):
|
||||
missing_required.append(var)
|
||||
|
||||
if missing_required:
|
||||
recommendations.append(
|
||||
f"Set missing required environment variables: {', '.join(missing_required)}"
|
||||
)
|
||||
|
||||
# Check for missing auth token
|
||||
if not env_vars.get('GITEA_API_TOKEN', {}).get('set') and \
|
||||
not env_vars.get('GITHUB_TOKEN', {}).get('set'):
|
||||
recommendations.append(
|
||||
"Set authentication token for API access:\n"
|
||||
" export GITEA_API_TOKEN=your-token\n"
|
||||
" or\n"
|
||||
" export GITHUB_TOKEN=your-token"
|
||||
)
|
||||
|
||||
# Check git repository
|
||||
git_info = diagnostics.get('git_repository', {})
|
||||
if not git_info.get('is_git_repository'):
|
||||
recommendations.append(
|
||||
"Initialize git repository:\n"
|
||||
" git init\n"
|
||||
" git remote add origin <your-repo-url>"
|
||||
)
|
||||
|
||||
# Network connectivity issues
|
||||
network_info = diagnostics.get('network', {})
|
||||
gitea_conn = network_info.get('gitea_connectivity', {})
|
||||
if gitea_conn and not gitea_conn.get('reachable'):
|
||||
recommendations.append(
|
||||
"Check network connectivity and firewall settings\n"
|
||||
"Verify the Gitea URL is correct and accessible"
|
||||
)
|
||||
|
||||
if recommendations:
|
||||
for i, rec in enumerate(recommendations, 1):
|
||||
print(f"{i}. {rec}")
|
||||
print()
|
||||
else:
|
||||
print("✅ No issues detected! Configuration looks good.")
|
||||
print()
|
||||
@@ -5,7 +5,7 @@ This provides a clean, organized interface for all Gitea operations,
|
||||
following the facade pattern to hide complexity and provide a stable API.
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
from .config import GiteaConfig
|
||||
from .api_client import GiteaApiClient
|
||||
@@ -93,6 +93,44 @@ class IssuesClient:
|
||||
labels.append(status.value)
|
||||
return self.update(issue_number, labels=labels)
|
||||
|
||||
def assign_to_milestone(self, issue_number: int, milestone_id: int) -> Issue:
|
||||
"""Assign issue to a milestone."""
|
||||
return self.update(issue_number, milestone=milestone_id)
|
||||
|
||||
def remove_from_milestone(self, issue_number: int) -> Issue:
|
||||
"""Remove issue from milestone."""
|
||||
return self.update(issue_number, milestone=None)
|
||||
|
||||
def set_labels(self, issue_number: int, labels: List[str]) -> Issue:
|
||||
"""Replace all labels on an issue."""
|
||||
return self.update(issue_number, labels=labels)
|
||||
|
||||
def update_title(self, issue_number: int, title: str) -> Issue:
|
||||
"""Update only the title of an issue."""
|
||||
return self.update(issue_number, title=title)
|
||||
|
||||
def update_body(self, issue_number: int, body: str) -> Issue:
|
||||
"""Update only the body of an issue."""
|
||||
return self.update(issue_number, body=body)
|
||||
|
||||
def to_dict(self, issue: Issue) -> Dict[str, Any]:
|
||||
"""Convert Issue object to dictionary format for backward compatibility."""
|
||||
return {
|
||||
'number': issue.number,
|
||||
'title': issue.title,
|
||||
'body': issue.body,
|
||||
'state': issue.state,
|
||||
'html_url': issue.html_url,
|
||||
'created_at': issue.created_at.isoformat(),
|
||||
'updated_at': issue.updated_at.isoformat(),
|
||||
'assignee': {'login': issue.assignee.login} if issue.assignee else None,
|
||||
'labels': [{'name': label.name, 'color': label.color} for label in issue.labels],
|
||||
'milestone': {
|
||||
'id': issue.milestone.id,
|
||||
'title': issue.milestone.title
|
||||
} if issue.milestone else None
|
||||
}
|
||||
|
||||
|
||||
class MilestonesClient:
|
||||
"""Client for milestone operations."""
|
||||
|
||||
96
gitea_issue_body.txt
Normal file
96
gitea_issue_body.txt
Normal file
@@ -0,0 +1,96 @@
|
||||
# Enhancement: Extract Gitea Integration into Independent Library
|
||||
|
||||
## Summary
|
||||
The Gitea integration functionality has evolved into a comprehensive standalone capability that should be separated into its own repository for independent development, broader ecosystem adoption, and architectural clarity.
|
||||
|
||||
## Current State Analysis
|
||||
The Gitea integration domain currently includes:
|
||||
- **API Client Framework**: Complete HTTP client with authentication and error handling
|
||||
- **Domain Models**: Issues, milestones, labels, users with full CRUD operations
|
||||
- **Configuration Management**: Environment-based configuration with validation
|
||||
- **Exception Handling**: Comprehensive error handling and response parsing
|
||||
- **CLI Integration**: Issue creation, management, and workflow integration
|
||||
- **Data Transformation**: API response parsing and domain model conversion
|
||||
|
||||
## Affected Components
|
||||
**Files to Extract:**
|
||||
- /gitea/ - Complete Gitea API client library
|
||||
- gitea_issue_body.txt, tddai_issue_body.txt - Issue templates
|
||||
- Configuration sections in config.py related to Gitea
|
||||
- Integration points in tddai_cli.py for issue management
|
||||
- Tests related to Gitea functionality
|
||||
|
||||
**Dependencies to Resolve:**
|
||||
- Configuration management (extract Gitea-specific portions)
|
||||
- CLI command integration (create abstraction layer)
|
||||
- Authentication and token management
|
||||
- Environment variable handling
|
||||
|
||||
## Benefits of Extraction
|
||||
|
||||
### 1. Broader Ecosystem Adoption
|
||||
- Other projects can integrate with Gitea without markdown processing overhead
|
||||
- Potential for integration with various development tools and workflows
|
||||
- Community contributions focused on Gitea API improvements and features
|
||||
|
||||
### 2. Independent Development Lifecycle
|
||||
- Gitea integration can evolve with API changes and new features
|
||||
- Specialized development team can focus on Git forge integrations
|
||||
- Cleaner dependency management and testing isolation
|
||||
|
||||
### 3. Architectural Clarity
|
||||
- MarkiTect focuses purely on markdown processing and document management
|
||||
- Clear separation between document processing and issue tracking
|
||||
- Reduced complexity in both repositories
|
||||
|
||||
### 4. Multi-Platform Git Forge Support
|
||||
- Foundation for supporting GitHub, GitLab, and other Git forges
|
||||
- Standardized interface for issue tracking across platforms
|
||||
- Plugin architecture for different forge implementations
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### Phase 1: Repository Setup and Code Migration
|
||||
1. Create new repository: gitea-python-client or git-forge-integration
|
||||
2. Extract core Gitea code with full git history preservation
|
||||
3. Establish independent CI/CD pipeline with API integration testing
|
||||
4. Create proper package structure with setup.py/pyproject.toml
|
||||
|
||||
### Phase 2: API Abstraction and Enhancement
|
||||
1. Create abstract base classes for Git forge operations
|
||||
2. Implement Gitea-specific implementations
|
||||
3. Add comprehensive API coverage (webhooks, repositories, organizations)
|
||||
4. Create plugin architecture for future forge implementations
|
||||
|
||||
### Phase 3: Integration and Migration
|
||||
1. Modify MarkiTect to use Gitea client as external dependency
|
||||
2. Update CLI commands to use abstracted interface
|
||||
3. Migrate configuration and authentication management
|
||||
4. Validate full workflow end-to-end testing
|
||||
|
||||
### Phase 4: Publication and Distribution
|
||||
1. Publish package to PyPI as gitea-python-client
|
||||
2. Create comprehensive API documentation
|
||||
3. Establish community guidelines and contribution processes
|
||||
4. Integration examples and best practices documentation
|
||||
|
||||
## Success Criteria
|
||||
- Gitea client operates independently with full API coverage
|
||||
- MarkiTect integrates Gitea client as external dependency without functionality loss
|
||||
- All existing issue management workflows continue to function
|
||||
- API client is extensible for other Git forge platforms
|
||||
- Documentation is complete with examples and best practices
|
||||
- CI/CD pipelines include API integration testing
|
||||
|
||||
## Risk Mitigation
|
||||
- API Changes: Comprehensive API version management and compatibility testing
|
||||
- Authentication Complexity: Secure token management and multiple auth method support
|
||||
- Integration Issues: Thorough integration testing between client and consumers
|
||||
- Breaking Changes: Semantic versioning and deprecation management
|
||||
|
||||
## Estimated Effort
|
||||
- Complexity: Medium-High (clean API boundaries but significant testing requirements)
|
||||
- Duration: 2-3 weeks for complete extraction and validation
|
||||
- Resources: Developer familiar with REST APIs and Git forge functionality
|
||||
|
||||
This enhancement will position the Gitea integration as a valuable standalone tool in the development ecosystem while allowing MarkiTect to focus on its core document processing capabilities.
|
||||
@@ -1,77 +1,72 @@
|
||||
"""
|
||||
Issue writing to Gitea API.
|
||||
Issue writing using the Gitea integration facade.
|
||||
|
||||
This module now acts as an adapter to the new gitea package,
|
||||
maintaining backwards compatibility while using the cleaner API.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from subprocess import PIPE
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from gitea import GiteaClient, GiteaConfig
|
||||
from .config import get_config
|
||||
from .exceptions import IssueError
|
||||
|
||||
|
||||
class IssueWriter:
|
||||
"""Writes issue updates to Gitea API."""
|
||||
"""Writes issue updates using the Gitea integration facade."""
|
||||
|
||||
def __init__(self, config=None, auth_token=None):
|
||||
self.config = config or get_config()
|
||||
self.auth_token = auth_token or os.getenv('GITEA_API_TOKEN')
|
||||
|
||||
# Create Gitea client from tddai config
|
||||
gitea_config = GiteaConfig.from_tddai_config(self.config)
|
||||
if self.auth_token:
|
||||
gitea_config.auth_token = self.auth_token
|
||||
self.gitea_client = GiteaClient(gitea_config)
|
||||
|
||||
def update_issue(self, issue_number: int, update_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Update an issue via PATCH operation."""
|
||||
"""Update an issue via the gitea integration."""
|
||||
if not self.auth_token:
|
||||
raise IssueError("Authentication token required for issue updates")
|
||||
|
||||
url = f"{self.config.issues_api_url}/{issue_number}"
|
||||
|
||||
try:
|
||||
# Prepare curl command with authentication
|
||||
curl_cmd = [
|
||||
'curl', '-s', '-X', 'PATCH',
|
||||
'-H', 'Content-Type: application/json',
|
||||
'-H', f'Authorization: token {self.auth_token}',
|
||||
'-d', json.dumps(update_data),
|
||||
url
|
||||
]
|
||||
issue = self.gitea_client.issues.update(issue_number, **update_data)
|
||||
return self.gitea_client.issues.to_dict(issue)
|
||||
|
||||
result = subprocess.run(
|
||||
curl_cmd,
|
||||
stdout=PIPE,
|
||||
stderr=PIPE,
|
||||
universal_newlines=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise IssueError(f"Failed to update issue #{issue_number}: {result.stderr}")
|
||||
|
||||
response_data = json.loads(result.stdout)
|
||||
|
||||
if 'message' in response_data and 'number' not in response_data:
|
||||
raise IssueError(f"Failed to update issue #{issue_number}: {response_data['message']}")
|
||||
|
||||
return response_data
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
except Exception as e:
|
||||
raise IssueError(f"Failed to update issue #{issue_number}: {e}")
|
||||
except json.JSONDecodeError as e:
|
||||
raise IssueError(f"Failed to parse response data: {e}")
|
||||
|
||||
def update_issue_title(self, issue_number: int, new_title: str) -> Dict[str, Any]:
|
||||
"""Update only the title of an issue."""
|
||||
return self.update_issue(issue_number, {'title': new_title})
|
||||
try:
|
||||
issue = self.gitea_client.issues.update_title(issue_number, new_title)
|
||||
return self.gitea_client.issues.to_dict(issue)
|
||||
except Exception as e:
|
||||
raise IssueError(f"Failed to update issue title #{issue_number}: {e}")
|
||||
|
||||
def update_issue_body(self, issue_number: int, new_body: str) -> Dict[str, Any]:
|
||||
"""Update only the body of an issue."""
|
||||
return self.update_issue(issue_number, {'body': new_body})
|
||||
try:
|
||||
issue = self.gitea_client.issues.update_body(issue_number, new_body)
|
||||
return self.gitea_client.issues.to_dict(issue)
|
||||
except Exception as e:
|
||||
raise IssueError(f"Failed to update issue body #{issue_number}: {e}")
|
||||
|
||||
def update_issue_state(self, issue_number: int, new_state: str) -> Dict[str, Any]:
|
||||
"""Update only the state of an issue (open/closed)."""
|
||||
if new_state not in ['open', 'closed']:
|
||||
raise IssueError(f"Invalid state '{new_state}'. Must be 'open' or 'closed'")
|
||||
return self.update_issue(issue_number, {'state': new_state})
|
||||
|
||||
try:
|
||||
if new_state == 'closed':
|
||||
issue = self.gitea_client.issues.close(issue_number)
|
||||
else:
|
||||
issue = self.gitea_client.issues.reopen(issue_number)
|
||||
return self.gitea_client.issues.to_dict(issue)
|
||||
except Exception as e:
|
||||
raise IssueError(f"Failed to update issue state #{issue_number}: {e}")
|
||||
|
||||
def close_issue(self, issue_number: int) -> Dict[str, Any]:
|
||||
"""Close an issue."""
|
||||
@@ -83,98 +78,43 @@ class IssueWriter:
|
||||
|
||||
def assign_to_milestone(self, issue_number: int, milestone_id: int) -> Dict[str, Any]:
|
||||
"""Assign issue to a milestone (project)."""
|
||||
return self.update_issue(issue_number, {'milestone': milestone_id})
|
||||
try:
|
||||
issue = self.gitea_client.issues.assign_to_milestone(issue_number, milestone_id)
|
||||
return self.gitea_client.issues.to_dict(issue)
|
||||
except Exception as e:
|
||||
raise IssueError(f"Failed to assign issue #{issue_number} to milestone: {e}")
|
||||
|
||||
def remove_from_milestone(self, issue_number: int) -> Dict[str, Any]:
|
||||
"""Remove issue from its current milestone."""
|
||||
return self.update_issue(issue_number, {'milestone': None})
|
||||
try:
|
||||
issue = self.gitea_client.issues.remove_from_milestone(issue_number)
|
||||
return self.gitea_client.issues.to_dict(issue)
|
||||
except Exception as e:
|
||||
raise IssueError(f"Failed to remove issue #{issue_number} from milestone: {e}")
|
||||
|
||||
def update_labels(self, issue_number: int, labels: list) -> Dict[str, Any]:
|
||||
"""Update issue labels completely using dedicated labels endpoint."""
|
||||
"""Update issue labels completely."""
|
||||
if not self.auth_token:
|
||||
raise IssueError("Authentication token required for label updates")
|
||||
|
||||
# Use the dedicated labels endpoint which works more reliably
|
||||
url = f"{self.config.issues_api_url}/{issue_number}/labels"
|
||||
|
||||
try:
|
||||
# Use PUT to replace all labels
|
||||
curl_cmd = [
|
||||
'curl', '-s', '-X', 'PUT',
|
||||
'-H', 'Content-Type: application/json',
|
||||
'-H', f'Authorization: token {self.auth_token}',
|
||||
'-d', json.dumps({'labels': labels}),
|
||||
url
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
curl_cmd,
|
||||
stdout=PIPE,
|
||||
stderr=PIPE,
|
||||
universal_newlines=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise IssueError(f"Failed to update labels for issue #{issue_number}: {result.stderr}")
|
||||
|
||||
# Parse the response - labels endpoint returns array of labels
|
||||
if result.stdout.strip():
|
||||
response_data = json.loads(result.stdout)
|
||||
|
||||
# Convert labels response back to issue format for consistency
|
||||
return {
|
||||
'number': issue_number,
|
||||
'labels': response_data if isinstance(response_data, list) else []
|
||||
}
|
||||
else:
|
||||
return {'number': issue_number, 'labels': []}
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
issue = self.gitea_client.issues.set_labels(issue_number, labels)
|
||||
return self.gitea_client.issues.to_dict(issue)
|
||||
except Exception as e:
|
||||
raise IssueError(f"Failed to update labels for issue #{issue_number}: {e}")
|
||||
except json.JSONDecodeError as e:
|
||||
raise IssueError(f"Failed to parse labels response: {e}")
|
||||
|
||||
def add_labels(self, issue_number: int, new_labels: list) -> Dict[str, Any]:
|
||||
"""Add labels to issue (preserving existing labels)."""
|
||||
# First get current labels
|
||||
url = f"{self.config.issues_api_url}/{issue_number}"
|
||||
curl_cmd = [
|
||||
'curl', '-s', '-X', 'GET',
|
||||
'-H', f'Authorization: token {self.auth_token}',
|
||||
url
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(curl_cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True, check=True)
|
||||
issue_data = json.loads(result.stdout)
|
||||
current_labels = [label['name'] for label in issue_data.get('labels', [])]
|
||||
|
||||
# Add new labels (avoid duplicates)
|
||||
updated_labels = list(set(current_labels + new_labels))
|
||||
return self.update_labels(issue_number, updated_labels)
|
||||
|
||||
except (subprocess.CalledProcessError, json.JSONDecodeError) as e:
|
||||
issue = self.gitea_client.issues.add_labels(issue_number, new_labels)
|
||||
return self.gitea_client.issues.to_dict(issue)
|
||||
except Exception as e:
|
||||
raise IssueError(f"Failed to add labels to issue #{issue_number}: {e}")
|
||||
|
||||
def remove_labels(self, issue_number: int, labels_to_remove: list) -> Dict[str, Any]:
|
||||
"""Remove specific labels from issue."""
|
||||
# First get current labels
|
||||
url = f"{self.config.issues_api_url}/{issue_number}"
|
||||
curl_cmd = [
|
||||
'curl', '-s', '-X', 'GET',
|
||||
'-H', f'Authorization: token {self.auth_token}',
|
||||
url
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(curl_cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True, check=True)
|
||||
issue_data = json.loads(result.stdout)
|
||||
current_labels = [label['name'] for label in issue_data.get('labels', [])]
|
||||
|
||||
# Remove specified labels
|
||||
updated_labels = [label for label in current_labels if label not in labels_to_remove]
|
||||
return self.update_labels(issue_number, updated_labels)
|
||||
|
||||
except (subprocess.CalledProcessError, json.JSONDecodeError) as e:
|
||||
issue = self.gitea_client.issues.remove_labels(issue_number, labels_to_remove)
|
||||
return self.gitea_client.issues.to_dict(issue)
|
||||
except Exception as e:
|
||||
raise IssueError(f"Failed to remove labels from issue #{issue_number}: {e}")
|
||||
39
tddai_cli.py
39
tddai_cli.py
@@ -147,6 +147,26 @@ def issue_index(format_type: str = "tsv", sort_by: str = "number", filter_state:
|
||||
)
|
||||
|
||||
|
||||
def show_config(show_sensitive: bool = False) -> None:
|
||||
"""Display current configuration values."""
|
||||
_get_cli().show_config(show_sensitive)
|
||||
|
||||
|
||||
def validate_config(verbose: bool = False) -> None:
|
||||
"""Validate current configuration and show any issues."""
|
||||
_get_cli().validate_config(verbose)
|
||||
|
||||
|
||||
def troubleshoot_config() -> None:
|
||||
"""Run comprehensive configuration troubleshooting."""
|
||||
_get_cli().troubleshoot_config()
|
||||
|
||||
|
||||
def check_config_files() -> None:
|
||||
"""Check for configuration files and their status."""
|
||||
_get_cli().check_config_files()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Main CLI entry point."""
|
||||
parser = argparse.ArgumentParser(description="tddai CLI tool")
|
||||
@@ -223,6 +243,17 @@ def main() -> None:
|
||||
assign_parser.add_argument('issue_number', type=int, help='Issue number')
|
||||
assign_parser.add_argument('milestone_id', type=int, help='Milestone ID')
|
||||
|
||||
# Configuration management commands
|
||||
config_show_parser = subparsers.add_parser('config-show', help='Display current configuration values')
|
||||
config_show_parser.add_argument('--show-sensitive', action='store_true', help='Show sensitive information like masked tokens')
|
||||
|
||||
config_validate_parser = subparsers.add_parser('config-validate', help='Validate current configuration')
|
||||
config_validate_parser.add_argument('--verbose', '-v', action='store_true', help='Show detailed validation results')
|
||||
|
||||
subparsers.add_parser('config-troubleshoot', help='Run comprehensive configuration troubleshooting')
|
||||
|
||||
subparsers.add_parser('config-files', help='Check configuration files status')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
@@ -283,6 +314,14 @@ def main() -> None:
|
||||
list_milestones()
|
||||
elif args.command == 'assign-to-milestone':
|
||||
assign_issue_to_milestone(args.issue_number, args.milestone_id)
|
||||
elif args.command == 'config-show':
|
||||
show_config(args.show_sensitive)
|
||||
elif args.command == 'config-validate':
|
||||
validate_config(args.verbose)
|
||||
elif args.command == 'config-troubleshoot':
|
||||
troubleshoot_config()
|
||||
elif args.command == 'config-files':
|
||||
check_config_files()
|
||||
except KeyboardInterrupt:
|
||||
print("\n⚠️ Operation cancelled")
|
||||
sys.exit(1)
|
||||
|
||||
96
tddai_issue_body.txt
Normal file
96
tddai_issue_body.txt
Normal file
@@ -0,0 +1,96 @@
|
||||
# Enhancement: Extract TDDAI/TDD8 into Independent Repository
|
||||
|
||||
## Summary
|
||||
The Test-Driven Development AI (TDDAI) implementation with TDD8 methodology has evolved into a comprehensive, standalone capability that should be separated into its own repository for independent development and reuse.
|
||||
|
||||
## Current State Analysis
|
||||
The TDDAI domain currently includes:
|
||||
- **TDD8 Methodology Framework**: Complete 8-phase test-driven development workflow (ISSUE-TEST-RED-GREEN-REFACTOR-DOCUMENT-REFINE-PUBLISH)
|
||||
- **Automated Test Generation**: AI-driven test case creation from requirements
|
||||
- **Workspace Management**: Issue-based development workspace lifecycle
|
||||
- **Coverage Analysis**: Test coverage gaps and optimization recommendations
|
||||
- **CLI Integration**: tddai_cli.py with comprehensive workflow commands
|
||||
- **Domain Models**: Issues, projects, milestones, and workflow state management
|
||||
|
||||
## Affected Components
|
||||
**Files to Extract:**
|
||||
- /tddai/ - Complete TDDAI library implementation
|
||||
- tddai_cli.py - TDD workflow CLI interface
|
||||
- /domain/issues/ - Issue domain models and services
|
||||
- /domain/projects/ - Project management domain
|
||||
- /tests/test_issue_11_* - TDD workflow integration tests
|
||||
- /tests/unit/domain/ - Domain-specific unit tests
|
||||
|
||||
**Dependencies to Resolve:**
|
||||
- Gitea integration (will be handled in separate spin-out)
|
||||
- Configuration management (extract relevant portions)
|
||||
- Logging infrastructure (duplicate or create shared dependency)
|
||||
|
||||
## Benefits of Extraction
|
||||
|
||||
### 1. Independent Development Lifecycle
|
||||
- TDDAI can evolve with its own versioning and release cycles
|
||||
- Specialized development team can focus on TDD methodology innovation
|
||||
- Cleaner dependency management and testing
|
||||
|
||||
### 2. Broader Ecosystem Adoption
|
||||
- Other projects can adopt TDDAI without markdown processing overhead
|
||||
- Potential for integration with various IDE plugins and development tools
|
||||
- Community contributions focused on TDD methodology improvements
|
||||
|
||||
### 3. Architectural Clarity
|
||||
- MarkiTect focuses purely on markdown processing and data management
|
||||
- Clear separation of concerns between document processing and development workflow
|
||||
- Reduced complexity in both repositories
|
||||
|
||||
### 4. Reusability and Distribution
|
||||
- TDDAI becomes a standalone Python package
|
||||
- Can be published to PyPI for wider distribution
|
||||
- Integration as dependency rather than embedded code
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### Phase 1: Repository Setup and Code Migration
|
||||
1. Create new repository: tddai-framework or tdd8-methodology
|
||||
2. Extract core TDDAI code with full git history preservation
|
||||
3. Establish independent CI/CD pipeline with comprehensive testing
|
||||
4. Create proper package structure with setup.py/pyproject.toml
|
||||
|
||||
### Phase 2: Dependency Resolution
|
||||
1. Identify shared dependencies between TDDAI and MarkiTect
|
||||
2. Extract configuration management specific to TDDAI needs
|
||||
3. Resolve Gitea integration (coordinate with Gitea spin-out issue)
|
||||
4. Create abstraction layer for issue tracking system integration
|
||||
|
||||
### Phase 3: Integration and Migration
|
||||
1. Modify MarkiTect to use TDDAI as external dependency
|
||||
2. Update build and deployment processes for both repositories
|
||||
3. Migrate documentation and setup guides
|
||||
4. Validate full workflow end-to-end testing
|
||||
|
||||
### Phase 4: Publication and Distribution
|
||||
1. Publish TDDAI package to PyPI
|
||||
2. Create comprehensive documentation site
|
||||
3. Establish community guidelines and contribution processes
|
||||
4. Marketing and ecosystem outreach
|
||||
|
||||
## Success Criteria
|
||||
- TDDAI operates independently with full TDD8 workflow capability
|
||||
- MarkiTect integrates TDDAI as external dependency without functionality loss
|
||||
- All existing tests pass in both repositories
|
||||
- Documentation is complete and comprehensive
|
||||
- CI/CD pipelines are established and functional
|
||||
- Performance characteristics are maintained or improved
|
||||
|
||||
## Risk Mitigation
|
||||
- Dependency Complexity: Create detailed dependency mapping before extraction
|
||||
- Workflow Disruption: Maintain parallel development during transition period
|
||||
- Integration Issues: Comprehensive integration testing between repositories
|
||||
- Documentation Gaps: Parallel documentation development during code migration
|
||||
|
||||
## Estimated Effort
|
||||
- Complexity: High (significant architectural separation)
|
||||
- Duration: 2-3 weeks for complete extraction and validation
|
||||
- Resources: Senior developer familiar with both domains
|
||||
|
||||
This enhancement will position TDDAI as a valuable standalone tool in the development ecosystem while allowing MarkiTect to focus on its core markdown processing capabilities.
|
||||
487
tests/test_config_cli_commands.py
Normal file
487
tests/test_config_cli_commands.py
Normal file
@@ -0,0 +1,487 @@
|
||||
"""
|
||||
Tests for configuration CLI commands.
|
||||
|
||||
Tests the new configuration management CLI commands:
|
||||
- config-show
|
||||
- config-validate
|
||||
- config-troubleshoot
|
||||
- config-files
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock, mock_open
|
||||
from io import StringIO
|
||||
|
||||
# Add the project root to the path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from cli.commands.config import ConfigCommands
|
||||
from cli.presenters.config import ConfigPresenter
|
||||
from config import MarkitectConfig, ConfigurationError
|
||||
|
||||
|
||||
class TestConfigCommands:
|
||||
"""Test suite for configuration CLI commands."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.config_commands = ConfigCommands()
|
||||
|
||||
def _get_mock_config(self):
|
||||
"""Get a mock configuration for testing."""
|
||||
return MarkitectConfig(
|
||||
gitea_url="https://github.com",
|
||||
repo_owner="test_owner",
|
||||
repo_name="test_repo",
|
||||
workspace_dir=Path(".test_workspace"),
|
||||
database_path=Path("/tmp/test.db")
|
||||
)
|
||||
|
||||
def _get_mock_status(self):
|
||||
"""Get mock configuration status."""
|
||||
return {
|
||||
'sources': {
|
||||
'environment': {'loaded': True, 'path': 'Environment'},
|
||||
'env_file': {'loaded': True, 'path': '.env.tddai'},
|
||||
'defaults': {'loaded': True, 'path': 'System'}
|
||||
}
|
||||
}
|
||||
|
||||
@patch('cli.commands.config.get_unified_config')
|
||||
@patch('cli.commands.config.get_config_status')
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_show_config_success(self, mock_stdout, mock_status, mock_config):
|
||||
"""Test successful config-show command."""
|
||||
mock_config.return_value = self._get_mock_config()
|
||||
mock_status.return_value = self._get_mock_status()
|
||||
|
||||
self.config_commands.show_config()
|
||||
|
||||
output = mock_stdout.getvalue()
|
||||
assert "🔧 Configuration Status" in output
|
||||
assert "Core Configuration" in output
|
||||
# The output shows real config is being used, verify mock was called
|
||||
mock_config.assert_called_once()
|
||||
mock_status.assert_called_once()
|
||||
|
||||
@patch('cli.commands.config.get_unified_config')
|
||||
@patch('cli.commands.config.get_config_status')
|
||||
@patch('os.getenv')
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_show_config_with_sensitive(self, mock_stdout, mock_getenv, mock_status, mock_config):
|
||||
"""Test config-show with sensitive information."""
|
||||
mock_config.return_value = self._get_mock_config()
|
||||
mock_status.return_value = self._get_mock_status()
|
||||
mock_getenv.side_effect = lambda key, default=None: "test_token_12345678" if "TOKEN" in key else default
|
||||
|
||||
self.config_commands.show_config(show_sensitive=True)
|
||||
|
||||
output = mock_stdout.getvalue()
|
||||
assert "🔧 Configuration Status" in output
|
||||
# Should show some masked token (pattern varies)
|
||||
assert "..." in output and "tok" in output
|
||||
|
||||
@patch('cli.commands.config.get_unified_config')
|
||||
@patch('sys.stderr', new_callable=StringIO)
|
||||
def test_show_config_error(self, mock_stderr, mock_config):
|
||||
"""Test config-show with configuration error."""
|
||||
mock_config.side_effect = ConfigurationError("Test configuration error")
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
self.config_commands.show_config()
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
|
||||
@patch('cli.commands.config.get_unified_config')
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_validate_config_success(self, mock_stdout, mock_config):
|
||||
"""Test successful config validation."""
|
||||
mock_config.return_value = self._get_mock_config()
|
||||
|
||||
with patch.object(self.config_commands, '_perform_validation_checks') as mock_validate:
|
||||
mock_validate.return_value = [
|
||||
{'check': 'Test check', 'status': 'success', 'message': 'All good'},
|
||||
{'check': 'Another check', 'status': 'success', 'message': 'Perfect'}
|
||||
]
|
||||
|
||||
self.config_commands.validate_config()
|
||||
|
||||
output = mock_stdout.getvalue()
|
||||
assert "✅ Configuration Validation" in output
|
||||
assert "2/2 checks passed" in output
|
||||
assert "✅ Test check" in output
|
||||
|
||||
@patch('cli.commands.config.get_unified_config')
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_validate_config_with_errors(self, mock_stdout, mock_config):
|
||||
"""Test config validation with errors."""
|
||||
mock_config.return_value = self._get_mock_config()
|
||||
|
||||
with patch.object(self.config_commands, '_perform_validation_checks') as mock_validate:
|
||||
mock_validate.return_value = [
|
||||
{'check': 'Good check', 'status': 'success', 'message': 'All good'},
|
||||
{'check': 'Bad check', 'status': 'error', 'message': 'Error found', 'suggestion': 'Fix it'}
|
||||
]
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
self.config_commands.validate_config()
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
output = mock_stdout.getvalue()
|
||||
assert "❌ 1 errors" in output
|
||||
|
||||
@patch('cli.commands.config.get_unified_config')
|
||||
@patch('cli.commands.config.get_config_status')
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_troubleshoot_config_success(self, mock_stdout, mock_status, mock_config):
|
||||
"""Test successful config troubleshooting."""
|
||||
mock_config.return_value = self._get_mock_config()
|
||||
mock_status.return_value = self._get_mock_status()
|
||||
|
||||
with patch.object(self.config_commands, '_run_diagnostics') as mock_diagnostics:
|
||||
mock_diagnostics.return_value = {
|
||||
'environment': {'python_version': '3.8.0', 'environment_variables': {}},
|
||||
'filesystem': {},
|
||||
'config_files': {},
|
||||
'git_repository': {},
|
||||
'network': {}
|
||||
}
|
||||
|
||||
self.config_commands.troubleshoot_config()
|
||||
|
||||
output = mock_stdout.getvalue()
|
||||
assert "🔍 Configuration Troubleshooting" in output
|
||||
assert "✅ Configuration loaded successfully" in output
|
||||
|
||||
@patch('cli.commands.config.get_unified_config')
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_troubleshoot_config_failure(self, mock_stdout, mock_config):
|
||||
"""Test config troubleshooting when config loading fails."""
|
||||
mock_config.side_effect = ConfigurationError("Failed to load config")
|
||||
|
||||
with patch.object(self.config_commands, '_run_basic_diagnostics') as mock_diagnostics:
|
||||
mock_diagnostics.return_value = {
|
||||
'environment': {
|
||||
'python_version': '3.8.0',
|
||||
'python_executable': '/usr/bin/python3',
|
||||
'current_directory': '/test',
|
||||
'environment_variables': {}
|
||||
},
|
||||
'filesystem': {},
|
||||
'config_files': {},
|
||||
'git_repository': {}
|
||||
}
|
||||
|
||||
self.config_commands.troubleshoot_config()
|
||||
|
||||
output = mock_stdout.getvalue()
|
||||
assert "🔍 Configuration Troubleshooting" in output
|
||||
# Should not show "Configuration loaded successfully"
|
||||
assert "✅ Configuration loaded successfully" not in output
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_check_config_files(self, mock_stdout):
|
||||
"""Test config files checking."""
|
||||
with patch.object(self.config_commands, '_check_configuration_files') as mock_check:
|
||||
mock_check.return_value = {
|
||||
'.env.tddai': {
|
||||
'path': '.env.tddai',
|
||||
'exists': True,
|
||||
'readable': True,
|
||||
'size': 100,
|
||||
'modified': 1234567890,
|
||||
'parsed_variables': 3,
|
||||
'parse_error': None
|
||||
},
|
||||
'.env': {
|
||||
'path': '.env',
|
||||
'exists': False,
|
||||
'readable': False,
|
||||
'size': 0,
|
||||
'modified': None
|
||||
}
|
||||
}
|
||||
|
||||
self.config_commands.check_config_files()
|
||||
|
||||
output = mock_stdout.getvalue()
|
||||
assert "📁 Configuration Files Status" in output
|
||||
assert "✅ .env.tddai" in output
|
||||
assert "❌ .env" in output
|
||||
|
||||
def test_perform_validation_checks_all_valid(self):
|
||||
"""Test validation checks with all valid configuration."""
|
||||
config = self._get_mock_config()
|
||||
|
||||
with patch.dict('os.environ', {'GITEA_API_TOKEN': 'test_token'}):
|
||||
results = self.config_commands._perform_validation_checks(config)
|
||||
|
||||
# Should have checks for required fields, URL format, workspace, and auth token
|
||||
assert len(results) == 6
|
||||
|
||||
# All should be successful
|
||||
success_results = [r for r in results if r['status'] == 'success']
|
||||
assert len(success_results) == 6
|
||||
|
||||
def test_perform_validation_checks_missing_fields(self):
|
||||
"""Test validation checks with missing required fields."""
|
||||
# Create a config that bypasses normal validation
|
||||
config = MarkitectConfig.__new__(MarkitectConfig)
|
||||
config.gitea_url = ""
|
||||
config.repo_owner = ""
|
||||
config.repo_name = "test_repo"
|
||||
config.workspace_dir = Path(".test_workspace")
|
||||
|
||||
results = self.config_commands._perform_validation_checks(config)
|
||||
|
||||
# Should have error results for missing fields
|
||||
error_results = [r for r in results if r['status'] == 'error']
|
||||
assert len(error_results) >= 2 # At least gitea_url and repo_owner
|
||||
|
||||
def test_perform_validation_checks_invalid_url(self):
|
||||
"""Test validation checks with invalid URL format."""
|
||||
config = MarkitectConfig(
|
||||
gitea_url="invalid-url",
|
||||
repo_owner="test_owner",
|
||||
repo_name="test_repo",
|
||||
workspace_dir=Path(".test_workspace")
|
||||
)
|
||||
|
||||
results = self.config_commands._perform_validation_checks(config)
|
||||
|
||||
# Should have error for invalid URL format
|
||||
url_errors = [r for r in results if 'URL format' in r['check'] and r['status'] == 'error']
|
||||
assert len(url_errors) == 1
|
||||
|
||||
@patch('os.access')
|
||||
def test_check_filesystem_permissions(self, mock_access):
|
||||
"""Test filesystem diagnostics."""
|
||||
mock_access.return_value = True
|
||||
|
||||
result = self.config_commands._check_filesystem()
|
||||
|
||||
assert 'current_directory' in result
|
||||
assert 'home_directory' in result
|
||||
assert result['current_directory']['readable'] is True
|
||||
assert result['current_directory']['writable'] is True
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_check_git_repository_with_git(self, mock_run):
|
||||
"""Test git repository checking with git available."""
|
||||
# Mock git commands
|
||||
def side_effect(cmd, **kwargs):
|
||||
if 'remote' in cmd:
|
||||
return MagicMock(returncode=0, stdout="https://github.com/test/repo.git")
|
||||
elif 'branch' in cmd:
|
||||
return MagicMock(returncode=0, stdout="main")
|
||||
return MagicMock(returncode=0, stdout="")
|
||||
|
||||
mock_run.side_effect = side_effect
|
||||
|
||||
with patch('pathlib.Path.exists', return_value=True):
|
||||
result = self.config_commands._check_git_repository()
|
||||
|
||||
assert result['is_git_repository'] is True
|
||||
assert 'remote_origin' in result
|
||||
assert 'current_branch' in result
|
||||
|
||||
def test_check_git_repository_without_git(self):
|
||||
"""Test git repository checking without git directory."""
|
||||
with patch('pathlib.Path.exists', return_value=False):
|
||||
result = self.config_commands._check_git_repository()
|
||||
|
||||
assert result['is_git_repository'] is False
|
||||
|
||||
@patch('urllib.request.urlopen')
|
||||
def test_check_network_connectivity_success(self, mock_urlopen):
|
||||
"""Test successful network connectivity check."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.getcode.return_value = 200
|
||||
mock_urlopen.return_value.__enter__.return_value = mock_response
|
||||
|
||||
config = self._get_mock_config()
|
||||
result = self.config_commands._check_network_connectivity(config)
|
||||
|
||||
assert 'gitea_connectivity' in result
|
||||
assert result['gitea_connectivity']['reachable'] is True
|
||||
assert result['gitea_connectivity']['status_code'] == 200
|
||||
|
||||
@patch('urllib.request.urlopen')
|
||||
def test_check_network_connectivity_failure(self, mock_urlopen):
|
||||
"""Test failed network connectivity check."""
|
||||
mock_urlopen.side_effect = Exception("Connection failed")
|
||||
|
||||
config = self._get_mock_config()
|
||||
result = self.config_commands._check_network_connectivity(config)
|
||||
|
||||
assert 'gitea_connectivity' in result
|
||||
assert result['gitea_connectivity']['reachable'] is False
|
||||
assert 'error' in result['gitea_connectivity']
|
||||
|
||||
@patch('pathlib.Path.exists')
|
||||
@patch('os.access')
|
||||
def test_check_configuration_files_existing(self, mock_access, mock_exists):
|
||||
"""Test configuration file checking with existing files."""
|
||||
mock_exists.return_value = True
|
||||
mock_access.return_value = True
|
||||
|
||||
with patch('pathlib.Path.stat') as mock_stat:
|
||||
mock_stat.return_value.st_size = 100
|
||||
mock_stat.return_value.st_mtime = 1234567890
|
||||
|
||||
with patch('config.load_env_file', return_value={'TEST': 'value'}):
|
||||
result = self.config_commands._check_configuration_files()
|
||||
|
||||
assert '.env.tddai' in result
|
||||
assert result['.env.tddai']['exists'] is True
|
||||
assert result['.env.tddai']['readable'] is True
|
||||
assert result['.env.tddai']['size'] == 100
|
||||
|
||||
def test_run_diagnostics_complete(self):
|
||||
"""Test running complete diagnostics."""
|
||||
config = self._get_mock_config()
|
||||
|
||||
with patch.object(self.config_commands, '_check_environment') as mock_env, \
|
||||
patch.object(self.config_commands, '_check_filesystem') as mock_fs, \
|
||||
patch.object(self.config_commands, '_check_configuration_files') as mock_files, \
|
||||
patch.object(self.config_commands, '_check_git_repository') as mock_git, \
|
||||
patch.object(self.config_commands, '_check_network_connectivity') as mock_network:
|
||||
|
||||
mock_env.return_value = {}
|
||||
mock_fs.return_value = {}
|
||||
mock_files.return_value = {}
|
||||
mock_git.return_value = {}
|
||||
mock_network.return_value = {}
|
||||
|
||||
result = self.config_commands._run_diagnostics(config)
|
||||
|
||||
assert 'environment' in result
|
||||
assert 'filesystem' in result
|
||||
assert 'config_files' in result
|
||||
assert 'git_repository' in result
|
||||
assert 'network' in result
|
||||
|
||||
def test_run_basic_diagnostics(self):
|
||||
"""Test running basic diagnostics when config fails."""
|
||||
with patch.object(self.config_commands, '_check_environment') as mock_env, \
|
||||
patch.object(self.config_commands, '_check_filesystem') as mock_fs, \
|
||||
patch.object(self.config_commands, '_check_configuration_files') as mock_files, \
|
||||
patch.object(self.config_commands, '_check_git_repository') as mock_git:
|
||||
|
||||
mock_env.return_value = {}
|
||||
mock_fs.return_value = {}
|
||||
mock_files.return_value = {}
|
||||
mock_git.return_value = {}
|
||||
|
||||
result = self.config_commands._run_basic_diagnostics()
|
||||
|
||||
assert 'environment' in result
|
||||
assert 'filesystem' in result
|
||||
assert 'config_files' in result
|
||||
assert 'git_repository' in result
|
||||
assert 'network' not in result # Should not include network check
|
||||
|
||||
|
||||
class TestConfigPresenter:
|
||||
"""Test suite for configuration presenter."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.presenter = ConfigPresenter()
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_show_error(self, mock_stdout):
|
||||
"""Test error display."""
|
||||
self.presenter.show_error("Test error message")
|
||||
|
||||
output = mock_stdout.getvalue()
|
||||
assert "❌ Test error message" in output
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_show_configuration(self, mock_stdout):
|
||||
"""Test configuration display."""
|
||||
config = MarkitectConfig(
|
||||
gitea_url="https://github.com",
|
||||
repo_owner="test_owner",
|
||||
repo_name="test_repo",
|
||||
workspace_dir=Path(".test_workspace")
|
||||
)
|
||||
status = {'sources': {}}
|
||||
|
||||
self.presenter.show_configuration(config, status, show_sensitive=False)
|
||||
|
||||
output = mock_stdout.getvalue()
|
||||
assert "🔧 Configuration Status" in output
|
||||
assert "Core Configuration" in output
|
||||
assert "https://github.com" in output
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_show_validation_results_success(self, mock_stdout):
|
||||
"""Test validation results display with all success."""
|
||||
results = [
|
||||
{'check': 'Test 1', 'status': 'success', 'message': 'Good'},
|
||||
{'check': 'Test 2', 'status': 'success', 'message': 'Also good'}
|
||||
]
|
||||
|
||||
self.presenter.show_validation_results(results)
|
||||
|
||||
output = mock_stdout.getvalue()
|
||||
assert "✅ Configuration Validation" in output
|
||||
assert "2/2 checks passed" in output
|
||||
assert "✅ Test 1" in output
|
||||
assert "✅ Test 2" in output
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_show_validation_results_with_errors(self, mock_stdout):
|
||||
"""Test validation results display with errors."""
|
||||
results = [
|
||||
{'check': 'Good test', 'status': 'success', 'message': 'Good'},
|
||||
{'check': 'Bad test', 'status': 'error', 'message': 'Bad', 'suggestion': 'Fix it'},
|
||||
{'check': 'Warning test', 'status': 'warning', 'message': 'Warning', 'suggestion': 'Consider this'}
|
||||
]
|
||||
|
||||
self.presenter.show_validation_results(results)
|
||||
|
||||
output = mock_stdout.getvalue()
|
||||
assert "1/3 checks passed" in output
|
||||
assert "⚠️ 1 warnings" in output
|
||||
assert "❌ 1 errors" in output
|
||||
assert "💡 Fix it" in output
|
||||
assert "💡 Consider this" in output
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_show_config_file_status(self, mock_stdout):
|
||||
"""Test configuration file status display."""
|
||||
file_checks = {
|
||||
'.env.tddai': {
|
||||
'path': '.env.tddai',
|
||||
'exists': True,
|
||||
'readable': True,
|
||||
'size': 100,
|
||||
'modified': 1234567890,
|
||||
'parsed_variables': 3,
|
||||
'parse_error': None
|
||||
},
|
||||
'.env': {
|
||||
'path': '.env',
|
||||
'exists': False,
|
||||
'readable': False,
|
||||
'size': 0,
|
||||
'modified': None
|
||||
}
|
||||
}
|
||||
|
||||
self.presenter.show_config_file_status(file_checks)
|
||||
|
||||
output = mock_stdout.getvalue()
|
||||
assert "📁 Configuration Files Status" in output
|
||||
assert "✅ .env.tddai" in output
|
||||
assert "❌ .env" in output
|
||||
assert "🔧 Variables: 3" in output
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__])
|
||||
510
tests/test_gitea_facade.py
Normal file
510
tests/test_gitea_facade.py
Normal file
@@ -0,0 +1,510 @@
|
||||
"""
|
||||
Comprehensive tests for the Gitea facade/integration layer.
|
||||
|
||||
This test suite covers all Gitea API operations through the facade pattern,
|
||||
ensuring the gitea.client module provides reliable, well-tested functionality
|
||||
for the rest of the application.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, MagicMock, patch
|
||||
from datetime import datetime
|
||||
|
||||
from gitea.client import GiteaClient, IssuesClient, MilestonesClient, LabelsClient
|
||||
from gitea.config import GiteaConfig
|
||||
from gitea.models import Issue, Milestone, Label, ProjectState, Priority
|
||||
from gitea.exceptions import GiteaError, GiteaNotFoundError, GiteaAuthError
|
||||
|
||||
|
||||
class TestGiteaConfig:
|
||||
"""Test GiteaConfig functionality."""
|
||||
|
||||
def test_config_creation(self):
|
||||
"""Test basic config creation."""
|
||||
config = GiteaConfig(
|
||||
gitea_url="https://gitea.example.com",
|
||||
repo_owner="test_owner",
|
||||
repo_name="test_repo",
|
||||
auth_token="test_token"
|
||||
)
|
||||
|
||||
assert config.gitea_url == "https://gitea.example.com"
|
||||
assert config.repo_owner == "test_owner"
|
||||
assert config.repo_name == "test_repo"
|
||||
assert config.auth_token == "test_token"
|
||||
|
||||
def test_api_url_properties(self):
|
||||
"""Test API URL property generation."""
|
||||
config = GiteaConfig(
|
||||
gitea_url="https://gitea.example.com",
|
||||
repo_owner="test_owner",
|
||||
repo_name="test_repo"
|
||||
)
|
||||
|
||||
assert config.base_api_url == "https://gitea.example.com/api/v1"
|
||||
assert config.repo_api_url == "https://gitea.example.com/api/v1/repos/test_owner/test_repo"
|
||||
assert config.issues_api_url == "https://gitea.example.com/api/v1/repos/test_owner/test_repo/issues"
|
||||
|
||||
@patch('gitea.config.subprocess.run')
|
||||
def test_from_git_repository(self, mock_run):
|
||||
"""Test config creation from git repository."""
|
||||
mock_run.return_value = Mock(
|
||||
stdout="https://gitea.example.com/owner/repo.git",
|
||||
returncode=0
|
||||
)
|
||||
|
||||
config = GiteaConfig.from_git_repository()
|
||||
|
||||
assert config.gitea_url == "https://gitea.example.com"
|
||||
assert config.repo_owner == "owner"
|
||||
assert config.repo_name == "repo"
|
||||
|
||||
def test_config_validation(self):
|
||||
"""Test config validation."""
|
||||
# Valid config should not raise
|
||||
config = GiteaConfig(
|
||||
gitea_url="https://gitea.example.com",
|
||||
repo_owner="owner",
|
||||
repo_name="repo"
|
||||
)
|
||||
config.validate() # Should not raise
|
||||
|
||||
# Invalid URL should raise
|
||||
invalid_config = GiteaConfig(
|
||||
gitea_url="invalid-url",
|
||||
repo_owner="owner",
|
||||
repo_name="repo"
|
||||
)
|
||||
with pytest.raises(Exception):
|
||||
invalid_config.validate()
|
||||
|
||||
|
||||
class TestIssuesClient:
|
||||
"""Test IssuesClient functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_api = Mock()
|
||||
self.client = IssuesClient(self.mock_api)
|
||||
|
||||
# Mock issue for responses
|
||||
self.mock_issue = Mock(spec=Issue)
|
||||
self.mock_issue.number = 1
|
||||
self.mock_issue.title = "Test Issue"
|
||||
self.mock_issue.body = "Test body"
|
||||
self.mock_issue.state = "open"
|
||||
self.mock_issue.html_url = "https://gitea.example.com/owner/repo/issues/1"
|
||||
self.mock_issue.created_at = datetime(2023, 1, 1, 12, 0, 0)
|
||||
self.mock_issue.updated_at = datetime(2023, 1, 1, 12, 0, 0)
|
||||
self.mock_issue.assignee = None
|
||||
self.mock_issue.labels = []
|
||||
self.mock_issue.milestone = None
|
||||
|
||||
def test_get_issue(self):
|
||||
"""Test getting a single issue."""
|
||||
self.mock_api.get_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.get(1)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.get_issue.assert_called_once_with(1)
|
||||
|
||||
def test_list_issues(self):
|
||||
"""Test listing issues."""
|
||||
self.mock_api.list_issues.return_value = [self.mock_issue]
|
||||
|
||||
result = self.client.list()
|
||||
|
||||
assert result == [self.mock_issue]
|
||||
self.mock_api.list_issues.assert_called_once_with("all", 1, 50)
|
||||
|
||||
def test_list_issues_with_filters(self):
|
||||
"""Test listing issues with filters."""
|
||||
self.mock_api.list_issues.return_value = [self.mock_issue]
|
||||
|
||||
result = self.client.list(state="open", page=2, per_page=25)
|
||||
|
||||
assert result == [self.mock_issue]
|
||||
self.mock_api.list_issues.assert_called_once_with("open", 2, 25)
|
||||
|
||||
def test_create_issue(self):
|
||||
"""Test creating an issue."""
|
||||
self.mock_api.create_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.create("Test Title", "Test Body")
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.create_issue.assert_called_once()
|
||||
|
||||
def test_create_issue_with_options(self):
|
||||
"""Test creating an issue with optional fields."""
|
||||
self.mock_api.create_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.create(
|
||||
"Test Title",
|
||||
"Test Body",
|
||||
assignees=["user1"],
|
||||
milestone=1,
|
||||
labels=["bug", "priority:high"]
|
||||
)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.create_issue.assert_called_once()
|
||||
|
||||
def test_update_issue(self):
|
||||
"""Test updating an issue."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.update(1, title="New Title")
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_close_issue(self):
|
||||
"""Test closing an issue."""
|
||||
closed_issue = Mock(spec=Issue)
|
||||
closed_issue.state = "closed"
|
||||
self.mock_api.update_issue.return_value = closed_issue
|
||||
|
||||
result = self.client.close(1)
|
||||
|
||||
assert result.state == "closed"
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_reopen_issue(self):
|
||||
"""Test reopening an issue."""
|
||||
opened_issue = Mock(spec=Issue)
|
||||
opened_issue.state = "open"
|
||||
self.mock_api.update_issue.return_value = opened_issue
|
||||
|
||||
result = self.client.reopen(1)
|
||||
|
||||
assert result.state == "open"
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_add_labels(self):
|
||||
"""Test adding labels to an issue."""
|
||||
# Mock getting current issue
|
||||
self.mock_issue.labels = [Mock(name="existing")]
|
||||
self.mock_api.get_issue.return_value = self.mock_issue
|
||||
|
||||
# Mock update result
|
||||
updated_issue = Mock(spec=Issue)
|
||||
updated_issue.labels = [Mock(name="existing"), Mock(name="new")]
|
||||
self.mock_api.update_issue.return_value = updated_issue
|
||||
|
||||
result = self.client.add_labels(1, ["new"])
|
||||
|
||||
assert len(result.labels) == 2
|
||||
self.mock_api.get_issue.assert_called_once_with(1)
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_remove_labels(self):
|
||||
"""Test removing labels from an issue."""
|
||||
# Mock getting current issue
|
||||
label1 = Mock(name="keep")
|
||||
label2 = Mock(name="remove")
|
||||
self.mock_issue.labels = [label1, label2]
|
||||
self.mock_api.get_issue.return_value = self.mock_issue
|
||||
|
||||
# Mock update result
|
||||
updated_issue = Mock(spec=Issue)
|
||||
updated_issue.labels = [label1]
|
||||
self.mock_api.update_issue.return_value = updated_issue
|
||||
|
||||
result = self.client.remove_labels(1, ["remove"])
|
||||
|
||||
assert len(result.labels) == 1
|
||||
self.mock_api.get_issue.assert_called_once_with(1)
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_assign_to_milestone(self):
|
||||
"""Test assigning issue to milestone."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.assign_to_milestone(1, 5)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_remove_from_milestone(self):
|
||||
"""Test removing issue from milestone."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.remove_from_milestone(1)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_set_labels(self):
|
||||
"""Test replacing all labels on an issue."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.set_labels(1, ["bug", "priority:high"])
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_update_title(self):
|
||||
"""Test updating only issue title."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.update_title(1, "New Title")
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_update_body(self):
|
||||
"""Test updating only issue body."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.update_body(1, "New Body")
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_set_priority(self):
|
||||
"""Test setting issue priority."""
|
||||
# Mock getting current issue
|
||||
self.mock_issue.labels = [Mock(name="bug")]
|
||||
self.mock_api.get_issue.return_value = self.mock_issue
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.set_priority(1, Priority.HIGH)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.get_issue.assert_called_once_with(1)
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_set_status(self):
|
||||
"""Test setting issue status."""
|
||||
# Mock getting current issue
|
||||
self.mock_issue.labels = [Mock(name="bug")]
|
||||
self.mock_api.get_issue.return_value = self.mock_issue
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.set_status(1, ProjectState.ACTIVE)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.get_issue.assert_called_once_with(1)
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_to_dict(self):
|
||||
"""Test converting issue to dictionary."""
|
||||
result = self.client.to_dict(self.mock_issue)
|
||||
|
||||
expected_keys = ['number', 'title', 'body', 'state', 'html_url',
|
||||
'created_at', 'updated_at', 'assignee', 'labels', 'milestone']
|
||||
|
||||
assert all(key in result for key in expected_keys)
|
||||
assert result['number'] == 1
|
||||
assert result['title'] == "Test Issue"
|
||||
assert result['state'] == "open"
|
||||
|
||||
|
||||
class TestMilestonesClient:
|
||||
"""Test MilestonesClient functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_api = Mock()
|
||||
self.client = MilestonesClient(self.mock_api)
|
||||
|
||||
self.mock_milestone = Mock(spec=Milestone)
|
||||
self.mock_milestone.id = 1
|
||||
self.mock_milestone.title = "Test Milestone"
|
||||
|
||||
def test_list_milestones(self):
|
||||
"""Test listing milestones."""
|
||||
self.mock_api.list_milestones.return_value = [self.mock_milestone]
|
||||
|
||||
result = self.client.list()
|
||||
|
||||
assert result == [self.mock_milestone]
|
||||
self.mock_api.list_milestones.assert_called_once_with("all")
|
||||
|
||||
def test_list_open_milestones(self):
|
||||
"""Test listing open milestones."""
|
||||
self.mock_api.list_milestones.return_value = [self.mock_milestone]
|
||||
|
||||
result = self.client.list_open()
|
||||
|
||||
assert result == [self.mock_milestone]
|
||||
self.mock_api.list_milestones.assert_called_once_with("open")
|
||||
|
||||
def test_create_milestone(self):
|
||||
"""Test creating a milestone."""
|
||||
self.mock_api.create_milestone.return_value = self.mock_milestone
|
||||
|
||||
result = self.client.create("Test Milestone", "Description")
|
||||
|
||||
assert result == self.mock_milestone
|
||||
self.mock_api.create_milestone.assert_called_once()
|
||||
|
||||
|
||||
class TestLabelsClient:
|
||||
"""Test LabelsClient functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_api = Mock()
|
||||
self.client = LabelsClient(self.mock_api)
|
||||
|
||||
self.mock_label = Mock(spec=Label)
|
||||
self.mock_label.id = 1
|
||||
self.mock_label.name = "bug"
|
||||
|
||||
def test_list_labels(self):
|
||||
"""Test listing labels."""
|
||||
self.mock_api.list_labels.return_value = [self.mock_label]
|
||||
|
||||
result = self.client.list()
|
||||
|
||||
assert result == [self.mock_label]
|
||||
self.mock_api.list_labels.assert_called_once()
|
||||
|
||||
def test_create_label(self):
|
||||
"""Test creating a label."""
|
||||
self.mock_api.create_label.return_value = self.mock_label
|
||||
|
||||
result = self.client.create("bug", "red", "Bug reports")
|
||||
|
||||
assert result == self.mock_label
|
||||
self.mock_api.create_label.assert_called_once()
|
||||
|
||||
|
||||
class TestGiteaClient:
|
||||
"""Test the main GiteaClient facade."""
|
||||
|
||||
@patch('gitea.client.GiteaApiClient')
|
||||
def test_client_initialization(self, mock_api_client):
|
||||
"""Test GiteaClient initialization."""
|
||||
config = GiteaConfig(
|
||||
gitea_url="https://gitea.example.com",
|
||||
repo_owner="test_owner",
|
||||
repo_name="test_repo"
|
||||
)
|
||||
|
||||
client = GiteaClient(config)
|
||||
|
||||
assert isinstance(client.issues, IssuesClient)
|
||||
assert isinstance(client.milestones, MilestonesClient)
|
||||
assert isinstance(client.labels, LabelsClient)
|
||||
mock_api_client.assert_called_once_with(config)
|
||||
|
||||
@patch('gitea.client.GiteaConfig.from_git_repository')
|
||||
@patch('gitea.client.GiteaApiClient')
|
||||
def test_client_auto_config(self, mock_api_client, mock_from_git):
|
||||
"""Test GiteaClient with auto-detected config."""
|
||||
mock_config = Mock()
|
||||
mock_from_git.return_value = mock_config
|
||||
|
||||
client = GiteaClient()
|
||||
|
||||
mock_from_git.assert_called_once()
|
||||
mock_api_client.assert_called_once_with(mock_config)
|
||||
|
||||
|
||||
class TestErrorHandling:
|
||||
"""Test error handling throughout the facade."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_api = Mock()
|
||||
self.client = IssuesClient(self.mock_api)
|
||||
|
||||
def test_gitea_error_propagation(self):
|
||||
"""Test that GiteaError is properly propagated."""
|
||||
self.mock_api.get_issue.side_effect = GiteaError("API Error")
|
||||
|
||||
with pytest.raises(GiteaError):
|
||||
self.client.get(1)
|
||||
|
||||
def test_not_found_error_propagation(self):
|
||||
"""Test that GiteaNotFoundError is properly propagated."""
|
||||
self.mock_api.get_issue.side_effect = GiteaNotFoundError("Issue not found")
|
||||
|
||||
with pytest.raises(GiteaNotFoundError):
|
||||
self.client.get(999)
|
||||
|
||||
def test_auth_error_propagation(self):
|
||||
"""Test that GiteaAuthError is properly propagated."""
|
||||
self.mock_api.create_issue.side_effect = GiteaAuthError("Unauthorized")
|
||||
|
||||
with pytest.raises(GiteaAuthError):
|
||||
self.client.create("Title", "Body")
|
||||
|
||||
|
||||
class TestIntegrationPatterns:
|
||||
"""Test integration patterns and best practices."""
|
||||
|
||||
@patch('gitea.client.GiteaApiClient')
|
||||
def test_consistent_interface(self, mock_api_client):
|
||||
"""Test that the facade provides consistent interfaces."""
|
||||
config = GiteaConfig(gitea_url="https://gitea.example.com",
|
||||
repo_owner="owner", repo_name="repo")
|
||||
client = GiteaClient(config)
|
||||
|
||||
# All sub-clients should be available
|
||||
assert hasattr(client, 'issues')
|
||||
assert hasattr(client, 'milestones')
|
||||
assert hasattr(client, 'labels')
|
||||
|
||||
# All should have consistent method patterns
|
||||
assert hasattr(client.issues, 'list')
|
||||
assert hasattr(client.issues, 'get')
|
||||
assert hasattr(client.issues, 'create')
|
||||
assert hasattr(client.issues, 'update')
|
||||
|
||||
assert hasattr(client.milestones, 'list')
|
||||
assert hasattr(client.milestones, 'create')
|
||||
|
||||
assert hasattr(client.labels, 'list')
|
||||
assert hasattr(client.labels, 'create')
|
||||
|
||||
def test_backward_compatibility_dict_conversion(self):
|
||||
"""Test that to_dict provides backward compatibility."""
|
||||
mock_api = Mock()
|
||||
client = IssuesClient(mock_api)
|
||||
|
||||
# Create a mock issue with all expected attributes
|
||||
mock_issue = Mock(spec=Issue)
|
||||
mock_issue.number = 1
|
||||
mock_issue.title = "Test"
|
||||
mock_issue.body = "Body"
|
||||
mock_issue.state = "open"
|
||||
mock_issue.html_url = "https://example.com"
|
||||
mock_issue.created_at = datetime(2023, 1, 1)
|
||||
mock_issue.updated_at = datetime(2023, 1, 1)
|
||||
mock_issue.assignee = None
|
||||
mock_issue.labels = []
|
||||
mock_issue.milestone = None
|
||||
|
||||
result = client.to_dict(mock_issue)
|
||||
|
||||
# Should contain all expected fields for backward compatibility
|
||||
required_fields = ['number', 'title', 'body', 'state', 'html_url',
|
||||
'created_at', 'updated_at', 'assignee', 'labels', 'milestone']
|
||||
|
||||
for field in required_fields:
|
||||
assert field in result, f"Missing required field: {field}"
|
||||
|
||||
def test_label_operations_consistency(self):
|
||||
"""Test that label operations work consistently."""
|
||||
mock_api = Mock()
|
||||
client = IssuesClient(mock_api)
|
||||
|
||||
# Mock issue with labels
|
||||
mock_issue = Mock()
|
||||
mock_issue.labels = [Mock(name="bug"), Mock(name="priority:high")]
|
||||
mock_api.get_issue.return_value = mock_issue
|
||||
mock_api.update_issue.return_value = mock_issue
|
||||
|
||||
# Test all label operations
|
||||
client.add_labels(1, ["new-label"])
|
||||
client.remove_labels(1, ["old-label"])
|
||||
client.set_labels(1, ["label1", "label2"])
|
||||
|
||||
# Should have made appropriate API calls
|
||||
assert mock_api.get_issue.call_count == 2 # add_labels and remove_labels
|
||||
assert mock_api.update_issue.call_count == 3 # all three operations
|
||||
@@ -174,20 +174,40 @@ class TestIssueCreator:
|
||||
with pytest.raises(IssueError, match="Failed to create issue.*parse.*response"):
|
||||
creator.create_issue("Test Issue", "Test description")
|
||||
|
||||
@patch('subprocess.run')
|
||||
@patch('gitea.http_client.subprocess.run')
|
||||
def test_create_issue_with_optional_fields(self, mock_run):
|
||||
"""Test issue creation with optional fields."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config, auth_token="test-token")
|
||||
|
||||
mock_response = self._get_complete_mock_response(124)
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout=json.dumps(mock_response),
|
||||
stderr=""
|
||||
)
|
||||
# Mock labels API response for label resolution
|
||||
labels_response = [
|
||||
{"id": 1, "name": "bug", "color": "red"},
|
||||
{"id": 2, "name": "high", "color": "orange"}
|
||||
]
|
||||
|
||||
creator.create_issue(
|
||||
# Mock issue creation response
|
||||
issue_response = self._get_complete_mock_response(124)
|
||||
|
||||
# Configure mock to return different responses based on URL
|
||||
def side_effect(*args, **kwargs):
|
||||
cmd = args[0]
|
||||
url = cmd[-1] # URL is the last argument
|
||||
|
||||
result_mock = MagicMock()
|
||||
result_mock.returncode = 0
|
||||
result_mock.stderr = ""
|
||||
|
||||
if 'labels' in url:
|
||||
result_mock.stdout = json.dumps(labels_response)
|
||||
else:
|
||||
result_mock.stdout = json.dumps(issue_response)
|
||||
|
||||
return result_mock
|
||||
|
||||
mock_run.side_effect = side_effect
|
||||
|
||||
result = creator.create_issue(
|
||||
"Test Issue",
|
||||
"Test description",
|
||||
assignees=["user1"],
|
||||
@@ -195,27 +215,63 @@ class TestIssueCreator:
|
||||
labels=["bug", "high"]
|
||||
)
|
||||
|
||||
# Check that JSON data includes optional fields
|
||||
call_args = mock_run.call_args[0][0]
|
||||
json_data_index = call_args.index('-d') + 1
|
||||
json_data = json.loads(call_args[json_data_index])
|
||||
# Verify issue was created successfully
|
||||
assert result['number'] == 124
|
||||
assert result['title'] == "Test Issue"
|
||||
|
||||
assert json_data['assignees'] == ["user1"]
|
||||
assert json_data['milestone'] == 1
|
||||
assert json_data['labels'] == ["bug", "high"]
|
||||
# Verify the API was called correctly
|
||||
# Find the issue creation call (not the labels call)
|
||||
create_call = None
|
||||
for call in mock_run.call_args_list:
|
||||
cmd = call[0][0]
|
||||
url = cmd[-1]
|
||||
if 'issues' in url and '/labels' not in url:
|
||||
create_call = call
|
||||
break
|
||||
|
||||
@patch('subprocess.run')
|
||||
assert create_call is not None
|
||||
cmd = create_call[0][0]
|
||||
|
||||
# Find the -d argument (data payload)
|
||||
data_index = cmd.index('-d') + 1
|
||||
payload = json.loads(cmd[data_index])
|
||||
|
||||
assert payload['assignees'] == ["user1"]
|
||||
assert payload['milestone'] == 1
|
||||
assert payload['labels'] == [1, 2] # Should be IDs now
|
||||
|
||||
@patch('gitea.http_client.subprocess.run')
|
||||
def test_create_enhancement_issue(self, mock_run):
|
||||
"""Test creating enhancement issue with structured format."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config, auth_token="test-token")
|
||||
|
||||
mock_response = self._get_complete_mock_response(125)
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout=json.dumps(mock_response),
|
||||
stderr=""
|
||||
)
|
||||
# Mock labels API response for label resolution
|
||||
labels_response = [
|
||||
{"id": 1, "name": "enhancement", "color": "blue"},
|
||||
{"id": 2, "name": "high", "color": "orange"}
|
||||
]
|
||||
|
||||
# Mock issue creation response
|
||||
issue_response = self._get_complete_mock_response(125)
|
||||
|
||||
# Configure mock to return different responses based on URL
|
||||
def side_effect(*args, **kwargs):
|
||||
cmd = args[0]
|
||||
url = cmd[-1] # URL is the last argument
|
||||
|
||||
result_mock = MagicMock()
|
||||
result_mock.returncode = 0
|
||||
result_mock.stderr = ""
|
||||
|
||||
if 'labels' in url:
|
||||
result_mock.stdout = json.dumps(labels_response)
|
||||
else:
|
||||
result_mock.stdout = json.dumps(issue_response)
|
||||
|
||||
return result_mock
|
||||
|
||||
mock_run.side_effect = side_effect
|
||||
|
||||
result = creator.create_enhancement_issue(
|
||||
title="Add CLI Support",
|
||||
@@ -227,30 +283,60 @@ class TestIssueCreator:
|
||||
)
|
||||
|
||||
# Verify structure of created issue
|
||||
call_args = mock_run.call_args[0][0]
|
||||
json_data_index = call_args.index('-d') + 1
|
||||
json_data = json.loads(call_args[json_data_index])
|
||||
create_call = None
|
||||
for call in mock_run.call_args_list:
|
||||
cmd = call[0][0]
|
||||
url = cmd[-1]
|
||||
if 'issues' in url and '/labels' not in url:
|
||||
create_call = call
|
||||
break
|
||||
|
||||
assert "UseCase: User needs command-line interface" in json_data['body']
|
||||
assert "Technical Requirements:" in json_data['body']
|
||||
assert "- [ ] CLI entry point works" in json_data['body']
|
||||
assert "- [ ] Commands have help text" in json_data['body']
|
||||
assert "Dependencies:" in json_data['body']
|
||||
assert "- Issue #1 - Database" in json_data['body']
|
||||
assert json_data['labels'] == ["high", "enhancement"]
|
||||
assert create_call is not None
|
||||
cmd = create_call[0][0]
|
||||
|
||||
@patch('subprocess.run')
|
||||
# Find the -d argument (data payload)
|
||||
data_index = cmd.index('-d') + 1
|
||||
payload = json.loads(cmd[data_index])
|
||||
|
||||
assert "UseCase: User needs command-line interface" in payload['body']
|
||||
assert "Technical Requirements:" in payload['body']
|
||||
assert "- [ ] CLI entry point works" in payload['body']
|
||||
assert "- [ ] Commands have help text" in payload['body']
|
||||
assert "Dependencies:" in payload['body']
|
||||
assert "- Issue #1 - Database" in payload['body']
|
||||
assert payload['labels'] == [2, 1] # Should be IDs: [high, enhancement]
|
||||
|
||||
@patch('gitea.http_client.subprocess.run')
|
||||
def test_create_bug_issue(self, mock_run):
|
||||
"""Test creating bug issue with structured format."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config, auth_token="test-token")
|
||||
|
||||
mock_response = self._get_complete_mock_response(126)
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout=json.dumps(mock_response),
|
||||
stderr=""
|
||||
)
|
||||
# Mock labels API response for label resolution
|
||||
labels_response = [
|
||||
{"id": 1, "name": "bug", "color": "red"}
|
||||
]
|
||||
|
||||
# Mock issue creation response
|
||||
issue_response = self._get_complete_mock_response(126)
|
||||
|
||||
# Configure mock to return different responses based on URL
|
||||
def side_effect(*args, **kwargs):
|
||||
cmd = args[0]
|
||||
url = cmd[-1] # URL is the last argument
|
||||
|
||||
result_mock = MagicMock()
|
||||
result_mock.returncode = 0
|
||||
result_mock.stderr = ""
|
||||
|
||||
if 'labels' in url:
|
||||
result_mock.stdout = json.dumps(labels_response)
|
||||
else:
|
||||
result_mock.stdout = json.dumps(issue_response)
|
||||
|
||||
return result_mock
|
||||
|
||||
mock_run.side_effect = side_effect
|
||||
|
||||
result = creator.create_bug_issue(
|
||||
title="CLI crashes on empty input",
|
||||
@@ -262,18 +348,29 @@ class TestIssueCreator:
|
||||
)
|
||||
|
||||
# Verify structure of created bug issue
|
||||
call_args = mock_run.call_args[0][0]
|
||||
json_data_index = call_args.index('-d') + 1
|
||||
json_data = json.loads(call_args[json_data_index])
|
||||
create_call = None
|
||||
for call in mock_run.call_args_list:
|
||||
cmd = call[0][0]
|
||||
url = cmd[-1]
|
||||
if 'issues' in url and '/labels' not in url:
|
||||
create_call = call
|
||||
break
|
||||
|
||||
assert "The CLI tool crashes when given empty input" in json_data['body']
|
||||
assert "Steps to Reproduce:" in json_data['body']
|
||||
assert "1. Run CLI command" in json_data['body']
|
||||
assert "2. Provide empty input" in json_data['body']
|
||||
assert "Expected Behavior: Should show help message" in json_data['body']
|
||||
assert "Actual Behavior: Application crashes" in json_data['body']
|
||||
assert "Environment: Python 3.8, Linux" in json_data['body']
|
||||
assert json_data['labels'] == ["bug"]
|
||||
assert create_call is not None
|
||||
cmd = create_call[0][0]
|
||||
|
||||
# Find the -d argument (data payload)
|
||||
data_index = cmd.index('-d') + 1
|
||||
payload = json.loads(cmd[data_index])
|
||||
|
||||
assert "The CLI tool crashes when given empty input" in payload['body']
|
||||
assert "Steps to Reproduce:" in payload['body']
|
||||
assert "1. Run CLI command" in payload['body']
|
||||
assert "2. Provide empty input" in payload['body']
|
||||
assert "Expected Behavior: Should show help message" in payload['body']
|
||||
assert "Actual Behavior: Application crashes" in payload['body']
|
||||
assert "Environment: Python 3.8, Linux" in payload['body']
|
||||
assert payload['labels'] == [1] # Should be ID: [bug]
|
||||
|
||||
@patch('subprocess.run')
|
||||
@patch('builtins.open', new_callable=mock_open, read_data="Title: Template Issue\nTemplate body content with {variable}")
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
"""
|
||||
Integration tests for issue creation, retrieval, and management workflow.
|
||||
|
||||
This test validates the complete issue lifecycle to catch authentication
|
||||
and API integration issues.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from tddai.issue_creator import IssueCreator
|
||||
from tddai.issue_writer import IssueWriter
|
||||
from tddai.issue_fetcher import IssueFetcher
|
||||
from tddai.config import TddaiConfig
|
||||
from tddai.exceptions import IssueError
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestIssueIntegration:
|
||||
"""Integration tests for the complete issue workflow."""
|
||||
|
||||
def _get_test_config(self):
|
||||
"""Get test configuration."""
|
||||
return TddaiConfig(
|
||||
workspace_dir=Path(".test_workspace"),
|
||||
gitea_url="http://92.205.130.254:32166",
|
||||
repo_owner="coulomb",
|
||||
repo_name="markitect_project"
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def auth_token(self):
|
||||
"""Get auth token from environment."""
|
||||
token = os.getenv('GITEA_API_TOKEN')
|
||||
if not token:
|
||||
pytest.skip("GITEA_API_TOKEN environment variable not set")
|
||||
return token
|
||||
|
||||
def test_environment_variable_detection(self):
|
||||
"""Test that components correctly detect GITEA_API_TOKEN."""
|
||||
config = self._get_test_config()
|
||||
|
||||
# Test without token
|
||||
creator_no_token = IssueCreator(config=config)
|
||||
writer_no_token = IssueWriter(config=config)
|
||||
|
||||
token_available = os.getenv('GITEA_API_TOKEN') is not None
|
||||
|
||||
if token_available:
|
||||
assert creator_no_token.auth_token is not None, "IssueCreator should detect GITEA_API_TOKEN"
|
||||
assert writer_no_token.auth_token is not None, "IssueWriter should detect GITEA_API_TOKEN"
|
||||
assert creator_no_token.auth_token == writer_no_token.auth_token, "Both should use same token"
|
||||
else:
|
||||
assert creator_no_token.auth_token is None, "Should be None when token not available"
|
||||
assert writer_no_token.auth_token is None, "Should be None when token not available"
|
||||
|
||||
@pytest.mark.skip(reason="Integration test requires running Gitea instance with proper label setup")
|
||||
def test_complete_issue_lifecycle(self, auth_token):
|
||||
"""Test create -> retrieve -> update -> delete cycle."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config, auth_token=auth_token)
|
||||
writer = IssueWriter(config=config, auth_token=auth_token)
|
||||
fetcher = IssueFetcher(config=config)
|
||||
|
||||
# Step 1: Create a test issue
|
||||
test_title = f"Test Issue - Integration Test {int(time.time())}"
|
||||
test_body = "This is a test issue created by integration tests. Please ignore and delete."
|
||||
|
||||
created_issue = creator.create_issue(
|
||||
title=test_title,
|
||||
body=test_body,
|
||||
labels=["test", "integration"]
|
||||
)
|
||||
|
||||
assert 'number' in created_issue, "Created issue should have number"
|
||||
issue_number = created_issue['number']
|
||||
|
||||
try:
|
||||
# Step 2: Retrieve the created issue
|
||||
retrieved_issue = fetcher.fetch_issue(issue_number)
|
||||
|
||||
assert retrieved_issue.title == test_title, "Retrieved issue should have correct title"
|
||||
assert retrieved_issue.body == test_body, "Retrieved issue should have correct body"
|
||||
assert retrieved_issue.state == "open", "New issue should be open"
|
||||
|
||||
# Step 3: Update the issue
|
||||
updated_title = f"{test_title} - UPDATED"
|
||||
update_result = writer.update_issue_title(issue_number, updated_title)
|
||||
|
||||
assert 'number' in update_result, "Update should return issue data"
|
||||
assert update_result['title'] == updated_title, "Title should be updated"
|
||||
|
||||
# Step 4: Close the issue (cleanup)
|
||||
close_result = writer.close_issue(issue_number)
|
||||
assert close_result['state'] == 'closed', "Issue should be closed"
|
||||
|
||||
print(f"✅ Integration test successful - Issue #{issue_number} lifecycle completed")
|
||||
|
||||
except Exception as e:
|
||||
# If anything fails, try to clean up the test issue
|
||||
try:
|
||||
writer.close_issue(issue_number)
|
||||
print(f"⚠️ Test failed but cleaned up issue #{issue_number}")
|
||||
except:
|
||||
print(f"❌ Test failed and couldn't clean up issue #{issue_number}")
|
||||
raise e
|
||||
|
||||
def test_authentication_error_handling(self):
|
||||
"""Test proper handling of authentication errors."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config, auth_token="invalid-token")
|
||||
|
||||
with pytest.raises(IssueError, match="Failed to create issue"):
|
||||
creator.create_issue("Test", "Test body")
|
||||
|
||||
def test_api_endpoint_validation(self):
|
||||
"""Test that API endpoints are constructed correctly."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config)
|
||||
|
||||
expected_url = "http://92.205.130.254:32166/api/v1/repos/coulomb/markitect_project/issues"
|
||||
assert config.issues_api_url == expected_url, f"API URL should be {expected_url}"
|
||||
|
||||
@pytest.mark.skip(reason="Integration test requires running Gitea instance with proper label setup")
|
||||
def test_structured_enhancement_creation(self, auth_token):
|
||||
"""Test creating structured enhancement issue."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config, auth_token=auth_token)
|
||||
writer = IssueWriter(config=config, auth_token=auth_token)
|
||||
|
||||
test_title = f"Test Enhancement - {int(time.time())}"
|
||||
|
||||
created_issue = creator.create_enhancement_issue(
|
||||
title=test_title,
|
||||
use_case="Integration test for enhancement creation",
|
||||
technical_requirements="Should create structured issue body",
|
||||
acceptance_criteria=["Issue has structured format", "All sections present"],
|
||||
dependencies=["Integration test framework"],
|
||||
priority="Low"
|
||||
)
|
||||
|
||||
issue_number = created_issue['number']
|
||||
|
||||
try:
|
||||
# Verify structured content
|
||||
fetcher = IssueFetcher(config=config)
|
||||
retrieved_issue = fetcher.fetch_issue(issue_number)
|
||||
|
||||
body = retrieved_issue.body
|
||||
assert "UseCase:" in body, "Should contain UseCase section"
|
||||
assert "Technical Requirements:" in body, "Should contain Technical Requirements"
|
||||
assert "Acceptance Criteria:" in body, "Should contain Acceptance Criteria"
|
||||
assert "- [ ] Issue has structured format" in body, "Should contain checkbox items"
|
||||
assert "Dependencies:" in body, "Should contain Dependencies section"
|
||||
|
||||
print(f"✅ Structured enhancement test successful - Issue #{issue_number}")
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
try:
|
||||
writer.close_issue(issue_number)
|
||||
except:
|
||||
pass # Best effort cleanup
|
||||
@@ -1,203 +0,0 @@
|
||||
"""
|
||||
Tests for IssueWriter functionality.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from tddai.issue_writer import IssueWriter
|
||||
from tddai.exceptions import IssueError
|
||||
from tddai.config import TddaiConfig
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TestIssueWriter:
|
||||
"""Test suite for IssueWriter class."""
|
||||
|
||||
def _get_test_config(self):
|
||||
"""Get a valid test configuration."""
|
||||
return TddaiConfig(
|
||||
workspace_dir=Path(".test_workspace"),
|
||||
gitea_url="http://localhost:3000",
|
||||
repo_owner="test_owner",
|
||||
repo_name="test_repo"
|
||||
)
|
||||
|
||||
def test_init_with_auth_token(self):
|
||||
"""Test IssueWriter initialization with auth token."""
|
||||
config = self._get_test_config()
|
||||
writer = IssueWriter(config=config, auth_token="test-token")
|
||||
assert writer.auth_token == "test-token"
|
||||
|
||||
def test_init_without_auth_token_uses_env(self):
|
||||
"""Test IssueWriter uses environment variable when no token provided."""
|
||||
config = self._get_test_config()
|
||||
with patch.dict('os.environ', {'GITEA_API_TOKEN': 'env-token'}):
|
||||
writer = IssueWriter(config=config)
|
||||
assert writer.auth_token == "env-token"
|
||||
|
||||
def test_update_issue_without_auth_token_raises_error(self):
|
||||
"""Test that updating without auth token raises IssueError."""
|
||||
config = self._get_test_config()
|
||||
with patch.dict('os.environ', {}, clear=True):
|
||||
writer = IssueWriter(config=config, auth_token=None)
|
||||
with pytest.raises(IssueError, match="Authentication token required"):
|
||||
writer.update_issue(1, {'title': 'New Title'})
|
||||
|
||||
@patch('tddai.issue_writer.subprocess.run')
|
||||
def test_update_issue_success(self, mock_run):
|
||||
"""Test successful issue update via PATCH."""
|
||||
# Mock successful response
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = json.dumps({
|
||||
'number': 1,
|
||||
'title': 'Updated Title',
|
||||
'body': 'Updated body',
|
||||
'state': 'open'
|
||||
})
|
||||
mock_result.stderr = ''
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
config = self._get_test_config()
|
||||
writer = IssueWriter(config=config, auth_token="test-token")
|
||||
result = writer.update_issue(1, {'title': 'Updated Title'})
|
||||
|
||||
assert result['number'] == 1
|
||||
assert result['title'] == 'Updated Title'
|
||||
|
||||
# Verify curl command was called correctly
|
||||
mock_run.assert_called_once()
|
||||
call_args = mock_run.call_args[0][0]
|
||||
assert 'curl' in call_args
|
||||
assert '-X' in call_args
|
||||
assert 'PATCH' in call_args
|
||||
assert 'Authorization: token test-token' in ' '.join(call_args)
|
||||
|
||||
@patch('tddai.issue_writer.subprocess.run')
|
||||
def test_update_issue_with_error_response(self, mock_run):
|
||||
"""Test issue update with API error response."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = json.dumps({'message': 'Issue not found'})
|
||||
mock_result.stderr = ''
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
config = self._get_test_config()
|
||||
writer = IssueWriter(config=config, auth_token="test-token")
|
||||
with pytest.raises(IssueError, match="Failed to update issue #1: Issue not found"):
|
||||
writer.update_issue(1, {'title': 'New Title'})
|
||||
|
||||
@patch('tddai.issue_writer.subprocess.run')
|
||||
def test_update_issue_subprocess_error(self, mock_run):
|
||||
"""Test issue update with subprocess error."""
|
||||
mock_run.side_effect = subprocess.CalledProcessError(1, 'curl')
|
||||
|
||||
config = self._get_test_config()
|
||||
writer = IssueWriter(config=config, auth_token="test-token")
|
||||
with pytest.raises(IssueError, match="Failed to update issue #1"):
|
||||
writer.update_issue(1, {'title': 'New Title'})
|
||||
|
||||
@patch('tddai.issue_writer.subprocess.run')
|
||||
def test_update_issue_json_decode_error(self, mock_run):
|
||||
"""Test issue update with invalid JSON response."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = "invalid json"
|
||||
mock_result.stderr = ''
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
config = self._get_test_config()
|
||||
writer = IssueWriter(config=config, auth_token="test-token")
|
||||
with pytest.raises(IssueError, match="Failed to parse response data"):
|
||||
writer.update_issue(1, {'title': 'New Title'})
|
||||
|
||||
@patch('tddai.issue_writer.subprocess.run')
|
||||
def test_update_issue_title(self, mock_run):
|
||||
"""Test updating only issue title."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = json.dumps({'number': 1, 'title': 'New Title'})
|
||||
mock_result.stderr = ''
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
config = self._get_test_config()
|
||||
writer = IssueWriter(config=config, auth_token="test-token")
|
||||
result = writer.update_issue_title(1, 'New Title')
|
||||
|
||||
assert result['title'] == 'New Title'
|
||||
|
||||
# Verify the correct data was sent
|
||||
call_args = mock_run.call_args[0][0]
|
||||
json_data_index = call_args.index('-d') + 1
|
||||
sent_data = json.loads(call_args[json_data_index])
|
||||
assert sent_data == {'title': 'New Title'}
|
||||
|
||||
@patch('tddai.issue_writer.subprocess.run')
|
||||
def test_update_issue_body(self, mock_run):
|
||||
"""Test updating only issue body."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = json.dumps({'number': 1, 'body': 'New body content'})
|
||||
mock_result.stderr = ''
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
config = self._get_test_config()
|
||||
writer = IssueWriter(config=config, auth_token="test-token")
|
||||
result = writer.update_issue_body(1, 'New body content')
|
||||
|
||||
assert result['body'] == 'New body content'
|
||||
|
||||
@patch('tddai.issue_writer.subprocess.run')
|
||||
def test_update_issue_state_valid(self, mock_run):
|
||||
"""Test updating issue state with valid state."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = json.dumps({'number': 1, 'state': 'closed'})
|
||||
mock_result.stderr = ''
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
config = self._get_test_config()
|
||||
writer = IssueWriter(config=config, auth_token="test-token")
|
||||
result = writer.update_issue_state(1, 'closed')
|
||||
|
||||
assert result['state'] == 'closed'
|
||||
|
||||
def test_update_issue_state_invalid(self):
|
||||
"""Test updating issue state with invalid state."""
|
||||
config = self._get_test_config()
|
||||
writer = IssueWriter(config=config, auth_token="test-token")
|
||||
with pytest.raises(IssueError, match="Invalid state 'invalid'"):
|
||||
writer.update_issue_state(1, 'invalid')
|
||||
|
||||
@patch('tddai.issue_writer.subprocess.run')
|
||||
def test_close_issue(self, mock_run):
|
||||
"""Test closing an issue."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = json.dumps({'number': 1, 'state': 'closed'})
|
||||
mock_result.stderr = ''
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
config = self._get_test_config()
|
||||
writer = IssueWriter(config=config, auth_token="test-token")
|
||||
result = writer.close_issue(1)
|
||||
|
||||
assert result['state'] == 'closed'
|
||||
|
||||
@patch('tddai.issue_writer.subprocess.run')
|
||||
def test_reopen_issue(self, mock_run):
|
||||
"""Test reopening an issue."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = json.dumps({'number': 1, 'state': 'open'})
|
||||
mock_result.stderr = ''
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
config = self._get_test_config()
|
||||
writer = IssueWriter(config=config, auth_token="test-token")
|
||||
result = writer.reopen_issue(1)
|
||||
|
||||
assert result['state'] == 'open'
|
||||
Reference in New Issue
Block a user