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:
parent
b0ea013042
commit
9cac0471ae
@ -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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user