Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,90 @@ describe("MaintainerPanel install health — Orb broker mode (#selfhost-runtime-
expect(screen.queryAllByText(/n\/a \(broker\)/)).toHaveLength(2); // scoped to the brokered install only
});
});

describe("MaintainerPanel MCP tool usage panel (#6241)", () => {
// MaintainerPanel's own isEmpty check requires a non-empty health OR reviewability array to reach
// the real dashboard content branch at all -- an all-empty payload short-circuits to its own
// top-level EmptyState before any qualityDashboard card (including this one) ever renders.
const nonEmptyHealth = [
{
installationId: 1,
accountLogin: "an-owner",
installedReposCount: 1,
status: "healthy" as const,
missingPermissions: [],
missingEvents: [],
checkedAt: "2026-07-03T00:00:00.000Z",
authMode: "local" as const,
},
];

it("shows the not-yet-available empty state when mcpToolUsage is absent from the payload", () => {
useSession.mockReturnValue({
session: { login: "maint", roles: ["maintainer"] },
hydrated: true,
});
useApiResource.mockReturnValue({
status: "ready",
data: {
metrics: [],
health: nonEmptyHealth,
reviewability: [],
settingsPreview: { removed: [], added: [] },
qualityDashboard: { topContributors: [], gateOutcomeBreakdown: emptyGateOutcomeBreakdown },
},
reload: () => {},
error: null,
});

render(<MaintainerPanel />);

// Scoped to this card's own copy, not the generic "Not yet available" title QueueHealthCard's
// own (also-absent) empty state shares.
expect(
screen.getByText(
"Per-tool MCP usage appears here once tool-call telemetry is aggregated into the dashboard payload.",
),
).toBeTruthy();
});

it("renders real per-tool rows once mcpToolUsage is present in the payload", () => {
useSession.mockReturnValue({
session: { login: "maint", roles: ["maintainer"] },
hydrated: true,
});
useApiResource.mockReturnValue({
status: "ready",
data: {
metrics: [],
health: nonEmptyHealth,
reviewability: [],
settingsPreview: { removed: [], added: [] },
qualityDashboard: {
topContributors: [],
gateOutcomeBreakdown: emptyGateOutcomeBreakdown,
mcpToolUsage: {
windowDays: 14,
tools: [
{
tool: "loopover_check_slop_risk",
callCount: 5,
successCount: 5,
failureCount: 0,
localCallCount: 5,
remoteCallCount: 0,
},
],
},
},
},
reload: () => {},
error: null,
});

render(<MaintainerPanel />);

expect(screen.getByText("loopover_check_slop_risk")).toBeTruthy();
expect(screen.getByText("14d window")).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import type { MaintainerTopContributor } from "@/components/site/app-panels/cont
import { GateOutcomeCard } from "@/components/site/app-panels/gate-outcome-card";
import type { GateOutcomeCardData } from "@/components/site/app-panels/gate-outcome-card-model";
import { GateRampControl } from "@/components/site/app-panels/gate-ramp-control";
import {
McpToolUsageCard,
type McpToolUsageSummary,
} from "@/components/site/app-panels/mcp-tool-usage-card";
import {
QueueHealthCard,
type MaintainerQueueHealth,
Expand Down Expand Up @@ -96,6 +100,7 @@ type MaintainerDashboard = {
qualityDashboard: {
topContributors: MaintainerTopContributor[];
gateOutcomeBreakdown: GateOutcomeCardData;
mcpToolUsage?: McpToolUsageSummary;
queueHealth?: MaintainerQueueHealth;
slopDuplicateTrend?: MaintainerSlopDuplicateTrend;
};
Expand Down Expand Up @@ -436,6 +441,8 @@ function MaintainerDashboardView({

<GateOutcomeCard breakdown={data.qualityDashboard.gateOutcomeBreakdown} />

<McpToolUsageCard usage={data.qualityDashboard.mcpToolUsage} />

<QueueHealthCard queueHealth={data.qualityDashboard.queueHealth} />

{data.qualityDashboard.slopDuplicateTrend ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { render, screen, within } from "@testing-library/react";
import { describe, expect, it } from "vitest";

import {
McpToolUsageCard,
type McpToolUsageSummary,
} from "@/components/site/app-panels/mcp-tool-usage-card";

const FORBIDDEN_PUBLIC_TERMS =
/wallet|hotkey|coldkey|mnemonic|reward|payout|farming|raw trust|trust score|scoreability|credibility|private ranking/i;

function usage(overrides: Partial<McpToolUsageSummary> = {}): McpToolUsageSummary {
return {
windowDays: 30,
tools: [
{
tool: "loopover_check_slop_risk",
callCount: 40,
successCount: 38,
failureCount: 2,
localCallCount: 30,
remoteCallCount: 10,
},
{
tool: "loopover_predict_gate",
callCount: 10,
successCount: 10,
failureCount: 0,
localCallCount: 0,
remoteCallCount: 10,
},
],
...overrides,
};
}

describe("McpToolUsageCard (#6241)", () => {
it("shows the 'not yet available' empty state when usage is undefined", () => {
render(<McpToolUsageCard />);
expect(screen.getByText("Not yet available")).toBeTruthy();
expect(
screen.getByText(
"Per-tool MCP usage appears here once tool-call telemetry is aggregated into the dashboard payload.",
),
).toBeTruthy();
});

it("shows a distinct 'no calls yet' empty state when the payload exists but has zero tools", () => {
render(<McpToolUsageCard usage={usage({ tools: [] })} />);
expect(screen.getByText("No MCP tool calls yet")).toBeTruthy();
expect(
screen.getByText(
"No loopover_* tool calls were recorded across local or remote servers in this window.",
),
).toBeTruthy();
});

it("renders one row per tool, sorted by call count descending, with success rate and local/remote split", () => {
render(<McpToolUsageCard usage={usage()} />);
expect(screen.getByText("30d window")).toBeTruthy();

const table = screen.getByRole("table", {
name: "Per-tool MCP call counts, success rate, and local vs. remote call split.",
});
const rows = within(table).getAllByRole("row").slice(1); // drop the header row
expect(rows).toHaveLength(2);
// Sorted descending by callCount: loopover_check_slop_risk (40) before loopover_predict_gate (10).
expect(within(rows[0]!).getByText("loopover_check_slop_risk")).toBeTruthy();
expect(within(rows[0]!).getByText("95%")).toBeTruthy(); // 38/40
expect(within(rows[1]!).getByText("loopover_predict_gate")).toBeTruthy();
expect(within(rows[1]!).getByText("100%")).toBeTruthy(); // 10/10
});

it("shows a dash success rate for a tool with zero calls (never divides by zero)", () => {
render(
<McpToolUsageCard
usage={usage({
tools: [
{
tool: "loopover_lint_pr_text",
callCount: 0,
successCount: 0,
failureCount: 0,
localCallCount: 0,
remoteCallCount: 0,
},
],
})}
/>,
);
expect(screen.getByText("—")).toBeTruthy();
});

it("wraps the table in a keyboard-focusable, labelled scroll region (#794 a11y pattern)", () => {
render(<McpToolUsageCard usage={usage()} />);
const region = screen.getByRole("region", { name: "MCP tool usage by tool" });
expect(region.tabIndex).toBe(0);
const table = within(region).getByRole("table");
expect(within(table).getByRole("columnheader", { name: "Tool" })).toBeTruthy();
expect(within(table).getByRole("columnheader", { name: "Success rate" })).toBeTruthy();
});

it("never surfaces forbidden reward/wallet/score terms", () => {
const { container } = render(<McpToolUsageCard usage={usage()} />);
expect(container.textContent ?? "").not.toMatch(FORBIDDEN_PUBLIC_TERMS);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { AnalyticsCardShell } from "@/components/site/app-panels/analytics-card-shell";
import { StatusPill } from "@/components/site/control-primitives";
import { TableScroll } from "@/components/site/data-table";

/** One MCP tool's aggregate call counts over the dashboard's window. All counts are aggregate-only —
* no call arguments, repo names, or other per-call detail (matches the PostHog telemetry wrappers'
* own privacy boundary in `src/mcp/telemetry.ts` / `packages/loopover-mcp/lib/telemetry.js`). */
export type McpToolUsageEntry = {
tool: string;
callCount: number;
successCount: number;
failureCount: number;
localCallCount: number;
remoteCallCount: number;
};

export type McpToolUsageSummary = {
windowDays: number;
tools: McpToolUsageEntry[];
};

function successRate(entry: McpToolUsageEntry): number | null {
return entry.callCount > 0 ? entry.successCount / entry.callCount : null;
}

function formatRate(rate: number | null): string {
return rate === null ? "—" : `${Math.round(rate * 100)}%`;
}

/** Maintainer dashboard panel (#6241, part of #6228): per-tool MCP call counts, success/failure rates, and a
* local-vs-remote split, over the dashboard's selectable window. Backend aggregation (from the PostHog
* telemetry wrappers #6235/#6236/#6358 already write to) is tracked separately, so — matching
* AcceptanceRateCard's own precedent — this card assumes the field may be absent from the dashboard payload
* today and degrades to a "not yet available" empty state until it lands, rather than assuming a value. */
export function McpToolUsageCard({ usage }: { usage?: McpToolUsageSummary }) {
if (!usage || usage.tools.length === 0) {
return (
<AnalyticsCardShell
title="MCP tool usage"
description="Per-tool call counts, success/failure rates, and local-vs-remote split."
state="empty"
emptyTitle={usage ? "No MCP tool calls yet" : "Not yet available"}
emptyHint={
usage
? "No loopover_* tool calls were recorded across local or remote servers in this window."
: "Per-tool MCP usage appears here once tool-call telemetry is aggregated into the dashboard payload."
}
/>
);
}

const sorted = [...usage.tools].sort((a, b) => b.callCount - a.callCount);

return (
<AnalyticsCardShell
title="MCP tool usage"
description="Per-tool call counts, success/failure rates, and local-vs-remote split."
state="ready"
>
<div className="mb-3 flex justify-end">
<StatusPill status="info">{usage.windowDays}d window</StatusPill>
</div>
<TableScroll className="rounded-token border-hairline" label="MCP tool usage by tool">
<table className="w-full text-left text-token-xs">
<caption className="sr-only">
Per-tool MCP call counts, success rate, and local vs. remote call split.
</caption>
<thead className="border-b-hairline font-mono uppercase tracking-wider text-muted-foreground">
<tr>
<th scope="col" className="px-3 py-2 font-normal">
Tool
</th>
<th scope="col" className="px-3 py-2 font-normal">
Calls
</th>
<th scope="col" className="px-3 py-2 font-normal">
Success rate
</th>
<th scope="col" className="px-3 py-2 font-normal">
Local
</th>
<th scope="col" className="px-3 py-2 font-normal">
Remote
</th>
</tr>
</thead>
<tbody>
{sorted.map((entry) => (
<tr key={entry.tool} className="border-b-hairline last:border-b-0">
<td className="px-3 py-2 font-mono text-foreground/90">{entry.tool}</td>
<td className="px-3 py-2">{entry.callCount}</td>
<td className="px-3 py-2 text-muted-foreground">
{formatRate(successRate(entry))}
</td>
<td className="px-3 py-2 text-muted-foreground">{entry.localCallCount}</td>
<td className="px-3 py-2 text-muted-foreground">{entry.remoteCallCount}</td>
</tr>
))}
</tbody>
</table>
</TableScroll>
</AnalyticsCardShell>
);
}
Loading