litellm/tests/e2e/coverage_registry
Yassin Kortam b5028b81c6 fix(e2e): make coverage collection independent of the runner
The coverage number is documented as static, a property of the source tree, but
the same tree reported 312, 314, or 315 covered cells depending on the machine.
Markers were harvested from session.items in pytest_collection_finish, which
runs after every pytest_collection_modifyitems hook, so anything the runner
dropped vanished from the numerator: the weekly load test is deselected unless
E2E_WEEKLY_ANOMALY is set, and the MCP OAuth module sits behind
pytest.importorskip("mcp") / importorskip("playwright.async_api").

Markers are now read straight off the source with ast, so a cell counts when a
test declaring it exists, whatever the runner does with it. The collect-only
pytest pass stays for the two things the source text cannot give: markers built
at import time (pytest.mark.covers(*fn(...)) inside a pytest.param) and the
nodeids that failed to import, whose cells are genuinely unknowable and still
drive --fail-on-collection-errors. Files that cannot be parsed are reported the
same way instead of being dropped.

The TypeScript Playwright suite emits no pytest markers, so the two
surface: ui mgmt rows were structurally uncoverable. It now declares its cells
in tests/e2e/ui/coverage.yaml, which the collector unions in; an id there that
is not in the registry surfaces as an orphan marker exactly like a mistyped
pytest marker. Each row also names the spec and the Playwright test title behind
it, and both are resolved against the tree on every run, so renaming, deleting
or commenting out that test drops the cell out of the numerator and fails
--strict instead of leaving it counted forever.

Title matching is per declaration, never per file. Comments are stripped before
titles are read, by a string-aware pass that keeps literals intact so a "//"
inside a title is never mistaken for a comment opener. An interpolated title is
matched against its literal segments, so it is checked as far as it can be, and
a dynamic title elsewhere in the spec never exempts a row naming a literal one.
A title with no literal text, or one assembled from variables, backs no row, so
an unmatchable shape fails loudly rather than waving a whole file through. Only
mgmt.key.update.happy_path is claimed, proven by the "Update key TPM and RPM
limits" spec. mgmt.key.generate.happy_path is left unclaimed and documented:
that row scopes itself to SSO-driven key gen and every role in the suite logs in
with username/password.

Pruned llm.embeddings.anthropic.basic.nonstream.works. Anthropic ships no
embeddings API, the anthropic handler's embedding() is a pass stub, no anthropic
dispatch exists in embedding(), and no anthropic row in
model_prices_and_context_window.json carries mode: embedding, so the row could
never pass.

Also corrected the suite-folder list in tests/e2e/CLAUDE.md: gateway/,
embeddings/, security/, and a top-level realtime/ do not exist (realtime lives
under llm_translation/), and guardrails/ was undocumented.

