fix: address Copilot review round 15 on #2251

Round 15 generated no new inline comments; all five suppressed findings
were real and unaddressed.

- SkillsScreen: claim the attempt BEFORE issuing the request, for both the
  SKILL.md preview and `skills/get`. Recording it only on settle left a
  window where an older request that happened to resolve first was still
  considered current and published while a newer one was in flight —
  `verifyRow` already claimed its row up front, these two did not. Tests
  answer the OLDER request first and assert nothing is published.
- SkillsScreen: "No skills listed", not "No skills". SEP-2640 lets a server
  return an empty or partial catalog and says an empty result is not proof
  it has none — an unlisted skill is still fetchable by URI — so the old
  copy had the tool asserting something the protocol explicitly does not.
- SkillsScreen: each row's Verify button gets an `aria-label` carrying its
  URI. Every row's visible text is "Verify" and the URI cell is not
  programmatically associated with the button, so a screen-reader user had
  no way to tell which file each control checked.
- inspectorClient: attribute a rejected `skills/get` envelope with
  `markResponseRejected`, so the Protocol tab stops rendering it as a clean
  success while the screen shows an error. In the client rather than a
  store because `skills/get` has none. Gated on `isClientDecodeRejection`,
  with a test that a transport failure is NOT attributed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kfrH3uRNUM4JAk1reKtyY
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-05 02:21:41 -04:00
commit a561cd1daa

View file

@ -2,6 +2,7 @@ import { describe, it, expect, vi } from "vitest";
import { InspectorClient } from "@inspector/core/mcp/inspectorClient.js";
import type { ServerCapabilities } from "@modelcontextprotocol/client";
import { SKILLS_EXTENSION_KEY } from "@inspector/core/mcp/skillsSchemas.js";
import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
/**
* Unit coverage for the Skills extension methods (#2234, SEP-2640).
@ -181,6 +182,52 @@ describe("InspectorClient skills methods (#2234)", () => {
});
});
it("attributes a rejected skills/get envelope to its Protocol entry", async () => {
// Without this the Skills screen shows an error while the Protocol tab
// renders the same exchange as a clean success. Done in the client rather
// than a store because `skills/get` has none — the screen calls it.
const client = makeClient();
const marked: [string, string][] = [];
(
client as unknown as {
markResponseRejected: (m: string, r: string) => void;
}
).markResponseRejected = (method, reason) => {
marked.push([method, reason]);
};
internals(client).client = {
request: async () => {
throw new SdkError(
SdkErrorCode.InvalidResult,
"Invalid result for skills/get",
);
},
};
await expect(client.getSkill("skill://demo/SKILL.md")).rejects.toThrow();
expect(marked).toEqual([["skills/get", "Invalid result for skills/get"]]);
});
it("does NOT attribute a transport failure on skills/get", async () => {
// No response frame arrived, so the last-answered id still points at an
// earlier, successful exchange; marking it would stamp that one.
const client = makeClient();
const marked: string[] = [];
(
client as unknown as {
markResponseRejected: (m: string, r: string) => void;
}
).markResponseRejected = (method) => {
marked.push(method);
};
internals(client).client = {
request: async () => {
throw new SdkError(SdkErrorCode.ConnectionClosed, "Connection closed");
},
};
await expect(client.getSkill("skill://demo/SKILL.md")).rejects.toThrow();
expect(marked).toEqual([]);
});
it("rejects a skills/list result that is not a skills page", async () => {
// The explicit result schema is the whole client-side mechanism for a
// consumer-owned extension method, so a nonconforming result must fail