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
This commit is contained in:
parent
fd54d096f5
commit
6fd0d8862b
4 changed files with 60 additions and 16 deletions
|
|
@ -1,15 +1,28 @@
|
|||
import * as path from "path";
|
||||
|
||||
export const UI_BASE_URL = (
|
||||
process.env.E2E_UI_BASE_URL ||
|
||||
process.env.LITELLM_PROXY_URL ||
|
||||
"http://localhost:4000"
|
||||
).replace(/\/+$/, "");
|
||||
|
||||
// Directory every artifact this suite writes must land in: storage states,
|
||||
// failure screenshots, playwright output. A local `./run_e2e.sh` run has a
|
||||
// writable cwd, so it keeps the historical behavior of writing beside the
|
||||
// suite. In the packaged e2e image the suite ships on a read-only filesystem
|
||||
// (/app/e2e/ui), so bare relative paths raise EROFS/ENOENT and the run dies in
|
||||
// globalSetup before a single test executes. Point E2E_UI_ARTIFACT_DIR at a
|
||||
// writable path (the image runner already exports TMPDIR) to relocate them.
|
||||
export const ARTIFACT_DIR = process.env.E2E_UI_ARTIFACT_DIR || ".";
|
||||
|
||||
const storagePath = (name: string): string => path.join(ARTIFACT_DIR, name);
|
||||
|
||||
// Storage state paths for each role
|
||||
export const ADMIN_STORAGE_PATH = "admin.storageState.json";
|
||||
export const ADMIN_VIEWER_STORAGE_PATH = "adminViewer.storageState.json";
|
||||
export const INTERNAL_USER_STORAGE_PATH = "internalUser.storageState.json";
|
||||
export const INTERNAL_VIEWER_STORAGE_PATH = "internalViewer.storageState.json";
|
||||
export const TEAM_ADMIN_STORAGE_PATH = "teamAdmin.storageState.json";
|
||||
export const ADMIN_STORAGE_PATH = storagePath("admin.storageState.json");
|
||||
export const ADMIN_VIEWER_STORAGE_PATH = storagePath("adminViewer.storageState.json");
|
||||
export const INTERNAL_USER_STORAGE_PATH = storagePath("internalUser.storageState.json");
|
||||
export const INTERNAL_VIEWER_STORAGE_PATH = storagePath("internalViewer.storageState.json");
|
||||
export const TEAM_ADMIN_STORAGE_PATH = storagePath("teamAdmin.storageState.json");
|
||||
|
||||
// Seeded user identities (match seed.sql)
|
||||
export const E2E_PROXY_ADMIN_USER_ID = "e2e-proxy-admin";
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
import {
|
||||
ADMIN_STORAGE_PATH,
|
||||
ADMIN_VIEWER_STORAGE_PATH,
|
||||
INTERNAL_USER_STORAGE_PATH,
|
||||
INTERNAL_VIEWER_STORAGE_PATH,
|
||||
TEAM_ADMIN_STORAGE_PATH,
|
||||
} from "../constants";
|
||||
|
||||
export enum Role {
|
||||
ProxyAdmin = "proxy_admin",
|
||||
ProxyAdminViewer = "proxy_admin_viewer",
|
||||
|
|
@ -29,10 +37,12 @@ export const users: Record<Role, { email: string; password: string }> = {
|
|||
},
|
||||
};
|
||||
|
||||
// Re-exported from constants so the paths have one definition; they must honor
|
||||
// ARTIFACT_DIR, since the suite runs from a read-only cwd in the e2e image.
|
||||
export const STORAGE_PATHS: Record<Role, string> = {
|
||||
[Role.ProxyAdmin]: "admin.storageState.json",
|
||||
[Role.ProxyAdminViewer]: "adminViewer.storageState.json",
|
||||
[Role.InternalUser]: "internalUser.storageState.json",
|
||||
[Role.InternalUserViewer]: "internalViewer.storageState.json",
|
||||
[Role.TeamAdmin]: "teamAdmin.storageState.json",
|
||||
[Role.ProxyAdmin]: ADMIN_STORAGE_PATH,
|
||||
[Role.ProxyAdminViewer]: ADMIN_VIEWER_STORAGE_PATH,
|
||||
[Role.InternalUser]: INTERNAL_USER_STORAGE_PATH,
|
||||
[Role.InternalUserViewer]: INTERNAL_VIEWER_STORAGE_PATH,
|
||||
[Role.TeamAdmin]: TEAM_ADMIN_STORAGE_PATH,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { chromium, expect, request } from "@playwright/test";
|
||||
import { users, Role, STORAGE_PATHS } from "./fixtures/users";
|
||||
import { UI_BASE_URL } from "./constants";
|
||||
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();
|
||||
|
|
@ -47,9 +48,24 @@ async function globalSetup() {
|
|||
await page.context().clearCookies({ name: "litellm_return_url" });
|
||||
await page.context().storageState({ path: storagePath });
|
||||
} catch (e) {
|
||||
fs.mkdirSync("test-results", { recursive: true });
|
||||
await page.screenshot({ path: `test-results/global-setup-${role}-failure.png`, fullPage: true });
|
||||
console.error(`Global setup failed for role ${role}. Screenshot saved. URL: ${page.url()}`);
|
||||
// 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();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { defineConfig, devices } from "@playwright/test";
|
||||
import { UI_BASE_URL } from "./constants";
|
||||
import * as path from "path";
|
||||
import { ARTIFACT_DIR, UI_BASE_URL } from "./constants";
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
|
|
@ -17,7 +18,11 @@ export default defineConfig({
|
|||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
/* The html reporter and the artifact dir both write relative to cwd, which is
|
||||
read-only in the packaged e2e image; keep them under ARTIFACT_DIR so a plain
|
||||
`npx playwright test` works there without extra flags. */
|
||||
reporter: [["html", { outputFolder: path.join(ARTIFACT_DIR, "playwright-report"), open: "never" }]],
|
||||
outputDir: path.join(ARTIFACT_DIR, "test-results"),
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue