tests(vcr): add LITELLM_VCR_VERBOSE per-test hit/miss reporting

Set LITELLM_VCR_VERBOSE=1 to print a one-line cassette verdict per
test (HIT / MISS / PARTIAL / NOOP) showing replay vs new-recording
counts. Useful for local QA to confirm which tests actually exercised
the cache and which fell through to the live provider.
This commit is contained in:
mateo-berri 2026-05-01 13:00:20 -07:00
parent a7e8189b17
commit cf4c9ede61
3 changed files with 79 additions and 0 deletions

View file

@ -146,3 +146,42 @@ def patch_vcrpy_aiohttp_record_path() -> None:
_aiohttp_stubs.record_response = _record_response_preserving_body
_PATCHED_AIOHTTP_RECORD = True
VCR_VERBOSE_ENV = "LITELLM_VCR_VERBOSE"
def vcr_verbose_enabled() -> bool:
return os.environ.get(VCR_VERBOSE_ENV) == "1"
def format_vcr_verdict(cassette: Any) -> str:
"""Build a one-line hit/miss verdict for a vcrpy Cassette.
HIT — at least one request was served from cache and nothing new was
recorded. (Pure replay.)
MISS — nothing from cache; one or more requests went live and were
recorded. (Cold cache.)
PARTIAL — mix of replay and new recordings. Usually means the cassette
matches some but not all requests for this test (e.g. retries,
new branches, or vcrpy match_on too strict).
NOOP — test made no HTTP calls (or VCR not engaged for it).
"""
if cassette is None:
return "[VCR NOOP]"
played = getattr(cassette, "play_count", 0) or 0
# cassette.data is the recorded request/response list; len(cassette) counts
# recorded episodes. New recordings during this test = len - prior_len, but
# we don't have prior_len here, so we use cassette.dirty (set when an append
# happened during this run) as the "new recording" signal.
dirty = getattr(cassette, "dirty", False)
total = len(cassette) if hasattr(cassette, "__len__") else 0
if played == 0 and not dirty:
return "[VCR NOOP] (no http traffic)"
if played > 0 and not dirty:
return f"[VCR HIT] {played} replayed, 0 new ({total} cassette entries)"
if played == 0 and dirty:
return f"[VCR MISS] 0 replayed, recorded new ({total} cassette entries)"
return (
f"[VCR PARTIAL] {played} replayed + new recordings ({total} cassette entries)"
)

View file

@ -15,8 +15,10 @@ import litellm # noqa: E402
from tests._vcr_redis_persister import ( # noqa: E402
filter_non_2xx_response,
format_vcr_verdict,
make_redis_persister,
patch_vcrpy_aiohttp_record_path,
vcr_verbose_enabled,
)
@ -102,6 +104,24 @@ def pytest_recording_configure(config, vcr):
patch_vcrpy_aiohttp_record_path()
@pytest.fixture(autouse=True)
def _vcr_hit_miss_report(request, vcr):
"""When LITELLM_VCR_VERBOSE=1, print a one-line cassette verdict per test.
Runs after the `vcr` fixture (which yields the active Cassette), so we can
inspect play_count / dirty / len in teardown."""
yield
if not vcr_verbose_enabled():
return
verdict = format_vcr_verdict(vcr)
reporter = request.config.pluginmanager.get_plugin("terminalreporter")
line = f"{verdict} :: {request.node.nodeid}"
if reporter is not None:
reporter.write_line(line)
else: # pragma: no cover - reporter is always present in normal runs
print(line)
@pytest.fixture(scope="session")
def event_loop():
try:

View file

@ -20,8 +20,10 @@ import litellm # noqa: E402
from tests._vcr_redis_persister import ( # noqa: E402
filter_non_2xx_response,
format_vcr_verdict,
make_redis_persister,
patch_vcrpy_aiohttp_record_path,
vcr_verbose_enabled,
)
@ -126,6 +128,24 @@ def pytest_recording_configure(config, vcr):
patch_vcrpy_aiohttp_record_path()
@pytest.fixture(autouse=True)
def _vcr_hit_miss_report(request, vcr):
"""When LITELLM_VCR_VERBOSE=1, print a one-line cassette verdict per test.
Runs after the `vcr` fixture (which yields the active Cassette), so we can
inspect play_count / dirty / len in teardown."""
yield
if not vcr_verbose_enabled():
return
verdict = format_vcr_verdict(vcr)
reporter = request.config.pluginmanager.get_plugin("terminalreporter")
line = f"{verdict} :: {request.node.nodeid}"
if reporter is not None:
reporter.write_line(line)
else: # pragma: no cover - reporter is always present in normal runs
print(line)
# ---------------------------------------------------------------------------
# Capture TRUE defaults at conftest import time (before test modules pollute).
# ---------------------------------------------------------------------------