"""Tests for commit message generator.""" from unittest.mock import patch import pytest from src.analyzer import ChangeSet, ChangeType, StagedChange from src.generator import ( GenerationError, detect_commit_type, detect_scope, generate_commit_message, generate_description, ) class TestDetectCommitType: """Tests for commit type detection.""" def test_detects_feat_for_src_files(self): """Test detecting 'feat' for source files.""" change_set = ChangeSet([ StagedChange("src/main.py", ChangeType.ADDED), StagedChange("src/cli.py", ChangeType.MODIFIED), ]) result = detect_commit_type(change_set) assert result == "feat" def test_detects_fix_for_bug_fixes(self): """Test detecting 'fix' for bug-related changes.""" change_set = ChangeSet([ StagedChange("bug_fix.py", ChangeType.MODIFIED), StagedChange("fix_login.py", ChangeType.MODIFIED), ]) result = detect_commit_type(change_set) assert result == "fix" def test_detects_docs_for_markdown(self): """Test detecting 'docs' for markdown files.""" change_set = ChangeSet([ StagedChange("README.md", ChangeType.MODIFIED), StagedChange("docs/guide.md", ChangeType.ADDED), ]) result = detect_commit_type(change_set) assert result == "docs" def test_detects_test_for_test_files(self): """Test detecting 'test' for test files.""" change_set = ChangeSet([ StagedChange("tests/test_main.py", ChangeType.ADDED), StagedChange("test_main.spec.js", ChangeType.MODIFIED), ]) result = detect_commit_type(change_set) assert result == "test" def test_detects_chore_for_config(self): """Test detecting 'chore' for configuration files.""" change_set = ChangeSet([ StagedChange("package.json", ChangeType.MODIFIED), StagedChange(".gitignore", ChangeType.ADDED), ]) result = detect_commit_type(change_set) assert result == "chore" def test_falls_back_to_chore(self): """Test falling back to 'chore' when no pattern matches.""" change_set = ChangeSet([ StagedChange("unknown/file.xyz", ChangeType.ADDED), ]) result = detect_commit_type(change_set) assert result == "chore" def test_custom_type_rules(self): """Test using custom type rules.""" change_set = ChangeSet([ StagedChange("custom/feature.py", ChangeType.ADDED), ]) custom_rules = {"custom_type": ["custom/"]} result = detect_commit_type(change_set, custom_rules) assert result == "custom_type" class TestDetectScope: """Tests for scope detection.""" def test_detects_single_scope(self): """Test detecting single scope from directory.""" change_set = ChangeSet([ StagedChange("src/cli/main.py", ChangeType.ADDED), StagedChange("src/cli/commands.py", ChangeType.MODIFIED), ]) result = detect_scope(change_set) assert result == "src" def test_detects_multiple_scopes(self): """Test detecting multiple scopes.""" change_set = ChangeSet([ StagedChange("src/a.py", ChangeType.ADDED), StagedChange("lib/b.py", ChangeType.MODIFIED), ]) result = detect_scope(change_set) assert "src" in result and "lib" in result def test_empty_for_no_changes(self): """Test returning empty string for no changes.""" change_set = ChangeSet([]) result = detect_scope(change_set) assert result == "" def test_root_file_no_scope(self): """Test no scope for root-level files.""" change_set = ChangeSet([ StagedChange("main.py", ChangeType.ADDED), ]) result = detect_scope(change_set) assert result == "" class TestGenerateDescription: """Tests for description generation.""" def test_describes_single_added(self): """Test generating description for single added file.""" change_set = ChangeSet([ StagedChange("new_file.py", ChangeType.ADDED), ]) result = generate_description(change_set) assert result == "add new_file.py" def test_describes_single_deleted(self): """Test generating description for deleted file.""" change_set = ChangeSet([ StagedChange("old_file.py", ChangeType.DELETED), ]) result = generate_description(change_set) assert result == "remove old_file.py" def test_describes_single_modified(self): """Test generating description for modified file.""" change_set = ChangeSet([ StagedChange("existing.py", ChangeType.MODIFIED), ]) result = generate_description(change_set) assert result == "update existing.py" def test_describes_multiple_added(self): """Test generating description for multiple added files.""" change_set = ChangeSet([ StagedChange("a.py", ChangeType.ADDED), StagedChange("b.py", ChangeType.ADDED), ]) result = generate_description(change_set) assert result == "add 2 files" def test_describes_multiple_modified(self): """Test generating description for multiple modified files.""" change_set = ChangeSet([ StagedChange("a.py", ChangeType.MODIFIED), StagedChange("b.py", ChangeType.MODIFIED), ]) result = generate_description(change_set) assert result == "update 2 files" def test_default_for_empty(self): """Test default description for empty change set.""" change_set = ChangeSet([]) result = generate_description(change_set) assert result == "update" class TestGenerateCommitMessage: """Tests for full message generation.""" @patch("src.generator.ChangeAnalyzer") def test_generates_valid_message(self, mock_analyzer): """Test generating a complete commit message.""" change_set = ChangeSet([ StagedChange("src/main.py", ChangeType.ADDED), ]) mock_analyzer.return_value.get_staged_changes.return_value = change_set result = generate_commit_message() assert "feat" in result assert "src" in result def test_raises_error_on_no_changes(self): """Test raising error when no staged changes.""" with patch("src.generator.ChangeAnalyzer") as mock_analyzer: mock_analyzer.return_value.get_staged_changes.return_value = ChangeSet([]) with pytest.raises(GenerationError, match="No staged changes"): generate_commit_message() def test_raises_error_on_invalid_repo(self): """Test raising error for invalid repository.""" with patch("src.generator.ChangeAnalyzer") as mock_analyzer: mock_analyzer.return_value.get_staged_changes.side_effect = ValueError( "Not a git repository" ) with pytest.raises(GenerationError, match="Not a git repository"): generate_commit_message() @patch("src.generator.ChangeAnalyzer") def test_respects_config_template(self, mock_analyzer): """Test using custom template from config.""" change_set = ChangeSet([ StagedChange("src/test.py", ChangeType.ADDED), ]) custom_config = {"template": "CUSTOM: {description}"} mock_analyzer.return_value.get_staged_changes.return_value = change_set with patch("src.generator.load_config") as mock_load_config: mock_load_config.return_value = custom_config result = generate_commit_message() assert result.startswith("CUSTOM:") @patch("src.generator.ChangeAnalyzer") def test_max_files_limit(self, mock_analyzer): """Test respecting max files configuration.""" changes = [StagedChange(f"file{i}.py", ChangeType.ADDED) for i in range(10)] change_set = ChangeSet(changes) custom_config = { "include_file_list": True, "max_files": 3, "template": "{files}" } mock_analyzer.return_value.get_staged_changes.return_value = change_set with patch("src.generator.load_config") as mock_load_config: mock_load_config.return_value = custom_config result = generate_commit_message() file_count = result.count("file") assert file_count <= 3