Same tree now reports 316/430 with and without E2E_WEEKLY_ANOMALY and with or
without mcp/playwright installed.
2026-07-27 17:15:30 -07:00
..
__init__.py test(e2e): add coverage registry and collector (#32304) 2026-07-07 15:51:20 -04:00
collector.py fix(e2e): make coverage collection independent of the runner 2026-07-27 17:15:30 -07:00
guardrail.yaml test(e2e): add Other suite and Guardrails coverage incl. an MCP tool-call guardrail (#34149) 2026-07-21 14:06:29 -07:00
llm_claude_code_compat.yaml fix(e2e/claude_code): unblock stage collection, align proxy env names, register compat models (#33433) 2026-07-16 11:05:31 -07:00
llm_conversational.yaml test(e2e): cover /openai chat passthrough cost logging (#34470) 2026-07-25 17:54:28 +00:00
llm_nonconversational.yaml fix(e2e): make coverage collection independent of the runner 2026-07-27 17:15:30 -07:00
logging.yaml test(e2e): datadog log delivery for streamed routes, read back from the real datadog api (#33566) 2026-07-16 19:37:07 -07:00
mcp.yaml test(e2e): cover MCP access-group tool selection at key creation (#34480) 2026-07-24 16:18:40 -07:00
mgmt.yaml fix(e2e): stop tests from breaking the shared proxy for every suite after them (#34664) 2026-07-25 23:12:55 +00:00
other.yaml test(e2e): invoke a real published a2a agent and assert it replies 2026-07-21 22:33:07 +00:00
quota_management.yaml test(true_rabbit): cover passthrough headers, batch assume-role, gemini, vllm, bedrock guardrails, batch rate-limit mapping (#33843) 2026-07-20 16:15:55 -07:00
README.md fix(e2e): make coverage collection independent of the runner 2026-07-27 17:15:30 -07:00
registry.py ci: gate tests/e2e on zero basedpyright errors in pre-commit and lint CI 2026-07-11 10:25:22 -07:00
reliability.yaml test(e2e): add weekly session-anomaly load test against real providers 2026-07-21 14:54:02 -07:00
schema.py test(e2e): cover /v1/images/edits (#34476) 2026-07-25 10:38:07 -07:00
test_collector.py fix(e2e): make coverage collection independent of the runner 2026-07-27 17:15:30 -07:00

e2e coverage registry

This directory is the denominator for e2e test coverage: the set of behaviors we want covered, one row per behavior, checked into the repo so coverage is a number we can track instead of a guess. It implements the plan in the "E2E Coverage Tracking" note; the naming grammar lives in tests/e2e/CLAUDE.md.

The model

A cell is one customer-noticeable behavior a single e2e test can assert pass/fail on, for example llm.chat_completions.bedrock_converse.tool_use.stream.works. Cells are grouped module > feature > test, with LLM cells split into Core LLMs and Non-Core LLMs for dashboarding. Each cell carries a tier (P0/P1/P2), a source, and a fail_before_fix flag.

The rows live in per-prefix YAML files (llm_*.yaml, mgmt.yaml, mcp.yaml, reliability.yaml, quota_management.yaml, logging.yaml, guardrail.yaml, other.yaml) and validate against the discriminated union in schema.py, so an LLM row cannot carry a guardrail field and vice versa. llm rows with subject_endpoint of chat_completions, messages, or responses roll up to Core LLMs; all other LLM endpoints roll up to Non-Core LLMs. LLM endpoint, route, and capability values are typed in schema.py, so new taxonomy values require an explicit schema change. logging and guardrail are two id-prefixes that roll up into the single Logging & Guardrails dashboard module.

A test declares what it covers with a marker:

@pytest.mark.covers("llm.chat_completions.openai.tool_use.stream.works")
def test_openai_streaming_tool_calls(self) -> None:
    ...

The number

collector.py diffs the registry against those markers and reports coverage per module. It is static in the strong sense: a cell is covered when a test declaring it exists in the tree, so the number is a property of the source and does not move with the runner. The markers are read off the source with ast, which keeps deselected tests (the weekly load test, gated on E2E_WEEKLY_ANOMALY) and modules behind an optional dependency (pytest.importorskip("mcp")) counted on every machine. A collect-only pytest pass runs alongside it for the two things the source text cannot give: markers built at import time (pytest.mark.covers(*fn(...)) inside a pytest.param) and the nodeids that failed to import, whose cells are genuinely unknowable. It runs no test and needs no live proxy. Whether a covered cell currently passes or fails is a separate, live concern.

The Playwright suite under tests/e2e/ui/ is TypeScript and emits no pytest markers, so it declares the cells it covers in tests/e2e/ui/coverage.yaml and the collector unions those ids in. An id there that is not in the registry surfaces as an orphan marker exactly like a typo in a pytest marker.

Each row names the spec it lives in and the Playwright test title that proves it, and the collector resolves both against the tree on every run. Rename or delete that test and the row stops counting and is listed as a stale declaration, so a claim cannot outlive the test behind it. Comments are stripped before titles are read, so a test commented out and forgotten fails the same way a deleted one does, and the strip is string-aware so a // inside a title is never mistaken for a comment opener. An interpolated title is matched against its literal segments, so it is still checked as far as it can be, and the check is per row rather than per file: a dynamic title elsewhere in the spec never exempts a row that names a literal one. A title with no literal text, or one assembled from variables, backs no row at all, so the contributor has to give that test a matchable title instead of the check being waved through.

cd tests/e2e && PYTHONPATH=. python -m coverage_registry.collector

Use --format loki after the e2e pytest run in the same Kubernetes job/pod to print structured stdout lines for Loki:

cd tests/e2e && PYTHONPATH=. python -m coverage_registry.collector --format loki --strict

This emits exactly one COVERAGE_TOTAL line and one COVERAGE_MODULE line per module in MODULE_ORDER, in that order. Loki uses log-safe module= labels from LOKI_MODULE_LABELS (core_llms, management_ui, etc.) so existing JSON and Prometheus consumers keep their human-readable module names unchanged.

The headline is overall coverage. The collector also lists markers that point at ids not in the registry, so a typo or an unenumerated behavior surfaces instead of being silently dropped.

Use strict mode in CI once existing draft markers are reconciled:

cd tests/e2e && PYTHONPATH=. python -m coverage_registry.collector --strict

Strict mode exits non-zero on @pytest.mark.covers(...) ids that are not checked into the registry, and on UI declarations whose spec or test title no longer exists. Both are the same failure: the registry claims something the tree does not support. Add --fail-on-collection-errors when the job should also fail on pytest collection errors.

Status: this is a draft for review

The cells were enumerated from the codebase and the tiers are a first proposal. Known things to settle before treating the set as final:

  • tiers are proposed, not signed off; 125 P0 is a lot to prove fail-before-fix, so P0 may want tightening
  • llm.embeddings.anthropic.basic.nonstream.works was removed: Anthropic ships no embeddings API, litellm has no anthropic embedding handler (the one on the chat handler is a pass stub and no anthropic row in model_prices_and_context_window.json carries mode: embedding), so the row could never pass. reliability.perf.throughput.under_slo still needs a support check
  • prune a row only when it is genuinely unreachable, with the evidence written down here; the denominator is the metric, and shrinking it for any other reason games it
  • auth is covered in two places (other.auth.* and the mgmt authz assertions); the boundary needs a decision, and the auth cluster may deserve promotion to its own module
  • the P2 "niche" cells each stand in for a large tail of integrations/providers by design, so the denominator is deliberately P0-weighted rather than a full inventory