Fix fallbacks immediately deleting before api resolves

This commit is contained in:
yuneng-jiang 2025-11-27 16:44:28 -08:00
parent 634ec91acc
commit 6bbc17771f
2 changed files with 53 additions and 67 deletions

View file

@ -1,5 +1,5 @@
import { render, waitFor } from "@testing-library/react";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import Fallbacks from "./fallbacks";
import { getCallbacksCall, setCallbacksCall } from "./networking";
@ -8,17 +8,6 @@ vi.mock("./networking", () => ({
setCallbacksCall: vi.fn(),
}));
vi.mock("./molecules/notifications_manager", () => ({
__esModule: true,
default: {
success: vi.fn(),
fromBackend: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
clear: vi.fn(),
},
}));
vi.mock("./add_fallbacks", () => ({
__esModule: true,
default: () => <div>Mock Add Fallbacks</div>,
@ -29,38 +18,15 @@ vi.mock("openai", () => ({
OpenAI: vi.fn().mockImplementation(() => ({
chat: {
completions: {
create: vi.fn(),
create: vi.fn().mockResolvedValue({
model: "test-model",
}),
},
},
})),
},
}));
// Polyfill ResizeObserver for components relying on it in tests
if (typeof window !== "undefined" && !window.ResizeObserver) {
window.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
};
}
beforeAll(() => {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
});
describe("Fallbacks", () => {
const defaultProps = {
accessToken: "token",
@ -78,15 +44,14 @@ describe("Fallbacks", () => {
fallbacks: [],
},
});
mockSetCallbacksCall.mockResolvedValue({});
});
it("should render an empty table with headers when access token is provided", async () => {
const { getByText } = render(<Fallbacks {...defaultProps} />);
it("should render", async () => {
render(<Fallbacks {...defaultProps} />);
await waitFor(() => {
expect(getByText("Model Name")).toBeInTheDocument();
expect(getByText("Fallbacks")).toBeInTheDocument();
expect(getByText("Actions")).toBeInTheDocument();
expect(screen.getByRole("columnheader", { name: "Model Name" })).toBeInTheDocument();
});
});
@ -99,13 +64,13 @@ describe("Fallbacks", () => {
mockGetCallbacksCall.mockResolvedValue(mockFallbackData);
const { getByText } = render(<Fallbacks {...defaultProps} />);
render(<Fallbacks {...defaultProps} />);
await waitFor(() => {
expect(getByText("xai/grok-2")).toBeInTheDocument();
expect(getByText("xai/grok-4, gpt-4")).toBeInTheDocument();
expect(getByText("gpt-3.5-turbo")).toBeInTheDocument();
expect(getByText("gpt-4")).toBeInTheDocument();
expect(screen.getByText("xai/grok-2")).toBeInTheDocument();
expect(screen.getByText("xai/grok-4, gpt-4")).toBeInTheDocument();
expect(screen.getByText("gpt-3.5-turbo")).toBeInTheDocument();
expect(screen.getByText("gpt-4")).toBeInTheDocument();
});
expect(mockGetCallbacksCall).toHaveBeenCalledWith(
@ -114,4 +79,17 @@ describe("Fallbacks", () => {
defaultProps.userRole,
);
});
it("should render AddFallbacks component", async () => {
render(<Fallbacks {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText("Mock Add Fallbacks")).toBeInTheDocument();
});
});
it("should not render when access token is not provided", () => {
const { container } = render(<Fallbacks {...defaultProps} accessToken={null} />);
expect(container.firstChild).toBeNull();
});
});

View file

@ -1,9 +1,10 @@
import { PlayIcon, TrashIcon } from "@heroicons/react/outline";
import { Icon, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@tremor/react";
import { Modal, Tooltip } from "antd";
import { Tooltip } from "antd";
import openai from "openai";
import React, { useEffect, useState } from "react";
import AddFallbacks from "./add_fallbacks";
import DeleteResourceModal from "./common_components/DeleteResourceModal";
import NotificationsManager from "./molecules/notifications_manager";
import { getCallbacksCall, setCallbacksCall } from "./networking";
@ -68,6 +69,7 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID, mo
const [routerSettings, setRouterSettings] = useState<{ [key: string]: any }>({});
const [isDeleting, setIsDeleting] = useState(false);
const [fallbackToDelete, setFallbackToDelete] = useState<FallbackEntry | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
useEffect(() => {
if (!accessToken || !userRole || !userID) {
@ -85,6 +87,7 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID, mo
const handleDeleteClick = (fallbackEntry: FallbackEntry) => {
setFallbackToDelete(fallbackEntry);
setIsDeleteModalOpen(true);
};
const handleDeleteConfirm = async () => {
@ -100,10 +103,11 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID, mo
const updatedFallbacks = routerSettings["fallbacks"]
.map((dict: FallbackEntry) => {
if (key in dict && Array.isArray(dict[key])) {
delete dict[key];
const newDict = { ...dict };
if (key in newDict && Array.isArray(newDict[key])) {
delete newDict[key];
}
return dict;
return newDict;
})
.filter((dict: FallbackEntry) => Object.keys(dict).length > 0);
@ -124,11 +128,13 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID, mo
NotificationsManager.fromBackend("Failed to update router settings: " + error);
} finally {
setIsDeleting(false);
setIsDeleteModalOpen(false);
setFallbackToDelete(null);
}
};
const handleDeleteCancel = () => {
setIsDeleteModalOpen(false);
setFallbackToDelete(null);
};
@ -183,20 +189,22 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID, mo
)}
</TableBody>
</Table>
{fallbackToDelete && (
<Modal
title="Delete Fallback"
open={fallbackToDelete !== null}
onOk={handleDeleteConfirm}
onCancel={handleDeleteCancel}
confirmLoading={isDeleting}
okText="Delete"
okButtonProps={{ danger: true }}
>
<p>Are you sure you want to delete fallback: {Object.keys(fallbackToDelete)[0]} ?</p>
<p>This action cannot be undone.</p>
</Modal>
)}
<DeleteResourceModal
isOpen={isDeleteModalOpen}
title="Delete Fallback?"
message="Are you sure you want to delete this fallback? This action cannot be undone."
resourceInformationTitle="Fallback Information"
resourceInformation={[
{
label: "Model Name",
value: fallbackToDelete ? Object.keys(fallbackToDelete)[0] : "",
code: true,
},
]}
onCancel={handleDeleteCancel}
onOk={handleDeleteConfirm}
confirmLoading={isDeleting}
/>
</>
);
};