litellm/tests/unit/test_pre_commit_lint.py
yuneng-jiang f6882246d4
test: move tests/test_litellm root and small trees into tests/unit (#43186)
* ci: run the unit_selection.sh shard files on every event instead of only fork pull requests

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

* ci: rename fork-flag to unit-flag now that it applies on every event

* test: move tests/test_litellm root and small trees into tests/unit

Pure renames, no content changes. Follow-up commits in this PR fix
references, merge the three files that already existed in tests/unit,
keep live-provider tests in tests/test_litellm and wire CI.

* test: carry tests/test_litellm conftest isolation into tests/unit

Callback lists, routing fallbacks, cached HTTP clients, logger state, AWS,
proxy-URL and keychain env, and session-end client cleanup now reset for
unit tests too. The environment isolation owns its MonkeyPatch so a test's
own monkeypatch is undone before the model-cost teardown runs.

* test: merge, split and prune the moved root and small-tree tests

Merge batches/test_batch_utils.py and the chat_completions and messages
dispatch tests into the files that already existed in tests/unit. Keep
the live Gemini interactions tests, the async image-fetch format test and
the OpenAI embedding scorer test in tests/test_litellm since they need
real network or keys. Put test_router.py under tests/unit/test_router so
the existing package no longer shadows it. Delete eight tests the audit
found superseded by stronger ones kept in this move.

* ci: run the moved root and small-tree tests under their legacy flags

Add the misc and responses-caching-types flags to unit_selection.sh and
CircleCI, extend enterprise-routing and mcp-integration, and point the
legacy GHA shards, Makefile, redis-compat workflow, merge smoke manifest
and change classifier at the new paths.

* test: make the new tests/unit directories packages

tests/unit/test_package_layout.py requires every directory to carry an
__init__.py, and without one the moved and retained
test_litellm_responses_bridge.py modules collide on import.

* test: scope the unit socket block to tests/unit in shared sessions

The GHA shards collect the legacy test-path and the unit selection in one
pytest session. The unit conftest's loopback-only block leaked into legacy
modules that reach the network at import. The legacy conftest now lifts the
restriction at collect and setup time, and the unit conftest re-applies it
when collecting its own modules.

* test: give the shard-script tests their own GITHUB_OUTPUT

They only passed where the runner set it. The CircleCI unit job's env
allowlist drops it, so the script's redirect failed there.

* test: point the router and module-deletion checks at tests/unit

router_code_coverage and code_qa_check_tests only searched tests/test_litellm,
so the moved router tests no longer counted. The two silent-experiment tests
the audit deleted were the only direct callers of those methods; they are
replaced with tests that assert the forwarded shadow request and the
recursion guard.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-25 11:30:43 -07:00

644 lines
24 KiB
Python

import os
import shutil
import signal
import subprocess
import time
from collections.abc import Callable
from contextlib import suppress
from pathlib import Path
import pytest
from tests._process_helpers import process_is_gone
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "scripts" / "pre_commit_lint.sh"
WHOLE_TREE_RUFF = "run --no-sync ruff check --config ruff-tests.toml tests"
TEST_TREE_RAN = "ran: test-tree lint (ruff-tests.toml + test-quality budget)"
TEST_TREE_SKIPPED = (
"skipped: test-tree lint (ruff-tests.toml + test-quality budget) "
"(no tests/ Python files or test-tree lint inputs in scope)"
)
BARRIER_HELPER = """barrier_sync() {
touch "$STUB_BARRIER_DIR/$1.started"
for other in $2; do
tries=0
while [ ! -f "$STUB_BARRIER_DIR/$other.started" ]; do
tries=$((tries + 1))
if [ "$tries" -gt 100 ]; then
echo "barrier timeout: $1 never saw $other start" >&2
exit 1
fi
sleep 0.1
done
done
}
"""
MAKE_STUB = """#!/bin/sh
. "$STUB_BIN/barrier.sh"
[ -n "${STUB_ARGS_DIR:-}" ] && echo "$*" >> "$STUB_ARGS_DIR/make.args"
case "$*" in
lint)
[ "${STUB_FAIL:-}" = "make-lint" ] && exit 1
[ -n "${STUB_BARRIER_DIR:-}" ] && barrier_sync python "dashboard genapi"
if [ -n "${STUB_HANG_DIR:-}" ]; then
echo "$$" > "$STUB_HANG_DIR/make.pid"
touch "$STUB_HANG_DIR/make.started"
sleep 60
fi
;;
lint-test-quality)
[ "${STUB_FAIL:-}" = "test-quality" ] && exit 1
;;
esac
exit 0
"""
NPX_STUB = """#!/bin/sh
. "$STUB_BIN/barrier.sh"
case "$*" in
prettier*)
[ -n "${STUB_BARRIER_DIR:-}" ] && barrier_sync dashboard "python genapi"
;;
"eslint --no-warn-ignored"*)
[ "${STUB_FAIL:-}" = "eslint" ] && exit 1
;;
"eslint . -f json"*)
if [ -n "${STUB_HANG_DIR:-}" ]; then
touch "$STUB_HANG_DIR/eslint_report.started"
sleep 60
fi
;;
esac
exit 0
"""
UV_STUB = """#!/bin/sh
. "$STUB_BIN/barrier.sh"
case "$*" in
*orjson*)
[ -n "${STUB_BARRIER_DIR:-}" ] && barrier_sync genapi "python dashboard"
;;
"run --no-sync ruff check --config ruff-tests.toml"*)
[ -n "${STUB_ARGS_DIR:-}" ] && echo "$*" >> "$STUB_ARGS_DIR/ruff_tests.args"
[ "${STUB_FAIL:-}" = "tests-ruff" ] && exit 1
;;
esac
exit 0
"""
NPM_STUB = """#!/bin/sh
case "$*" in
"run gen:api")
[ "${STUB_FAIL:-}" = "gen-api" ] && exit 1
;;
esac
exit 0
"""
NODE_STUB = """#!/bin/sh
exit 0
"""
def _write_executable(path: Path, body: str) -> None:
path.write_text(body)
path.chmod(0o755)
def _sandbox(tmp_path: Path) -> tuple[Path, Path]:
repo = tmp_path / "repo"
(repo / "litellm" / "proxy").mkdir(parents=True)
(repo / "litellm" / "foo.py").write_text("x = 1\n")
(repo / "litellm" / "proxy" / "spec.py").write_text("y = 2\n")
dashboard = repo / "ui" / "litellm-dashboard"
(dashboard / "src").mkdir(parents=True)
(dashboard / "node_modules").mkdir()
(dashboard / "src" / "app.ts").write_text("export {}\n")
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
subprocess.run(["git", "add", "."], cwd=repo, check=True)
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
(bin_dir / "barrier.sh").write_text(BARRIER_HELPER)
_write_executable(bin_dir / "make", MAKE_STUB)
_write_executable(bin_dir / "npx", NPX_STUB)
_write_executable(bin_dir / "uv", UV_STUB)
_write_executable(bin_dir / "npm", NPM_STUB)
_write_executable(bin_dir / "node", NODE_STUB)
return repo, bin_dir
def _env(repo: Path, bin_dir: Path, extra_env: dict[str, str]) -> dict[str, str]:
return {
"PATH": os.pathsep.join([str(bin_dir), "/usr/bin", "/bin"]),
"HOME": str(repo.parent),
"STUB_BIN": str(bin_dir),
**extra_env,
}
def _run(repo: Path, bin_dir: Path, extra_env: dict[str, str]) -> subprocess.CompletedProcess[str]:
env = _env(repo, bin_dir, extra_env)
return subprocess.run(
[str(SCRIPT)],
cwd=repo,
capture_output=True,
text=True,
env=env,
timeout=120,
)
def _commit_all(repo: Path, message: str) -> None:
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(
["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", message],
cwd=repo,
check=True,
)
def _set_base_ref(repo: Path, branch: str = "release_branch") -> None:
remote = repo.parent / "remote.git"
subprocess.run(["git", "clone", "-q", "--bare", str(repo), str(remote)], check=True)
subprocess.run(["git", "update-ref", f"refs/heads/{branch}", "HEAD"], cwd=remote, check=True)
subprocess.run(["git", "symbolic-ref", "HEAD", f"refs/heads/{branch}"], cwd=remote, check=True)
subprocess.run(["git", "remote", "add", "origin", str(remote)], cwd=repo, check=True)
def _stage_file(repo: Path, relative: str, body: str) -> None:
path = repo / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(body)
subprocess.run(["git", "add", relative], cwd=repo, check=True)
@pytest.mark.parametrize("branch", ["release_branch", "main"])
def test_nothing_staged_scopes_to_working_tree_diff_and_runs_checks(tmp_path: Path, branch: str) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
_set_base_ref(repo, branch)
(repo / "litellm" / "foo.py").write_text("x = 2\n")
proc = _run(repo, bin_dir, {})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "nothing staged; scoping to the working tree's diff" in proc.stdout
assert "litellm/foo.py" in proc.stdout
assert "linting Python" in proc.stdout
def test_nothing_staged_checks_committed_branch_changes(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
_set_base_ref(repo)
(repo / "litellm" / "foo.py").write_text("x = 2\n")
_commit_all(repo, "branch change")
proc = _run(repo, bin_dir, {})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "nothing staged; scoping to the working tree's diff" in proc.stdout
assert "linting Python" in proc.stdout
def test_nothing_staged_includes_untracked_files_in_scope(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
_set_base_ref(repo)
(repo / "litellm" / "brand_new.py").write_text("z = 3\n")
proc = _run(repo, bin_dir, {})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "litellm/brand_new.py" in proc.stdout
assert "linting Python" in proc.stdout
def test_nothing_staged_deletion_only_branch_triggers_checks(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
_set_base_ref(repo)
(repo / "litellm" / "foo.py").unlink()
_commit_all(repo, "delete module")
proc = _run(repo, bin_dir, {})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "nothing to check" not in proc.stdout
assert "litellm/foo.py" in proc.stdout
assert "linting Python" in proc.stdout
assert "ruff format --check" not in proc.stdout
def test_staged_deletion_triggers_checks_without_feeding_missing_files_to_tools(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
subprocess.run(["git", "rm", "-q", "litellm/foo.py"], cwd=repo, check=True)
proc = _run(repo, bin_dir, {})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "nothing staged" not in proc.stdout
assert "linting Python" in proc.stdout
assert "ruff format --check" not in proc.stdout
def test_deleted_dashboard_file_still_triggers_dashboard_lint(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
_set_base_ref(repo)
(repo / "ui" / "litellm-dashboard" / "src" / "app.ts").unlink()
_commit_all(repo, "delete dashboard file")
proc = _run(repo, bin_dir, {})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "linting dashboard" in proc.stdout
def test_nothing_staged_and_no_changes_is_an_explicit_no_op(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
_set_base_ref(repo)
proc = _run(repo, bin_dir, {})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "nothing to check" in proc.stdout
assert "check: PASS" in proc.stdout
assert "linting Python" not in proc.stdout
def test_nothing_staged_without_a_base_ref_fails_with_a_fetch_hint(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
proc = _run(repo, bin_dir, {})
assert proc.returncode == 1
assert "Cannot verify the base branch against origin" in proc.stdout
assert "explicit base ref" in proc.stdout
assert "check: FAIL" in proc.stdout
def test_partial_staging_warns_which_checks_were_skipped(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
(repo / "notes.md").write_text("hi\n")
subprocess.run(["git", "add", "notes.md"], cwd=repo, check=True)
(repo / "litellm" / "foo.py").write_text("x = 4\n")
proc = _run(repo, bin_dir, {})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "SKIPPED Python lint (make lint)" in proc.stdout
assert "litellm/foo.py" in proc.stdout
assert "linting Python" not in proc.stdout
def test_python_dashboard_and_gen_api_blocks_run_concurrently_with_grouped_output(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
barrier_dir = tmp_path / "barrier"
barrier_dir.mkdir()
proc = _run(repo, bin_dir, {"STUB_BARRIER_DIR": str(barrier_dir)})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "barrier timeout" not in proc.stdout + proc.stderr
python_at = proc.stdout.index("linting Python")
dashboard_at = proc.stdout.index("linting dashboard")
gen_api_at = proc.stdout.index("API types")
assert python_at < dashboard_at < gen_api_at
def test_all_blocks_passing_exits_zero(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
proc = _run(repo, bin_dir, {})
assert proc.returncode == 0, proc.stdout + proc.stderr
def test_full_output_is_saved_to_a_log_file_in_the_git_dir(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
(repo / "scratch.txt").write_text("")
proc = _run(repo, bin_dir, {})
assert proc.returncode == 0, proc.stdout + proc.stderr
log_file = repo / ".git" / "pre_commit_lint.log"
log = log_file.read_text()
assert "linting Python" in log
assert "linting dashboard" in log
assert "API types" in log
assert "unstaged/untracked changes" in log
assert f"check: full log: {log_file}" in proc.stdout
assert "check: full log:" not in log
def test_unwritable_log_warns_and_falls_back_to_running_without_one(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
(repo / ".git" / "pre_commit_lint.log").mkdir()
proc = _run(repo, bin_dir, {})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "linting Python" in proc.stdout
assert "output will not be saved" in proc.stderr
assert "check: full log:" not in proc.stdout
failing = _run(repo, bin_dir, {"STUB_FAIL": "make-lint"})
assert failing.returncode == 1
def test_failing_run_exit_code_survives_the_log_pipeline(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
proc = _run(repo, bin_dir, {"STUB_FAIL": "make-lint"})
assert proc.returncode == 1
log = (repo / ".git" / "pre_commit_lint.log").read_text()
assert "Python lint failed" in log
def _wait_until(predicate: Callable[[], bool], timeout_seconds: float) -> bool:
deadline = time.monotonic() + timeout_seconds
while time.monotonic() < deadline:
if predicate():
return True
time.sleep(0.05)
return predicate()
def test_interrupt_kills_background_jobs_and_removes_logs(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
hang_dir = tmp_path / "hang"
hang_dir.mkdir()
tmp_dir = tmp_path / "tmpdir"
tmp_dir.mkdir()
extra = {"STUB_HANG_DIR": str(hang_dir), "TMPDIR": str(tmp_dir)}
proc = subprocess.Popen(
[str(SCRIPT)],
cwd=repo,
env=_env(repo, bin_dir, extra),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
try:
assert _wait_until((hang_dir / "make.started").exists, 10)
assert _wait_until((hang_dir / "eslint_report.started").exists, 10)
os.killpg(proc.pid, signal.SIGINT)
assert proc.wait(timeout=10) != 0
make_pid = int((hang_dir / "make.pid").read_text())
assert process_is_gone(make_pid, within_seconds=5)
assert _wait_until(lambda: not any(tmp_dir.iterdir()), 5), list(tmp_dir.iterdir())
finally:
with suppress(ProcessLookupError, PermissionError):
os.killpg(proc.pid, signal.SIGTERM)
def test_interrupt_spares_the_invoking_process(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
hang_dir = tmp_path / "hang"
hang_dir.mkdir()
marker = tmp_path / "invoker_survived"
proc = subprocess.Popen(
["bash", "-c", 'trap : INT; "$1"; echo "$?" > "$2"', "bash", str(SCRIPT), str(marker)],
cwd=repo,
env=_env(repo, bin_dir, {"STUB_HANG_DIR": str(hang_dir)}),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
try:
assert _wait_until((hang_dir / "make.started").exists, 10)
os.killpg(proc.pid, signal.SIGINT)
assert proc.wait(timeout=10) == 0
assert _wait_until(marker.exists, 5)
assert marker.read_text().strip() == "130"
finally:
with suppress(ProcessLookupError, PermissionError):
os.killpg(proc.pid, signal.SIGTERM)
@pytest.mark.parametrize(
("fail", "message"),
[
("make-lint", "Python lint failed"),
("eslint", "Dashboard lint failed"),
("gen-api", "npm run gen:api failed"),
],
)
def test_a_failing_block_fails_the_whole_run(tmp_path: Path, fail: str, message: str) -> None:
repo, bin_dir = _sandbox(tmp_path)
proc = _run(repo, bin_dir, {"STUB_FAIL": fail})
assert proc.returncode == 1
assert message in proc.stdout + proc.stderr
def test_run_ends_with_a_summary_of_ran_and_skipped_blocks(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
proc = _run(repo, bin_dir, {})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "check: summary" in proc.stdout
assert "ran: Python lint (make lint)" in proc.stdout
assert "ran: dashboard lint (prettier + eslint + lint budgets)" in proc.stdout
assert "ran: dashboard API-type sync (npm run gen:api)" in proc.stdout
assert "skipped: tests/e2e checks (basedpyright + raw HTTP client ban) (no tests/e2e Python files in scope)" in proc.stdout
assert TEST_TREE_SKIPPED in proc.stdout
assert "check: PASS" in proc.stdout
assert "check: FAIL" not in proc.stdout
def test_staged_files_matching_no_check_print_an_explicit_noop_note_and_nonempty_log(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
_stage_file(repo, "scripts/tool.py", "def main() -> None: ...\n")
proc = _run(repo, bin_dir, {})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "no gating lint check matches the files in scope, so nothing ran" in proc.stdout
assert "scripts/tool.py" in proc.stdout
assert "a no-op, not a lint verdict" in proc.stdout
assert "check: PASS" in proc.stdout
assert "linting Python" not in proc.stdout
log = (repo / ".git" / "pre_commit_lint.log").read_text()
assert "check: summary" in log
assert "skipped: Python lint (make lint) (no litellm/ Python files in scope)" in log
assert TEST_TREE_SKIPPED in log
def _recorded(args_dir: Path, name: str) -> list[str]:
path = args_dir / name
return path.read_text().splitlines() if path.exists() else []
def test_tests_only_change_runs_the_whole_test_tree_ruff_and_the_quality_gate(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
args_dir = tmp_path / "args"
args_dir.mkdir()
_stage_file(repo, "tests/test_a.py", "def test_a() -> None: ...\n")
_stage_file(repo, "tests/fixtures/data.json", "{}\n")
proc = _run(repo, bin_dir, {"STUB_ARGS_DIR": str(args_dir)})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert _recorded(args_dir, "ruff_tests.args") == [WHOLE_TREE_RUFF]
assert _recorded(args_dir, "make.args") == ["lint-test-quality"]
assert TEST_TREE_RAN in proc.stdout
assert "no gating lint check matches" not in proc.stdout
assert "linting Python" not in proc.stdout
assert "check: PASS" in proc.stdout
@pytest.mark.parametrize(
"changed",
[
"ruff-tests.toml",
"test-quality-budget.json",
"scripts/check_test_quality.py",
"scripts/test_quality_gate.py",
"tests/e2e/test_x.py",
],
)
def test_test_tree_lint_inputs_trigger_the_test_tree_checks(tmp_path: Path, changed: str) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
args_dir = tmp_path / "args"
args_dir.mkdir()
_stage_file(repo, changed, "x = 1\n")
proc = _run(repo, bin_dir, {"STUB_ARGS_DIR": str(args_dir)})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert _recorded(args_dir, "ruff_tests.args") == [WHOLE_TREE_RUFF]
assert "lint-test-quality" in _recorded(args_dir, "make.args")
assert TEST_TREE_RAN in proc.stdout
def test_nothing_staged_tests_only_working_tree_change_runs_the_test_tree_checks(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_stage_file(repo, "tests/test_a.py", "def test_a() -> None: ...\n")
_commit_all(repo, "base")
_set_base_ref(repo)
args_dir = tmp_path / "args"
args_dir.mkdir()
(repo / "tests" / "test_a.py").write_text("def test_a() -> None:\n assert True\n")
proc = _run(repo, bin_dir, {"STUB_ARGS_DIR": str(args_dir)})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "nothing staged; scoping to the working tree's diff" in proc.stdout
assert _recorded(args_dir, "ruff_tests.args") == [WHOLE_TREE_RUFF]
assert _recorded(args_dir, "make.args") == ["lint-test-quality"]
def test_a_failing_test_tree_ruff_fails_the_run_and_still_runs_the_quality_gate(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
args_dir = tmp_path / "args"
args_dir.mkdir()
_stage_file(repo, "tests/test_a.py", "def test_a() -> None: ...\n")
proc = _run(repo, bin_dir, {"STUB_ARGS_DIR": str(args_dir), "STUB_FAIL": "tests-ruff"})
assert proc.returncode == 1
assert "Test-tree ruff failed" in proc.stdout + proc.stderr
assert "check: FAIL" in proc.stdout
assert _recorded(args_dir, "make.args") == ["lint-test-quality"]
def test_a_failing_quality_gate_fails_a_tests_only_run(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
_stage_file(repo, "tests/test_a.py", "def test_a() -> None: ...\n")
proc = _run(repo, bin_dir, {"STUB_FAIL": "test-quality"})
assert proc.returncode == 1
assert "Test-quality budget failed" in proc.stdout + proc.stderr
assert "check: FAIL" in proc.stdout
def test_tests_changed_alongside_litellm_files_defer_to_make_lint(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
args_dir = tmp_path / "args"
args_dir.mkdir()
_stage_file(repo, "litellm/foo.py", "x = 2\n")
_stage_file(repo, "tests/test_a.py", "def test_a() -> None: ...\n")
proc = _run(repo, bin_dir, {"STUB_ARGS_DIR": str(args_dir), "STUB_FAIL": "test-quality"})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "linting Python" in proc.stdout
assert _recorded(args_dir, "ruff_tests.args") == []
assert _recorded(args_dir, "make.args") == ["lint"]
assert TEST_TREE_RAN in proc.stdout
def test_deleted_test_file_still_runs_the_test_tree_checks(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_stage_file(repo, "tests/test_a.py", "def test_a() -> None: ...\n")
_commit_all(repo, "base")
args_dir = tmp_path / "args"
args_dir.mkdir()
subprocess.run(["git", "rm", "-q", "tests/test_a.py"], cwd=repo, check=True)
proc = _run(repo, bin_dir, {"STUB_ARGS_DIR": str(args_dir)})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert _recorded(args_dir, "ruff_tests.args") == [WHOLE_TREE_RUFF]
assert _recorded(args_dir, "make.args") == ["lint-test-quality"]
assert TEST_TREE_RAN in proc.stdout
def test_partial_staging_warns_when_test_files_are_left_unstaged(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_stage_file(repo, "tests/test_a.py", "def test_a() -> None: ...\n")
_commit_all(repo, "base")
args_dir = tmp_path / "args"
args_dir.mkdir()
_stage_file(repo, "notes.md", "hi\n")
(repo / "tests" / "test_a.py").write_text("def test_a() -> None:\n assert True\n")
proc = _run(repo, bin_dir, {"STUB_ARGS_DIR": str(args_dir)})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "SKIPPED test-tree lint (ruff-tests.toml + test-quality budget)" in proc.stdout
assert "tests/test_a.py" in proc.stdout
assert _recorded(args_dir, "ruff_tests.args") == []
assert _recorded(args_dir, "make.args") == []
def test_run_queues_through_the_machine_wide_gate_slot_lock(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
lock_dir = tmp_path / "gate-locks"
proc = _run(repo, bin_dir, {"LITELLM_GATE_SLOT_DIR": str(lock_dir)})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert (lock_dir / "slot-0.lock").exists()
def test_run_under_a_held_slot_skips_reacquiring_the_gate_lock(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
lock_dir = tmp_path / "gate-locks"
proc = _run(
repo,
bin_dir,
{"LITELLM_GATE_SLOT_DIR": str(lock_dir), "LITELLM_GATE_SLOT_HELD": "1"},
)
assert proc.returncode == 0, proc.stdout + proc.stderr
assert not lock_dir.exists()
def test_hook_symlink_install_still_resolves_the_slot_lock_helper(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
scripts_dir = repo / "scripts"
scripts_dir.mkdir()
shutil.copy(SCRIPT, scripts_dir / "pre_commit_lint.sh")
shutil.copy(SCRIPT.parent / "gate_slot_lock.py", scripts_dir / "gate_slot_lock.py")
(repo / ".git" / "hooks" / "pre-commit").symlink_to(Path("../../scripts/pre_commit_lint.sh"))
lock_dir = tmp_path / "gate-locks"
proc = subprocess.run(
["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "hooked"],
cwd=repo,
capture_output=True,
text=True,
env=_env(repo, bin_dir, {"LITELLM_GATE_SLOT_DIR": str(lock_dir)}),
timeout=120,
)
assert proc.returncode == 0, proc.stdout + proc.stderr
assert (lock_dir / "slot-0.lock").exists()
def test_failing_run_ends_with_a_fail_verdict(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
proc = _run(repo, bin_dir, {"STUB_FAIL": "make-lint"})
assert proc.returncode == 1
assert "check: FAIL" in proc.stdout
assert "check: PASS" not in proc.stdout
def test_explicit_base_scopes_offline_without_a_remote(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
(repo / "litellm" / "foo.py").write_text("x = 2\n")
proc = _run(repo, bin_dir, {"BASE_REF": "HEAD"})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "merge base with HEAD" in proc.stdout
assert "linting Python" in proc.stdout
def test_symlinked_hook_can_resolve_default_branch(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
_set_base_ref(repo, "main")
hook = repo / ".git" / "hooks" / "pre-commit"
hook.symlink_to(SCRIPT)
proc = subprocess.run(
[str(hook)], cwd=repo, capture_output=True, text=True,
env=_env(repo, bin_dir, {}), timeout=120,
)
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "no branch changes vs origin/main" in proc.stdout