The four list-fetching state managers fired their list RPC against every server on connect regardless of advertised capabilities, so a server that doesn't implement a given primitive replied -32601 "Method not found", surfacing in the console on every connect. Mirrors the `tasks`-capability gate added for ManagedRequestorTasksState in #1349. - managedToolsState → gate on capabilities.tools - managedPromptsState → gate on capabilities.prompts - managedResourcesState → gate on capabilities.resources - managedResourceTemplatesState → gate on capabilities.resources (the spec defines no separate resourceTemplates capability; resources/templates/list is part of the resources surface) When the capability is absent each manager sets an empty list, dispatches its change event, and returns — the right semantics for "this server doesn't support X." Tests: existing flow tests now construct their FakeInspectorClient with the relevant capability so the live list path is still exercised; added a "refresh skips listX when capability absent" and a "connect against an X-less server doesn't fire listX" test per manager. Updated the useManagedX peers and resourceSubscriptionsState (which drives a ManagedResourcesState refresh) to advertise the capability too. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
905b80a88c
commit
9e494544e7
9 changed files with 206 additions and 9 deletions
|
|
@ -20,7 +20,10 @@ describe("ManagedPromptsState", () => {
|
|||
let state: ManagedPromptsState;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new FakeInspectorClient();
|
||||
// Default to a server that advertises `prompts` so the existing flow tests
|
||||
// exercise the live `listPrompts` path; capability-absent tests below
|
||||
// override this.
|
||||
client = new FakeInspectorClient({ capabilities: { prompts: {} } });
|
||||
state = new ManagedPromptsState(client);
|
||||
});
|
||||
|
||||
|
|
@ -40,6 +43,35 @@ describe("ManagedPromptsState", () => {
|
|||
expect(client.listPrompts).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refresh skips listPrompts when the server doesn't advertise prompts capability", async () => {
|
||||
// Regression (#1350): a prompts-less server replied to prompts/list with
|
||||
// -32601 "Method not found", surfacing in the console on every connect.
|
||||
const promptless = new FakeInspectorClient({
|
||||
capabilities: { tools: {}, resources: {} },
|
||||
});
|
||||
promptless.setStatus("connected");
|
||||
const promptlessState = new ManagedPromptsState(promptless);
|
||||
|
||||
const result = await promptlessState.refresh();
|
||||
expect(result).toEqual([]);
|
||||
expect(promptless.listPrompts).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("connect against a prompts-less server doesn't fire listPrompts", async () => {
|
||||
// The connect event runs refresh; the capability gate must also catch it
|
||||
// there, not only the publicly-callable refresh().
|
||||
const promptless = new FakeInspectorClient({ capabilities: { tools: {} } });
|
||||
promptless.setStatus("connected");
|
||||
const promptlessState = new ManagedPromptsState(promptless);
|
||||
|
||||
promptless.dispatchTypedEvent("connect");
|
||||
// Yield so the async refresh chained off connect runs.
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
expect(promptless.listPrompts).not.toHaveBeenCalled();
|
||||
expect(promptlessState.getPrompts()).toEqual([]);
|
||||
});
|
||||
|
||||
it("refresh fetches a single page and dispatches promptsChange", async () => {
|
||||
client.setStatus("connected");
|
||||
client.queuePromptPages({ prompts: [prompt("a"), prompt("b")] });
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ describe("ManagedResourceTemplatesState", () => {
|
|||
let state: ManagedResourceTemplatesState;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new FakeInspectorClient();
|
||||
// Default to a server that advertises `resources` so the existing flow
|
||||
// tests exercise the live `listResourceTemplates` path; capability-absent
|
||||
// tests below override this. (Templates are gated on the `resources`
|
||||
// capability — the spec defines no separate `resourceTemplates` one.)
|
||||
client = new FakeInspectorClient({ capabilities: { resources: {} } });
|
||||
state = new ManagedResourceTemplatesState(client);
|
||||
});
|
||||
|
||||
|
|
@ -44,6 +48,38 @@ describe("ManagedResourceTemplatesState", () => {
|
|||
expect(client.listResourceTemplates).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refresh skips listResourceTemplates when the server doesn't advertise resources capability", async () => {
|
||||
// Regression (#1350): templates are part of the resources surface, so a
|
||||
// resources-less server replied to resources/templates/list with -32601
|
||||
// "Method not found", surfacing in the console on every connect.
|
||||
const resourceless = new FakeInspectorClient({
|
||||
capabilities: { tools: {}, prompts: {} },
|
||||
});
|
||||
resourceless.setStatus("connected");
|
||||
const resourcelessState = new ManagedResourceTemplatesState(resourceless);
|
||||
|
||||
const result = await resourcelessState.refresh();
|
||||
expect(result).toEqual([]);
|
||||
expect(resourceless.listResourceTemplates).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("connect against a resources-less server doesn't fire listResourceTemplates", async () => {
|
||||
// The connect event runs refresh; the capability gate must also catch it
|
||||
// there, not only the publicly-callable refresh().
|
||||
const resourceless = new FakeInspectorClient({
|
||||
capabilities: { tools: {} },
|
||||
});
|
||||
resourceless.setStatus("connected");
|
||||
const resourcelessState = new ManagedResourceTemplatesState(resourceless);
|
||||
|
||||
resourceless.dispatchTypedEvent("connect");
|
||||
// Yield so the async refresh chained off connect runs.
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
expect(resourceless.listResourceTemplates).not.toHaveBeenCalled();
|
||||
expect(resourcelessState.getResourceTemplates()).toEqual([]);
|
||||
});
|
||||
|
||||
it("refresh fetches a single page and dispatches resourceTemplatesChange", async () => {
|
||||
client.setStatus("connected");
|
||||
client.queueResourceTemplatePages({
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ describe("ManagedResourcesState", () => {
|
|||
let state: ManagedResourcesState;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new FakeInspectorClient();
|
||||
// Default to a server that advertises `resources` so the existing flow
|
||||
// tests exercise the live `listResources` path; capability-absent tests
|
||||
// below override this.
|
||||
client = new FakeInspectorClient({ capabilities: { resources: {} } });
|
||||
state = new ManagedResourcesState(client);
|
||||
});
|
||||
|
||||
|
|
@ -42,6 +45,38 @@ describe("ManagedResourcesState", () => {
|
|||
expect(client.listResources).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refresh skips listResources when the server doesn't advertise resources capability", async () => {
|
||||
// Regression (#1350): a resources-less server replied to resources/list
|
||||
// with -32601 "Method not found", surfacing in the console on every
|
||||
// connect.
|
||||
const resourceless = new FakeInspectorClient({
|
||||
capabilities: { tools: {}, prompts: {} },
|
||||
});
|
||||
resourceless.setStatus("connected");
|
||||
const resourcelessState = new ManagedResourcesState(resourceless);
|
||||
|
||||
const result = await resourcelessState.refresh();
|
||||
expect(result).toEqual([]);
|
||||
expect(resourceless.listResources).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("connect against a resources-less server doesn't fire listResources", async () => {
|
||||
// The connect event runs refresh; the capability gate must also catch it
|
||||
// there, not only the publicly-callable refresh().
|
||||
const resourceless = new FakeInspectorClient({
|
||||
capabilities: { tools: {} },
|
||||
});
|
||||
resourceless.setStatus("connected");
|
||||
const resourcelessState = new ManagedResourcesState(resourceless);
|
||||
|
||||
resourceless.dispatchTypedEvent("connect");
|
||||
// Yield so the async refresh chained off connect runs.
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
expect(resourceless.listResources).not.toHaveBeenCalled();
|
||||
expect(resourcelessState.getResources()).toEqual([]);
|
||||
});
|
||||
|
||||
it("refresh fetches a single page and dispatches resourcesChange", async () => {
|
||||
client.setStatus("connected");
|
||||
client.queueResourcePages({
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ describe("ManagedToolsState", () => {
|
|||
let state: ManagedToolsState;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new FakeInspectorClient();
|
||||
// Default to a server that advertises `tools` so the existing flow tests
|
||||
// exercise the live `listTools` path; capability-absent tests below
|
||||
// override this.
|
||||
client = new FakeInspectorClient({ capabilities: { tools: {} } });
|
||||
state = new ManagedToolsState(client);
|
||||
});
|
||||
|
||||
|
|
@ -40,6 +43,35 @@ describe("ManagedToolsState", () => {
|
|||
expect(client.listTools).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refresh skips listTools when the server doesn't advertise tools capability", async () => {
|
||||
// Regression (#1350): a tools-less server replied to tools/list with
|
||||
// -32601 "Method not found", surfacing in the console on every connect.
|
||||
const toolless = new FakeInspectorClient({
|
||||
capabilities: { prompts: {}, resources: {} },
|
||||
});
|
||||
toolless.setStatus("connected");
|
||||
const toollessState = new ManagedToolsState(toolless);
|
||||
|
||||
const result = await toollessState.refresh();
|
||||
expect(result).toEqual([]);
|
||||
expect(toolless.listTools).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("connect against a tools-less server doesn't fire listTools", async () => {
|
||||
// The connect event runs refresh; the capability gate must also catch it
|
||||
// there, not only the publicly-callable refresh().
|
||||
const toolless = new FakeInspectorClient({ capabilities: { prompts: {} } });
|
||||
toolless.setStatus("connected");
|
||||
const toollessState = new ManagedToolsState(toolless);
|
||||
|
||||
toolless.dispatchTypedEvent("connect");
|
||||
// Yield so the async refresh chained off connect runs.
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
expect(toolless.listTools).not.toHaveBeenCalled();
|
||||
expect(toollessState.getTools()).toEqual([]);
|
||||
});
|
||||
|
||||
it("refresh fetches a single page and dispatches toolsChange", async () => {
|
||||
client.setStatus("connected");
|
||||
client.queueToolPages({ tools: [tool("a"), tool("b")] });
|
||||
|
|
|
|||
|
|
@ -25,7 +25,12 @@ describe("ResourceSubscriptionsState", () => {
|
|||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-05-19T10:00:00Z"));
|
||||
client = new FakeInspectorClient({ status: "connected" });
|
||||
// `resources` capability so the ManagedResourcesState refresh used by the
|
||||
// reference-resolution test exercises the live `listResources` path.
|
||||
client = new FakeInspectorClient({
|
||||
status: "connected",
|
||||
capabilities: { resources: {} },
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ describe("useManagedPrompts", () => {
|
|||
let state: ManagedPromptsState;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new FakeInspectorClient({ status: "connected" });
|
||||
client = new FakeInspectorClient({
|
||||
status: "connected",
|
||||
capabilities: { prompts: {} },
|
||||
});
|
||||
state = new ManagedPromptsState(client);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ describe("useManagedResourceTemplates", () => {
|
|||
let state: ManagedResourceTemplatesState;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new FakeInspectorClient({ status: "connected" });
|
||||
client = new FakeInspectorClient({
|
||||
status: "connected",
|
||||
capabilities: { resources: {} },
|
||||
});
|
||||
state = new ManagedResourceTemplatesState(client);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ describe("useManagedResources", () => {
|
|||
let state: ManagedResourcesState;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new FakeInspectorClient({ status: "connected" });
|
||||
client = new FakeInspectorClient({
|
||||
status: "connected",
|
||||
capabilities: { resources: {} },
|
||||
});
|
||||
state = new ManagedResourcesState(client);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ describe("useManagedTools", () => {
|
|||
let state: ManagedToolsState;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new FakeInspectorClient({ status: "connected" });
|
||||
client = new FakeInspectorClient({
|
||||
status: "connected",
|
||||
capabilities: { tools: {} },
|
||||
});
|
||||
state = new ManagedToolsState(client);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue