fix(ui): pass is_proxy_admin for proxy admins on the models page team drill-in (#43003)

* fix(ui): pass is_proxy_admin for proxy admins on the models page team drill-in

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(ui): drop explanatory comments from the models page team drill-in tests

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): exclude view-only admins from is_proxy_admin on the models page team drill-in

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(ui): add browser integration contract for the team guardrail kill switch on the models page

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: yucheng <yucheng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-09-24 11:54:52 -07:00 • committed by GitHub
parent 0ac435bd01
commit 12960f3edf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 340 additions and 6 deletions

View file

@ -1,5 +1,6 @@
[
"tests/e2e/ui/tests/integrationCritical/projectDetachment.spec.ts::project creation and explicit detachment preserve saved scope and restore serving",
"tests/e2e/ui/tests/integrationCritical/teamGlobalGuardrailKillSwitch.spec.ts::proxy admin can enable the global guardrail kill switch from the models page team drill-in",
"tests/e2e/ui/tests/integrationCritical/mcpUserEnvVars.spec.ts::per-user MCP env var stays updatable and clearable from the card after it is set",
"tests/e2e/ui/tests/integrationCritical/mcpUserEnvVars.spec.ts::cancelling the clear confirmation keeps the stored value and sends no delete",
"tests/e2e/ui/tests/integrationCritical/mcpUserEnvVars.spec.ts::pressing Enter on Update opens the credentials modal instead of the server editor",

View file

@ -0,0 +1,77 @@
import {
test,
expect,
APIRequestContext,
Page as PlaywrightPage,
} from "@playwright/test";
import { randomUUID } from "node:crypto";
const master = process.env.LITELLM_MASTER_KEY ?? "sk-integration-master";
const headers = { Authorization: `Bearer ${master}` };
async function createTeam(request: APIRequestContext): Promise<string> {
const created = await request.post("/team/new", {
headers,
data: {
team_alias: `int_kill_switch_${randomUUID().replace(/-/g, "").slice(0, 12)}`,
},
});
expect(created.ok(), await created.text()).toBe(true);
return (await created.json()).team_id as string;
}
async function loginAsAdmin(page: PlaywrightPage): Promise<void> {
await page.goto("/ui/login");
await page.getByPlaceholder("Enter your username").fill("admin");
await page.getByPlaceholder("Enter your password").fill(master);
await page.getByRole("button", { name: "Login", exact: true }).click();
await expect(page).toHaveURL(
(url) => url.pathname.startsWith("/ui") && !url.pathname.includes("login"),
);
}
test("proxy admin can enable the global guardrail kill switch from the models page team drill-in", async ({
page,
request,
}) => {
const teamId = await createTeam(request);
try {
await loginAsAdmin(page);
await page.goto(`/ui/models-and-endpoints?team=${teamId}`);
await page.getByRole("tab", { name: "Settings" }).click();
await page.getByRole("button", { name: /edit settings/i }).click();
await expect(page.getByLabel(/Team Name/)).toBeVisible();
const killSwitch = page.getByRole("switch", {
name: /disable all global guardrails/i,
});
await expect(killSwitch).toBeVisible();
await expect(killSwitch).not.toBeChecked();
await killSwitch.click();
await page.getByRole("button", { name: "Save Changes" }).click();
await expect
.poll(async () => {
const response = await request.get(`/team/info?team_id=${teamId}`, {
headers,
});
expect(response.ok(), await response.text()).toBe(true);
const json = await response.json();
return json.team_info?.metadata?.disable_global_guardrails;
})
.toBe(true);
await page.reload();
await page.getByRole("tab", { name: "Settings" }).click();
await page.getByRole("button", { name: /edit settings/i }).click();
await expect(page.getByLabel(/Team Name/)).toBeVisible();
await expect(
page.getByRole("switch", { name: /disable all global guardrails/i }),
).toBeChecked();
} finally {
const removed = await request.post("/team/delete", {
headers,
data: { team_ids: [teamId] },
});
expect(removed.ok() || removed.status() === 404, await removed.text()).toBe(
true,
);
}
});

View file

@ -0,0 +1,243 @@
/* @vitest-environment jsdom */
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as networking from "@/components/networking";
import { renderWithProviders } from "../../../../tests/test-utils";
import ModelsAndEndpointsPage from "./page";
vi.mock("./panels/AllModelsPanel", () => ({ default: () => <div data-testid="panel-all-models" /> }));
vi.mock("./panels/AddModelPanel", () => ({ default: () => <div data-testid="panel-add" /> }));
vi.mock("./panels/AutoRoutersTabPanel", () => ({ default: () => <div data-testid="panel-auto-routers" /> }));
vi.mock("./panels/LlmCredentialsPanel", () => ({ default: () => <div data-testid="panel-credentials" /> }));
vi.mock("./panels/PassThroughPanel", () => ({ default: () => <div data-testid="panel-pass-through" /> }));
vi.mock("./panels/HealthStatusPanel", () => ({ default: () => <div data-testid="panel-health" /> }));
vi.mock("./panels/ModelRetrySettingsPanel", () => ({ default: () => <div data-testid="panel-retry" /> }));
vi.mock("./panels/ModelGroupAliasPanel", () => ({ default: () => <div data-testid="panel-alias" /> }));
vi.mock("./panels/PriceDataPanel", () => ({ default: () => <div data-testid="panel-price" /> }));
vi.mock("./panels/AccessGroupBudgetsPanel", () => ({ default: () => <div data-testid="panel-budgets" /> }));
vi.mock("@/components/molecules/cost_optimization_feedback_banner", () => ({ default: () => null }));
vi.mock("@/components/model_info_view", () => ({
default: ({ modelId }: { modelId: string }) => <div data-testid="model-info">model:{modelId}</div>,
}));
vi.mock("./useModelDashboardData", () => ({
useModelDashboardData: () => ({ availableModelAccessGroups: [], allModelsOnProxy: [], availableModelGroups: [] }),
}));
const authState = vi.hoisted(() => ({ userRole: "Admin", isViewOnly: false }));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: () => ({
token: "123",
accessToken: "123",
userId: "user-1",
userEmail: "admin@example.com",
userRole: authState.userRole,
premiumUser: true,
isViewOnly: authState.isViewOnly,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
}),
}));
vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) }));
vi.mock("@/components/networking", () => ({
serverRootPath: "",
teamInfoCall: vi.fn(),
teamMemberDeleteCall: vi.fn(),
teamMemberAddCall: vi.fn(),
teamMemberUpdateCall: vi.fn(),
teamUpdateCall: vi.fn(),
getGuardrailsList: vi.fn(),
getPoliciesList: vi.fn(),
getPolicyInfoWithGuardrails: vi.fn(),
fetchMCPAccessGroups: vi.fn(),
getTeamPermissionsCall: vi.fn(),
organizationInfoCall: vi.fn(),
getRouterSettingsCall: vi.fn().mockResolvedValue({ fields: [] }),
getPassThroughEndpointsCall: vi.fn().mockResolvedValue({ endpoints: [] }),
fetchMCPServers: vi.fn().mockResolvedValue([]),
fetchMCPToolsets: vi.fn().mockResolvedValue([]),
listMCPTools: vi.fn().mockResolvedValue({ tools: [] }),
vectorStoreListCall: vi.fn().mockResolvedValue({ data: [] }),
getAgentsList: vi.fn().mockResolvedValue({ agents: [] }),
getClaudeCodePluginsList: vi.fn().mockResolvedValue({ plugins: [], count: 0 }),
}));
const can = vi.fn();
vi.mock("@/app/(dashboard)/hooks/useCan", () => ({
default: (...args: unknown[]) => can(...args),
}));
vi.mock("@/components/utils/dataUtils", () => ({
copyToClipboard: vi.fn().mockResolvedValue(true),
formatNumberWithCommas: vi.fn((value: number) => value.toLocaleString()),
}));
vi.mock("@/app/(dashboard)/hooks/teams/useTeamMetadataSchema", () => ({
useTeamMetadataSchema: vi.fn(() => ({ data: [], isLoading: false })),
}));
vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({
useUISettings: vi.fn(() => ({ data: { values: {} }, isLoading: false })),
}));
vi.mock("@/app/(dashboard)/hooks/models/useModels", () => ({
useAllProxyModels: vi.fn(() => ({ data: { data: [] }, isLoading: false })),
}));
vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({
useTeams: vi.fn(() => ({ data: [], isLoading: false })),
useTeam: vi.fn(() => ({ data: undefined, isLoading: false })),
}));
vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({
organizationKeys: { all: ["organizations"] },
useOrganization: vi.fn(() => ({ data: undefined, isLoading: false })),
useOrganizations: vi.fn().mockReturnValue({ data: [], isLoading: false }),
}));
vi.mock("@/app/(dashboard)/hooks/users/useCurrentUser", () => ({
useCurrentUser: vi.fn(() => ({ data: { models: [] }, isLoading: false })),
}));
vi.mock("@/app/(dashboard)/hooks/mcpServers/useMCPServers", () => ({
useMCPServers: vi.fn(() => ({ data: [], isLoading: false, isError: false })),
}));
vi.mock("@/app/(dashboard)/hooks/mcpServers/useMCPToolsets", () => ({
useMCPToolsets: vi.fn(() => ({ data: [], isLoading: false, isError: false })),
}));
vi.mock("@/components/mcp_server_management/MCPServerSelector", () => ({
default: () => <div>mcp server selector</div>,
}));
vi.mock("@/components/team/TeamMemberTab", () => ({
default: vi.fn(() => <div>member tab</div>),
}));
vi.mock("@/components/common_components/user_search_modal", () => ({
default: vi.fn(() => null),
}));
vi.mock("@/components/team/EditMembership", () => ({
default: vi.fn(() => null),
}));
vi.mock("@/components/common_components/DeleteResourceModal", () => ({
default: vi.fn(() => null),
}));
vi.mock("@/components/team/member_permissions", () => ({
default: vi.fn(() => <div>Member Permissions</div>),
}));
vi.mock("@/components/common_components/ModelAliasManager", () => ({
default: vi.fn(() => <div>alias manager</div>),
}));
vi.mock("@/app/(dashboard)/hooks/accessGroups/useAccessGroups", () => ({
useAccessGroups: vi.fn().mockReturnValue({ data: [], isLoading: false, isError: false }),
}));
vi.mock("@/components/common_components/AccessGroupSelector", () => ({
default: () => <div>access group selector</div>,
}));
vi.mock("@/app/(dashboard)/hooks/keys/useKeys", () => {
const keysResult = {
data: { keys: [], total_count: 0, current_page: 1, total_pages: 1 },
isPending: false,
isFetching: false,
refetch: vi.fn(),
};
return { useKeys: vi.fn(() => keysResult) };
});
vi.mock("@/components/key_team_helpers/filter_helpers", () => ({
fetchTeamFilterOptions: vi.fn().mockResolvedValue({ keyAliases: [], organizationIds: [], userIds: [] }),
fetchAllKeyAliases: vi.fn().mockResolvedValue([]),
fetchAllOrganizations: vi.fn().mockResolvedValue([]),
}));
const createMockTeamData = (overrides = {}) => ({
team_id: "team-a1b2",
team_info: {
team_alias: "Test Team",
team_id: "team-a1b2",
organization_id: null,
admins: ["admin@test.com"],
members: ["user1@test.com"],
members_with_roles: [
{ user_id: "user1@test.com", user_email: "user1@test.com", role: "member", spend: 0, budget_id: "budget1" },
],
metadata: { disable_global_guardrails: true },
tpm_limit: null,
rpm_limit: null,
max_budget: null,
budget_duration: null,
models: [],
blocked: false,
spend: 0,
max_parallel_requests: null,
budget_reset_at: null,
model_id: null,
litellm_model_table: null,
created_at: "2024-01-01T00:00:00Z",
team_member_budget_table: null,
guardrails: [],
policies: [],
object_permission: null,
...overrides,
},
keys: [],
team_memberships: [],
});
describe("ModelsAndEndpointsPage ?team drill-in", () => {
beforeEach(() => {
authState.userRole = "Admin";
authState.isViewOnly = false;
can.mockReturnValue(true);
vi.mocked(networking.getGuardrailsList).mockResolvedValue({ guardrails: [] });
vi.mocked(networking.getPoliciesList).mockResolvedValue({ policies: [] });
vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]);
vi.mocked(networking.getTeamPermissionsCall).mockResolvedValue({
all_available_permissions: [],
team_member_permissions: [],
} as never);
vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData() as never);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- jsdom has no ResizeObserver global to type against
(global as any).ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
});
afterEach(() => {
vi.clearAllMocks();
});
it("shows the Disable all global guardrails switch to a proxy admin session", async () => {
const user = userEvent.setup({ delay: null });
renderWithProviders(<ModelsAndEndpointsPage />, { searchParams: { team: "team-a1b2" } });
await user.click(await screen.findByRole("tab", { name: "Settings" }));
await user.click(await screen.findByRole("button", { name: /edit settings/i }));
await screen.findByLabelText(/Team Name/);
expect(screen.getByRole("switch", { name: /Disable all global guardrails/i })).toBeChecked();
});
it("keeps the switch hidden from an internal user session on the same team", async () => {
authState.userRole = "Internal User";
renderWithProviders(<ModelsAndEndpointsPage />, { searchParams: { team: "team-a1b2" } });
await screen.findByText("Test Team");
expect(screen.queryByRole("button", { name: /edit settings/i })).not.toBeInTheDocument();
expect(screen.queryByText("Disable all global guardrails")).not.toBeInTheDocument();
});
});

View file

@ -25,12 +25,16 @@ vi.mock("@/components/molecules/cost_optimization_feedback_banner", () => ({ def
vi.mock("@/components/model_info_view", () => ({
default: ({ modelId }: { modelId: string }) => <div data-testid="model-info">model:{modelId}</div>,
}));
const teamInfoProps = vi.hoisted(() => vi.fn());
vi.mock("@/components/team/TeamInfo", () => ({
default: ({ teamId, is_team_admin }: { teamId: string; is_team_admin: boolean }) => (
<div data-testid="team-info" data-team-admin={String(is_team_admin)}>
team:{teamId}
</div>
),
default: (props: { teamId: string; is_team_admin: boolean; is_proxy_admin: boolean }) => {
teamInfoProps(props);
return (
<div data-testid="team-info" data-team-admin={String(props.is_team_admin)}>
team:{props.teamId}
</div>
);
},
}));
const mockUseAuthorized = vi.fn();
@ -107,12 +111,21 @@ describe("ModelsAndEndpointsPage", () => {
expect(screen.getByTestId("team-info")).toHaveAttribute("data-team-admin", "true");
});
it("passes is_proxy_admin for an admin session on the ?team drill-in", () => {
detailState.teamId = "team-a1b2";
renderPage();
expect(teamInfoProps).toHaveBeenLastCalledWith(
expect.objectContaining({ is_proxy_admin: true, is_team_admin: true }),
);
});
it("opens the ?team drill-in without edit rights for a view-only admin", () => {
mockUseAuthorized.mockReturnValue(VIEW_ONLY_ADMIN);
detailState.teamId = "team-9";
renderPage();
expect(screen.getByTestId("team-info")).toHaveTextContent("team:team-9");
expect(screen.getByTestId("team-info")).toHaveAttribute("data-team-admin", "false");
expect(teamInfoProps).toHaveBeenLastCalledWith(expect.objectContaining({ is_proxy_admin: false }));
});
it("hides admin-only tabs for a non-admin user", () => {

View file

@ -151,7 +151,7 @@ export default function ModelsAndEndpointsPage() {
onClose={close}
accessToken={accessToken}
is_team_admin={userRole === "Admin" && !isViewOnly}
is_proxy_admin={userRole === "Proxy Admin"}
is_proxy_admin={userRole === "Admin" && !isViewOnly}
userModels={allModelsOnProxy}
editTeam={false}
onUpdate={invalidateModels}