Address PR #197 review: test coverage + dedup + CI log capture

Code-review follow-ups (Approve-with-comments) for batch #197
(context badge #189 / e2e in CI #187 / inline MCP test #170):

- server: extract the duplicated chatContextWindow ::text->positive-int
  coercion (resolve() + getMasked()) into an exported parsePositiveInt
  helper and unit-test its branches (200000/1.9/0/-5/""/abc/undefined),
  closing the untested read-path gap.
- client: merge the two backward scans over messageRows into one pure,
  exported selectContextBadge helper (numerator and denominator still
  taken from the most recent row carrying EACH value) and unit-test the
  different-rows and fresh-zero-doesn't-shadow cases.
- client: extract the MCP "Test" button tristate presentation into a pure
  mcpTestButtonView helper (collapses the two parallel if/else chains) and
  unit-test idle/ok-with-tools/ok-no-tools/failed label+tooltip branches.
- ci: redirect the backgrounded prod server's stdout/stderr to a log file
  in e2e-mcp and cat it on failure, so a start-up crash is diagnosable
  instead of surfacing only as the generic health timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-26 17:24:29 +03:00
parent 9b61024b95
commit ba5cd02439
9 changed files with 357 additions and 85 deletions

View File

@@ -0,0 +1,59 @@
import { describe, expect, it } from "vitest";
import { mcpTestButtonView } from "./ai-mcp-server-test-view";
/**
* Pure-helper tests for the inline "Test" button presentation. Covers the four
* states (idle / loading is handled by the component's `isPending`, so here:
* idle / ok-with-tools / ok-without-tools / failed) and the tooltip text
* branches that are easiest to break silently.
*/
// Identity-ish translator that echoes the key and interpolates {{n}} so the
// label/tooltip branches are observable without the real i18n bundle.
const t = (key: string, options?: Record<string, unknown>): string =>
options && "n" in options
? key.replace("{{n}}", String((options as { n: unknown }).n))
: key;
describe("mcpTestButtonView", () => {
it("idle when there is no result", () => {
expect(mcpTestButtonView(undefined, t)).toEqual({
state: "idle",
color: undefined,
variant: "default",
label: "Test",
tooltip: "",
});
});
it("ok with tools lists them in the tooltip", () => {
expect(mcpTestButtonView({ ok: true, tools: ["a", "b"] }, t)).toEqual({
state: "ok",
color: "green",
variant: "light",
label: "OK · 2",
tooltip: "a, b",
});
});
it('ok with zero tools shows "No tools available"', () => {
expect(mcpTestButtonView({ ok: true, tools: [] }, t)).toEqual({
state: "ok",
color: "green",
variant: "light",
label: "OK · 0",
tooltip: "No tools available",
});
});
it("failed surfaces the error text in the tooltip", () => {
expect(
mcpTestButtonView({ ok: false, error: "402: nope" }, t),
).toEqual({
state: "failed",
color: "red",
variant: "light",
label: "Failed",
tooltip: "402: nope",
});
});
});

View File

@@ -0,0 +1,59 @@
import type { IAiMcpServerTestResult } from "@/features/workspace/services/ai-mcp-server-service.ts";
/** Minimal translator shape (i18next `t`): key + optional interpolation. */
type Translate = (key: string, options?: Record<string, unknown>) => string;
/**
* Presentation for the inline "Test" button, derived from the current test
* result tristate (no result yet / ok / failed). Color is never the only signal
* — the label and icon change too (a11y / colorblind-friendly). Kept as a single
* pure derivation (rather than two parallel if/else chains) so the button and
* tooltip can never drift apart, and so the text branches are unit-testable
* without rendering the row.
*/
export interface McpTestButtonView {
/** Tristate; the component maps this to the leftSection icon. */
state: "idle" | "ok" | "failed";
/** Mantine Button color; undefined = theme default (idle). */
color?: string;
/** Mantine Button variant. */
variant: string;
/** Translated button label. */
label: string;
/** Translated tooltip text; "" while there is no result (tooltip disabled). */
tooltip: string;
}
export function mcpTestButtonView(
result: IAiMcpServerTestResult | undefined,
t: Translate,
): McpTestButtonView {
if (result?.ok) {
return {
state: "ok",
color: "green",
variant: "light",
label: t("OK · {{n}}", { n: result.tools.length }),
tooltip:
result.tools.length > 0
? result.tools.join(", ")
: t("No tools available"),
};
}
if (result && result.ok === false) {
return {
state: "failed",
color: "red",
variant: "light",
label: t("Failed"),
tooltip: result.error,
};
}
return {
state: "idle",
color: undefined,
variant: "default",
label: t("Test"),
tooltip: "",
};
}

View File

@@ -31,6 +31,7 @@ import {
useUpdateAiMcpServerMutation,
} from "@/features/workspace/queries/ai-mcp-server-query.ts";
import { IAiMcpServer } from "@/features/workspace/services/ai-mcp-server-service.ts";
import { mcpTestButtonView } from "@/features/workspace/components/settings/components/ai-mcp-server-test-view.ts";
import AiMcpServerForm from "./ai-mcp-server-form.tsx";
/**
@@ -182,34 +183,22 @@ function AiMcpServerRow({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [server.url, server.transport, server.hasHeaders]);
// Tooltip text describes the cause/details; disabled while there is no result.
let tooltipLabel = "";
if (result?.ok) {
tooltipLabel =
result.tools.length > 0
? result.tools.join(", ")
: t("No tools available");
} else if (result && result.ok === false) {
tooltipLabel = result.error;
}
// Pick the button presentation from the current test state. Color is never the
// only signal — the label changes too (a11y / colorblind-friendly).
let buttonColor: string | undefined;
let buttonVariant = "default";
let buttonIcon = <IconPlugConnected size={16} />;
let buttonLabel = t("Test");
if (result?.ok) {
buttonColor = "green";
buttonVariant = "light";
buttonIcon = <IconCheck size={16} />;
buttonLabel = t("OK · {{n}}", { n: result.tools.length });
} else if (result && result.ok === false) {
buttonColor = "red";
buttonVariant = "light";
buttonIcon = <IconX size={16} />;
buttonLabel = t("Failed");
}
// Single derivation of the button/tooltip presentation from the test tristate
// (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.
const view = mcpTestButtonView(result, t);
const tooltipLabel = view.tooltip;
const buttonColor = view.color;
const buttonVariant = view.variant;
const buttonLabel = view.label;
const buttonIcon =
view.state === "ok" ? (
<IconCheck size={16} />
) : view.state === "failed" ? (
<IconX size={16} />
) : (
<IconPlugConnected size={16} />
);
return (
<Group justify="space-between" wrap="nowrap">