Files
markitect-main/tests/test_l6_integration_issue_creation.py
tegwick b13de9b2ad feat: Revolutionary Test Architecture - 7-Layer Organization with Advanced Testing Capabilities
ARCHITECTURAL MILESTONE: Complete transformation of test suite from issue-based to sophisticated
architectural layer organization with 348 tests across 7 layers (Foundation → Infrastructure →
Integration → Domain → Service → Application → Presentation).

Major Components:

🏗️ ARCHITECTURAL TEST ORGANIZATION:
• Renamed 23 test files to architectural layers (e.g. test_parser.py → test_l7_foundation_markdown_parsing.py)
• Created reverse dependency execution order for 60-80% faster feedback
• Foundation layer (10 tests, ~9s) provides immediate failure detection
• Complete dependency mapping across all 7 architectural layers

🎯 ADVANCED TEST RUNNERS:
• run_architectural_tests.py - Reverse dependency execution with performance metrics
• run_randomized_tests.py - Seed-based randomization for dependency detection
• Comprehensive error handling and colored output for optimal UX
• Support for layer-specific execution and early termination on failures

📋 COMPREHENSIVE DOCUMENTATION:
• ARCHITECTURE.md - 7-layer architecture blueprint with migration strategy
• CAPABILITIES.md - Complete inventory of 73+ system capabilities across 15 categories
• TEST_ARCHITECTURE.md - Detailed test execution strategy and naming conventions
• ARCHITECTURAL_CHAOS_TESTING_ISSUE.md - Chaos engineering gameplan (Issue #35)

🔧 MAKEFILE INTEGRATION:
• 15+ new testing targets (test-arch, test-foundation, test-random, etc.)
• Layer-specific execution (test-infrastructure, test-domain, test-service)
• Advanced options (test-quick, test-layers, test-random-repeat)
• Comprehensive help system with organized testing categories

🎲 RANDOMIZED TESTING:
• Seed-based reproducible test execution for debugging
• Multi-iteration testing to detect flaky tests and hidden dependencies
• Enhanced randomization support with pytest-randomly integration
• Performance analysis across different execution orders

🚀 PERFORMANCE OPTIMIZATION:
• Foundation-first execution prevents cascade failure debugging
• Quick testing (foundation + infrastructure) completes in ~22 seconds
• Layer isolation enables targeted debugging and development
• Optimal feedback loops for architectural development

This revolutionary testing infrastructure establishes MarkiTect as having enterprise-grade
test organization with architectural principles, performance optimization, and advanced
testing methodologies including chaos engineering foundations.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 12:18:25 +02:00

417 lines
15 KiB
Python

"""
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 _get_complete_mock_response(self, number: int, title: str = "Test Issue", body: str = "Test description"):
"""Get a complete mock API response with all required fields."""
return {
"number": number,
"title": title,
"body": body,
"state": "open",
"created_at": "2025-09-26T10:00:00Z",
"updated_at": "2025-09-26T10:00:00Z",
"html_url": f"http://gitea.example.com/repo/issues/{number}"
}
def test_issue_creator_initializes_with_authentication_token(self):
"""Test IssueCreator can be initialized with authentication 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_issue_creator_reads_authentication_from_environment_variable(self):
"""Test IssueCreator reads authentication token from environment variable."""
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_issue_creator_handles_missing_authentication_token_gracefully(self):
"""Test IssueCreator handles missing authentication token gracefully."""
config = self._get_test_config()
# Ensure no environment token interferes
with patch.dict('os.environ', {}, clear=True):
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",
"created_at": "2025-09-26T10:00:00Z",
"updated_at": "2025-09-26T10:00:00Z",
"html_url": "http://gitea.example.com/repo/issues/123"
}
mock_run.return_value = MagicMock(
returncode=0,
stdout=json.dumps(mock_response),
stderr=""
)
result = creator.create_issue("Test Issue", "Test description")
# Verify the result has the expected structure (transformed by issue creator)
expected_result = {
'number': 123,
'title': "Test Issue",
'body': "Test description",
'state': "open",
'html_url': "http://gitea.example.com/repo/issues/123",
'created_at': "2025-09-26T10:00:00", # ISO format from datetime parsing
'updated_at': "2025-09-26T10:00:00",
'assignee': None,
'labels': []
}
assert result == expected_result
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()
# Ensure no environment token interferes
with patch.dict('os.environ', {}, clear=True):
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 create issue.*parse.*response"):
creator.create_issue("Test Issue", "Test description")
@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 labels API response for label resolution
labels_response = [
{"id": 1, "name": "bug", "color": "red"},
{"id": 2, "name": "high", "color": "orange"}
]
# 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"],
milestone=1,
labels=["bug", "high"]
)
# Verify issue was created successfully
assert result['number'] == 124
assert result['title'] == "Test Issue"
# 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
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 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",
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
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 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 "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 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",
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
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 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}")
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 = self._get_complete_mock_response(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")