feat: Implement comprehensive IssueCreator system and create CLI roadmap issues
IssueCreator Implementation: - Add tddai/issue_creator.py with full POST API functionality for issue creation - Support multiple creation methods: basic, enhancement, bug, template-based - Include structured issue formatting with acceptance criteria and dependencies - Template system with variable substitution for reusable issue creation Authentication Fix: - Fix critical authentication bug: use GITEA_API_TOKEN instead of GITEA_TOKEN - Update both IssueCreator and IssueWriter for consistency - Update all tests and documentation to reflect correct environment variable Comprehensive Test Suite: - Add 15 unit tests for IssueCreator (tests/test_issue_creator.py) - Add 5 integration tests for full API lifecycle (tests/test_issue_integration.py) - Create test_environment_variable_detection to prevent future auth issues - Total 33 tests covering complete issue handling workflow CLI Integration: - Enhance tddai_cli.py with 3 new commands: create-issue, create-enhancement, create-from-template - Add comprehensive argument parsing with optional fields and priority support - Include user-friendly output with next step guidance - Update package exports to include IssueCreator CLI Roadmap Execution: - Successfully create 8 CLI implementation issues (#12-#19) in Gitea - Resolve mismatch between NEXT.md roadmap and actual Gitea issues - Issues prioritized for core USPs: Database Query CLI and AST Query CLI - Remove local MISSING_ISSUES.md file after successful creation Framework Maturity: - Complete CRUD operations for issue management (Create, Read, Update, Delete) - Robust error handling and API integration patterns - Full authentication and environment variable management - Ready for production CLI implementation workflow 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
288
tests/test_issue_creator.py
Normal file
288
tests/test_issue_creator.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""
|
||||
Tests for IssueCreator functionality.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock, mock_open
|
||||
from pathlib import Path
|
||||
|
||||
from tddai.issue_creator import IssueCreator
|
||||
from tddai.exceptions import IssueError
|
||||
from tddai.config import TddaiConfig
|
||||
|
||||
|
||||
class TestIssueCreator:
|
||||
"""Test suite for IssueCreator 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 IssueCreator initialization with auth token."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config, auth_token="test-token")
|
||||
|
||||
assert creator.config == config
|
||||
assert creator.auth_token == "test-token"
|
||||
|
||||
def test_init_with_env_token(self):
|
||||
"""Test IssueCreator initialization with environment token."""
|
||||
config = self._get_test_config()
|
||||
|
||||
with patch.dict('os.environ', {'GITEA_API_TOKEN': 'env-token'}):
|
||||
creator = IssueCreator(config=config)
|
||||
assert creator.auth_token == 'env-token'
|
||||
|
||||
def test_init_without_token(self):
|
||||
"""Test IssueCreator initialization without token."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config)
|
||||
|
||||
assert creator.auth_token is None
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_create_issue_success(self, mock_run):
|
||||
"""Test successful issue creation."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config, auth_token="test-token")
|
||||
|
||||
# Mock successful API response
|
||||
mock_response = {
|
||||
"number": 123,
|
||||
"title": "Test Issue",
|
||||
"body": "Test description",
|
||||
"state": "open"
|
||||
}
|
||||
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout=json.dumps(mock_response),
|
||||
stderr=""
|
||||
)
|
||||
|
||||
result = creator.create_issue("Test Issue", "Test description")
|
||||
|
||||
assert result == mock_response
|
||||
mock_run.assert_called_once()
|
||||
|
||||
# Check curl command structure
|
||||
call_args = mock_run.call_args[0][0]
|
||||
assert 'curl' in call_args
|
||||
assert '-X' in call_args
|
||||
assert 'POST' in call_args
|
||||
assert 'Authorization: token test-token' in ' '.join(call_args)
|
||||
|
||||
def test_create_issue_without_auth_token(self):
|
||||
"""Test issue creation without authentication token."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config)
|
||||
|
||||
with pytest.raises(IssueError, match="Authentication token required"):
|
||||
creator.create_issue("Test Issue", "Test description")
|
||||
|
||||
def test_create_issue_empty_title(self):
|
||||
"""Test issue creation with empty title."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config, auth_token="test-token")
|
||||
|
||||
with pytest.raises(IssueError, match="Issue title cannot be empty"):
|
||||
creator.create_issue("", "Test description")
|
||||
|
||||
with pytest.raises(IssueError, match="Issue title cannot be empty"):
|
||||
creator.create_issue(" ", "Test description")
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_create_issue_api_error(self, mock_run):
|
||||
"""Test issue creation with API error response."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config, auth_token="test-token")
|
||||
|
||||
# Mock API error response
|
||||
mock_response = {"message": "Repository not found"}
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout=json.dumps(mock_response),
|
||||
stderr=""
|
||||
)
|
||||
|
||||
with pytest.raises(IssueError, match="Failed to create issue: Repository not found"):
|
||||
creator.create_issue("Test Issue", "Test description")
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_create_issue_subprocess_error(self, mock_run):
|
||||
"""Test issue creation with subprocess error."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config, auth_token="test-token")
|
||||
|
||||
mock_run.side_effect = subprocess.CalledProcessError(1, 'curl')
|
||||
|
||||
with pytest.raises(IssueError, match="Failed to create issue"):
|
||||
creator.create_issue("Test Issue", "Test description")
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_create_issue_json_error(self, mock_run):
|
||||
"""Test issue creation with invalid JSON response."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config, auth_token="test-token")
|
||||
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout="invalid json",
|
||||
stderr=""
|
||||
)
|
||||
|
||||
with pytest.raises(IssueError, match="Failed to parse response data"):
|
||||
creator.create_issue("Test Issue", "Test description")
|
||||
|
||||
@patch('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 = {"number": 124}
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout=json.dumps(mock_response),
|
||||
stderr=""
|
||||
)
|
||||
|
||||
creator.create_issue(
|
||||
"Test Issue",
|
||||
"Test description",
|
||||
assignees=["user1"],
|
||||
milestone=1,
|
||||
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])
|
||||
|
||||
assert json_data['assignees'] == ["user1"]
|
||||
assert json_data['milestone'] == 1
|
||||
assert json_data['labels'] == ["bug", "high"]
|
||||
|
||||
@patch('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 = {"number": 125}
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout=json.dumps(mock_response),
|
||||
stderr=""
|
||||
)
|
||||
|
||||
result = creator.create_enhancement_issue(
|
||||
title="Add CLI Support",
|
||||
use_case="User needs command-line interface",
|
||||
technical_requirements="Implement Click framework",
|
||||
acceptance_criteria=["CLI entry point works", "Commands have help text"],
|
||||
dependencies=["Issue #1 - Database"],
|
||||
priority="High"
|
||||
)
|
||||
|
||||
# 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])
|
||||
|
||||
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"]
|
||||
|
||||
@patch('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 = {"number": 126}
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout=json.dumps(mock_response),
|
||||
stderr=""
|
||||
)
|
||||
|
||||
result = creator.create_bug_issue(
|
||||
title="CLI crashes on empty input",
|
||||
description="The CLI tool crashes when given empty input",
|
||||
steps_to_reproduce=["Run CLI command", "Provide empty input", "Observe crash"],
|
||||
expected_behavior="Should show help message",
|
||||
actual_behavior="Application crashes",
|
||||
environment="Python 3.8, Linux"
|
||||
)
|
||||
|
||||
# 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])
|
||||
|
||||
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"]
|
||||
|
||||
@patch('subprocess.run')
|
||||
@patch('builtins.open', new_callable=mock_open, read_data="Title: Template Issue\nTemplate body content with {variable}")
|
||||
def test_create_from_template(self, mock_file, mock_run):
|
||||
"""Test creating issue from template file."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config, auth_token="test-token")
|
||||
|
||||
mock_response = {"number": 127}
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout=json.dumps(mock_response),
|
||||
stderr=""
|
||||
)
|
||||
|
||||
result = creator.create_from_template(
|
||||
"template.md",
|
||||
variable="test value"
|
||||
)
|
||||
|
||||
# Verify template processing
|
||||
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])
|
||||
|
||||
assert json_data['title'] == "Template Issue"
|
||||
assert "Template body content with test value" in json_data['body']
|
||||
|
||||
def test_create_from_template_file_not_found(self):
|
||||
"""Test creating issue from non-existent template."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config, auth_token="test-token")
|
||||
|
||||
with pytest.raises(IssueError, match="Template file not found"):
|
||||
creator.create_from_template("nonexistent.md")
|
||||
|
||||
@patch('builtins.open', new_callable=mock_open, read_data="")
|
||||
def test_create_from_empty_template(self, mock_file):
|
||||
"""Test creating issue from empty template."""
|
||||
config = self._get_test_config()
|
||||
creator = IssueCreator(config=config, auth_token="test-token")
|
||||
|
||||
with pytest.raises(IssueError, match="Template file is empty"):
|
||||
creator.create_from_template("empty.md")
|
||||
Reference in New Issue
Block a user