Fix all test failures introduced by recent architectural changes: • Issue Creator Tests: - Fixed mock API responses to include required fields (created_at, updated_at, html_url) - Added input validation for empty titles back to issue creator - Updated test expectations to match new error handling patterns - Created helper function for complete mock responses • Issue Fetcher Test: - Updated mock target from tddai.issue_fetcher.subprocess to gitea.http_client.subprocess - Fixed test assertions to match new error handling with specific exception chaining - Test now properly validates API error translation • Makefile Integration Test: - Implemented lazy initialization in tddai_cli.py to prevent import-time configuration errors - Replaced eager CLI framework initialization with _get_cli() lazy pattern - Preserves normal CLI functionality while fixing test environment compatibility • Result: All 171 tests now pass (169 passed, 2 skipped) • Maintains backward compatibility of CLI interface • Validates that refactored error handling works correctly 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
211 lines
6.9 KiB
Python
211 lines
6.9 KiB
Python
"""
|
|
Issue creation using the Gitea facade.
|
|
|
|
This module now acts as an adapter to the new gitea package,
|
|
maintaining backwards compatibility while using the cleaner API.
|
|
"""
|
|
|
|
import os
|
|
from typing import Dict, Any, Optional, List
|
|
|
|
from gitea import GiteaClient, GiteaConfig, Priority
|
|
from .config import get_config
|
|
from .exceptions import IssueError
|
|
|
|
|
|
class IssueCreator:
|
|
"""Creates new issues using the Gitea 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 create_issue(self, title: str, body: str, **kwargs) -> Dict[str, Any]:
|
|
"""Create a new issue via POST operation.
|
|
|
|
Args:
|
|
title: Issue title (required)
|
|
body: Issue description/body (required)
|
|
**kwargs: Optional fields (assignees, milestone, labels, etc.)
|
|
|
|
Returns:
|
|
Dict containing created issue data including issue number
|
|
|
|
Raises:
|
|
IssueError: If creation fails
|
|
"""
|
|
# Validate input
|
|
if not title or not title.strip():
|
|
raise IssueError("Issue title cannot be empty")
|
|
|
|
try:
|
|
issue = self.gitea_client.issues.create(
|
|
title=title,
|
|
body=body,
|
|
assignees=kwargs.get('assignees', []),
|
|
milestone=kwargs.get('milestone'),
|
|
labels=kwargs.get('labels', [])
|
|
)
|
|
|
|
# Convert back to dict format for backwards 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} for label in issue.labels]
|
|
}
|
|
|
|
except Exception as e:
|
|
raise IssueError(f"Failed to create issue: {e}")
|
|
|
|
def create_enhancement_issue(self, title: str, use_case: str,
|
|
technical_requirements: str = "",
|
|
acceptance_criteria: List[str] = None,
|
|
dependencies: List[str] = None,
|
|
priority: str = "Medium") -> Dict[str, Any]:
|
|
"""Create an enhancement issue with structured format.
|
|
|
|
Args:
|
|
title: Issue title
|
|
use_case: UseCase description
|
|
technical_requirements: Technical implementation details
|
|
acceptance_criteria: List of acceptance criteria
|
|
dependencies: List of dependency descriptions
|
|
priority: Priority level (High, Medium, Low)
|
|
|
|
Returns:
|
|
Dict containing created issue data
|
|
"""
|
|
# Build structured body
|
|
body_parts = [f"UseCase: {use_case}"]
|
|
|
|
if technical_requirements:
|
|
body_parts.extend([
|
|
"",
|
|
"Technical Requirements:",
|
|
technical_requirements
|
|
])
|
|
|
|
if acceptance_criteria:
|
|
body_parts.extend([
|
|
"",
|
|
"Acceptance Criteria:"
|
|
])
|
|
for criterion in acceptance_criteria:
|
|
body_parts.append(f"- [ ] {criterion}")
|
|
|
|
if dependencies:
|
|
body_parts.extend([
|
|
"",
|
|
"Dependencies:"
|
|
])
|
|
for dep in dependencies:
|
|
body_parts.append(f"- {dep}")
|
|
|
|
body = "\n".join(body_parts)
|
|
|
|
# Create with enhancement label
|
|
return self.create_issue(
|
|
title=title,
|
|
body=body,
|
|
labels=[priority.lower(), "enhancement"]
|
|
)
|
|
|
|
def create_bug_issue(self, title: str, description: str,
|
|
steps_to_reproduce: List[str] = None,
|
|
expected_behavior: str = "",
|
|
actual_behavior: str = "",
|
|
environment: str = "") -> Dict[str, Any]:
|
|
"""Create a bug issue with structured format.
|
|
|
|
Args:
|
|
title: Bug title
|
|
description: Bug description
|
|
steps_to_reproduce: List of reproduction steps
|
|
expected_behavior: What should happen
|
|
actual_behavior: What actually happens
|
|
environment: Environment details
|
|
|
|
Returns:
|
|
Dict containing created issue data
|
|
"""
|
|
body_parts = [description]
|
|
|
|
if steps_to_reproduce:
|
|
body_parts.extend([
|
|
"",
|
|
"Steps to Reproduce:"
|
|
])
|
|
for i, step in enumerate(steps_to_reproduce, 1):
|
|
body_parts.append(f"{i}. {step}")
|
|
|
|
if expected_behavior:
|
|
body_parts.extend([
|
|
"",
|
|
f"Expected Behavior: {expected_behavior}"
|
|
])
|
|
|
|
if actual_behavior:
|
|
body_parts.extend([
|
|
"",
|
|
f"Actual Behavior: {actual_behavior}"
|
|
])
|
|
|
|
if environment:
|
|
body_parts.extend([
|
|
"",
|
|
f"Environment: {environment}"
|
|
])
|
|
|
|
body = "\n".join(body_parts)
|
|
|
|
# Create with bug label
|
|
return self.create_issue(
|
|
title=title,
|
|
body=body,
|
|
labels=["bug"]
|
|
)
|
|
|
|
def create_from_template(self, template_file: str, **template_vars) -> Dict[str, Any]:
|
|
"""Create issue from a template file.
|
|
|
|
Args:
|
|
template_file: Path to template file
|
|
**template_vars: Variables to substitute in template
|
|
|
|
Returns:
|
|
Dict containing created issue data
|
|
"""
|
|
try:
|
|
with open(template_file, 'r') as f:
|
|
template_content = f.read()
|
|
|
|
# Simple template variable substitution
|
|
for key, value in template_vars.items():
|
|
template_content = template_content.replace(f"{{{key}}}", str(value))
|
|
|
|
# Extract title (first line) and body (rest)
|
|
lines = template_content.strip().split('\n')
|
|
if not lines or (len(lines) == 1 and not lines[0].strip()):
|
|
raise IssueError("Template file is empty")
|
|
|
|
title = lines[0].replace('Title: ', '').strip()
|
|
body = '\n'.join(lines[1:]).strip()
|
|
|
|
return self.create_issue(title=title, body=body)
|
|
|
|
except FileNotFoundError:
|
|
raise IssueError(f"Template file not found: {template_file}")
|
|
except Exception as e:
|
|
raise IssueError(f"Failed to process template: {e}") |