chore(web): move App.tsx's module-level block into files (#2127)
Phase 0 of #2126. Lines 239-668 of clients/web/src/App.tsx were already at module scope — helpers, formatters, constants, and the toast subcomponents, none of which close over component state — so the whole block moves verbatim into files. App.tsx drops from 5,303 to 4,892 lines with no behavior change. Placement follows the lib-vs-utils rule in AGENTS.md: pure computation to utils/, anything touching the environment to lib/. EMPTY_SETTINGS and the two loose toast ids deliberately do NOT land in a new top-level src/constants.ts, which the issue suggested: the web coverage include is a whitelist naming components/hooks/theme/lib/utils/server, so a module there would have fallen out of the >=90 gate silently. They go under utils/ instead. ReAuthBannerBar is in the block but not in the issue's table — it was added after the issue was written (#2108). It moves to components/groups/ReAuthBanner/, beside the banner it wraps. Every new module is at 100% on all four coverage dimensions. Two v8 ignores, each justified in place: getAuthToken's typeof-window guard (a happy-dom inherent path) and replayProtocolRequest's provably-dead default case. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
ca12561f41
commit
b6ed5072f4
6 changed files with 1232 additions and 453 deletions
|
|
@ -0,0 +1,38 @@
|
|||
import { List, Stack, Text } from "@mantine/core";
|
||||
import { ToastCauseList, ToastLinkButton } from "./ToastPrimitives";
|
||||
|
||||
// Body of the "response body dropped" warning toast: a one-line summary of what
|
||||
// happened, the likely causes, and a link that opens this server's settings
|
||||
// (on the Options section) so the user can raise the Network Log Size if it's
|
||||
// just a high-traffic server. Surfaces the otherwise-invisible rotation drop
|
||||
// described in #1390.
|
||||
export const FetchBodyDroppedToastMessage = ({
|
||||
maxFetchRequests,
|
||||
onAdjust,
|
||||
}: {
|
||||
maxFetchRequests: number;
|
||||
onAdjust: () => void;
|
||||
}) => (
|
||||
<Stack gap={4}>
|
||||
<Text size="sm">
|
||||
A response body arrived after its Network log entry had already rotated
|
||||
out (the log hit its {maxFetchRequests}-request limit), so the body
|
||||
couldn't be shown. This usually indicates:
|
||||
</Text>
|
||||
<ToastCauseList>
|
||||
<List.Item>
|
||||
a chatty or misbehaving server (notification storms, rapid polling)
|
||||
</List.Item>
|
||||
<List.Item>an SSE/transport reconnect or retry storm</List.Item>
|
||||
<List.Item>
|
||||
a slow streaming call racing against high request volume
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
the Network Log Size set too low for this server's traffic
|
||||
</List.Item>
|
||||
</ToastCauseList>
|
||||
<ToastLinkButton onClick={onAdjust}>
|
||||
Adjust Network Log Size for this server
|
||||
</ToastLinkButton>
|
||||
</Stack>
|
||||
);
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { Stack, Text } from "@mantine/core";
|
||||
import { ToastLinkButton } from "./ToastPrimitives";
|
||||
|
||||
// Body of the output-schema-mismatch warning toast: a one-line summary plus a
|
||||
// link that opens the full validation details in a modal (the raw error is far
|
||||
// too long for a toast).
|
||||
export const OutputValidationToastMessage = ({
|
||||
onViewDetails,
|
||||
}: {
|
||||
onViewDetails: () => void;
|
||||
}) => (
|
||||
<Stack gap={4}>
|
||||
<Text size="sm">
|
||||
The tool result's structuredContent doesn't match the
|
||||
tool's outputSchema. The inspector renders it anyway, but strict MCP
|
||||
clients may not.
|
||||
</Text>
|
||||
<ToastLinkButton onClick={onViewDetails}>
|
||||
View validation details
|
||||
</ToastLinkButton>
|
||||
</Stack>
|
||||
);
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Anchor, List } from "@mantine/core";
|
||||
|
||||
// Shared "list of likely causes" styling for the warning-toast bodies.
|
||||
export const ToastCauseList = List.withProps({ size: "sm", spacing: 2 });
|
||||
|
||||
// The "open the relevant settings/details" link rendered at the bottom of each
|
||||
// warning-toast body. Same static shape across all three toasts; each passes
|
||||
// its own `onClick`.
|
||||
export const ToastLinkButton = Anchor.withProps({
|
||||
component: "button",
|
||||
type: "button",
|
||||
size: "sm",
|
||||
});
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { FetchBodyDroppedToastMessage } from "./FetchBodyDroppedToastMessage";
|
||||
import { OutputValidationToastMessage } from "./OutputValidationToastMessage";
|
||||
import { UrlElicitationErrorToastMessage } from "./UrlElicitationErrorToastMessage";
|
||||
|
||||
// The bodies rendered inside a Mantine notification. They are shown here on
|
||||
// their own so the copy and the affordance can be reviewed without provoking
|
||||
// the failure that raises each one.
|
||||
const meta: Meta = {
|
||||
title: "Elements/Toasts",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
/** #1390 — a response body arrived after its Network log entry rotated out. */
|
||||
export const FetchBodyDropped: StoryObj<typeof FetchBodyDroppedToastMessage> = {
|
||||
render: (args) => <FetchBodyDroppedToastMessage {...args} />,
|
||||
args: { maxFetchRequests: 250, onAdjust: fn() },
|
||||
};
|
||||
|
||||
/** A tool result whose `structuredContent` doesn't match its `outputSchema`. */
|
||||
export const OutputValidation: StoryObj<typeof OutputValidationToastMessage> = {
|
||||
render: (args) => <OutputValidationToastMessage {...args} />,
|
||||
args: { onViewDetails: fn() },
|
||||
};
|
||||
|
||||
/** A non-spec `-32042` carrying no `elicitations`, so there is no URL to open. */
|
||||
export const UrlElicitationError: StoryObj<
|
||||
typeof UrlElicitationErrorToastMessage
|
||||
> = {
|
||||
render: (args) => <UrlElicitationErrorToastMessage {...args} />,
|
||||
args: { onViewDetails: fn() },
|
||||
};
|
||||
87
clients/web/src/components/elements/Toasts/Toasts.test.tsx
Normal file
87
clients/web/src/components/elements/Toasts/Toasts.test.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithMantine } from "../../../test/renderWithMantine";
|
||||
import { FetchBodyDroppedToastMessage } from "./FetchBodyDroppedToastMessage";
|
||||
import { OutputValidationToastMessage } from "./OutputValidationToastMessage";
|
||||
import { UrlElicitationErrorToastMessage } from "./UrlElicitationErrorToastMessage";
|
||||
import { ToastCauseList, ToastLinkButton } from "./ToastPrimitives";
|
||||
|
||||
describe("ToastPrimitives", () => {
|
||||
it("renders the shared cause list and link button", () => {
|
||||
const onClick = vi.fn();
|
||||
renderWithMantine(
|
||||
<>
|
||||
<ToastCauseList>
|
||||
<li>a cause</li>
|
||||
</ToastCauseList>
|
||||
<ToastLinkButton onClick={onClick}>Do the thing</ToastLinkButton>
|
||||
</>,
|
||||
);
|
||||
expect(screen.getByText("a cause")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Do the thing" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("FetchBodyDroppedToastMessage", () => {
|
||||
it("names the limit that dropped the body and lists the likely causes", () => {
|
||||
renderWithMantine(
|
||||
<FetchBodyDroppedToastMessage
|
||||
maxFetchRequests={250}
|
||||
onAdjust={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText(/250/)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/an SSE\/transport reconnect or retry storm/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onAdjust from the settings link", async () => {
|
||||
const onAdjust = vi.fn();
|
||||
renderWithMantine(
|
||||
<FetchBodyDroppedToastMessage
|
||||
maxFetchRequests={10}
|
||||
onAdjust={onAdjust}
|
||||
/>,
|
||||
);
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", {
|
||||
name: "Adjust Network Log Size for this server",
|
||||
}),
|
||||
);
|
||||
expect(onAdjust).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("OutputValidationToastMessage", () => {
|
||||
it("summarizes the mismatch and opens the details modal", async () => {
|
||||
const onViewDetails = vi.fn();
|
||||
renderWithMantine(
|
||||
<OutputValidationToastMessage onViewDetails={onViewDetails} />,
|
||||
);
|
||||
expect(screen.getByText(/outputSchema/)).toBeInTheDocument();
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: "View validation details" }),
|
||||
);
|
||||
expect(onViewDetails).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("UrlElicitationErrorToastMessage", () => {
|
||||
it("explains the empty elicitations list and opens the details modal", async () => {
|
||||
const onViewDetails = vi.fn();
|
||||
renderWithMantine(
|
||||
<UrlElicitationErrorToastMessage onViewDetails={onViewDetails} />,
|
||||
);
|
||||
expect(
|
||||
screen.getByText(/listed no required\s+elicitations/),
|
||||
).toBeInTheDocument();
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: "View error details" }),
|
||||
);
|
||||
expect(onViewDetails).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { Stack, Text } from "@mantine/core";
|
||||
import { ToastLinkButton } from "./ToastPrimitives";
|
||||
|
||||
// Body of the non-spec URLElicitationRequired toast: the server returned a
|
||||
// -32042 error with no `elicitations` list, so there's no URL to open. We keep
|
||||
// the toast short and link to a modal with the raw error body.
|
||||
export const UrlElicitationErrorToastMessage = ({
|
||||
onViewDetails,
|
||||
}: {
|
||||
onViewDetails: () => void;
|
||||
}) => (
|
||||
<Stack gap={4}>
|
||||
<Text size="sm">
|
||||
The server reported a URLElicitationRequired error but listed no required
|
||||
elicitations, so there's nothing to open.
|
||||
</Text>
|
||||
<ToastLinkButton onClick={onViewDetails}>
|
||||
View error details
|
||||
</ToastLinkButton>
|
||||
</Stack>
|
||||
);
|
||||
Loading…
Reference in a new issue