feat: apply auto-refresh setting live + diff-aware list-changed indicator (#1444) (#1445)

* feat: apply auto-refresh setting live + diff-aware list-changed indicator (#1444)

Two follow-ups to #1402.

1. Live setting application — InspectorClient gains setServerSettings()
   (on the protocol + fake); App pushes edited settings onto the live
   client when the Server Settings modal closes for the connected server,
   so toggling auto-refresh-on-list-changed takes effect without a
   reconnect (the managed state reads getServerSettings() at notification
   time).

2. Diff-aware indicator — in flag-only mode, on */list_changed the managed
   Tools/Prompts/Resources state now fetches the new list and compares it
   to the current one, lighting the indicator only when it actually
   differs. The displayed list stays put until the user pulls via Refresh
   (pull-on-demand preserved). Servers like everything re-send identical
   lists on list_changed; this suppresses the false-positive indicator.
   Refactors each manager's refresh into fetch + apply helpers shared with
   the peek path.

Trade-off: item 2 reintroduces a fetch on */list_changed (to compare), but
unlike the pre-#1402 behavior it never replaces the displayed list under
the user — only the indicator reflects a real change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(core): make the diff-aware peek clear the indicator on revert (#1444 review)

peekForChange now sets the list-changed flag to whether the server list
differs from the displayed one (setListChanged(differs)) instead of only
ever setting it true. So a later notification that reverts the server back
to the displayed list clears the indicator — nothing left to pull. Adds a
clear-on-revert regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(core): consolidate managed-list state managers onto a shared base (#1444)

The four per-primitive list managers (tools, prompts, resources, resource
templates) were near-identical copies of the same fetch/apply/peek/connect/
disconnect/list_changed machinery, a standing drift risk. Extract a generic
ManagedListState<T, M> base parameterized by a config (change event,
list_changed notification, capability key, page fetcher, and whether the
list drives an indicator). Each manager is now a ~40-line subclass: its
config plus a typed getter alias.

Public contracts are unchanged — same per-manager *Change / listChangedChange
events and get*/refresh/getListChanged/clearListChanged methods — so the
hooks and all existing tests pass untouched. Templates opt out of the
indicator (supportsIndicator: false): a list_changed auto-refreshes only
when the server opts in, else does nothing. Net -245 lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(core): coalesce bursts of list_changed peeks (#1444 review)

A rapid burst of */list_changed notifications fired one concurrent
paginated fetch per notification. Guard peekForChange with an in-flight
flag: while a peek is fetching, a new notification queues a single re-run
instead of launching a concurrent fetch, and the running peek loops once
more on completion to capture the latest server state. Collapses a burst
into sequential peeks. Adds a coalescing test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(core): debounce list_changed bursts into a single fetch (#1444)

The in-flight guard only coalesced fetches that actually overlapped — when
a server (e.g. everything) emits a burst of */list_changed spaced just far
enough apart that each list call returns before the next notification, we
still fired one list call per notification. Add a real debounce in the
ManagedListState base: a notification (re)starts a timer and the
refresh/peek runs once the burst settles, collapsing N notifications into
one fetch. The in-flight coalescing guard stays as a second line of defense
for a post-debounce notification landing during a slow paginated fetch.

The delay is injectable (DEFAULT_LIST_CHANGED_DEBOUNCE_MS = 250; subclasses
take an optional constructor arg) so tests run with 0 for fast, determinist
ic behavior. Timer is cleared on disconnect, unsubscribe, and destroy. Adds
debounce + in-flight regression tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(core): extend the in-flight guard to the auto-refresh path (#1444 review)

The overlap guard was only on the peek path; a burst-during-a-slow auto-
refresh could let a slower older fetch clobber a newer list (applyItems is
last-write-wins). Lift the guard from peekForChange up to runListChanged so
it wraps both the auto-refresh and peek actions: while one is fetching, a
new post-debounce notification queues a single re-run. Adds an auto-refresh
coalescing test alongside the peek one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(core): light the indicator without fetching when auto-refresh is off (#1444)

Per maintainer feedback: with auto-refresh off, a list_changed should not
issue any list call. The diff-aware peek (fetch-to-compare) meant one
tools/list went out per settled burst even with auto-refresh off, which the
maintainer doesn't want. Revert flag-only mode to lighting the indicator
blindly on a (debounced) list_changed — no network — and the user pulls the
new list via Refresh.

Trade-off (accepted): the indicator lights even when the server re-sent an
identical list (the everything-server case), since we no longer fetch to
compare. Keeps the debounce (one indicator light per burst), the
auto-refresh path + its in-flight guard, the live setServerSettings, and the
ManagedListState consolidation. Removes peekForChange and its tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(core): scrub stale peek docstrings after the blind-indicator revert (#1445 review)

The diff-aware revert removed peekForChange's body but left its docstring,
which orphaned onto destroy() and described fetch-to-compare/clear-on-revert
behavior that no longer exists. Replace it with a one-line destroy() doc, and
update clearListChanged's doc (peek -> blind light).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(core): finish scrubbing stale peek references after the revert (#1445 review)

Round-7's scrub missed four more peek/fetch-to-compare mentions in
managedListState.ts (supportsIndicator, fetchAll, the debounce-default and
onListChanged comments) plus six 'async peek' comments in the state tests.
Update them all to the blind-indicator semantics. Doc/comment-only; no
behavior change. grep -i peek over core/mcp/state now returns nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cliff Hall 2026-06-09 11:00:28 -04:00 • committed by GitHub
commit 721dae08c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 597 additions and 560 deletions

View file

@ -26,6 +26,14 @@ function waitForPromptsChange(state: ManagedPromptsState): Promise<Prompt[]> {
});
}
function waitForListChanged(state: ManagedPromptsState): Promise<boolean> {
return new Promise((resolve) => {
state.addEventListener("listChangedChange", (e) => resolve(e.detail), {
once: true,
});
});
}
describe("ManagedPromptsState", () => {
let client: FakeInspectorClient;
let state: ManagedPromptsState;
@ -35,7 +43,7 @@ describe("ManagedPromptsState", () => {
// exercise the live `listPrompts` path; capability-absent tests below
// override this.
client = new FakeInspectorClient({ capabilities: { prompts: {} } });
state = new ManagedPromptsState(client);
state = new ManagedPromptsState(client, 0);
});
it("starts with empty prompts", () => {
@ -61,7 +69,7 @@ describe("ManagedPromptsState", () => {
capabilities: { tools: {}, resources: {} },
});
promptless.setStatus("connected");
const promptlessState = new ManagedPromptsState(promptless);
const promptlessState = new ManagedPromptsState(promptless, 0);
const result = await promptlessState.refresh();
expect(result).toEqual([]);
@ -73,7 +81,7 @@ describe("ManagedPromptsState", () => {
// there, not only the publicly-callable refresh().
const promptless = new FakeInspectorClient({ capabilities: { tools: {} } });
promptless.setStatus("connected");
const promptlessState = new ManagedPromptsState(promptless);
const promptlessState = new ManagedPromptsState(promptless, 0);
promptless.dispatchTypedEvent("connect");
// Yield so the async refresh chained off connect runs.
@ -135,15 +143,15 @@ describe("ManagedPromptsState", () => {
expect(next.map((p) => p.name)).toEqual(["a"]);
});
it("promptsListChanged does NOT auto-refresh by default (the user pulls via Refresh)", async () => {
it("promptsListChanged lights the indicator without fetching by default (#1444)", async () => {
// Auto-refresh off: a list_changed lights the indicator with NO list call;
// the user pulls the new list via Refresh.
client.setStatus("connected");
client.queuePromptPages({ prompts: [prompt("a"), prompt("b")] });
const changed = waitForListChanged(state);
client.dispatchTypedEvent("promptsListChanged");
// Yield so a stray refresh would have landed.
await Promise.resolve();
await Promise.resolve();
expect(client.listPrompts).not.toHaveBeenCalled();
expect(state.getPrompts()).toEqual([]);
expect(await changed).toBe(true);
expect(client.listPrompts).not.toHaveBeenCalled(); // no automatic fetch
expect(state.getPrompts()).toEqual([]); // displayed list untouched
});
it("promptsListChanged auto-refreshes when the server opts in", async () => {
@ -152,7 +160,7 @@ describe("ManagedPromptsState", () => {
serverSettings: AUTO_REFRESH_SETTINGS,
});
autoClient.setStatus("connected");
const autoState = new ManagedPromptsState(autoClient);
const autoState = new ManagedPromptsState(autoClient, 0);
autoClient.queuePromptPages({ prompts: [prompt("a")] });
const changed = waitForPromptsChange(autoState);
autoClient.dispatchTypedEvent("promptsListChanged");
@ -206,14 +214,6 @@ describe("ManagedPromptsState", () => {
});
describe("listChanged (#1402)", () => {
function waitForListChanged(s: ManagedPromptsState): Promise<boolean> {
return new Promise((resolve) => {
s.addEventListener("listChangedChange", (e) => resolve(e.detail), {
once: true,
});
});
}
it("starts cleared", () => {
expect(state.getListChanged()).toBe(false);
});
@ -230,7 +230,9 @@ describe("ManagedPromptsState", () => {
it("clearListChanged resets the flag and dispatches false", async () => {
client.setStatus("connected");
client.queuePromptPages({ prompts: [prompt("a")] });
const set = waitForListChanged(state);
client.dispatchTypedEvent("promptsListChanged");
await set; // wait for the debounced notification to set the flag
expect(state.getListChanged()).toBe(true);
const changed = waitForListChanged(state);
@ -251,7 +253,9 @@ describe("ManagedPromptsState", () => {
it("disconnect clears the flag", async () => {
client.setStatus("connected");
client.queuePromptPages({ prompts: [prompt("a")] });
const set = waitForListChanged(state);
client.dispatchTypedEvent("promptsListChanged");
await set; // wait for the debounced notification to set the flag
expect(state.getListChanged()).toBe(true);
const changed = waitForListChanged(state);

View file

@ -40,7 +40,7 @@ describe("ManagedResourceTemplatesState", () => {
// 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);
state = new ManagedResourceTemplatesState(client, 0);
});
it("starts with empty resource templates", () => {
@ -67,7 +67,10 @@ describe("ManagedResourceTemplatesState", () => {
capabilities: { tools: {}, prompts: {} },
});
resourceless.setStatus("connected");
const resourcelessState = new ManagedResourceTemplatesState(resourceless);
const resourcelessState = new ManagedResourceTemplatesState(
resourceless,
0,
);
const result = await resourcelessState.refresh();
expect(result).toEqual([]);
@ -81,7 +84,10 @@ describe("ManagedResourceTemplatesState", () => {
capabilities: { tools: {} },
});
resourceless.setStatus("connected");
const resourcelessState = new ManagedResourceTemplatesState(resourceless);
const resourcelessState = new ManagedResourceTemplatesState(
resourceless,
0,
);
resourceless.dispatchTypedEvent("connect");
// Yield so the async refresh chained off connect runs.
@ -166,7 +172,7 @@ describe("ManagedResourceTemplatesState", () => {
serverSettings: AUTO_REFRESH_SETTINGS,
});
autoClient.setStatus("connected");
const autoState = new ManagedResourceTemplatesState(autoClient);
const autoState = new ManagedResourceTemplatesState(autoClient, 0);
autoClient.queueResourceTemplatePages({
resourceTemplates: [template("a")],
});

View file

@ -28,6 +28,14 @@ function waitForResourcesChange(
});
}
function waitForListChanged(state: ManagedResourcesState): Promise<boolean> {
return new Promise((resolve) => {
state.addEventListener("listChangedChange", (e) => resolve(e.detail), {
once: true,
});
});
}
describe("ManagedResourcesState", () => {
let client: FakeInspectorClient;
let state: ManagedResourcesState;
@ -37,7 +45,7 @@ describe("ManagedResourcesState", () => {
// tests exercise the live `listResources` path; capability-absent tests
// below override this.
client = new FakeInspectorClient({ capabilities: { resources: {} } });
state = new ManagedResourcesState(client);
state = new ManagedResourcesState(client, 0);
});
it("starts with empty resources", () => {
@ -64,7 +72,7 @@ describe("ManagedResourcesState", () => {
capabilities: { tools: {}, prompts: {} },
});
resourceless.setStatus("connected");
const resourcelessState = new ManagedResourcesState(resourceless);
const resourcelessState = new ManagedResourcesState(resourceless, 0);
const result = await resourcelessState.refresh();
expect(result).toEqual([]);
@ -78,7 +86,7 @@ describe("ManagedResourcesState", () => {
capabilities: { tools: {} },
});
resourceless.setStatus("connected");
const resourcelessState = new ManagedResourcesState(resourceless);
const resourcelessState = new ManagedResourcesState(resourceless, 0);
resourceless.dispatchTypedEvent("connect");
// Yield so the async refresh chained off connect runs.
@ -142,17 +150,15 @@ describe("ManagedResourcesState", () => {
expect(next.map((r) => r.uri)).toEqual(["a://1"]);
});
it("resourcesListChanged does NOT auto-refresh by default (the user pulls via Refresh)", async () => {
it("resourcesListChanged lights the indicator without fetching by default (#1444)", async () => {
// Auto-refresh off: a list_changed lights the indicator with NO list call;
// the user pulls the new list via Refresh.
client.setStatus("connected");
client.queueResourcePages({
resources: [resource("a://1"), resource("a://2")],
});
const changed = waitForListChanged(state);
client.dispatchTypedEvent("resourcesListChanged");
// Yield so a stray refresh would have landed.
await Promise.resolve();
await Promise.resolve();
expect(client.listResources).not.toHaveBeenCalled();
expect(state.getResources()).toEqual([]);
expect(await changed).toBe(true);
expect(client.listResources).not.toHaveBeenCalled(); // no automatic fetch
expect(state.getResources()).toEqual([]); // displayed list untouched
});
it("resourcesListChanged auto-refreshes when the server opts in", async () => {
@ -161,7 +167,7 @@ describe("ManagedResourcesState", () => {
serverSettings: AUTO_REFRESH_SETTINGS,
});
autoClient.setStatus("connected");
const autoState = new ManagedResourcesState(autoClient);
const autoState = new ManagedResourcesState(autoClient, 0);
autoClient.queueResourcePages({ resources: [resource("a://1")] });
const changed = waitForResourcesChange(autoState);
autoClient.dispatchTypedEvent("resourcesListChanged");
@ -215,14 +221,6 @@ describe("ManagedResourcesState", () => {
});
describe("listChanged (#1402)", () => {
function waitForListChanged(s: ManagedResourcesState): Promise<boolean> {
return new Promise((resolve) => {
s.addEventListener("listChangedChange", (e) => resolve(e.detail), {
once: true,
});
});
}
it("starts cleared", () => {
expect(state.getListChanged()).toBe(false);
});
@ -239,7 +237,9 @@ describe("ManagedResourcesState", () => {
it("clearListChanged resets the flag and dispatches false", async () => {
client.setStatus("connected");
client.queueResourcePages({ resources: [resource("a://1")] });
const set = waitForListChanged(state);
client.dispatchTypedEvent("resourcesListChanged");
await set; // wait for the debounced notification to set the flag
expect(state.getListChanged()).toBe(true);
const changed = waitForListChanged(state);
@ -260,7 +260,9 @@ describe("ManagedResourcesState", () => {
it("disconnect clears the flag", async () => {
client.setStatus("connected");
client.queueResourcePages({ resources: [resource("a://1")] });
const set = waitForListChanged(state);
client.dispatchTypedEvent("resourcesListChanged");
await set; // wait for the debounced notification to set the flag
expect(state.getListChanged()).toBe(true);
const changed = waitForListChanged(state);

View file

@ -26,6 +26,14 @@ function waitForToolsChange(state: ManagedToolsState): Promise<Tool[]> {
});
}
function waitForListChanged(state: ManagedToolsState): Promise<boolean> {
return new Promise((resolve) => {
state.addEventListener("listChangedChange", (e) => resolve(e.detail), {
once: true,
});
});
}
describe("ManagedToolsState", () => {
let client: FakeInspectorClient;
let state: ManagedToolsState;
@ -35,7 +43,7 @@ describe("ManagedToolsState", () => {
// exercise the live `listTools` path; capability-absent tests below
// override this.
client = new FakeInspectorClient({ capabilities: { tools: {} } });
state = new ManagedToolsState(client);
state = new ManagedToolsState(client, 0);
});
it("starts with empty tools", () => {
@ -61,7 +69,7 @@ describe("ManagedToolsState", () => {
capabilities: { prompts: {}, resources: {} },
});
toolless.setStatus("connected");
const toollessState = new ManagedToolsState(toolless);
const toollessState = new ManagedToolsState(toolless, 0);
const result = await toollessState.refresh();
expect(result).toEqual([]);
@ -73,7 +81,7 @@ describe("ManagedToolsState", () => {
// there, not only the publicly-callable refresh().
const toolless = new FakeInspectorClient({ capabilities: { prompts: {} } });
toolless.setStatus("connected");
const toollessState = new ManagedToolsState(toolless);
const toollessState = new ManagedToolsState(toolless, 0);
toolless.dispatchTypedEvent("connect");
// Yield so the async refresh chained off connect runs.
@ -133,15 +141,65 @@ describe("ManagedToolsState", () => {
expect(next.map((t) => t.name)).toEqual(["a"]);
});
it("toolsListChanged does NOT auto-refresh by default (the user pulls via Refresh)", async () => {
it("toolsListChanged lights the indicator without fetching by default (#1444)", async () => {
// Auto-refresh off: a list_changed lights the indicator with NO list call;
// the user pulls the new list via Refresh.
client.setStatus("connected");
client.queueToolPages({ tools: [tool("a"), tool("b")] });
const changed = waitForListChanged(state);
client.dispatchTypedEvent("toolsListChanged");
// Yield so a stray refresh would have landed.
await Promise.resolve();
await Promise.resolve();
expect(await changed).toBe(true);
expect(client.listTools).not.toHaveBeenCalled(); // no automatic fetch
expect(state.getTools()).toEqual([]); // displayed list untouched
});
it("debounces a burst of list_changed into a single indicator light, no fetch (#1444)", async () => {
// The everything-server case: a rapid burst collapses to one indicator
// light once it settles, and never fetches in flag-only mode.
client.setStatus("connected");
let fired = 0;
state.addEventListener("listChangedChange", () => {
fired++;
});
client.dispatchTypedEvent("toolsListChanged");
client.dispatchTypedEvent("toolsListChanged");
client.dispatchTypedEvent("toolsListChanged");
await new Promise((r) => setTimeout(r, 0));
expect(fired).toBe(1); // one debounced flip
expect(state.getListChanged()).toBe(true);
expect(client.listTools).not.toHaveBeenCalled();
expect(state.getTools()).toEqual([]);
});
it("coalesces an auto-refresh that fires while an earlier one is still fetching (#1444)", async () => {
// The guard covers the auto-refresh path too, so a slow refresh can't be
// clobbered by an overlapping one from a later notification.
const autoClient = new FakeInspectorClient({
capabilities: { tools: {} },
serverSettings: AUTO_REFRESH_SETTINGS,
});
autoClient.setStatus("connected");
const autoState = new ManagedToolsState(autoClient, 0);
let release: (value: { tools: Tool[] }) => void = () => {};
autoClient.listTools.mockImplementationOnce(
() =>
new Promise<{ tools: Tool[] }>((resolve) => {
release = resolve;
}),
);
autoClient.queueToolPages({ tools: [tool("a")] });
autoClient.dispatchTypedEvent("toolsListChanged");
await new Promise((r) => setTimeout(r, 0));
expect(autoClient.listTools).toHaveBeenCalledTimes(1);
autoClient.dispatchTypedEvent("toolsListChanged");
await new Promise((r) => setTimeout(r, 0));
expect(autoClient.listTools).toHaveBeenCalledTimes(1); // coalesced
release({ tools: [tool("a")] });
await new Promise((r) => setTimeout(r, 0));
expect(autoClient.listTools).toHaveBeenCalledTimes(2);
// The (coalesced) auto-refresh applied the new list.
expect(autoState.getTools().map((t) => t.name)).toEqual(["a"]);
});
it("toolsListChanged auto-refreshes when the server opts in", async () => {
@ -150,7 +208,7 @@ describe("ManagedToolsState", () => {
serverSettings: AUTO_REFRESH_SETTINGS,
});
autoClient.setStatus("connected");
const autoState = new ManagedToolsState(autoClient);
const autoState = new ManagedToolsState(autoClient, 0);
autoClient.queueToolPages({ tools: [tool("a")] });
const changed = waitForToolsChange(autoState);
autoClient.dispatchTypedEvent("toolsListChanged");
@ -158,6 +216,18 @@ describe("ManagedToolsState", () => {
expect(autoClient.listTools).toHaveBeenCalled();
});
it("honors a live setServerSettings toggle without a reconnect (#1444)", async () => {
// Starts in flag-only mode (no settings); flip auto-refresh on live.
client.setStatus("connected");
client.setServerSettings(AUTO_REFRESH_SETTINGS);
client.queueToolPages({ tools: [tool("a")] });
const changed = waitForToolsChange(state);
client.dispatchTypedEvent("toolsListChanged");
// The list is applied (auto-refresh), proving the manager read the new
// setting at notification time rather than a connect-time snapshot.
expect((await changed).map((t) => t.name)).toEqual(["a"]);
});
it("statusChange to disconnected clears tools and dispatches toolsChange", async () => {
client.setStatus("connected");
client.queueToolPages({ tools: [tool("a")] });
@ -207,14 +277,6 @@ describe("ManagedToolsState", () => {
});
describe("listChanged (#1402)", () => {
function waitForListChanged(s: ManagedToolsState): Promise<boolean> {
return new Promise((resolve) => {
s.addEventListener("listChangedChange", (e) => resolve(e.detail), {
once: true,
});
});
}
it("starts cleared", () => {
expect(state.getListChanged()).toBe(false);
});
@ -231,7 +293,9 @@ describe("ManagedToolsState", () => {
it("clearListChanged resets the flag and dispatches false", async () => {
client.setStatus("connected");
client.queueToolPages({ tools: [tool("a")] });
const set = waitForListChanged(state);
client.dispatchTypedEvent("toolsListChanged");
await set; // wait for the debounced notification to set the flag
expect(state.getListChanged()).toBe(true);
const changed = waitForListChanged(state);
@ -252,7 +316,9 @@ describe("ManagedToolsState", () => {
it("disconnect clears the flag", async () => {
client.setStatus("connected");
client.queueToolPages({ tools: [tool("a")] });
const set = waitForListChanged(state);
client.dispatchTypedEvent("toolsListChanged");
await set; // wait for the debounced notification to set the flag
expect(state.getListChanged()).toBe(true);
const changed = waitForListChanged(state);

View file

@ -60,7 +60,7 @@ describe("ResourceSubscriptionsState", () => {
});
it("resolves Resource references via ManagedResourcesState when provided", async () => {
const resourcesState = new ManagedResourcesState(client);
const resourcesState = new ManagedResourcesState(client, 0);
client.queueResourcePages({
resources: [resource("file:///a", { name: "Alpha", title: "Title A" })],
});
@ -154,7 +154,7 @@ describe("ResourceSubscriptionsState", () => {
});
it("re-resolves Resource references when ManagedResourcesState refreshes", async () => {
const resourcesState = new ManagedResourcesState(client);
const resourcesState = new ManagedResourcesState(client, 0);
const state = new ResourceSubscriptionsState(client, resourcesState);
client.dispatchTypedEvent("resourceSubscriptionsChange", ["file:///a"]);
expect(state.getSubscriptions()[0].resource.name).toBe("file:///a");
@ -169,7 +169,7 @@ describe("ResourceSubscriptionsState", () => {
});
it("does not re-emit on resourcesChange when no URIs are subscribed", async () => {
const resourcesState = new ManagedResourcesState(client);
const resourcesState = new ManagedResourcesState(client, 0);
const state = new ResourceSubscriptionsState(client, resourcesState);
const handler = vi.fn();
state.addEventListener("subscriptionsChange", handler);
@ -199,7 +199,7 @@ describe("ResourceSubscriptionsState", () => {
});
it("destroy unsubscribes from client and resources state events", () => {
const resourcesState = new ManagedResourcesState(client);
const resourcesState = new ManagedResourcesState(client, 0);
const state = new ResourceSubscriptionsState(client, resourcesState);
client.dispatchTypedEvent("resourceSubscriptionsChange", ["file:///a"]);
expect(state.getSubscriptions()).toHaveLength(1);

View file

@ -18,7 +18,7 @@ describe("useManagedPrompts", () => {
status: "connected",
capabilities: { prompts: {} },
});
state = new ManagedPromptsState(client);
state = new ManagedPromptsState(client, 0);
});
it("returns the initial prompts snapshot from the state", async () => {

View file

@ -18,7 +18,7 @@ describe("useManagedResourceTemplates", () => {
status: "connected",
capabilities: { resources: {} },
});
state = new ManagedResourceTemplatesState(client);
state = new ManagedResourceTemplatesState(client, 0);
});
it("returns the initial templates snapshot from the state", async () => {

View file

@ -18,7 +18,7 @@ describe("useManagedResources", () => {
status: "connected",
capabilities: { resources: {} },
});
state = new ManagedResourcesState(client);
state = new ManagedResourcesState(client, 0);
});
it("returns the initial resources snapshot from the state", async () => {

View file

@ -18,7 +18,7 @@ describe("useManagedTools", () => {
status: "connected",
capabilities: { tools: {} },
});
state = new ManagedToolsState(client);
state = new ManagedToolsState(client, 0);
});
it("returns the initial tools snapshot from the state", async () => {

View file

@ -654,6 +654,32 @@ describe("InspectorClient", () => {
});
describe("Server Data Management", () => {
it("getServerSettings returns the constructor settings; setServerSettings replaces them live (#1444)", () => {
const initial = {
headers: [],
metadata: [],
connectionTimeout: 0,
requestTimeout: 0,
taskTtl: 0,
autoRefreshOnListChanged: false,
roots: [],
};
client = new InspectorClient(
{
type: "stdio",
command: serverCommand.command,
args: serverCommand.args,
},
{
environment: { transport: createTransportNode },
serverSettings: initial,
},
);
expect(client.getServerSettings()?.autoRefreshOnListChanged).toBe(false);
client.setServerSettings({ ...initial, autoRefreshOnListChanged: true });
expect(client.getServerSettings()?.autoRefreshOnListChanged).toBe(true);
});
it("should auto-fetch server contents when enabled", async () => {
client = new InspectorClient(
{