fix(ai-mcp): show Failed when the inline Test request itself rejects (#170)
The per-row MCP Test button derived its presentation solely from the test
mutation's data ({ ok, tools } | { ok, error }). When the request itself
rejected (401/403/500/network) there is no payload, so the row silently spun
back to the idle "Test" instead of reporting the failure.
Feed the mutation error into mcpTestButtonView so a reject also renders a red
"Failed", with the tooltip taken from the server message
(error.response.data.message) or a generic i18n fallback. Enable the tooltip
for any non-idle state. Cover the reject branch (with and without a server
message) in the helper unit test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,4 +56,32 @@ describe("mcpTestButtonView", () => {
|
|||||||
tooltip: "402: nope",
|
tooltip: "402: nope",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("failed when the request itself rejects (no result payload)", () => {
|
||||||
|
// 401/403/500/network: there is no { ok } body, only a thrown error. The
|
||||||
|
// row must still show a red "Failed" rather than reverting to idle "Test".
|
||||||
|
expect(
|
||||||
|
mcpTestButtonView(undefined, t, {
|
||||||
|
response: { data: { message: "Unauthorized" } },
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
state: "failed",
|
||||||
|
color: "red",
|
||||||
|
variant: "light",
|
||||||
|
label: "Failed",
|
||||||
|
tooltip: "Unauthorized",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reject without a server message falls back to the generic label", () => {
|
||||||
|
// A bare network error (no response body) still surfaces as failed, using
|
||||||
|
// the i18n fallback for the tooltip.
|
||||||
|
expect(mcpTestButtonView(undefined, t, new Error("network down"))).toEqual({
|
||||||
|
state: "failed",
|
||||||
|
color: "red",
|
||||||
|
variant: "light",
|
||||||
|
label: "Failed",
|
||||||
|
tooltip: "Failed to update data",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,23 @@ import type { IAiMcpServerTestResult } from "@/features/workspace/services/ai-mc
|
|||||||
/** Minimal translator shape (i18next `t`): key + optional interpolation. */
|
/** Minimal translator shape (i18next `t`): key + optional interpolation. */
|
||||||
type Translate = (key: string, options?: Record<string, unknown>) => string;
|
type Translate = (key: string, options?: Record<string, unknown>) => string;
|
||||||
|
|
||||||
|
/** Subset of an axios-style rejection we read for the reject tooltip. */
|
||||||
|
type McpTestRequestError = {
|
||||||
|
response?: { data?: { message?: string } };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-effort extraction of a server-sent message from a rejected test request
|
||||||
|
* (axios stores it at `error.response.data.message`). Returns undefined for a
|
||||||
|
* bare/network error so the caller can fall back to a generic label.
|
||||||
|
*/
|
||||||
|
function readRequestErrorMessage(error: unknown): string | undefined {
|
||||||
|
if (error && typeof error === "object" && "response" in error) {
|
||||||
|
return (error as McpTestRequestError).response?.data?.message;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presentation for the inline "Test" button, derived from the current test
|
* Presentation for the inline "Test" button, derived from the current test
|
||||||
* result tristate (no result yet / ok / failed). Color is never the only signal
|
* result tristate (no result yet / ok / failed). Color is never the only signal
|
||||||
@@ -27,6 +44,7 @@ export interface McpTestButtonView {
|
|||||||
export function mcpTestButtonView(
|
export function mcpTestButtonView(
|
||||||
result: IAiMcpServerTestResult | undefined,
|
result: IAiMcpServerTestResult | undefined,
|
||||||
t: Translate,
|
t: Translate,
|
||||||
|
error?: unknown,
|
||||||
): McpTestButtonView {
|
): McpTestButtonView {
|
||||||
if (result?.ok) {
|
if (result?.ok) {
|
||||||
return {
|
return {
|
||||||
@@ -49,6 +67,19 @@ export function mcpTestButtonView(
|
|||||||
tooltip: result.error,
|
tooltip: result.error,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (error) {
|
||||||
|
// The test request itself rejected (401/403/500/network) — there is no
|
||||||
|
// `{ ok }` payload, so without this branch the row would silently revert to
|
||||||
|
// the idle "Test" instead of reporting the failure. Tooltip prefers the
|
||||||
|
// server-sent message, else the generic i18n fallback.
|
||||||
|
return {
|
||||||
|
state: "failed",
|
||||||
|
color: "red",
|
||||||
|
variant: "light",
|
||||||
|
label: t("Failed"),
|
||||||
|
tooltip: readRequestErrorMessage(error) ?? t("Failed to update data"),
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
state: "idle",
|
state: "idle",
|
||||||
color: undefined,
|
color: undefined,
|
||||||
|
|||||||
@@ -185,8 +185,15 @@ function AiMcpServerRow({
|
|||||||
|
|
||||||
// Single derivation of the button/tooltip presentation from the test tristate
|
// Single derivation of the button/tooltip presentation from the test tristate
|
||||||
// (idle / ok / failed), so the two can never drift apart. Tooltip is "" while
|
// (idle / ok / failed), so the two can never drift apart. Tooltip is "" while
|
||||||
// there is no result; the icon is mapped from `view.state` below.
|
// there is no result; the icon is mapped from `view.state` below. When the
|
||||||
const view = mcpTestButtonView(result, t);
|
// request itself rejects (401/403/500/network) there is no `data` payload, so
|
||||||
|
// we feed the mutation error in too — otherwise the row would silently revert
|
||||||
|
// to "Test" instead of showing a red "Failed".
|
||||||
|
const view = mcpTestButtonView(
|
||||||
|
result,
|
||||||
|
t,
|
||||||
|
testMutation.isError ? testMutation.error : undefined,
|
||||||
|
);
|
||||||
const tooltipLabel = view.tooltip;
|
const tooltipLabel = view.tooltip;
|
||||||
const buttonColor = view.color;
|
const buttonColor = view.color;
|
||||||
const buttonVariant = view.variant;
|
const buttonVariant = view.variant;
|
||||||
@@ -225,7 +232,7 @@ function AiMcpServerRow({
|
|||||||
{/* Always clickable: testing a disabled server before enabling it is useful. */}
|
{/* Always clickable: testing a disabled server before enabling it is useful. */}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={tooltipLabel}
|
label={tooltipLabel}
|
||||||
disabled={!result}
|
disabled={view.state === "idle"}
|
||||||
multiline
|
multiline
|
||||||
maw={320}
|
maw={320}
|
||||||
withinPortal
|
withinPortal
|
||||||
|
|||||||
Reference in New Issue
Block a user