test: remove checks that pin implementation details and add a shadcn testing rule (#48663)

* test: remove e2e checks and a CI line count that pin implementation details

Deletes the Auto Router template select anchoring spec, the 390px logs pagination variant, and the call id tooltip and clipboard step in the logs spec. Each one tests library or test helper behavior rather than what users see, and each has been red or flaky on main without a product regression

Also drops the exact line count on the fan-in requirements export in the integration coverage job. It went from 6 to 10 when uv.lock gained a Python 3.15 fork, while the install was still correct. A package the filter drops still fails the job, either at the hash-checked install or at the next step's import

* docs(agents): do not test what shadcn and Base UI already guarantee

Adds the boundary to the dashboard AGENTS.md next to the test tiers, and points the Playwright suite's entry in tests/e2e/AGENTS.md at it
This commit is contained in:
yuneng-jiang 2026-10-27 15:26:15 -07:00 • committed by GitHub
parent cb70bc560e
commit 9b2fc9db40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 38 additions and 105 deletions

View file

@ -1379,7 +1379,6 @@ jobs:
uv export --frozen --all-groups --all-extras --no-emit-project --no-emit-workspace --no-header --no-annotate \
| awk '/^[^ ]/ { keep = $0 ~ /^(annotated-types|coverage|pydantic|pydantic-core|typing-extensions|typing-inspection)==/ } keep' \
> /tmp/fan-in-requirements.txt
test "$(grep -c '^[^ ]' /tmp/fan-in-requirements.txt)" -eq 6
uv venv .venv --python 3.12
uv pip install --no-config --python .venv --require-hashes -r /tmp/fan-in-requirements.txt
- attach_workspace:

View file

@ -49,7 +49,7 @@ Each subdirectory under `tests/e2e/` is one suite, scoped to an endpoint family
- `secret_manager/` - the gateway's `key_management_system` against a real secret manager: deployment keys resolved from it (`os.environ/<name>` where the name exists only in the manager) and virtual keys written to and deleted from it. The tests are backend-agnostic and each backend is its own lane, because the setting is global to the proxy: `E2E_SECRET_MANAGER=<system>` opts in and picks the backend from `secret_backends.BACKENDS`, the proxy is booted from `gateway/secret_manager_<system>_ci_config.yml` against the live manager, and the tests reach that manager through the backend's `SecretStore` (`secret_store_<system>.py`). A test needing something not every backend does carries `requires_capability(...)` and is deselected on lanes that lack it. `secret_manager/backend.sh up <system>` runs a backend in Docker and writes the proxy's and the tests' env. Marked `secret_manager`, deselected unless `E2E_SECRET_MANAGER` is set, and kept out of the per-PR selector. Backends today: `hashicorp_vault` and `cyberark` (CyberArk Conjur, which cannot delete, so the delete test is Vault-only)
- `gateway/` - proxy configuration only (`litellm-config.yml`). No tests
- `claude_code/` - the Claude Code compatibility matrix: drives the real `claude` CLI (and HTTP probes) against a proxy for each feature x provider cell, reporting tagged-union outcomes via the `compat_result` fixture. Ships its own driver/builder/publisher, covered by `tests/e2e_harness/claude_code/`. The HTTP probes ride the shared transport (`ProxyClient.count_tokens` / `ProxyClient.messages`). The CLI-driving path stays bespoke
- `ui/` - the Admin UI browser suite: Playwright in TypeScript, driving the dashboard served by a live proxy on port 4000 (seeded postgres + mock LLM upstream, see its `run_e2e.sh`). It is a self-contained npm package with its own lockfile and does not use the Python harness, pytest markers, or the shared transport. The Python rules in this file (typed models, `Result` unions, basedpyright zero-error gate) do not apply inside it. Its only Python file, `fixtures/mock_llm_server/server.py`, is excluded from the e2e basedpyright gate via the root `pyrightconfig.json`
- `ui/` - the Admin UI browser suite: Playwright in TypeScript, driving the dashboard served by a live proxy on port 4000 (seeded postgres + mock LLM upstream, see its `run_e2e.sh`). It is a self-contained npm package with its own lockfile and does not use the Python harness, pytest markers, or the shared transport. The Python rules in this file (typed models, `Result` unions, basedpyright zero-error gate) do not apply inside it. Its only Python file, `fixtures/mock_llm_server/server.py`, is excluded from the e2e basedpyright gate via the root `pyrightconfig.json`. Its specs follow the dashboard's rule against testing what shadcn and Base UI already guarantee, in `ui/litellm-dashboard/AGENTS.md`
## MCP suite: real Datadog only

View file

@ -194,17 +194,6 @@ test.describe("Logs page", () => {
await expect(row, `no logs row for call id ${callId}`).toHaveCount(1, { timeout: 30_000 });
await expect(row, "the row itself shows only the request id").not.toContainText(callId);
await row.getByText(requestId).hover();
const tooltip = page.locator("[data-slot='tooltip-content']");
await expect(tooltip, "hovering the Request ID cell does not list the x-litellm-call-id").toContainText(
`x-litellm-call-id: ${callId}`,
{ timeout: 10_000 },
);
await tooltip.getByRole("button", { name: "Copy x-litellm-call-id" }).click();
if (await page.evaluate(() => window.isSecureContext)) {
await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(callId);
}
await row.click();
const drawer = page.getByRole("dialog").first();
await expect(drawer.getByText("Request & Response")).toBeVisible({ timeout: 20_000 });

View file

@ -54,52 +54,47 @@ async function setRowsPerPage(page: PlaywrightPage, size: "25" | "50" | "100"):
test.describe("Logs pagination with Aria in the header", () => {
test.use({ storageState: ADMIN_STORAGE_PATH });
for (const viewport of [
{ width: 1440, height: 900 },
{ width: 390, height: 844 },
]) {
test(`keeps pagination reachable and Aria available at ${viewport.width}px`, async ({ page, request }) => {
await page.setViewportSize(viewport);
const alias = `e2e-aria-header-${uniqueSuffix()}`;
const key = await createVirtualKey(request, { key_alias: alias });
try {
const ids = await Promise.all(
Array.from({ length: 26 }, (_, index) =>
sendChatCompletion(request, {
model: CHAT_MODEL_A,
prompt: `${alias}-${index}`,
apiKey: key.key,
}),
),
);
await Promise.all(ids.map((id) => waitForSpendLog(request, id)));
await openLogs(page);
await setAriaHidden(page, false);
await applyKeyAliasFilter(page, await openFilterDrawer(page), alias);
await setRowsPerPage(page, "25");
await expect(visibleTestId(page, "pagination-range")).toHaveText("Showing 1-25 of 26");
await visibleTestId(page, "pagination-next").click();
await expect(visibleTestId(page, "pagination-range")).toHaveText("Showing 26-26 of 26");
test("keeps pagination reachable and Aria available at 1440px", async ({ page, request }) => {
await page.setViewportSize({ width: 1440, height: 900 });
const alias = `e2e-aria-header-${uniqueSuffix()}`;
const key = await createVirtualKey(request, { key_alias: alias });
try {
const ids = await Promise.all(
Array.from({ length: 26 }, (_, index) =>
sendChatCompletion(request, {
model: CHAT_MODEL_A,
prompt: `${alias}-${index}`,
apiKey: key.key,
}),
),
);
await Promise.all(ids.map((id) => waitForSpendLog(request, id)));
await openLogs(page);
await setAriaHidden(page, false);
await applyKeyAliasFilter(page, await openFilterDrawer(page), alias);
await setRowsPerPage(page, "25");
await expect(visibleTestId(page, "pagination-range")).toHaveText("Showing 1-25 of 26");
await visibleTestId(page, "pagination-next").click();
await expect(visibleTestId(page, "pagination-range")).toHaveText("Showing 26-26 of 26");
const aria = page.getByRole("banner").getByRole("button", { name: "Aria Agent", exact: true });
const aria = page.getByRole("banner").getByRole("button", { name: "Aria Agent", exact: true });
await aria.click();
await expect(page.getByRole("dialog", { name: "Aria Agent", exact: true })).toBeVisible();
await page.keyboard.press("Escape");
await expect(aria).toBeFocused();
await visibleTestId(page, "pagination-first").click();
await expect(visibleTestId(page, "pagination-range")).toHaveText("Showing 1-25 of 26");
for (const destination of [Page.Usage, Page.LlmPlayground, Page.ApiKeys]) {
await navigateToPage(page, destination);
await aria.click();
await expect(page.getByRole("dialog", { name: "Aria Agent", exact: true })).toBeVisible();
await page.keyboard.press("Escape");
await expect(aria).toBeFocused();
await visibleTestId(page, "pagination-first").click();
await expect(visibleTestId(page, "pagination-range")).toHaveText("Showing 1-25 of 26");
for (const destination of [Page.Usage, Page.LlmPlayground, Page.ApiKeys]) {
await navigateToPage(page, destination);
await aria.click();
await expect(page.getByRole("dialog", { name: "Aria Agent", exact: true })).toBeVisible();
await page.keyboard.press("Escape");
await expect(aria).toBeFocused();
}
} finally {
await deleteVirtualKey(request, key.token);
}
});
}
} finally {
await deleteVirtualKey(request, key.token);
}
});
});
test.describe("Logs page session-grouped pagination", () => {

View file

@ -1,52 +0,0 @@
import { expect, test, type Locator, type Page as PlaywrightPage } from "@playwright/test";
import { ADMIN_STORAGE_PATH } from "../../constants";
import { navigateToPage } from "../../helpers/navigation";
import { Page } from "../../fixtures/pages";
/**
* Opens Add Auto Router and returns the Template select's trigger, which is the
* shallowest real page that renders SelectContent with tall multi-line options.
*/
async function openTemplateSelect(page: PlaywrightPage) {
await navigateToPage(page, Page.Models);
await page.getByRole("tab", { name: "Auto-Routers" }).click();
await page.getByRole("button", { name: "Add Auto Router" }).click();
const trigger = page.getByTestId("template-selector");
await expect(trigger).toBeVisible();
return trigger;
}
async function boxes(trigger: Locator, options: Locator) {
const triggerBox = await trigger.boundingBox();
const optionsBox = await options.boundingBox();
return triggerBox && optionsBox ? { triggerBox, optionsBox } : null;
}
const clippedPopup = (page: PlaywrightPage) => page.locator('[data-slot="select-content"]');
function pollOptionsCoverTrigger(trigger: Locator, options: Locator) {
return expect.poll(async () => {
const box = await boxes(trigger, options);
return (
box &&
box.optionsBox.y < box.triggerBox.y + box.triggerBox.height &&
box.optionsBox.y + box.optionsBox.height > box.triggerBox.y
);
});
}
test.describe("Auto Router template select anchoring", () => {
test.use({ storageState: ADMIN_STORAGE_PATH });
test("keeps the trigger uncovered when the options open with no room below it", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 560 });
const trigger = await openTemplateSelect(page);
await trigger.scrollIntoViewIfNeeded();
await trigger.click();
await expect(page.getByRole("listbox")).toBeVisible();
await pollOptionsCoverTrigger(trigger, clippedPopup(page)).toBe(false);
});
});

View file

@ -8,6 +8,8 @@ When you fix lint violations that are grandfathered in `eslint-suppressions.json
Tests come in three tiers, named by the standard definitions. `Foo.test.tsx` is a unit test: one module, collaborators replaced by doubles, no multi-component tree, and it should run in milliseconds. `Foo.integration.test.tsx` renders a real component tree with real children and only stubs the network boundary. It costs seconds per case, so it earns its place by proving wiring that a unit test cannot reach. Browser-level tests live in `tests/e2e/ui/` as Playwright specs against a live proxy
Do not test what shadcn and Base UI already guarantee, such as a tooltip opening on hover, a popup positioning itself against its trigger, focus trapping and Escape in a dialog, keyboard movement through a menu or select, and open and close animations. Those are the library's contract, and a test of them only fails when we restyle or move the page. Test what we put inside the primitive and what happens because of it, like the text a tooltip carries, the options a select offers, or the request a confirm button sends. Reach that content the most direct way, by a click, the keyboard, or the data the page renders, rather than through a hover race. If we change a primitive's behavior in `src/components/ui/`, its test lives next to that primitive as `*.test.tsx`, never in a page spec that borrows the page as a host
When a component holds logic worth asserting, extract the logic and unit-test it there rather than driving it through a render. `CreateMCPServer` is the worked example: its payload building lives in `createServerPayload.ts` with 46 unit tests that run in single-digit milliseconds, while `CreateMCPServer.integration.test.tsx` keeps only the cases that prove a form field reaches the right payload key. A test that renders a whole modal to assert the shape of one object belongs in the first category, not the second
Most of the suite predates this split and is not yet classified, so an unsuffixed `*.test.tsx` is not evidence that a file is really a unit test. Classify what you touch