From 4a893758eedb6ddbf49334608431e18d960d6ae9 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Wed, 27 May 2026 14:05:54 -0700 Subject: [PATCH 1/3] test(e2e): cover AI Hub make-public flow and public model_hub_table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three previously-uncovered manual-QA paths land in one spec: - Admin opens "Select Models to Make Public", advances through the multi-step modal, and verifies the success toast. - AI Hub tab strip exposes Model Hub / Agent Hub / MCP Hub / Skill Hub — note the manual-QA "Claude Code Plugin Marketplace" label was renamed to Skill Hub; the test pins the current name. - Anonymous /ui/model_hub_table loads with the master key as `?key=` and renders the Model Hub tab. Agent Hub / MCP Hub tabs are conditional on public data and are not asserted here. --- .../e2e_tests/tests/modelHub/modelHub.spec.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 ui/litellm-dashboard/e2e_tests/tests/modelHub/modelHub.spec.ts diff --git a/ui/litellm-dashboard/e2e_tests/tests/modelHub/modelHub.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/modelHub/modelHub.spec.ts new file mode 100644 index 000000000000..b372600488bc --- /dev/null +++ b/ui/litellm-dashboard/e2e_tests/tests/modelHub/modelHub.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from "@playwright/test"; +import { ADMIN_STORAGE_PATH } from "../../constants"; +import { navigateToPage, dismissFeedbackPopup } from "../../helpers/navigation"; +import { Page } from "../../fixtures/pages"; + +test.describe("AI Hub (internal admin view)", () => { + test.use({ storageState: ADMIN_STORAGE_PATH }); + + test("Make models public via the multi-step modal", async ({ page }) => { + await navigateToPage(page, Page.ModelHubTable); + + // Open the "Select Models to Make Public" modal + await page.getByRole("button", { name: /Select Models to Make Public/i }).click(); + + const modal = page.locator(".ant-modal:visible").filter({ hasText: "Make Models Public" }); + await expect(modal).toBeVisible({ timeout: 5_000 }); + + // Step 1: pick the seeded models via "Select All" + await modal.getByText(/Select All/i).click(); + + // Move to confirm step + await modal.getByRole("button", { name: "Next" }).click(); + await expect(modal.getByText("Confirm Making Models Public")).toBeVisible({ timeout: 5_000 }); + + // Submit + await modal.getByRole("button", { name: "Make Public" }).click(); + + await expect(page.getByText(/Successfully made .* model group\(s\) public/i).first()) + .toBeVisible({ timeout: 15_000 }); + }); + + test("AI Hub tab list renders Model Hub, Agent Hub, MCP Hub and Skill Hub", async ({ page }) => { + await navigateToPage(page, Page.ModelHubTable); + + // The tab strip lives in the main view; check each tab is present and clickable. + // (The "Claude Code Plugin Marketplace" tab from the manual-QA checklist was + // renamed to "Skill Hub" — verify the current label here so the test stays + // in sync with the UI.) + for (const tabName of ["Model Hub", "Agent Hub", "MCP Hub", "Skill Hub"]) { + const tab = page.getByRole("tab", { name: tabName }); + await expect(tab, `${tabName} tab should be present`).toBeVisible({ timeout: 5_000 }); + await tab.click(); + } + }); +}); + +test.describe("Public model hub (/ui/model_hub_table)", () => { + // No storageState — the public page is reached anonymously with a `key` query param. + + test("Public model_hub_table loads and renders the Model Hub tab", async ({ page }) => { + // The page expects the proxy key as the `key` query param. Use the master + // key the e2e runner already exports — this matches what the AI Hub copy + // button hands out. + const masterKey = process.env.LITELLM_MASTER_KEY || "sk-1234"; + await page.goto(`/ui/model_hub_table?key=${masterKey}`); + + // Page loads (no auth redirect) and the Model Hub tab is always present. + // Agent Hub and MCP Hub tabs are conditionally rendered only when public + // agents/MCP servers exist, so we don't assert on them in a fresh CI run. + await expect(page.getByRole("tab", { name: "Model Hub" })).toBeVisible({ timeout: 10_000 }); + await dismissFeedbackPopup(page); + }); +}); From 07d47af1c72217fc730f34928237bc9436439f89 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Wed, 27 May 2026 16:44:50 -0700 Subject: [PATCH 2/3] test(e2e): harden AI Hub make-public + public hub assertions Address Greptile review: - Make-public test now asserts "Select All (N)" with N>=1 before clicking, so a missing-seed-data run surfaces immediately instead of timing out on the disabled Next button or the success toast. - Public model_hub_table test dismisses the feedback popup before the tab visibility assertion, matching the ordering used by navigateToPage so a popup race can't mask the tab mid-evaluation. --- .../e2e_tests/tests/modelHub/modelHub.spec.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/e2e_tests/tests/modelHub/modelHub.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/modelHub/modelHub.spec.ts index b372600488bc..07a851fd1964 100644 --- a/ui/litellm-dashboard/e2e_tests/tests/modelHub/modelHub.spec.ts +++ b/ui/litellm-dashboard/e2e_tests/tests/modelHub/modelHub.spec.ts @@ -15,6 +15,12 @@ test.describe("AI Hub (internal admin view)", () => { const modal = page.locator(".ant-modal:visible").filter({ hasText: "Make Models Public" }); await expect(modal).toBeVisible({ timeout: 5_000 }); + // Guard: the "Select All (N)" label only shows a count when filteredData + // has at least one row. Asserting N>=1 here turns a missing-seed-data + // failure into an immediate diagnostic rather than a downstream timeout + // on the disabled-Next button or the success toast. + await expect(modal.getByText(/Select All \(\d+\)/)).toBeVisible({ timeout: 5_000 }); + // Step 1: pick the seeded models via "Select All" await modal.getByText(/Select All/i).click(); @@ -54,10 +60,13 @@ test.describe("Public model hub (/ui/model_hub_table)", () => { const masterKey = process.env.LITELLM_MASTER_KEY || "sk-1234"; await page.goto(`/ui/model_hub_table?key=${masterKey}`); + // Dismiss the feedback popup before asserting on the tab, so a popup + // race can't briefly mask the tab while we're evaluating visibility. + await dismissFeedbackPopup(page); + // Page loads (no auth redirect) and the Model Hub tab is always present. // Agent Hub and MCP Hub tabs are conditionally rendered only when public // agents/MCP servers exist, so we don't assert on them in a fresh CI run. await expect(page.getByRole("tab", { name: "Model Hub" })).toBeVisible({ timeout: 10_000 }); - await dismissFeedbackPopup(page); }); }); From cde548fef97a397de2f1cf22de69cc605d6bbacb Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Wed, 27 May 2026 18:13:08 -0700 Subject: [PATCH 3/3] docs(e2e): explain admin vs public AI Hub tab asymmetry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greptile flagged the all-4-tabs assertion as a potential CI flake, inferring from the public-page comment that Agent Hub / MCP Hub might be data-conditional in the admin view too. They aren't — ModelHubTable renders all four tabs unconditionally for admins. Document the asymmetry inline so future readers (and future review passes) don't re-derive it. --- .../e2e_tests/tests/modelHub/modelHub.spec.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ui/litellm-dashboard/e2e_tests/tests/modelHub/modelHub.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/modelHub/modelHub.spec.ts index 07a851fd1964..ada4dfb735e8 100644 --- a/ui/litellm-dashboard/e2e_tests/tests/modelHub/modelHub.spec.ts +++ b/ui/litellm-dashboard/e2e_tests/tests/modelHub/modelHub.spec.ts @@ -42,6 +42,13 @@ test.describe("AI Hub (internal admin view)", () => { // (The "Claude Code Plugin Marketplace" tab from the manual-QA checklist was // renamed to "Skill Hub" — verify the current label here so the test stays // in sync with the UI.) + // + // Note: unlike the public /ui/model_hub_table view (test below), the admin + // ModelHubTable renders all four tabs unconditionally — there are no `&&` + // guards around Agent Hub or MCP Hub in the source + // (ModelHubTable.tsx ~L436-439). Asserting all four here is intentional: + // this pins the manual-QA contract that the AI Hub tab strip exposes + // exactly these labels regardless of seeded agent/MCP data. for (const tabName of ["Model Hub", "Agent Hub", "MCP Hub", "Skill Hub"]) { const tab = page.getByRole("tab", { name: tabName }); await expect(tab, `${tabName} tab should be present`).toBeVisible({ timeout: 5_000 });