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:
2025-09-24 23:36:07 +02:00
parent 978e925b60
commit 72f341279a
10 changed files with 1180 additions and 3 deletions

View File

@@ -0,0 +1,316 @@
"""
Test suite for Issue #2: Fast Document Loading & CLI Manipulation
Focus: Subtask 2a - File Ingestion & AST Caching
This test suite covers the core file ingestion and AST caching functionality
that forms the foundation of the performance-optimized document system.
"""
import json
import os
import tempfile
import time
from pathlib import Path
from unittest.mock import patch, MagicMock
import pytest
from markitect.database import DatabaseManager
from markitect.parser import parse_markdown_to_ast
class TestFileIngestion:
"""Test file ingestion and basic AST processing."""
def setup_method(self):
"""Set up test database and temporary files."""
self.temp_dir = tempfile.mkdtemp()
self.db_path = Path(self.temp_dir) / "test.db"
self.db_manager = DatabaseManager(self.db_path)
self.db_manager.initialize_database() # Initialize the database
# Create test markdown file
self.test_md_content = """---
title: Test Document
author: Test User
date: "2025-09-24"
---
# Test Document
This is a test document with **bold** and *italic* text.
## Section 1
- Item 1
- Item 2
- Item 3
## Section 2
Some more content here.
"""
self.test_file = Path(self.temp_dir) / "test.md"
self.test_file.write_text(self.test_md_content)
def teardown_method(self):
"""Clean up test files."""
import shutil
shutil.rmtree(self.temp_dir)
def test_parse_markdown_file_to_ast(self):
"""Test parsing markdown file to AST representation."""
# This test should fail initially - we need to implement document ingestion
from markitect.document_manager import DocumentManager # This will fail initially
doc_manager = DocumentManager(self.db_manager)
result = doc_manager.ingest_file(self.test_file)
# Verify the result contains parsed AST
assert result is not None
assert 'ast' in result
assert 'metadata' in result
assert result['metadata']['filename'] == 'test.md'
assert result['metadata']['title'] == 'Test Document'
def test_ast_contains_expected_structure(self):
"""Test that parsed AST contains expected document structure."""
# Parse the test file
ast = parse_markdown_to_ast(self.test_md_content)
# Verify AST structure contains expected elements
assert isinstance(ast, list)
assert len(ast) > 0
# Should contain front matter, headings, paragraphs, lists
token_types = [token['type'] for token in ast]
assert 'heading_open' in token_types
assert 'paragraph_open' in token_types
assert 'bullet_list_open' in token_types
class TestASTCaching:
"""Test AST caching system for performance optimization."""
def setup_method(self):
"""Set up test environment with caching."""
self.temp_dir = tempfile.mkdtemp()
self.db_path = Path(self.temp_dir) / "test.db"
self.cache_dir = Path(self.temp_dir) / "ast_cache"
self.cache_dir.mkdir()
self.test_file = Path(self.temp_dir) / "performance_test.md"
# Create a larger test file for performance testing
large_content = """---
title: Large Test Document
---
# Large Document
""" + "\n\n".join([f"## Section {i}\n\nContent for section {i} with multiple paragraphs." for i in range(50)])
self.test_file.write_text(large_content)
def teardown_method(self):
"""Clean up test files."""
import shutil
shutil.rmtree(self.temp_dir)
def test_create_ast_cache_file(self):
"""Test creating AST cache file from markdown."""
# This will fail initially - need to implement AST cache system
from markitect.ast_cache import ASTCache # This will fail initially
cache = ASTCache(self.cache_dir)
cache_info = cache.cache_file(self.test_file)
# Verify cache file was created
assert cache_info['cache_file'].exists()
assert cache_info['cache_file'].suffix == '.json'
# Verify cache contains valid AST
with open(cache_info['cache_file']) as f:
cached_ast = json.load(f)
assert isinstance(cached_ast, list)
assert len(cached_ast) > 0
def test_cache_faster_than_parsing(self):
"""Test that cache loading is faster than re-parsing markdown."""
# This test validates the core performance requirement
from markitect.ast_cache import ASTCache
cache = ASTCache(self.cache_dir)
# Time the initial parse and cache creation
start_time = time.time()
cache_info = cache.cache_file(self.test_file)
initial_parse_time = time.time() - start_time
# Time loading from cache
start_time = time.time()
cached_ast = cache.load_cached_ast(self.test_file)
cache_load_time = time.time() - start_time
# Cache loading should be significantly faster
assert cache_load_time < (initial_parse_time * 0.5) # Less than 50% as per requirements
assert cached_ast is not None
def test_cache_invalidation_on_file_change(self):
"""Test that cache is invalidated when source file changes."""
from markitect.ast_cache import ASTCache
cache = ASTCache(self.cache_dir)
original_cache = cache.cache_file(self.test_file)
original_mtime = original_cache['cache_file'].stat().st_mtime
# Modify the source file
time.sleep(0.1) # Ensure different timestamp
modified_content = self.test_file.read_text() + "\n\n## New Section\n\nAdded content."
self.test_file.write_text(modified_content)
# Cache should detect the change and regenerate
new_cache = cache.cache_file(self.test_file)
new_mtime = new_cache['cache_file'].stat().st_mtime
assert new_mtime > original_mtime
class TestDatabaseIntegration:
"""Test integration with existing database system from Issue #1."""
def setup_method(self):
"""Set up test database."""
self.temp_dir = tempfile.mkdtemp()
self.db_path = Path(self.temp_dir) / "test.db"
self.db_manager = DatabaseManager(self.db_path)
self.db_manager.initialize_database() # Initialize the database
self.test_file = Path(self.temp_dir) / "integration_test.md"
self.test_content = """---
title: Integration Test
category: testing
---
# Integration Test
Testing database integration.
"""
self.test_file.write_text(self.test_content)
def teardown_method(self):
"""Clean up test files."""
import shutil
shutil.rmtree(self.temp_dir)
def test_store_document_metadata_in_database(self):
"""Test storing document metadata in existing database structure."""
# This should build on Issue #1's database functionality
from markitect.document_manager import DocumentManager
doc_manager = DocumentManager(self.db_manager)
result = doc_manager.ingest_file(self.test_file)
# Verify metadata is stored in database
stored_files = self.db_manager.list_markdown_files()
assert len(stored_files) == 1
stored_file = stored_files[0]
assert stored_file['filename'] == 'integration_test.md'
assert stored_file['front_matter']['title'] == 'Integration Test'
assert stored_file['front_matter']['category'] == 'testing'
def test_store_ast_cache_reference_in_database(self):
"""Test storing AST cache file reference in database."""
from markitect.document_manager import DocumentManager
doc_manager = DocumentManager(self.db_manager)
result = doc_manager.ingest_file(self.test_file)
# Verify AST cache reference is stored
assert 'ast_cache_path' in result
assert result['ast_cache_path'].exists()
# Verify database contains cache reference
stored_files = self.db_manager.list_markdown_files()
stored_file = stored_files[0]
# For now, cache reference is tracked in the result object
assert result['ast_cache_path'].exists()
def test_performance_metadata_tracking(self):
"""Test tracking performance metrics for cache validation."""
from markitect.document_manager import DocumentManager
doc_manager = DocumentManager(self.db_manager)
result = doc_manager.ingest_file(self.test_file)
# Verify performance metrics are tracked
assert 'parse_time' in result
assert 'cache_time' in result
assert result['parse_time'] > 0
assert result['cache_time'] >= 0
class TestErrorHandling:
"""Test error handling for file ingestion and caching."""
def setup_method(self):
"""Set up test environment."""
self.temp_dir = tempfile.mkdtemp()
self.db_path = Path(self.temp_dir) / "test.db"
def teardown_method(self):
"""Clean up test files."""
import shutil
shutil.rmtree(self.temp_dir)
def test_handle_nonexistent_file(self):
"""Test handling of nonexistent file."""
from markitect.document_manager import DocumentManager
db_manager = DatabaseManager(self.db_path)
doc_manager = DocumentManager(db_manager)
nonexistent_file = Path(self.temp_dir) / "nonexistent.md"
with pytest.raises(FileNotFoundError):
doc_manager.ingest_file(nonexistent_file)
def test_handle_invalid_markdown(self):
"""Test handling of invalid or malformed markdown."""
from markitect.document_manager import DocumentManager
# Create file with malformed front matter
invalid_file = Path(self.temp_dir) / "invalid.md"
invalid_content = """---
title: Test
invalid_yaml: [unclosed bracket
---
# Content
"""
invalid_file.write_text(invalid_content)
db_manager = DatabaseManager(self.db_path)
doc_manager = DocumentManager(db_manager)
# Should handle gracefully, not crash
result = doc_manager.ingest_file(invalid_file)
assert result is not None
# Front matter parsing should fail gracefully
def test_handle_cache_directory_permissions(self):
"""Test handling of cache directory permission issues."""
from markitect.ast_cache import ASTCache
# Create read-only directory to simulate permission issues
readonly_dir = Path(self.temp_dir) / "readonly"
readonly_dir.mkdir()
readonly_dir.chmod(0o444) # Read-only
test_file = Path(self.temp_dir) / "test.md"
test_file.write_text("# Test")
cache = ASTCache(readonly_dir)
with pytest.raises(PermissionError):
cache.cache_file(test_file)

288
tests/test_issue_creator.py Normal file
View 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")

View File

@@ -0,0 +1,163 @@
"""
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"
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}"
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

View File

@@ -34,7 +34,7 @@ class TestIssueWriter:
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_TOKEN': 'env-token'}):
with patch.dict('os.environ', {'GITEA_API_TOKEN': 'env-token'}):
writer = IssueWriter(config=config)
assert writer.auth_token == "env-token"