litellm/tests/unit/test_openapi_docs_gate.py
yujonglee e93e01ca4b
fix(ci): build base native extension for OpenAPI lint (#49136)
* fix(openapi): build native extension for base counts

* test(openapi): exercise native import in base worktree

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-29 16:56:44 -07:00

85 lines
3.2 KiB
Python

"""Tests for scripts/openapi_docs_gate.py.
The blame rule lives in scripts/lint_base_counts.py and is tested there; what is tested
here is the checker output parse and `introduced`, the diff scan that names the new
endpoint behind a breach.
"""
import subprocess
from pathlib import Path
from typing import Final
from unittest.mock import patch
import openapi_docs_gate as gate
import pytest
def test_base_counts_stops_when_the_native_build_fails(tmp_path: Path) -> None:
parent: Final = tmp_path / "oa_base"
parent.mkdir()
checker: Final = tmp_path / "check_openapi_docs.py"
checker.write_text("print('POST /v1/test OA001')\n")
with (
patch("openapi_docs_gate.tempfile.mkdtemp", return_value=str(parent)),
patch(
"subprocess.run",
side_effect=(
subprocess.CompletedProcess([], 0, stdout="", stderr=""),
subprocess.CompletedProcess([], 1, stdout="", stderr="build failed"),
subprocess.CompletedProcess([], 0, stdout="", stderr=""),
),
) as run,
patch("signal.signal"),
):
with pytest.raises(SystemExit, match="uv exited 1"):
gate.base_counts("HEAD", repo_root=tmp_path, checker=checker)
assert run.call_count == 3
def test_editing_the_checker_rekeys_the_base_counts(tmp_path: Path) -> None:
checker: Final = tmp_path / "check.py"
checker.write_text("print('v1')\n")
before: Final = gate.checker_identity(checker).artifact_name("abc123")
checker.write_text("print('v2')\n")
assert gate.checker_identity(checker).artifact_name("abc123") != before
def test_parse_violations_skips_anything_that_is_not_a_finding_line() -> None:
out: Final = "Some warning from an import\nPOST /v1/decisions OA001\nPOST /v1/decisions OA003\nnot a line\n"
assert gate.parse_violations(out) == (
gate.Violation("POST", "/v1/decisions", "OA001"),
gate.Violation("POST", "/v1/decisions", "OA003"),
)
def test_count_by_rule_sums_findings_per_code() -> None:
violations: Final = (
gate.Violation("POST", "/a", "OA001"),
gate.Violation("POST", "/b", "OA001"),
gate.Violation("POST", "/b", "OA003"),
)
assert dict(gate.count_by_rule(violations)) == {"OA001": 2, "OA003": 1}
def test_introduced_keeps_only_routes_registered_on_added_lines() -> None:
diff: Final = (
"diff --git a/litellm/proxy/x.py b/litellm/proxy/x.py\n"
"--- a/litellm/proxy/x.py\n"
"+++ b/litellm/proxy/x.py\n"
"@@ -0,0 +3,2 @@\n"
'+@router.post("/v1/new")\n'
"+async def new(request: Request): ...\n"
"@@ -10,1 +20,1 @@\n"
'-@router.post("/v1/removed")\n'
)
violations: Final = (
gate.Violation("POST", "/v1/new", "OA001"),
gate.Violation("POST", "/v1/old", "OA001"),
gate.Violation("POST", "/v1/removed", "OA001"),
)
assert gate.introduced(violations, gate.added_text(diff)) == (gate.Violation("POST", "/v1/new", "OA001"),)
def test_introduced_does_not_match_a_path_that_is_only_a_prefix_of_an_added_one() -> None:
added: Final = gate.added_text('+@router.post("/v1/decisions/batch")\n')
assert gate.introduced((gate.Violation("POST", "/v1/decisions", "OA002"),), added) == ()