mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
* test(e2e): let the ui suite run from a read-only cwd The playwright suite never executed on stage. It died in globalSetup before a single test ran, and the reported error was a red herring. /app/e2e/ui is a read-only filesystem in the packaged e2e image (the image runner already redirects playwright's own artifacts to TMPDIR for this reason), but the suite wrote three things relative to cwd: the per-role storageState files, the failure-screenshot directory, and the html report. Reproduced in the pod: storageState raises EROFS, mkdir test-results raises ENOENT. Worse, the catch block that exists to capture a screenshot threw its own ENOENT while handling a failure, so the real login error was replaced by a filesystem error. That is why the run looked like a missing directory rather than whatever actually went wrong. Route every artifact through ARTIFACT_DIR (E2E_UI_ARTIFACT_DIR, default "." to keep run_e2e.sh behavior unchanged), make the diagnostic screenshot best-effort so it can never mask the underlying failure, and point playwright's reporter and outputDir at the same place so a bare `npx playwright test` works there too. fixtures/users.ts had its own copy of the five storageState filenames; it now re-exports the ones from constants so the paths have a single definition. Verified in the read-only pod: both writes fail before, both succeed after. 85 tests enumerate and tsc --noEmit is clean. Refs LIT-4821 * fix(e2e): create the ui artifact root before writing into it storageState() does not create missing parents, and nothing created ARTIFACT_DIR itself. Pointing E2E_UI_ARTIFACT_DIR at a writable path that did not exist yet therefore failed with ENOENT on the very first role's snapshot, before any UI test ran; the same class of failure the artifact-dir change was meant to remove, just moved one level up. Reproduced: writing admin.storageState.json into a missing directory raises ENOENT. My earlier pod verification masked this because the probe called mkdirSync itself, which the real code path never did. mkdir the root once at the top of globalSetup, before the login loop. recursive makes it idempotent, handles nested paths, and keeps the default "." a no-op. Playwright creates its own outputDir lazily, so globalSetup is the only place that needs this, and migration.serverRootPath.globalSetup delegates here so it is covered too. * test(e2e): skip the mid-conversation cache checks pending LIT-4873 A mid-conversation role="system" reminder invalidates the prompt cache on the vertex_ai, azure_ai and bedrock_invoke Messages paths. Measured on the reminder turn, same conversation shape throughout: direct to api.anthropic.com 7013 read cache preserved litellm -> anthropic/claude-opus-4-8 7013 read cache preserved litellm -> vertex_ai/claude-opus-4-8 0 read cache destroyed and the Vertex control with the same added assistant/user turns but no reminder reads 7013, so it is the reminder on the non-first-party paths and not the extra turns. Anthropic keeping the cache rules out provider behavior; litellm's first-party anthropic path keeping it rules out the shared Messages transform. That makes these assertions correct and the failure a real billing bug, so the tests are skipped rather than weakened; the bodies stay intact and must be restored unchanged with the fix. Registry rows are left in place, so the three mid_conversation_system.nonstream.cache_hit cells report as uncovered gaps. Skips are decorators rather than a pytest.skip() inside the shared helper: a mid-function skip fires only after setup has already registered a real deployment via /model/new and left the rest of the body unreachable. Only Vertex was measured end to end. Azure Foundry and Bedrock Invoke are inferred from matching nightly failures and should be confirmed with the fix. Refs LIT-4821, LIT-4873
86 lines
4 KiB
TypeScript
86 lines
4 KiB
TypeScript
import { chromium, expect, request } from "@playwright/test";
|
|
import { users, Role, STORAGE_PATHS } from "./fixtures/users";
|
|
import { ARTIFACT_DIR, UI_BASE_URL } from "./constants";
|
|
import * as fs from "fs";
|
|
import * as path from "path";
|
|
|
|
async function globalSetup() {
|
|
const browser = await chromium.launch();
|
|
const rootPath = process.env.SERVER_ROOT_PATH ?? "";
|
|
|
|
// Create the artifact root before anything writes into it. storageState() does
|
|
// not create missing parents, so pointing E2E_UI_ARTIFACT_DIR at a path that
|
|
// does not exist yet would fail with ENOENT on the first role's snapshot,
|
|
// before any test ran. Playwright creates its own outputDir lazily, so this is
|
|
// the only place that has to do it. recursive:true makes it idempotent, and
|
|
// keeps the default "." a no-op.
|
|
fs.mkdirSync(ARTIFACT_DIR, { recursive: true });
|
|
|
|
// The Projects sidebar item is hidden unless the enterprise-gated
|
|
// enable_projects_ui setting is on, and the seeded DB starts with it off.
|
|
// The proxy runs with LITELLM_LICENSE in CI, so enable it the same way
|
|
// the admin UI toggle does; the projects migration smoke needs the link.
|
|
const masterKey = process.env.LITELLM_MASTER_KEY || "sk-1234";
|
|
const api = await request.newContext();
|
|
const settingsRes = await api.patch(`${UI_BASE_URL}${rootPath}/update/ui_settings`, {
|
|
headers: { Authorization: `Bearer ${masterKey}` },
|
|
data: { enable_projects_ui: true },
|
|
});
|
|
if (!settingsRes.ok()) {
|
|
throw new Error(`Enabling enable_projects_ui failed (${settingsRes.status()}): ${await settingsRes.text()}`);
|
|
}
|
|
await api.dispose();
|
|
|
|
for (const role of Object.values(Role)) {
|
|
const { email, password } = users[role];
|
|
const storagePath = STORAGE_PATHS[role];
|
|
const page = await browser.newPage();
|
|
try {
|
|
await page.goto(`${UI_BASE_URL}${rootPath}/ui/login`);
|
|
await page.getByPlaceholder("Enter your username").fill(email);
|
|
await page.getByPlaceholder("Enter your password").fill(password);
|
|
await page.getByRole("button", { name: "Login", exact: true }).click();
|
|
await page.waitForURL((url) => url.pathname.startsWith(`${rootPath}/ui`) && !url.pathname.includes("/login"), {
|
|
timeout: 30_000,
|
|
});
|
|
await expect(page.locator("a", { hasText: "Virtual Keys" })).toBeVisible({ timeout: 30_000 });
|
|
// Dismiss feedback popup if present
|
|
const dismiss = page.getByText("Don't ask me again");
|
|
if (await dismiss.isVisible({ timeout: 1_500 }).catch(() => false)) {
|
|
await dismiss.click();
|
|
}
|
|
// The login flow stores a post-login return URL in the litellm_return_url
|
|
// cookie. If the snapshot captures it before the app consumes it, every
|
|
// test inheriting this storageState gets yanked to that stale URL the
|
|
// first time it mounts a page (the e2e suite's main flake source).
|
|
await page.context().clearCookies({ name: "litellm_return_url" });
|
|
await page.context().storageState({ path: storagePath });
|
|
} catch (e) {
|
|
// Best-effort diagnostics only: this handler must never replace the real
|
|
// failure with its own. Writing the screenshot used to throw ENOENT/EROFS
|
|
// on the read-only cwd in the e2e image, which masked every underlying
|
|
// login error and made the run look like a filesystem bug.
|
|
try {
|
|
const failureDir = path.join(ARTIFACT_DIR, "test-results");
|
|
fs.mkdirSync(failureDir, { recursive: true });
|
|
await page.screenshot({
|
|
path: path.join(failureDir, `global-setup-${role}-failure.png`),
|
|
fullPage: true,
|
|
});
|
|
console.error(`Global setup failed for role ${role}. Screenshot saved. URL: ${page.url()}`);
|
|
} catch (diagnosticError) {
|
|
console.error(
|
|
`Global setup failed for role ${role} at URL: ${page.url()}. ` +
|
|
`Could not save a screenshot: ${diagnosticError}`,
|
|
);
|
|
}
|
|
throw e;
|
|
} finally {
|
|
await page.close();
|
|
}
|
|
}
|
|
|
|
await browser.close();
|
|
}
|
|
|
|
export default globalSetup;
|