test(e2e): cover Team-BYOK add-model flow as proxy admin (#29068)

* test(e2e): cover Team-BYOK add-model flow as proxy admin

The team-only model + team assignment was an uncovered manual-QA path.
This adds a premium-gated test that toggles Team-BYOK, picks the seeded
E2E Team CRUD, submits, and verifies the model lands in All Models with
the team alias attached.

* test(e2e): apply greptile fixes to Team-BYOK test

- Add the 2s networkidle settle that the sibling addModel tests use —
  networkidle fires before the All Models table finishes re-rendering,
  so the search input was racing with the render.
- Assert on `models-results-count` before inspecting the table body so
  an empty search result fails with a clear "expected results count"
  message instead of timing out on a missing row.

Addresses Greptile P2s on PR #29068.

* test(e2e): harden Team-BYOK test against flake and stale state

- Add before/after cleanup that deletes any Cohere model already scoped
  to e2e-team-crud via /v2/model/info + /model/delete, so Playwright
  retries and local reruns don't accumulate rows.
- Pick the team from the dropdown by role/option name instead of a
  global getByText match — avoids matching a previously-rendered tag
  elsewhere in the form.
- Scope the "created successfully" assertion to .ant-notification so a
  stale toast from an earlier test in the same browser context can't
  vacuously satisfy it.
- Tighten the All Models assertion: require a single row that contains
  BOTH the cohere model name AND the e2e-team-crud alias, so the
  team-less wildcard from the sibling "Add wildcard route" test can't
  satisfy the check.
This commit is contained in:
ryan-crabbe-berri 2026-05-27 16:05:27 -07:00 committed by GitHub
parent b0ea013042
commit 9cac0471ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test";
import { ADMIN_STORAGE_PATH, E2E_TEAM_CRUD_ID } from "../../constants";
import { ADMIN_STORAGE_PATH, E2E_TEAM_CRUD_ALIAS, E2E_TEAM_CRUD_ID } from "../../constants";
import { Role, users } from "../../fixtures/users";
import { navigateToPage } from "../../helpers/navigation";
import { Page } from "../../fixtures/pages";
@ -150,6 +150,111 @@ test.describe("Add Model", () => {
await expect(tableBody.getByText("claude-haiku-4-5").first()).toBeVisible({ timeout: 15_000 });
});
test("Add team-only model via Team-BYOK toggle and verify it appears with the team", async ({ page, request }) => {
// The Team-BYOK switch is gated on `premiumUser` — without a license set
// for the proxy under test, the toggle is disabled and this manual-QA
// step cannot be exercised.
test.skip(
!process.env.LITELLM_LICENSE,
"LITELLM_LICENSE not set in test env — Team-BYOK switch is disabled",
);
// Make the test idempotent across retries and local reruns: delete any
// Cohere model already scoped to the e2e team before we start, and again
// after we finish. The sibling "Add wildcard route" test creates a
// team-less Cohere wildcard, so we only target rows that have BOTH the
// cohere/* model_name AND team_id == e2e-team-crud.
const masterKey = users[Role.ProxyAdmin].password;
const auth = { Authorization: `Bearer ${masterKey}` };
const deleteTeamScopedCohereModels = async () => {
const res = await request.get("/v2/model/info", { headers: auth });
if (!res.ok()) return;
const body = await res.json();
const matches: Array<{ id: string }> = (body?.data ?? []).filter((m: any) =>
typeof m?.model_name === "string" &&
m.model_name.startsWith("cohere") &&
m?.model_info?.team_id === E2E_TEAM_CRUD_ID,
);
for (const m of matches) {
await request.post("/model/delete", { headers: auth, data: { id: m.id } });
}
};
await deleteTeamScopedCohereModels();
try {
await navigateToPage(page, Page.Models);
await page.getByRole("tab", { name: "Add Model" }).click();
await selectProvider(page, "Cohere");
const modelDropdown = page.locator(".ant-select-selection-overflow").first();
await modelDropdown.click();
const wildcardOption = page.getByTitle(/All .* Models \(Wildcard\)/);
await wildcardOption.click();
await page.keyboard.press("Escape");
const apiKeyInput = page.locator('input[type="password"]').first();
await apiKeyInput.fill("sk-any-key-for-team-byok-test");
// Flip the Team-BYOK switch on (Form.Item label "Team-BYOK Model")
const teamByokRow = page.locator(".ant-form-item", { hasText: "Team-BYOK Model" });
await teamByokRow.getByRole("switch").click();
// The Team dropdown appears underneath once the switch is on. TeamDropdown
// renders its Select.Option children with custom <span>/<Text> markup, so
// the popup items don't carry role="option" — match by text content,
// scoped to the visible dropdown so a stale tag elsewhere in the form
// can't satisfy it.
const teamDropdown = page.getByTestId("team-dropdown");
await expect(teamDropdown).toBeVisible({ timeout: 5_000 });
await teamDropdown.click();
const teamOption = page.locator(".ant-select-dropdown:visible")
.getByText(E2E_TEAM_CRUD_ID)
.first();
await expect(teamOption).toBeVisible({ timeout: 5_000 });
await teamOption.click();
await page.getByRole("button", { name: "Add Model" }).last().click();
// Scope the success toast to antd's notification container so a stale
// success message from an earlier test in the same context can't satisfy
// the assertion.
await expect(page.locator(".ant-notification").getByText("created successfully").last())
.toBeVisible({ timeout: 15_000 });
// Verify the model is now in All Models with the team_id attached. The
// Models table renders team-scoped models with the team id in the row.
await page.getByRole("tab", { name: "All Models" }).click();
await page.waitForLoadState("networkidle");
// Match the sibling tests in this file — networkidle fires before the
// table finishes re-rendering, so give it the same 2s settle before
// searching.
await page.waitForTimeout(2000);
await page.locator('input[placeholder="Search model names..."]').fill("cohere");
await page.waitForTimeout(1000);
// Confirm the search returned at least one result — gives a clear
// failure message when the table is empty instead of timing out on a
// row assertion.
await expect(page.getByTestId("models-results-count")).toHaveText(
/Showing \d+ - \d+ of \d+ results/,
{ timeout: 15_000 },
);
// Stronger than "alias appears somewhere in tbody" — pin the assertion
// to a single row that has BOTH the cohere model_name AND the seeded
// team alias, so a stale cohere row from "Add wildcard route" (no team)
// can't satisfy the check.
const teamCohereRow = page.locator("table tbody tr")
.filter({ hasText: "cohere/" })
.filter({ hasText: E2E_TEAM_CRUD_ALIAS });
await expect(teamCohereRow).toHaveCount(1, { timeout: 15_000 });
} finally {
await deleteTeamScopedCohereModels();
}
});
test("Add wildcard route and verify it appears in All Models", async ({ page }) => {
await navigateToPage(page, Page.Models);
await page.getByRole("tab", { name: "Add Model" }).click();