git-commit-msg-ai 1.7.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/PKG-INFO +4 -4
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/README.md +2 -2
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/git_commit_msg_ai/ai_client.py +8 -10
- git_commit_msg_ai-2.0.0/git_commit_msg_ai/cli.py +59 -0
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/git_commit_msg_ai/editor.py +16 -12
- git_commit_msg_ai-2.0.0/git_commit_msg_ai/git_ops.py +41 -0
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/git_commit_msg_ai.egg-info/PKG-INFO +4 -4
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/pyproject.toml +4 -4
- git_commit_msg_ai-2.0.0/tests/test_ai_client.py +95 -0
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/tests/test_cli.py +127 -46
- git_commit_msg_ai-2.0.0/tests/test_editor.py +139 -0
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/tests/test_generate_release_notes.py +65 -46
- git_commit_msg_ai-2.0.0/tests/test_git_ops.py +82 -0
- git_commit_msg_ai-1.7.0/git_commit_msg_ai/cli.py +0 -51
- git_commit_msg_ai-1.7.0/git_commit_msg_ai/git_ops.py +0 -52
- git_commit_msg_ai-1.7.0/tests/test_ai_client.py +0 -158
- git_commit_msg_ai-1.7.0/tests/test_editor.py +0 -245
- git_commit_msg_ai-1.7.0/tests/test_git_ops.py +0 -189
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/git_commit_msg_ai/__init__.py +0 -0
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/git_commit_msg_ai/exceptions.py +0 -0
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/git_commit_msg_ai.egg-info/SOURCES.txt +0 -0
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/git_commit_msg_ai.egg-info/dependency_links.txt +0 -0
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/git_commit_msg_ai.egg-info/entry_points.txt +0 -0
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/git_commit_msg_ai.egg-info/requires.txt +0 -0
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/git_commit_msg_ai.egg-info/top_level.txt +0 -0
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/setup.cfg +0 -0
- {git_commit_msg_ai-1.7.0 → git_commit_msg_ai-2.0.0}/tests/test_exceptions.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-commit-msg-ai
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: AI-powered git commit message generator following Conventional Commits
|
|
5
5
|
License-Expression: MIT
|
|
6
|
-
Requires-Python: >=3.
|
|
6
|
+
Requires-Python: >=3.14
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Requires-Dist: anthropic
|
|
9
9
|
Provides-Extra: dev
|
|
@@ -20,7 +20,7 @@ AI-powered git commit message generator that follows the [Conventional Commits](
|
|
|
20
20
|
|
|
21
21
|
## Prerequisites
|
|
22
22
|
|
|
23
|
-
- Python 3.
|
|
23
|
+
- Python 3.14+
|
|
24
24
|
- An Anthropic API key set as an environment variable:
|
|
25
25
|
|
|
26
26
|
```sh
|
|
@@ -156,6 +156,6 @@ git-commit-msg-ai
|
|
|
156
156
|
| Level | What you see |
|
|
157
157
|
|---|---|
|
|
158
158
|
| `DEBUG` | git commands run, API model/token params, temp file paths, char counts |
|
|
159
|
-
| `INFO` | commit message generated, commit created |
|
|
159
|
+
| `INFO` | commit message generated (with char count), commit created |
|
|
160
160
|
| `WARNING` | no staged changes found |
|
|
161
161
|
| `ERROR` | git not found, API failures, editor errors |
|
|
@@ -4,7 +4,7 @@ AI-powered git commit message generator that follows the [Conventional Commits](
|
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
7
|
-
- Python 3.
|
|
7
|
+
- Python 3.14+
|
|
8
8
|
- An Anthropic API key set as an environment variable:
|
|
9
9
|
|
|
10
10
|
```sh
|
|
@@ -140,6 +140,6 @@ git-commit-msg-ai
|
|
|
140
140
|
| Level | What you see |
|
|
141
141
|
|---|---|
|
|
142
142
|
| `DEBUG` | git commands run, API model/token params, temp file paths, char counts |
|
|
143
|
-
| `INFO` | commit message generated, commit created |
|
|
143
|
+
| `INFO` | commit message generated (with char count), commit created |
|
|
144
144
|
| `WARNING` | no staged changes found |
|
|
145
145
|
| `ERROR` | git not found, API failures, editor errors |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import textwrap
|
|
3
|
-
from typing import Final
|
|
3
|
+
from typing import Final
|
|
4
4
|
|
|
5
5
|
import anthropic
|
|
6
6
|
|
|
@@ -24,7 +24,7 @@ def generate_commit_message(diff: str) -> str:
|
|
|
24
24
|
try:
|
|
25
25
|
anthropic_client = anthropic.Anthropic()
|
|
26
26
|
|
|
27
|
-
logger.debug("Calling Anthropic API: model
|
|
27
|
+
logger.debug(f"Calling Anthropic API: model={MODEL} max_tokens={MAX_TOKENS}")
|
|
28
28
|
anthropic_api_response = anthropic_client.messages.create(
|
|
29
29
|
model=MODEL,
|
|
30
30
|
max_tokens=MAX_TOKENS,
|
|
@@ -32,21 +32,19 @@ def generate_commit_message(diff: str) -> str:
|
|
|
32
32
|
messages=[{"role": "user", "content": diff}],
|
|
33
33
|
)
|
|
34
34
|
except anthropic.AuthenticationError:
|
|
35
|
-
logger.error("Anthropic authentication error")
|
|
36
35
|
raise AIError("Anthropic API key is missing or invalid. Set the ANTHROPIC_API_KEY environment variable.")
|
|
37
36
|
except anthropic.RateLimitError:
|
|
38
|
-
logger.error("Anthropic rate limit error")
|
|
39
37
|
raise AIError("Anthropic API rate limit reached. Wait a moment and try again.")
|
|
40
38
|
except anthropic.APIConnectionError:
|
|
41
|
-
logger.error("Anthropic API connection error")
|
|
42
39
|
raise AIError("Could not reach the Anthropic API. Check your network connection.")
|
|
43
|
-
except anthropic.APIStatusError as
|
|
44
|
-
|
|
45
|
-
raise AIError(f"Anthropic API returned an error: {error.status_code}.")
|
|
40
|
+
except anthropic.APIStatusError as api_status_error:
|
|
41
|
+
raise AIError(f"Anthropic API returned an error: {api_status_error.status_code}.")
|
|
46
42
|
|
|
47
43
|
anthropic_api_response_message = anthropic_api_response.content[0]
|
|
48
|
-
|
|
44
|
+
if not isinstance(anthropic_api_response_message, anthropic.types.TextBlock):
|
|
45
|
+
raise AIError("Unexpected response format from Anthropic API.")
|
|
46
|
+
commit_message = anthropic_api_response_message.text.strip()
|
|
49
47
|
|
|
50
|
-
logger.info("Commit message generated:
|
|
48
|
+
logger.info(f"Commit message generated: {len(commit_message)} chars")
|
|
51
49
|
|
|
52
50
|
return commit_message
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Final
|
|
5
|
+
|
|
6
|
+
from git_commit_msg_ai import ai_client, editor, git_ops
|
|
7
|
+
from git_commit_msg_ai.exceptions import GitCommitAIError
|
|
8
|
+
|
|
9
|
+
LOG_LEVEL_ENVIRONMENT_VARIABLE: Final[str] = "GIT_COMMIT_AI_LOG_LEVEL"
|
|
10
|
+
DEFAULT_LOG_LEVEL: Final[str] = "WARNING"
|
|
11
|
+
LOG_FORMAT: Final[str] = "%(levelname)s:%(name)s:%(message)s"
|
|
12
|
+
SELECTION_PROMPT: Final[str] = "[a]ccept / [e]dit / [r]eject: "
|
|
13
|
+
SELECTION_ACCEPT: Final[str] = "a"
|
|
14
|
+
SELECTION_EDIT: Final[str] = "e"
|
|
15
|
+
SELECTION_REJECT: Final[str] = "r"
|
|
16
|
+
REJECTED_MESSAGE: Final[str] = "User rejected the generated commit message. No commit made."
|
|
17
|
+
INVALID_SELECTION_MESSAGE: Final[str] = "Invalid selection."
|
|
18
|
+
ABORTED_MESSAGE: Final[str] = "Aborted."
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main() -> None:
|
|
22
|
+
log_level_name = os.environ.get(LOG_LEVEL_ENVIRONMENT_VARIABLE, DEFAULT_LOG_LEVEL).upper()
|
|
23
|
+
numeric_log_level = getattr(logging, log_level_name, None)
|
|
24
|
+
valid_log_level = isinstance(numeric_log_level, int)
|
|
25
|
+
effective_log_level = numeric_log_level if valid_log_level else logging.WARNING
|
|
26
|
+
|
|
27
|
+
if not valid_log_level:
|
|
28
|
+
logging.warning(f"Invalid {LOG_LEVEL_ENVIRONMENT_VARIABLE} value {log_level_name!r}, defaulting to {DEFAULT_LOG_LEVEL}")
|
|
29
|
+
|
|
30
|
+
logging.basicConfig(level=effective_log_level, format=LOG_FORMAT, stream=sys.stderr)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
diff = git_ops.get_staged_diff()
|
|
34
|
+
commit_message = ai_client.generate_commit_message(diff)
|
|
35
|
+
print(commit_message)
|
|
36
|
+
|
|
37
|
+
print()
|
|
38
|
+
user_selection = input(SELECTION_PROMPT).strip().lower()
|
|
39
|
+
|
|
40
|
+
if user_selection == SELECTION_ACCEPT:
|
|
41
|
+
git_ops.commit(commit_message)
|
|
42
|
+
elif user_selection == SELECTION_EDIT:
|
|
43
|
+
updated_commit_message = editor.open_in_editor(commit_message)
|
|
44
|
+
git_ops.commit(updated_commit_message)
|
|
45
|
+
elif user_selection == SELECTION_REJECT:
|
|
46
|
+
print(REJECTED_MESSAGE)
|
|
47
|
+
else:
|
|
48
|
+
print(INVALID_SELECTION_MESSAGE)
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
except GitCommitAIError as error:
|
|
51
|
+
logging.error(str(error))
|
|
52
|
+
sys.exit(1)
|
|
53
|
+
except KeyboardInterrupt, EOFError:
|
|
54
|
+
print(ABORTED_MESSAGE)
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__": # pragma: no cover
|
|
59
|
+
main()
|
|
@@ -3,44 +3,49 @@ import os
|
|
|
3
3
|
import platform
|
|
4
4
|
import subprocess
|
|
5
5
|
import tempfile
|
|
6
|
+
from typing import Final
|
|
6
7
|
|
|
7
8
|
from git_commit_msg_ai.exceptions import EditorError
|
|
8
9
|
|
|
9
10
|
logger = logging.getLogger(__name__)
|
|
10
11
|
|
|
12
|
+
_WINDOWS_PLATFORM: Final[str] = "Windows"
|
|
13
|
+
WINDOWS_EDITOR: Final[str] = "notepad"
|
|
14
|
+
FALLBACK_EDITOR: Final[str] = "vi"
|
|
15
|
+
_EDITOR_ENVIRONMENT_VARIABLE: Final[str] = "EDITOR"
|
|
16
|
+
_TEMP_FILE_SUFFIX: Final[str] = ".txt"
|
|
17
|
+
_TEMP_FILE_MODE: Final[str] = "w"
|
|
18
|
+
|
|
11
19
|
|
|
12
20
|
def get_default_editor() -> str:
|
|
13
21
|
current_platform = platform.system()
|
|
14
|
-
is_windows = current_platform ==
|
|
22
|
+
is_windows = current_platform == _WINDOWS_PLATFORM
|
|
15
23
|
|
|
16
|
-
return
|
|
24
|
+
return WINDOWS_EDITOR if is_windows else FALLBACK_EDITOR
|
|
17
25
|
|
|
18
26
|
|
|
19
27
|
def open_in_editor(initial_text: str) -> str:
|
|
20
28
|
try:
|
|
21
|
-
with tempfile.NamedTemporaryFile(suffix=
|
|
29
|
+
with tempfile.NamedTemporaryFile(suffix=_TEMP_FILE_SUFFIX, mode=_TEMP_FILE_MODE, delete=False) as temp_file:
|
|
22
30
|
temp_file.write(initial_text)
|
|
23
31
|
temp_file_path = temp_file.name
|
|
24
32
|
except OSError:
|
|
25
|
-
logger.error("Could not create temporary file")
|
|
26
33
|
raise EditorError("Could not create a temporary file.")
|
|
27
34
|
|
|
28
|
-
logger.debug("Temporary file created:
|
|
35
|
+
logger.debug(f"Temporary file created: {temp_file_path}")
|
|
29
36
|
|
|
30
37
|
platform_default_editor = get_default_editor()
|
|
31
|
-
editor_command = os.environ.get(
|
|
38
|
+
editor_command = os.environ.get(_EDITOR_ENVIRONMENT_VARIABLE, platform_default_editor)
|
|
32
39
|
|
|
33
|
-
logger.debug("Selected editor:
|
|
40
|
+
logger.debug(f"Selected editor: {editor_command}")
|
|
34
41
|
|
|
35
42
|
try:
|
|
36
43
|
try:
|
|
37
|
-
logger.debug("Opening editor:
|
|
44
|
+
logger.debug(f"Opening editor: {editor_command} {temp_file_path}")
|
|
38
45
|
subprocess.run([editor_command, temp_file_path], check=True)
|
|
39
46
|
except FileNotFoundError:
|
|
40
|
-
logger.error("Editor not found: %s", editor_command)
|
|
41
47
|
raise EditorError(f'Editor "{editor_command}" was not found. Set the EDITOR environment variable to a valid editor.')
|
|
42
48
|
except subprocess.CalledProcessError:
|
|
43
|
-
logger.error("Editor exited with error: %s", editor_command)
|
|
44
49
|
raise EditorError(f'Editor "{editor_command}" exited with an error.')
|
|
45
50
|
|
|
46
51
|
logger.debug("Editor closed, reading edited content")
|
|
@@ -49,11 +54,10 @@ def open_in_editor(initial_text: str) -> str:
|
|
|
49
54
|
with open(temp_file_path) as temp_file:
|
|
50
55
|
edited_text = temp_file.read().strip()
|
|
51
56
|
except OSError:
|
|
52
|
-
logger.error("Could not read temporary file: %s", temp_file_path)
|
|
53
57
|
raise EditorError("Could not read the edited commit message from the temporary file.")
|
|
54
58
|
finally:
|
|
55
59
|
os.unlink(temp_file_path)
|
|
56
60
|
|
|
57
|
-
logger.debug("Edited text read:
|
|
61
|
+
logger.debug(f"Edited text read: {len(edited_text)} chars")
|
|
58
62
|
|
|
59
63
|
return edited_text
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
from git_commit_msg_ai.exceptions import GitError
|
|
6
|
+
|
|
7
|
+
GIT_COMMAND: Final[str] = "git"
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_staged_diff() -> str:
|
|
13
|
+
try:
|
|
14
|
+
logger.debug(f"Running: {GIT_COMMAND} diff --cached")
|
|
15
|
+
raw_diff_bytes = subprocess.check_output([GIT_COMMAND, "diff", "--cached"])
|
|
16
|
+
staged_diff = raw_diff_bytes.decode("utf-8")
|
|
17
|
+
except FileNotFoundError:
|
|
18
|
+
raise GitError("git is not installed or not on PATH.")
|
|
19
|
+
except subprocess.CalledProcessError:
|
|
20
|
+
raise GitError("Failed to get staged diff. Are you inside a git repository?")
|
|
21
|
+
except UnicodeDecodeError:
|
|
22
|
+
raise GitError("Staged diff contains bytes that could not be decoded as UTF-8.")
|
|
23
|
+
|
|
24
|
+
logger.debug(f"Staged diff received: {len(staged_diff)} chars")
|
|
25
|
+
|
|
26
|
+
if not staged_diff:
|
|
27
|
+
raise GitError("No staged changes found. Stage files with git add before running.")
|
|
28
|
+
|
|
29
|
+
return staged_diff
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def commit(message: str) -> None:
|
|
33
|
+
try:
|
|
34
|
+
logger.debug(f"Running: {GIT_COMMAND} commit -m <message>")
|
|
35
|
+
subprocess.run([GIT_COMMAND, "commit", "-m", message], check=True)
|
|
36
|
+
except FileNotFoundError:
|
|
37
|
+
raise GitError("git is not installed or not on PATH.")
|
|
38
|
+
except subprocess.CalledProcessError:
|
|
39
|
+
raise GitError("git commit failed.")
|
|
40
|
+
|
|
41
|
+
logger.info("Commit created successfully")
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-commit-msg-ai
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: AI-powered git commit message generator following Conventional Commits
|
|
5
5
|
License-Expression: MIT
|
|
6
|
-
Requires-Python: >=3.
|
|
6
|
+
Requires-Python: >=3.14
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Requires-Dist: anthropic
|
|
9
9
|
Provides-Extra: dev
|
|
@@ -20,7 +20,7 @@ AI-powered git commit message generator that follows the [Conventional Commits](
|
|
|
20
20
|
|
|
21
21
|
## Prerequisites
|
|
22
22
|
|
|
23
|
-
- Python 3.
|
|
23
|
+
- Python 3.14+
|
|
24
24
|
- An Anthropic API key set as an environment variable:
|
|
25
25
|
|
|
26
26
|
```sh
|
|
@@ -156,6 +156,6 @@ git-commit-msg-ai
|
|
|
156
156
|
| Level | What you see |
|
|
157
157
|
|---|---|
|
|
158
158
|
| `DEBUG` | git commands run, API model/token params, temp file paths, char counts |
|
|
159
|
-
| `INFO` | commit message generated, commit created |
|
|
159
|
+
| `INFO` | commit message generated (with char count), commit created |
|
|
160
160
|
| `WARNING` | no staged changes found |
|
|
161
161
|
| `ERROR` | git not found, API failures, editor errors |
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "git-commit-msg-ai"
|
|
7
|
-
version = "
|
|
7
|
+
version = "2.0.0"
|
|
8
8
|
description = "AI-powered git commit message generator following Conventional Commits"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.14"
|
|
12
12
|
dependencies = ["anthropic"]
|
|
13
13
|
|
|
14
14
|
[project.scripts]
|
|
@@ -18,14 +18,14 @@ git-commit-msg-ai = "git_commit_msg_ai.cli:main"
|
|
|
18
18
|
dev = ["mypy", "ruff", "pytest", "pytest-cov", "build", "twine"]
|
|
19
19
|
|
|
20
20
|
[tool.ruff]
|
|
21
|
-
target-version = "
|
|
21
|
+
target-version = "py314"
|
|
22
22
|
line-length = 320
|
|
23
23
|
|
|
24
24
|
[tool.ruff.lint]
|
|
25
25
|
select = ["E", "F", "I"]
|
|
26
26
|
|
|
27
27
|
[tool.mypy]
|
|
28
|
-
python_version = "3.
|
|
28
|
+
python_version = "3.14"
|
|
29
29
|
strict = true
|
|
30
30
|
|
|
31
31
|
[tool.pytest.ini_options]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from contextlib import ExitStack
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
import anthropic
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from git_commit_msg_ai.ai_client import generate_commit_message
|
|
8
|
+
from git_commit_msg_ai.exceptions import AIError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _make_api_response(text: str) -> MagicMock:
|
|
12
|
+
mock_text_block = MagicMock(spec=anthropic.types.TextBlock)
|
|
13
|
+
mock_text_block.text = text
|
|
14
|
+
|
|
15
|
+
mock_response = MagicMock(spec=anthropic.types.Message)
|
|
16
|
+
mock_response.content = [mock_text_block]
|
|
17
|
+
|
|
18
|
+
return mock_response
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _make_status_error(exception_class: type[anthropic.APIStatusError], status_code: int) -> anthropic.APIStatusError:
|
|
22
|
+
mock_response = MagicMock()
|
|
23
|
+
mock_response.status_code = status_code
|
|
24
|
+
|
|
25
|
+
return exception_class("error", response=mock_response, body={})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestGenerateCommitMessage:
|
|
29
|
+
def test_generate_commit_message_returns_stripped_message(self) -> None:
|
|
30
|
+
with ExitStack() as stack:
|
|
31
|
+
mock_anthropic_class = stack.enter_context(patch("git_commit_msg_ai.ai_client.anthropic.Anthropic"))
|
|
32
|
+
mock_client = MagicMock()
|
|
33
|
+
mock_client.messages.create.return_value = _make_api_response(" feat: add feature ")
|
|
34
|
+
mock_anthropic_class.return_value = mock_client
|
|
35
|
+
|
|
36
|
+
result = generate_commit_message("diff content")
|
|
37
|
+
|
|
38
|
+
assert result == "feat: add feature"
|
|
39
|
+
|
|
40
|
+
def test_generate_commit_message_raises_ai_error_on_authentication_error(self) -> None:
|
|
41
|
+
with ExitStack() as stack:
|
|
42
|
+
mock_anthropic_class = stack.enter_context(patch("git_commit_msg_ai.ai_client.anthropic.Anthropic"))
|
|
43
|
+
mock_client = MagicMock()
|
|
44
|
+
mock_client.messages.create.side_effect = _make_status_error(anthropic.AuthenticationError, 401)
|
|
45
|
+
mock_anthropic_class.return_value = mock_client
|
|
46
|
+
|
|
47
|
+
with pytest.raises(AIError, match="ANTHROPIC_API_KEY"):
|
|
48
|
+
generate_commit_message("diff content")
|
|
49
|
+
|
|
50
|
+
def test_generate_commit_message_raises_ai_error_on_rate_limit_error(self) -> None:
|
|
51
|
+
with ExitStack() as stack:
|
|
52
|
+
mock_anthropic_class = stack.enter_context(patch("git_commit_msg_ai.ai_client.anthropic.Anthropic"))
|
|
53
|
+
mock_client = MagicMock()
|
|
54
|
+
mock_client.messages.create.side_effect = _make_status_error(anthropic.RateLimitError, 429)
|
|
55
|
+
mock_anthropic_class.return_value = mock_client
|
|
56
|
+
|
|
57
|
+
with pytest.raises(AIError, match="rate limit"):
|
|
58
|
+
generate_commit_message("diff content")
|
|
59
|
+
|
|
60
|
+
def test_generate_commit_message_raises_ai_error_on_api_connection_error(self) -> None:
|
|
61
|
+
mock_request = MagicMock()
|
|
62
|
+
|
|
63
|
+
with ExitStack() as stack:
|
|
64
|
+
mock_anthropic_class = stack.enter_context(patch("git_commit_msg_ai.ai_client.anthropic.Anthropic"))
|
|
65
|
+
mock_client = MagicMock()
|
|
66
|
+
mock_client.messages.create.side_effect = anthropic.APIConnectionError(request=mock_request)
|
|
67
|
+
mock_anthropic_class.return_value = mock_client
|
|
68
|
+
|
|
69
|
+
with pytest.raises(AIError, match="network"):
|
|
70
|
+
generate_commit_message("diff content")
|
|
71
|
+
|
|
72
|
+
def test_generate_commit_message_raises_ai_error_on_api_status_error_with_status_code(self) -> None:
|
|
73
|
+
with ExitStack() as stack:
|
|
74
|
+
mock_anthropic_class = stack.enter_context(patch("git_commit_msg_ai.ai_client.anthropic.Anthropic"))
|
|
75
|
+
mock_client = MagicMock()
|
|
76
|
+
mock_client.messages.create.side_effect = _make_status_error(anthropic.APIStatusError, 500)
|
|
77
|
+
mock_anthropic_class.return_value = mock_client
|
|
78
|
+
|
|
79
|
+
with pytest.raises(AIError, match="500"):
|
|
80
|
+
generate_commit_message("diff content")
|
|
81
|
+
|
|
82
|
+
def test_generate_commit_message_raises_ai_error_on_non_text_block_response(self) -> None:
|
|
83
|
+
mock_non_text_block = MagicMock() # no spec=TextBlock → isinstance check fails
|
|
84
|
+
|
|
85
|
+
mock_response = MagicMock(spec=anthropic.types.Message)
|
|
86
|
+
mock_response.content = [mock_non_text_block]
|
|
87
|
+
|
|
88
|
+
with ExitStack() as stack:
|
|
89
|
+
mock_anthropic_class = stack.enter_context(patch("git_commit_msg_ai.ai_client.anthropic.Anthropic"))
|
|
90
|
+
mock_client = MagicMock()
|
|
91
|
+
mock_client.messages.create.return_value = mock_response
|
|
92
|
+
mock_anthropic_class.return_value = mock_client
|
|
93
|
+
|
|
94
|
+
with pytest.raises(AIError, match="Unexpected response format"):
|
|
95
|
+
generate_commit_message("diff content")
|