diff --git a/tddai/issue_creator.py b/tddai/issue_creator.py index e7d19694..1ad6725d 100644 --- a/tddai/issue_creator.py +++ b/tddai/issue_creator.py @@ -40,6 +40,10 @@ class IssueCreator: 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, diff --git a/tddai_cli.py b/tddai_cli.py index feea9642..2ff607e7 100644 --- a/tddai_cli.py +++ b/tddai_cli.py @@ -15,48 +15,55 @@ sys.path.insert(0, str(Path(__file__).parent)) from cli import CLIFramework -# Initialize CLI framework -cli = CLIFramework() +# Lazy initialization of CLI framework +_cli_framework = None + +def _get_cli(): + """Get CLI framework instance (lazy initialization).""" + global _cli_framework + if _cli_framework is None: + _cli_framework = CLIFramework() + return _cli_framework def workspace_status(): """Show current workspace status.""" - cli.workspace_status() + _get_cli().workspace_status() def start_issue(issue_number: int): """Start working on an issue.""" - cli.start_issue(issue_number) + _get_cli().start_issue(issue_number) def finish_issue(): """Finish current issue workspace.""" - cli.finish_issue() + _get_cli().finish_issue() def add_test_guidance(): """Show guidance for adding tests.""" - cli.add_test_guidance() + _get_cli().add_test_guidance() def list_issues(): """List all issues.""" - cli.list_issues() + _get_cli().list_issues() def list_open_issues(): """List only open issues.""" - cli.list_open_issues() + _get_cli().list_open_issues() def show_issue(issue_number: int): """Show detailed issue information.""" - cli.show_issue(issue_number) + _get_cli().show_issue(issue_number) def create_issue(title: str, body: str, issue_type: str = "enhancement"): """Create a new issue.""" - cli.create_issue(title, body, issue_type) + _get_cli().create_issue(title, body, issue_type) def create_enhancement_issue(title: str, use_case: str, technical_requirements: str = "", @@ -73,7 +80,7 @@ def create_enhancement_issue(title: str, use_case: str, technical_requirements: if dependencies: deps_list = [line.strip() for line in dependencies.split('\n') if line.strip()] - cli.create_enhancement_issue( + _get_cli().create_enhancement_issue( title=title, use_case=use_case, technical_requirements=technical_requirements, @@ -85,52 +92,52 @@ def create_enhancement_issue(title: str, use_case: str, technical_requirements: def create_from_template(template_file: str, **kwargs): """Create issue from template file.""" - cli.create_from_template(template_file, **kwargs) + _get_cli().create_from_template(template_file, **kwargs) def analyze_coverage(issue_number: int): """Analyze test coverage for a specific issue.""" - cli.analyze_coverage(issue_number) + _get_cli().analyze_coverage(issue_number) def setup_project_management(): """Setup project management labels and milestones.""" - cli.setup_project_management() + _get_cli().setup_project_management() def move_issue_to_state(issue_number: int, state: str): """Move issue to a specific project state.""" - cli.move_issue_to_state(issue_number, state) + _get_cli().move_issue_to_state(issue_number, state) def set_issue_priority(issue_number: int, priority: str): """Set issue priority.""" - cli.set_issue_priority(issue_number, priority) + _get_cli().set_issue_priority(issue_number, priority) def create_milestone(title: str, description: str = ""): """Create a new milestone (project).""" - cli.create_milestone(title, description) + _get_cli().create_milestone(title, description) def list_milestones(): """List all milestones.""" - cli.list_milestones() + _get_cli().list_milestones() def assign_issue_to_milestone(issue_number: int, milestone_id: int): """Assign issue to a milestone.""" - cli.assign_issue_to_milestone(issue_number, milestone_id) + _get_cli().assign_issue_to_milestone(issue_number, milestone_id) def project_overview(): """Show project management overview.""" - cli.project_overview() + _get_cli().project_overview() def issue_index(format_type="tsv", sort_by="number", filter_state=None, filter_priority=None, include_state=False): """Output compact index of all issues for Unix processing.""" - cli.issue_index( + _get_cli().issue_index( format_type=format_type, sort_by=sort_by, filter_state=filter_state, diff --git a/tests/test_issue_11_workspace_creation.py b/tests/test_issue_11_workspace_creation.py index d32bf3de..73a4a334 100644 --- a/tests/test_issue_11_workspace_creation.py +++ b/tests/test_issue_11_workspace_creation.py @@ -108,16 +108,16 @@ class TestWorkspaceCreation: with pytest.raises(WorkspaceError, match="Workspace already active"): manager.create_workspace(second_issue_data) - @patch('tddai.issue_fetcher.subprocess.run') + @patch('gitea.http_client.subprocess.run') def test_issue_fetcher_handles_invalid_issue(self, mock_run, temp_workspace): """Test error handling for invalid issue numbers.""" - # Mock curl response for non-existent issue - mock_run.return_value.returncode = 0 - mock_run.return_value.stdout = '{"message": "404 Not Found"}' + # Mock curl response for non-existent issue (404 error) + from subprocess import CalledProcessError + mock_run.side_effect = CalledProcessError(22, 'curl') # HTTP 404 error fetcher = IssueFetcher(temp_workspace) - with pytest.raises(IssueError, match="not found"): + with pytest.raises(IssueError, match="API error fetching issue.*HTTP request failed"): fetcher.fetch_issue(999) def test_workspace_cleanup(self, temp_workspace, mock_issue_data): diff --git a/tests/test_issue_creator.py b/tests/test_issue_creator.py index fb5bdd24..13a6efbf 100644 --- a/tests/test_issue_creator.py +++ b/tests/test_issue_creator.py @@ -25,6 +25,18 @@ class TestIssueCreator: 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_init_with_auth_token(self): """Test IssueCreator initialization with auth token.""" config = self._get_test_config() @@ -62,7 +74,10 @@ class TestIssueCreator: "number": 123, "title": "Test Issue", "body": "Test description", - "state": "open" + "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( @@ -73,7 +88,19 @@ class TestIssueCreator: result = creator.create_issue("Test Issue", "Test description") - assert result == mock_response + # 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 @@ -144,7 +171,7 @@ class TestIssueCreator: stderr="" ) - with pytest.raises(IssueError, match="Failed to parse response data"): + with pytest.raises(IssueError, match="Failed to create issue.*parse.*response"): creator.create_issue("Test Issue", "Test description") @patch('subprocess.run') @@ -153,7 +180,7 @@ class TestIssueCreator: config = self._get_test_config() creator = IssueCreator(config=config, auth_token="test-token") - mock_response = {"number": 124} + mock_response = self._get_complete_mock_response(124) mock_run.return_value = MagicMock( returncode=0, stdout=json.dumps(mock_response), @@ -183,7 +210,7 @@ class TestIssueCreator: config = self._get_test_config() creator = IssueCreator(config=config, auth_token="test-token") - mock_response = {"number": 125} + mock_response = self._get_complete_mock_response(125) mock_run.return_value = MagicMock( returncode=0, stdout=json.dumps(mock_response), @@ -218,7 +245,7 @@ class TestIssueCreator: config = self._get_test_config() creator = IssueCreator(config=config, auth_token="test-token") - mock_response = {"number": 126} + mock_response = self._get_complete_mock_response(126) mock_run.return_value = MagicMock( returncode=0, stdout=json.dumps(mock_response), @@ -255,7 +282,7 @@ class TestIssueCreator: config = self._get_test_config() creator = IssueCreator(config=config, auth_token="test-token") - mock_response = {"number": 127} + mock_response = self._get_complete_mock_response(127) mock_run.return_value = MagicMock( returncode=0, stdout=json.dumps(mock_response),