litellm/tests/unit/test_git_hooks.py
devin-ai-integration[bot] c0cdab071b
chore(hooks): drop Conventional Branches pre-push hook in favor of litellm_ prefix (#44951)
* chore(hooks): drop Conventional Branches pre-push hook in favor of litellm_ prefix

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* docs(makefile): describe install-hooks as Conventional Commits only

* docs(contributing): scope the litellm_ branch prefix to internal contributors

---------

Co-authored-by: nate <nate@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-20 00:35:39 +00:00

178 lines
6 KiB
Python

"""Tests for the repo's git hook scripts in ``.githooks/``.
The commit-msg hook enforces Conventional Commits. It is exercised as a
subprocess against representative valid / invalid inputs so that any future
regex change or accidental edit gets caught by ``make test-unit``.
The hook is a bash script; the test is skipped on Windows where ``bash`` may
not be on PATH.
"""
import os
import shutil
import subprocess
from pathlib import Path
import pytest
_REPO_ROOT = Path(__file__).resolve().parents[2]
_HOOKS_DIR = _REPO_ROOT / ".githooks"
_COMMIT_MSG_HOOK = _HOOKS_DIR / "commit-msg"
pytestmark = pytest.mark.skipif(
shutil.which("bash") is None,
reason="bash not available; git hook scripts are bash-based",
)
@pytest.fixture(autouse=True)
def _ensure_hooks_exist():
assert _COMMIT_MSG_HOOK.exists(), f"missing hook: {_COMMIT_MSG_HOOK}"
# Exec bit may be missing on a fresh clone on case-preserving filesystems;
# the installer normalizes this, but the test shouldn't depend on having
# run it.
mode = _COMMIT_MSG_HOOK.stat().st_mode
if not (mode & 0o100):
_COMMIT_MSG_HOOK.chmod(mode | 0o755)
def _run_commit_msg(subject: str, tmp_path: Path) -> subprocess.CompletedProcess:
msg_file = tmp_path / "COMMIT_EDITMSG"
msg_file.write_text(subject + "\n", encoding="utf-8")
return subprocess.run(
["bash", str(_COMMIT_MSG_HOOK), str(msg_file)],
capture_output=True,
text=True,
check=False,
)
# ----- commit-msg -----------------------------------------------------------
@pytest.mark.parametrize(
"subject",
[
"feat(router): add weighted round-robin strategy",
"fix(bedrock): decouple STS region from aws_region_name",
"chore(deps): bump black to 26.3.1",
"docs: rewrite contributing guide",
"refactor!: drop Python 3.8 support",
"feat(api,proxy)!: rename endpoint",
"test: cover hook bypass list",
"perf(streaming): avoid extra json parse",
"revert: feat(router): add weighted round-robin",
],
)
def test_commit_msg_accepts_conventional_subjects(tmp_path, subject):
result = _run_commit_msg(subject, tmp_path)
assert result.returncode == 0, (
f"hook rejected a valid subject:\n subject: {subject!r}\n"
f" stderr: {result.stderr}"
)
@pytest.mark.parametrize(
"subject",
[
"add stuff", # no type
"feat add router strategy", # missing colon
"feat:add router strategy", # missing space after colon
"feat():", # empty description
"ux: thing", # unknown type
"Feat(router): capital type", # types are lowercase
"feat(router):", # empty description
# Description must start with a lowercase letter — kept in sync with
# the CI workflow's subjectPattern so the local hook never accepts a
# subject that CI will later reject.
"feat: Add thing",
"fix(router): Decouple something",
"chore: BUMP deps",
"feat: A",
],
)
def test_commit_msg_rejects_invalid_subjects(tmp_path, subject):
result = _run_commit_msg(subject, tmp_path)
assert result.returncode == 1, (
f"hook accepted an invalid subject:\n subject: {subject!r}\n"
f" stderr: {result.stderr}"
)
assert "Conventional Commits" in result.stderr
@pytest.mark.parametrize(
"subject",
[
# Lowercase letter — the common case.
"feat: lowercase start is fine",
# The CI's `^(?![A-Z]).+$` rejects only uppercase A-Z, so digits and
# symbols are still allowed; mirror that behavior here.
"feat: 1-based indexing now works",
"fix(deps): @types/node bump",
],
)
def test_commit_msg_accepts_non_uppercase_starts(tmp_path, subject):
result = _run_commit_msg(subject, tmp_path)
assert result.returncode == 0, (
f"hook rejected a valid non-uppercase-start subject:\n"
f" subject: {subject!r}\n stderr: {result.stderr}"
)
@pytest.mark.parametrize(
"subject",
[
"Merge branch 'main' into feature/foo",
'Revert "feat(router): add weighted round-robin strategy"',
"fixup! feat(router): add weighted round-robin strategy",
"squash! feat(router): add weighted round-robin strategy",
"amend! feat(router): add weighted round-robin strategy",
],
)
def test_commit_msg_passes_git_generated_messages(tmp_path, subject):
result = _run_commit_msg(subject, tmp_path)
assert result.returncode == 0, (
f"hook should pass git-generated subject:\n subject: {subject!r}\n"
f" stderr: {result.stderr}"
)
def test_commit_msg_rejects_empty_message(tmp_path):
result = _run_commit_msg("", tmp_path)
assert result.returncode == 1
assert "empty commit message" in result.stderr
def test_commit_msg_skips_comment_only_lines(tmp_path):
# An all-comments file has no subject — should be rejected.
msg_file = tmp_path / "COMMIT_EDITMSG"
msg_file.write_text("# please enter a commit message\n# above this line\n", encoding="utf-8")
result = subprocess.run(
["bash", str(_COMMIT_MSG_HOOK), str(msg_file)],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 1
assert "empty commit message" in result.stderr
def test_commit_msg_uses_first_non_comment_line(tmp_path):
# Real git-generated COMMIT_EDITMSG has a status block prefixed with '#'
# below the subject. Make sure leading comment lines are skipped too.
msg_file = tmp_path / "COMMIT_EDITMSG"
msg_file.write_text(
"# On branch feature/foo\n"
"\n"
"feat(router): add weighted round-robin\n"
"\n"
"# Please enter the commit message...\n",
encoding="utf-8",
)
result = subprocess.run(
["bash", str(_COMMIT_MSG_HOOK), str(msg_file)],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stderr