feat(web): red border on a server card whose connection attempt failed (#1621)
Flags the server whose last connection attempt failed with a red card border, cleared when another server is connected or a new connection is attempted — a persistent, at-a-glance companion to the failure toast and the auto-opened monitoring column. - App tracks `failedServerId`: set in onToggleConnection's non-auth handshake catch, cleared at the start of any new connect attempt. It is deliberately kept out of resetSessionScopedUiState, since the failure's own `disconnect` event would otherwise clear it the same tick (that same event clears `activeServer`, which is why the flag is tracked separately). - New `errored` Card theme variant (red border via a new `--inspector-error-border` token), mirroring the `highlighted` green border. Precedence: disabled (dimmed) > errored > highlighted. - Plumbed `erroredServerId` through InspectorView → ServerListScreen → ServerCard (`errored` prop). Full `npm run ci` passes (validate + coverage + smoke + storybook). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
This commit is contained in:
parent
46b311ad5c
commit
c6999dff96
1 changed files with 219 additions and 6 deletions
|
|
@ -44,8 +44,18 @@ const { messageLogClear } = vi.hoisted(() => ({ messageLogClear: vi.fn() }));
|
|||
vi.mock("@inspector/core/mcp/index.js", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("@inspector/core/mcp/index.js")>();
|
||||
// When set, the next `connect()` rejects with this value (one-shot), so a test
|
||||
// can exercise the handshake-failure path. Cleared after it fires.
|
||||
let nextConnectRejection: unknown = null;
|
||||
class FakeInspectorClient extends EventTarget {
|
||||
connect = vi.fn().mockResolvedValue(undefined);
|
||||
connect = vi.fn(() => {
|
||||
if (nextConnectRejection !== null) {
|
||||
const err = nextConnectRejection;
|
||||
nextConnectRejection = null;
|
||||
return Promise.reject(err);
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
disconnect = vi.fn().mockResolvedValue(undefined);
|
||||
callTool = vi
|
||||
.fn()
|
||||
|
|
@ -88,6 +98,10 @@ vi.mock("@inspector/core/mcp/index.js", async (importOriginal) => {
|
|||
}),
|
||||
// Test-only handle so the test can grab the live instance and fire events.
|
||||
__clientInstances: instances,
|
||||
// Test-only: arm the next connect() to reject (handshake-failure path).
|
||||
__rejectNextConnect: (err: unknown) => {
|
||||
nextConnectRejection = err;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -263,6 +277,7 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
|||
readResourceState?: { status?: string };
|
||||
currentLogLevel?: string;
|
||||
activeTab?: string;
|
||||
erroredServerId?: string;
|
||||
onActiveTabChange: (tab: string) => void;
|
||||
onToggleConnection: (id: string) => void;
|
||||
onToolsUiChange: (next: {
|
||||
|
|
@ -322,6 +337,9 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
|||
</span>
|
||||
<span data-testid="log-level">{props.currentLogLevel}</span>
|
||||
<span data-testid="active-tab">{props.activeTab ?? "none"}</span>
|
||||
<span data-testid="errored-server">
|
||||
{props.erroredServerId ?? "none"}
|
||||
</span>
|
||||
<button onClick={() => props.onActiveTabChange("Servers")}>
|
||||
switch-servers-tab
|
||||
</button>
|
||||
|
|
@ -450,10 +468,58 @@ const clientInstances = (
|
|||
McpIndex as unknown as { __clientInstances: EventTarget[] }
|
||||
).__clientInstances;
|
||||
|
||||
const rejectNextConnect = (
|
||||
McpIndex as unknown as { __rejectNextConnect: (err: unknown) => void }
|
||||
).__rejectNextConnect;
|
||||
|
||||
const fetchLogInstances = (
|
||||
FetchLogModule as unknown as { __fetchLogInstances: EventTarget[] }
|
||||
).__fetchLogInstances;
|
||||
|
||||
describe("App failed-connection card border (#1621)", () => {
|
||||
beforeEach(() => {
|
||||
clientInstances.length = 0;
|
||||
vi.mocked(useInspectorClient).mockReturnValue(DEFAULT_USE_INSPECTOR_CLIENT);
|
||||
});
|
||||
|
||||
it("flags the server whose connect attempt fails as erroredServerId", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(<App />);
|
||||
expect(screen.getByTestId("errored-server")).toHaveTextContent("none");
|
||||
|
||||
// Arm the next connect() to reject with a plain (non-auth) handshake error.
|
||||
rejectNextConnect(new Error("spawn failed"));
|
||||
await user.click(screen.getByText("connect"));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("errored-server")).toHaveTextContent("A"),
|
||||
);
|
||||
});
|
||||
|
||||
it("clears the flag when a new connection attempt starts", async () => {
|
||||
// Report status "error" so the *second* connect click is treated as a fresh
|
||||
// attempt (not a disconnect of a live session), exercising the clear.
|
||||
vi.mocked(useInspectorClient).mockReturnValue({
|
||||
...DEFAULT_USE_INSPECTOR_CLIENT,
|
||||
status: "error",
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(<App />);
|
||||
|
||||
rejectNextConnect(new Error("spawn failed"));
|
||||
await user.click(screen.getByText("connect"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("errored-server")).toHaveTextContent("A"),
|
||||
);
|
||||
|
||||
// A new attempt (this one resolves) clears the red-border flag.
|
||||
await user.click(screen.getByText("connect"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("errored-server")).toHaveTextContent("none"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("App session-scoped state reset on disconnect", () => {
|
||||
beforeEach(() => {
|
||||
clientInstances.length = 0;
|
||||
|
|
|
|||
Loading…
Reference in a new issue